agora inbox for pgsql-hackers@postgresql.org
help / color / mirror / Atom feed[PATCH v2 07/15] heapam: Add batch mode mvcc check and use it in page mode
44+ messages / 9 participants
[nested] [flat]
* [PATCH v2 07/15] heapam: Add batch mode mvcc check and use it in page mode
@ 2024-10-17 17:16 Andres Freund <andres@anarazel.de>
0 siblings, 0 replies; 44+ messages in thread
From: Andres Freund @ 2024-10-17 17:16 UTC (permalink / raw)
There are two reasons for doing so:
1) It is generally faster to perform checks in a batched fashion and making
sequential scans faster is nice.
2) We would like to stop setting hint bits while pages are being written
out. The necessary locking becomes visible for page mode scans if done for
every tuple. With batching the overhead can be amortized to only happen
once per page.
There are substantial further optimization opportunities along these
lines:
- Right now HeapTupleSatisfiesMVCCBatch() simply uses the single-tuple
HeapTupleSatisfiesMVCC(), relying on the compiler to inline it. We could
instead write an explicitly optimized version that avoids repeated xid
tests.
- Introduce batched version of the serializability test
- Introduce batched version of HeapTupleSatisfiesVacuum
Author:
Reviewed-by:
Discussion: https://postgr.es/m/
Backpatch:
---
src/include/access/heapam.h | 28 ++++++
src/backend/access/heap/heapam.c | 104 ++++++++++++++++----
src/backend/access/heap/heapam_visibility.c | 50 ++++++++++
src/tools/pgindent/typedefs.list | 1 +
4 files changed, 163 insertions(+), 20 deletions(-)
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 96cf82f97b7..d4a790251cc 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -417,6 +417,34 @@ extern bool HeapTupleHeaderIsOnlyLocked(HeapTupleHeader tuple);
extern bool HeapTupleIsSurelyDead(HeapTuple htup,
struct GlobalVisState *vistest);
+/*
+ * FIXME: define to be removed
+ *
+ * Without this I see worse performance. But it's a bit ugly, so I thought
+ * it'd be useful to leave a way in for others to experiment with this.
+ */
+#define BATCHMVCC_FEWER_ARGS
+
+#ifdef BATCHMVCC_FEWER_ARGS
+typedef struct BatchMVCCState
+{
+ HeapTupleData tuples[MaxHeapTuplesPerPage];
+ bool visible[MaxHeapTuplesPerPage];
+} BatchMVCCState;
+#endif
+
+extern int HeapTupleSatisfiesMVCCBatch(Snapshot snapshot, Buffer buffer,
+ int ntups,
+#ifdef BATCHMVCC_FEWER_ARGS
+ BatchMVCCState *batchmvcc,
+#else
+ HeapTupleData *tuples,
+ bool *visible,
+#endif
+ OffsetNumber *vistuples_dense);
+
+
+
/*
* To avoid leaking too much knowledge about reorderbuffer implementation
* details this is implemented in reorderbuffer.c not heapam_visibility.c
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index f9086369728..1781e2c0571 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -437,42 +437,106 @@ page_collect_tuples(HeapScanDesc scan, Snapshot snapshot,
BlockNumber block, int lines,
bool all_visible, bool check_serializable)
{
+ Oid relid = RelationGetRelid(scan->rs_base.rs_rd);
+#ifdef BATCHMVCC_FEWER_ARGS
+ BatchMVCCState batchmvcc;
+#else
+ HeapTupleData tuples[MaxHeapTuplesPerPage];
+ bool visible[MaxHeapTuplesPerPage];
+#endif
int ntup = 0;
- OffsetNumber lineoff;
+ int nvis = 0;
- for (lineoff = FirstOffsetNumber; lineoff <= lines; lineoff++)
+ /* page at a time should have been disabled otherwise */
+ Assert(IsMVCCSnapshot(snapshot));
+
+ /* first find all tuples on the page */
+ for (OffsetNumber lineoff = FirstOffsetNumber; lineoff <= lines; lineoff++)
{
ItemId lpp = PageGetItemId(page, lineoff);
- HeapTupleData loctup;
- bool valid;
+ HeapTuple tup;
- if (!ItemIdIsNormal(lpp))
+ if (unlikely(!ItemIdIsNormal(lpp)))
continue;
- loctup.t_data = (HeapTupleHeader) PageGetItem(page, lpp);
- loctup.t_len = ItemIdGetLength(lpp);
- loctup.t_tableOid = RelationGetRelid(scan->rs_base.rs_rd);
- ItemPointerSet(&(loctup.t_self), block, lineoff);
+ /*
+ * If the page is not all-visible or we need to check serializability,
+ * maintain enough state to be able to refind the tuple efficiently,
+ * without again needing to extract it from the page.
+ */
+ if (!all_visible || check_serializable)
+ {
+#ifdef BATCHMVCC_FEWER_ARGS
+ tup = &batchmvcc.tuples[ntup];
+#else
+ tup = &tuples[ntup];
+#endif
+ tup->t_data = (HeapTupleHeader) PageGetItem(page, lpp);
+ tup->t_len = ItemIdGetLength(lpp);
+ tup->t_tableOid = relid;
+ ItemPointerSet(&(tup->t_self), block, lineoff);
+ }
+
+ /*
+ * If the page is all visible, these fields won'otherwise wont be
+ * populated in loop below.
+ */
if (all_visible)
- valid = true;
- else
- valid = HeapTupleSatisfiesVisibility(&loctup, snapshot, buffer);
-
- if (check_serializable)
- HeapCheckForSerializableConflictOut(valid, scan->rs_base.rs_rd,
- &loctup, buffer, snapshot);
-
- if (valid)
{
+ if (check_serializable)
+ {
+#ifdef BATCHMVCC_FEWER_ARGS
+ batchmvcc.visible[ntup] = true;
+#else
+ visible[ntup] = true;
+#endif
+ }
scan->rs_vistuples[ntup] = lineoff;
- ntup++;
}
+
+ ntup++;
}
Assert(ntup <= MaxHeapTuplesPerPage);
- return ntup;
+ /* unless the page is all visible, test visibility for all tuples one go */
+ if (all_visible)
+ nvis = ntup;
+ else
+ nvis = HeapTupleSatisfiesMVCCBatch(snapshot, buffer,
+ ntup,
+#ifdef BATCHMVCC_FEWER_ARGS
+ &batchmvcc,
+#else
+ tuples, visible,
+#endif
+ scan->rs_vistuples
+ );
+
+ /*
+ * So far we don't have batch API for testing serializabilty, so do so
+ * one-by-one.
+ */
+ if (check_serializable)
+ {
+ for (int i = 0; i < ntup; i++)
+ {
+#ifdef BATCHMVCC_FEWER_ARGS
+ HeapCheckForSerializableConflictOut(batchmvcc.visible[i],
+ scan->rs_base.rs_rd,
+ &batchmvcc.tuples[i],
+ buffer, snapshot);
+#else
+ HeapCheckForSerializableConflictOut(visible[i],
+ scan->rs_base.rs_rd,
+ &tuples[i],
+ buffer, snapshot);
+#endif
+ }
+ }
+
+ return nvis;
}
/*
diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c
index b7aa8bb7a52..4baafc01a2c 100644
--- a/src/backend/access/heap/heapam_visibility.c
+++ b/src/backend/access/heap/heapam_visibility.c
@@ -1575,6 +1575,56 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
return true;
}
+/*
+ * Perform HeaptupleSatisfiesMVCC() on each passed in tuple. This is more
+ * efficient than doing HeapTupleSatisfiesMVCC() one-by-one.
+ *
+ * To be checked tuples are passed via BatchMVCCState->tuples. Each tuple's
+ * visibility is set in batchmvcc->visible[]. In addition, ->vistuples_dense
+ * is set to contain the offsets of visible tuples.
+ *
+ * Returns the number of visible tuples.
+ */
+int
+HeapTupleSatisfiesMVCCBatch(Snapshot snapshot, Buffer buffer,
+ int ntups,
+#ifdef BATCHMVCC_FEWER_ARGS
+ BatchMVCCState *batchmvcc,
+#else
+ HeapTupleData *tuples,
+ bool *visible,
+#endif
+ OffsetNumber *vistuples_dense)
+{
+ int nvis = 0;
+
+ Assert(IsMVCCSnapshot(snapshot));
+
+ for (int i = 0; i < ntups; i++)
+ {
+ bool valid;
+#ifdef BATCHMVCC_FEWER_ARGS
+ HeapTuple tup = &batchmvcc->tuples[i];
+#else
+ HeapTuple tup = &tuples[i];
+#endif
+
+ valid = HeapTupleSatisfiesMVCC(tup, snapshot, buffer);
+#ifdef BATCHMVCC_FEWER_ARGS
+ batchmvcc->visible[i] = valid;
+#else
+ visible[i] = valid;
+#endif
+ if (likely(valid))
+ {
+ vistuples_dense[nvis] = tup->t_self.ip_posid;
+ nvis++;
+ }
+ }
+
+ return nvis;
+}
+
/*
* HeapTupleSatisfiesVisibility
* True iff heap tuple satisfies a time qual.
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 08521d51a9b..2d278960078 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -244,6 +244,7 @@ Barrier
BaseBackupCmd
BaseBackupTargetHandle
BaseBackupTargetType
+BatchMVCCState
BeginDirectModify_function
BeginForeignInsert_function
BeginForeignModify_function
--
2.45.2.746.g06e570c0df.dirty
--ftvuw4gllm5odk5g
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v2-0008-bufmgr-Make-it-easier-to-change-number-of-buffer-.patch"
^ permalink raw reply [nested|flat] 44+ messages in thread
* [PATCH v1 06/14] heapam: Add batch mode mvcc check and use it in page mode
@ 2024-10-17 17:16 Andres Freund <andres@anarazel.de>
0 siblings, 0 replies; 44+ messages in thread
From: Andres Freund @ 2024-10-17 17:16 UTC (permalink / raw)
There are two reasons for doing so:
1) It is generally faster to perform checks in a batched fashion and making
sequential scans faster is nice.
2) We would like to stop setting hint bits while pages are being written
out. The necessary locking becomes visible for page mode scans if done for
every tuple. With batching the overhead can be amortized to only happen
once per page.
There are substantial further optimization opportunities along these
lines:
- Right now HeapTupleSatisfiesMVCCBatch() simply uses the single-tuple
HeapTupleSatisfiesMVCC(), relying on the compiler to inline it. We could
instead write an explicitly optimized version that avoids repeated xid
tests.
- Introduce batched version of the serializability test
- Introduce batched version of HeapTupleSatisfiesVacuum
Author:
Reviewed-by:
Discussion: https://postgr.es/m/
Backpatch:
---
src/include/access/heapam.h | 28 +++++
src/backend/access/heap/heapam.c | 110 ++++++++++++++++----
src/backend/access/heap/heapam_visibility.c | 50 +++++++++
src/tools/pgindent/typedefs.list | 1 +
4 files changed, 166 insertions(+), 23 deletions(-)
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 96cf82f97b7..d4a790251cc 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -417,6 +417,34 @@ extern bool HeapTupleHeaderIsOnlyLocked(HeapTupleHeader tuple);
extern bool HeapTupleIsSurelyDead(HeapTuple htup,
struct GlobalVisState *vistest);
+/*
+ * FIXME: define to be removed
+ *
+ * Without this I see worse performance. But it's a bit ugly, so I thought
+ * it'd be useful to leave a way in for others to experiment with this.
+ */
+#define BATCHMVCC_FEWER_ARGS
+
+#ifdef BATCHMVCC_FEWER_ARGS
+typedef struct BatchMVCCState
+{
+ HeapTupleData tuples[MaxHeapTuplesPerPage];
+ bool visible[MaxHeapTuplesPerPage];
+} BatchMVCCState;
+#endif
+
+extern int HeapTupleSatisfiesMVCCBatch(Snapshot snapshot, Buffer buffer,
+ int ntups,
+#ifdef BATCHMVCC_FEWER_ARGS
+ BatchMVCCState *batchmvcc,
+#else
+ HeapTupleData *tuples,
+ bool *visible,
+#endif
+ OffsetNumber *vistuples_dense);
+
+
+
/*
* To avoid leaking too much knowledge about reorderbuffer implementation
* details this is implemented in reorderbuffer.c not heapam_visibility.c
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index d0cb4b1e29b..99656a1aac5 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -437,42 +437,106 @@ page_collect_tuples(HeapScanDesc scan, Snapshot snapshot,
BlockNumber block, int lines,
bool all_visible, bool check_serializable)
{
+ Oid relid = RelationGetRelid(scan->rs_base.rs_rd);
+#ifdef BATCHMVCC_FEWER_ARGS
+ BatchMVCCState batchmvcc;
+#else
+ HeapTupleData tuples[MaxHeapTuplesPerPage];
+ bool visible[MaxHeapTuplesPerPage];
+#endif
int ntup = 0;
- OffsetNumber lineoff;
+ int nvis = 0;
- for (lineoff = FirstOffsetNumber; lineoff <= lines; lineoff++)
+ /* page at a time should have been disabled otherwise */
+ Assert(IsMVCCSnapshot(snapshot));
+
+ /* first find all tuples on the page */
+ for (OffsetNumber lineoff = FirstOffsetNumber; lineoff <= lines; lineoff++)
{
ItemId lpp = PageGetItemId(page, lineoff);
- HeapTupleData loctup;
- bool valid;
+ HeapTuple tup;
- if (!ItemIdIsNormal(lpp))
+ if (unlikely(!ItemIdIsNormal(lpp)))
continue;
- loctup.t_data = (HeapTupleHeader) PageGetItem(page, lpp);
- loctup.t_len = ItemIdGetLength(lpp);
- loctup.t_tableOid = RelationGetRelid(scan->rs_base.rs_rd);
- ItemPointerSet(&(loctup.t_self), block, lineoff);
-
- if (all_visible)
- valid = true;
- else
- valid = HeapTupleSatisfiesVisibility(&loctup, snapshot, buffer);
-
- if (check_serializable)
- HeapCheckForSerializableConflictOut(valid, scan->rs_base.rs_rd,
- &loctup, buffer, snapshot);
-
- if (valid)
+ /*
+ * If the page is not all-visible or we need to check serializability,
+ * maintain enough state to be able to refind the tuple efficiently,
+ * without again needing to extract it from the page.
+ */
+ if (!all_visible || check_serializable)
{
- scan->rs_vistuples[ntup] = lineoff;
- ntup++;
+#ifdef BATCHMVCC_FEWER_ARGS
+ tup = &batchmvcc.tuples[ntup];
+#else
+ tup = &tuples[ntup];
+#endif
+
+ tup->t_data = (HeapTupleHeader) PageGetItem(page, lpp);
+ tup->t_len = ItemIdGetLength(lpp);
+ tup->t_tableOid = relid;
+ ItemPointerSet(&(tup->t_self), block, lineoff);
}
+
+ /*
+ * If the page is all visible, these fields won'otherwise wont be
+ * populated in loop below.
+ */
+ if (all_visible)
+ {
+ if (check_serializable)
+ {
+#ifdef BATCHMVCC_FEWER_ARGS
+ batchmvcc.visible[ntup] = true;
+#else
+ visible[ntup] = true;
+#endif
+ }
+ scan->rs_vistuples[ntup] = lineoff;
+ }
+
+ ntup++;
}
Assert(ntup <= MaxHeapTuplesPerPage);
- return ntup;
+ /* unless the page is all visible, test visibility for all tuples one go */
+ if (all_visible)
+ nvis = ntup;
+ else
+ nvis = HeapTupleSatisfiesMVCCBatch(snapshot, buffer,
+ ntup,
+#ifdef BATCHMVCC_FEWER_ARGS
+ &batchmvcc,
+#else
+ tuples, visible,
+#endif
+ scan->rs_vistuples
+ );
+
+ /*
+ * So far we don't have batch API for testing serializabilty, so do so
+ * one-by-one.
+ */
+ if (check_serializable)
+ {
+ for (int i = 0; i < ntup; i++)
+ {
+#ifdef BATCHMVCC_FEWER_ARGS
+ HeapCheckForSerializableConflictOut(batchmvcc.visible[i],
+ scan->rs_base.rs_rd,
+ &batchmvcc.tuples[i],
+ buffer, snapshot);
+#else
+ HeapCheckForSerializableConflictOut(visible[i],
+ scan->rs_base.rs_rd,
+ &tuples[i],
+ buffer, snapshot);
+#endif
+ }
+ }
+
+ return nvis;
}
/*
diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c
index b7aa8bb7a52..4baafc01a2c 100644
--- a/src/backend/access/heap/heapam_visibility.c
+++ b/src/backend/access/heap/heapam_visibility.c
@@ -1575,6 +1575,56 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
return true;
}
+/*
+ * Perform HeaptupleSatisfiesMVCC() on each passed in tuple. This is more
+ * efficient than doing HeapTupleSatisfiesMVCC() one-by-one.
+ *
+ * To be checked tuples are passed via BatchMVCCState->tuples. Each tuple's
+ * visibility is set in batchmvcc->visible[]. In addition, ->vistuples_dense
+ * is set to contain the offsets of visible tuples.
+ *
+ * Returns the number of visible tuples.
+ */
+int
+HeapTupleSatisfiesMVCCBatch(Snapshot snapshot, Buffer buffer,
+ int ntups,
+#ifdef BATCHMVCC_FEWER_ARGS
+ BatchMVCCState *batchmvcc,
+#else
+ HeapTupleData *tuples,
+ bool *visible,
+#endif
+ OffsetNumber *vistuples_dense)
+{
+ int nvis = 0;
+
+ Assert(IsMVCCSnapshot(snapshot));
+
+ for (int i = 0; i < ntups; i++)
+ {
+ bool valid;
+#ifdef BATCHMVCC_FEWER_ARGS
+ HeapTuple tup = &batchmvcc->tuples[i];
+#else
+ HeapTuple tup = &tuples[i];
+#endif
+
+ valid = HeapTupleSatisfiesMVCC(tup, snapshot, buffer);
+#ifdef BATCHMVCC_FEWER_ARGS
+ batchmvcc->visible[i] = valid;
+#else
+ visible[i] = valid;
+#endif
+ if (likely(valid))
+ {
+ vistuples_dense[nvis] = tup->t_self.ip_posid;
+ nvis++;
+ }
+ }
+
+ return nvis;
+}
+
/*
* HeapTupleSatisfiesVisibility
* True iff heap tuple satisfies a time qual.
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 171a7dd5d2b..15c7673465b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -244,6 +244,7 @@ Barrier
BaseBackupCmd
BaseBackupTargetHandle
BaseBackupTargetType
+BatchMVCCState
BeginDirectModify_function
BeginForeignInsert_function
BeginForeignModify_function
--
2.46.0.519.g2e7b89e038
--gpbnw6zzq3spol54
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v1-0007-bufmgr-Make-it-easier-to-change-number-of-buffer-.patch"
^ permalink raw reply [nested|flat] 44+ messages in thread
* [PATCH v6 11/14] heapam: Add batch mode mvcc check and use it in page mode
@ 2024-10-17 17:16 Andres Freund <andres@anarazel.de>
0 siblings, 0 replies; 44+ messages in thread
From: Andres Freund @ 2024-10-17 17:16 UTC (permalink / raw)
There are two reasons for doing so:
1) It is generally faster to perform checks in a batched fashion and making
sequential scans faster is nice.
2) We would like to stop setting hint bits while pages are being written
out. The necessary locking becomes visible for page mode scans if done for
every tuple. With batching the overhead can be amortized to only happen
once per page.
There are substantial further optimization opportunities along these
lines:
- Right now HeapTupleSatisfiesMVCCBatch() simply uses the single-tuple
HeapTupleSatisfiesMVCC(), relying on the compiler to inline it. We could
instead write an explicitly optimized version that avoids repeated xid
tests.
- Introduce batched version of the serializability test
- Introduce batched version of HeapTupleSatisfiesVacuum
Author:
Reviewed-by:
Discussion: https://postgr.es/m/
Backpatch:
---
src/include/access/heapam.h | 28 +++++++
src/backend/access/heap/heapam.c | 91 ++++++++++++++++-----
src/backend/access/heap/heapam_visibility.c | 47 +++++++++++
src/tools/pgindent/typedefs.list | 1 +
4 files changed, 147 insertions(+), 20 deletions(-)
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 909db73b7bb..13e4a4096c3 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -410,6 +410,34 @@ extern bool HeapTupleHeaderIsOnlyLocked(HeapTupleHeader tuple);
extern bool HeapTupleIsSurelyDead(HeapTuple htup,
GlobalVisState *vistest);
+/*
+ * FIXME: define to be removed
+ *
+ * Without this I see worse performance. But it's a bit ugly, so I thought
+ * it'd be useful to leave a way in for others to experiment with this.
+ */
+#define BATCHMVCC_FEWER_ARGS
+
+#ifdef BATCHMVCC_FEWER_ARGS
+typedef struct BatchMVCCState
+{
+ HeapTupleData tuples[MaxHeapTuplesPerPage];
+ bool visible[MaxHeapTuplesPerPage];
+} BatchMVCCState;
+#endif
+
+extern int HeapTupleSatisfiesMVCCBatch(Snapshot snapshot, Buffer buffer,
+ int ntups,
+#ifdef BATCHMVCC_FEWER_ARGS
+ BatchMVCCState *batchmvcc,
+#else
+ HeapTupleData *tuples,
+ bool *visible,
+#endif
+ OffsetNumber *vistuples_dense);
+
+
+
/*
* To avoid leaking too much knowledge about reorderbuffer implementation
* details this is implemented in reorderbuffer.c not heapam_visibility.c
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 4b0c49f4bb0..ddabd1a3ec3 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -504,42 +504,93 @@ page_collect_tuples(HeapScanDesc scan, Snapshot snapshot,
BlockNumber block, int lines,
bool all_visible, bool check_serializable)
{
+ Oid relid = RelationGetRelid(scan->rs_base.rs_rd);
+#ifdef BATCHMVCC_FEWER_ARGS
+ BatchMVCCState batchmvcc;
+ HeapTupleData *tuples = batchmvcc.tuples;
+ bool *visible = batchmvcc.visible;
+#else
+ HeapTupleData tuples[MaxHeapTuplesPerPage];
+ bool visible[MaxHeapTuplesPerPage];
+#endif
int ntup = 0;
- OffsetNumber lineoff;
+ int nvis = 0;
- for (lineoff = FirstOffsetNumber; lineoff <= lines; lineoff++)
+ /* page at a time should have been disabled otherwise */
+ Assert(IsMVCCSnapshot(snapshot));
+
+ /* first find all tuples on the page */
+ for (OffsetNumber lineoff = FirstOffsetNumber; lineoff <= lines; lineoff++)
{
ItemId lpp = PageGetItemId(page, lineoff);
- HeapTupleData loctup;
- bool valid;
+ HeapTuple tup;
- if (!ItemIdIsNormal(lpp))
+ if (unlikely(!ItemIdIsNormal(lpp)))
continue;
- loctup.t_data = (HeapTupleHeader) PageGetItem(page, lpp);
- loctup.t_len = ItemIdGetLength(lpp);
- loctup.t_tableOid = RelationGetRelid(scan->rs_base.rs_rd);
- ItemPointerSet(&(loctup.t_self), block, lineoff);
+ /*
+ * If the page is not all-visible or we need to check serializability,
+ * maintain enough state to be able to refind the tuple efficiently,
+ * without again needing to extract it from the page.
+ */
+ if (!all_visible || check_serializable)
+ {
+ tup = &tuples[ntup];
+ tup->t_data = (HeapTupleHeader) PageGetItem(page, lpp);
+ tup->t_len = ItemIdGetLength(lpp);
+ tup->t_tableOid = relid;
+ ItemPointerSet(&(tup->t_self), block, lineoff);
+ }
+
+ /*
+ * If the page is all visible, these fields won'otherwise wont be
+ * populated in loop below.
+ */
if (all_visible)
- valid = true;
- else
- valid = HeapTupleSatisfiesVisibility(&loctup, snapshot, buffer);
-
- if (check_serializable)
- HeapCheckForSerializableConflictOut(valid, scan->rs_base.rs_rd,
- &loctup, buffer, snapshot);
-
- if (valid)
{
+ if (check_serializable)
+ {
+ visible[ntup] = true;
+ }
scan->rs_vistuples[ntup] = lineoff;
- ntup++;
}
+
+ ntup++;
}
Assert(ntup <= MaxHeapTuplesPerPage);
- return ntup;
+ /* unless the page is all visible, test visibility for all tuples one go */
+ if (all_visible)
+ nvis = ntup;
+ else
+ nvis = HeapTupleSatisfiesMVCCBatch(snapshot, buffer,
+ ntup,
+#ifdef BATCHMVCC_FEWER_ARGS
+ &batchmvcc,
+#else
+ tuples, visible,
+#endif
+ scan->rs_vistuples
+ );
+
+ /*
+ * So far we don't have batch API for testing serializabilty, so do so
+ * one-by-one.
+ */
+ if (check_serializable)
+ {
+ for (int i = 0; i < ntup; i++)
+ {
+ HeapCheckForSerializableConflictOut(visible[i],
+ scan->rs_base.rs_rd,
+ &tuples[i],
+ buffer, snapshot);
+ }
+ }
+
+ return nvis;
}
/*
diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c
index 762538a2040..5645cfd8a49 100644
--- a/src/backend/access/heap/heapam_visibility.c
+++ b/src/backend/access/heap/heapam_visibility.c
@@ -1584,6 +1584,53 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
return true;
}
+/*
+ * Perform HeaptupleSatisfiesMVCC() on each passed in tuple. This is more
+ * efficient than doing HeapTupleSatisfiesMVCC() one-by-one.
+ *
+ * To be checked tuples are passed via BatchMVCCState->tuples. Each tuple's
+ * visibility is set in batchmvcc->visible[]. In addition, ->vistuples_dense
+ * is set to contain the offsets of visible tuples.
+ *
+ * Returns the number of visible tuples.
+ */
+int
+HeapTupleSatisfiesMVCCBatch(Snapshot snapshot, Buffer buffer,
+ int ntups,
+#ifdef BATCHMVCC_FEWER_ARGS
+ BatchMVCCState *batchmvcc,
+#else
+ HeapTupleData *tuples,
+ bool *visible,
+#endif
+ OffsetNumber *vistuples_dense)
+{
+ int nvis = 0;
+#ifdef BATCHMVCC_FEWER_ARGS
+ HeapTupleData *tuples = batchmvcc->tuples;
+ bool *visible = batchmvcc->visible;
+#endif
+
+ Assert(IsMVCCSnapshot(snapshot));
+
+ for (int i = 0; i < ntups; i++)
+ {
+ bool valid;
+ HeapTuple tup = &tuples[i];
+
+ valid = HeapTupleSatisfiesMVCC(tup, snapshot, buffer);
+ visible[i] = valid;
+
+ if (likely(valid))
+ {
+ vistuples_dense[nvis] = tup->t_self.ip_posid;
+ nvis++;
+ }
+ }
+
+ return nvis;
+}
+
/*
* HeapTupleSatisfiesVisibility
* True iff heap tuple satisfies a time qual.
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9a89e68c59c..9d14239b4c4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -249,6 +249,7 @@ Barrier
BaseBackupCmd
BaseBackupTargetHandle
BaseBackupTargetType
+BatchMVCCState
BeginDirectModify_function
BeginForeignInsert_function
BeginForeignModify_function
--
2.48.1.76.g4e746b1a31.dirty
--mhhc7c45w3dmihtp
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v6-0012-Require-share-exclusive-lock-to-set-hint-bits.patch"
^ permalink raw reply [nested|flat] 44+ messages in thread
* [PATCH v7 10/15] heapam: Add batch mode mvcc check and use it in page mode
@ 2024-10-17 17:16 Andres Freund <andres@anarazel.de>
0 siblings, 0 replies; 44+ messages in thread
From: Andres Freund @ 2024-10-17 17:16 UTC (permalink / raw)
There are two reasons for doing so:
1) It is generally faster to perform checks in a batched fashion and making
sequential scans faster is nice.
2) We would like to stop setting hint bits while pages are being written
out. The necessary locking becomes visible for page mode scans if done for
every tuple. With batching the overhead can be amortized to only happen
once per page.
There are substantial further optimization opportunities along these
lines:
- Right now HeapTupleSatisfiesMVCCBatch() simply uses the single-tuple
HeapTupleSatisfiesMVCC(), relying on the compiler to inline it. We could
instead write an explicitly optimized version that avoids repeated xid
tests.
- Introduce batched version of the serializability test
- Introduce batched version of HeapTupleSatisfiesVacuum
Author:
Reviewed-by:
Discussion: https://postgr.es/m/
Backpatch:
---
src/include/access/heapam.h | 28 +++++++
src/backend/access/heap/heapam.c | 91 ++++++++++++++++-----
src/backend/access/heap/heapam_visibility.c | 47 +++++++++++
src/tools/pgindent/typedefs.list | 1 +
4 files changed, 147 insertions(+), 20 deletions(-)
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 632c4332a8c..3528e6e4b59 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -449,6 +449,34 @@ extern bool HeapTupleHeaderIsOnlyLocked(HeapTupleHeader tuple);
extern bool HeapTupleIsSurelyDead(HeapTuple htup,
GlobalVisState *vistest);
+/*
+ * FIXME: define to be removed
+ *
+ * Without this I see worse performance. But it's a bit ugly, so I thought
+ * it'd be useful to leave a way in for others to experiment with this.
+ */
+#define BATCHMVCC_FEWER_ARGS
+
+#ifdef BATCHMVCC_FEWER_ARGS
+typedef struct BatchMVCCState
+{
+ HeapTupleData tuples[MaxHeapTuplesPerPage];
+ bool visible[MaxHeapTuplesPerPage];
+} BatchMVCCState;
+#endif
+
+extern int HeapTupleSatisfiesMVCCBatch(Snapshot snapshot, Buffer buffer,
+ int ntups,
+#ifdef BATCHMVCC_FEWER_ARGS
+ BatchMVCCState *batchmvcc,
+#else
+ HeapTupleData *tuples,
+ bool *visible,
+#endif
+ OffsetNumber *vistuples_dense);
+
+
+
/*
* To avoid leaking too much knowledge about reorderbuffer implementation
* details this is implemented in reorderbuffer.c not heapam_visibility.c
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 4d382a04338..7e56da9b196 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -519,42 +519,93 @@ page_collect_tuples(HeapScanDesc scan, Snapshot snapshot,
BlockNumber block, int lines,
bool all_visible, bool check_serializable)
{
+ Oid relid = RelationGetRelid(scan->rs_base.rs_rd);
+#ifdef BATCHMVCC_FEWER_ARGS
+ BatchMVCCState batchmvcc;
+ HeapTupleData *tuples = batchmvcc.tuples;
+ bool *visible = batchmvcc.visible;
+#else
+ HeapTupleData tuples[MaxHeapTuplesPerPage];
+ bool visible[MaxHeapTuplesPerPage];
+#endif
int ntup = 0;
- OffsetNumber lineoff;
+ int nvis = 0;
- for (lineoff = FirstOffsetNumber; lineoff <= lines; lineoff++)
+ /* page at a time should have been disabled otherwise */
+ Assert(IsMVCCSnapshot(snapshot));
+
+ /* first find all tuples on the page */
+ for (OffsetNumber lineoff = FirstOffsetNumber; lineoff <= lines; lineoff++)
{
ItemId lpp = PageGetItemId(page, lineoff);
- HeapTupleData loctup;
- bool valid;
+ HeapTuple tup;
- if (!ItemIdIsNormal(lpp))
+ if (unlikely(!ItemIdIsNormal(lpp)))
continue;
- loctup.t_data = (HeapTupleHeader) PageGetItem(page, lpp);
- loctup.t_len = ItemIdGetLength(lpp);
- loctup.t_tableOid = RelationGetRelid(scan->rs_base.rs_rd);
- ItemPointerSet(&(loctup.t_self), block, lineoff);
+ /*
+ * If the page is not all-visible or we need to check serializability,
+ * maintain enough state to be able to refind the tuple efficiently,
+ * without again needing to extract it from the page.
+ */
+ if (!all_visible || check_serializable)
+ {
+ tup = &tuples[ntup];
+ tup->t_data = (HeapTupleHeader) PageGetItem(page, lpp);
+ tup->t_len = ItemIdGetLength(lpp);
+ tup->t_tableOid = relid;
+ ItemPointerSet(&(tup->t_self), block, lineoff);
+ }
+
+ /*
+ * If the page is all visible, these fields otherwise won't be
+ * populated in loop below.
+ */
if (all_visible)
- valid = true;
- else
- valid = HeapTupleSatisfiesVisibility(&loctup, snapshot, buffer);
-
- if (check_serializable)
- HeapCheckForSerializableConflictOut(valid, scan->rs_base.rs_rd,
- &loctup, buffer, snapshot);
-
- if (valid)
{
+ if (check_serializable)
+ {
+ visible[ntup] = true;
+ }
scan->rs_vistuples[ntup] = lineoff;
- ntup++;
}
+
+ ntup++;
}
Assert(ntup <= MaxHeapTuplesPerPage);
- return ntup;
+ /* unless the page is all visible, test visibility for all tuples one go */
+ if (all_visible)
+ nvis = ntup;
+ else
+ nvis = HeapTupleSatisfiesMVCCBatch(snapshot, buffer,
+ ntup,
+#ifdef BATCHMVCC_FEWER_ARGS
+ &batchmvcc,
+#else
+ tuples, visible,
+#endif
+ scan->rs_vistuples
+ );
+
+ /*
+ * So far we don't have batch API for testing serializabilty, so do so
+ * one-by-one.
+ */
+ if (check_serializable)
+ {
+ for (int i = 0; i < ntup; i++)
+ {
+ HeapCheckForSerializableConflictOut(visible[i],
+ scan->rs_base.rs_rd,
+ &tuples[i],
+ buffer, snapshot);
+ }
+ }
+
+ return nvis;
}
/*
diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c
index 762538a2040..5645cfd8a49 100644
--- a/src/backend/access/heap/heapam_visibility.c
+++ b/src/backend/access/heap/heapam_visibility.c
@@ -1584,6 +1584,53 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
return true;
}
+/*
+ * Perform HeaptupleSatisfiesMVCC() on each passed in tuple. This is more
+ * efficient than doing HeapTupleSatisfiesMVCC() one-by-one.
+ *
+ * To be checked tuples are passed via BatchMVCCState->tuples. Each tuple's
+ * visibility is set in batchmvcc->visible[]. In addition, ->vistuples_dense
+ * is set to contain the offsets of visible tuples.
+ *
+ * Returns the number of visible tuples.
+ */
+int
+HeapTupleSatisfiesMVCCBatch(Snapshot snapshot, Buffer buffer,
+ int ntups,
+#ifdef BATCHMVCC_FEWER_ARGS
+ BatchMVCCState *batchmvcc,
+#else
+ HeapTupleData *tuples,
+ bool *visible,
+#endif
+ OffsetNumber *vistuples_dense)
+{
+ int nvis = 0;
+#ifdef BATCHMVCC_FEWER_ARGS
+ HeapTupleData *tuples = batchmvcc->tuples;
+ bool *visible = batchmvcc->visible;
+#endif
+
+ Assert(IsMVCCSnapshot(snapshot));
+
+ for (int i = 0; i < ntups; i++)
+ {
+ bool valid;
+ HeapTuple tup = &tuples[i];
+
+ valid = HeapTupleSatisfiesMVCC(tup, snapshot, buffer);
+ visible[i] = valid;
+
+ if (likely(valid))
+ {
+ vistuples_dense[nvis] = tup->t_self.ip_posid;
+ nvis++;
+ }
+ }
+
+ return nvis;
+}
+
/*
* HeapTupleSatisfiesVisibility
* True iff heap tuple satisfies a time qual.
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index c4656bfe858..6641a988ae8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -249,6 +249,7 @@ Barrier
BaseBackupCmd
BaseBackupTargetHandle
BaseBackupTargetType
+BatchMVCCState
BeginDirectModify_function
BeginForeignInsert_function
BeginForeignModify_function
--
2.48.1.76.g4e746b1a31.dirty
--kad4mm4awdo7v2ki
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v7-0011-bufmgr-Change-BufferDesc.state-to-be-a-64bit-atom.patch"
^ permalink raw reply [nested|flat] 44+ messages in thread
* [PATCH v8 05/10] heapam: Add batch mode mvcc check and use it in page mode
@ 2024-10-17 17:16 Andres Freund <andres@anarazel.de>
0 siblings, 0 replies; 44+ messages in thread
From: Andres Freund @ 2024-10-17 17:16 UTC (permalink / raw)
There are two reasons for doing so:
1) It is generally faster to perform checks in a batched fashion and making
sequential scans faster is nice.
2) We would like to stop setting hint bits while pages are being written
out. The necessary locking becomes visible for page mode scans if done for
every tuple. With batching the overhead can be amortized to only happen
once per page.
There are substantial further optimization opportunities along these
lines:
- Right now HeapTupleSatisfiesMVCCBatch() simply uses the single-tuple
HeapTupleSatisfiesMVCC(), relying on the compiler to inline it. We could
instead write an explicitly optimized version that avoids repeated xid
tests.
- Introduce batched version of the serializability test
- Introduce batched version of HeapTupleSatisfiesVacuum
Reviewed-by: Melanie Plageman <melanieplageman@gmail.com>
Discussion: https://postgr.es/m/6rgb2nvhyvnszz4ul3wfzlf5rheb2kkwrglthnna7qhe24onwr@vw27225tkyar
---
src/include/access/heapam.h | 17 +++++
src/backend/access/heap/heapam.c | 84 ++++++++++++++++-----
src/backend/access/heap/heapam_visibility.c | 42 +++++++++++
src/tools/pgindent/typedefs.list | 1 +
4 files changed, 124 insertions(+), 20 deletions(-)
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index f7e4ae3843c..cf4cd3e4dbd 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -449,6 +449,23 @@ extern bool HeapTupleHeaderIsOnlyLocked(HeapTupleHeader tuple);
extern bool HeapTupleIsSurelyDead(HeapTuple htup,
GlobalVisState *vistest);
+/*
+ * The output of HeapTupleSatisfiesMVCCBatch() is passed via this struct, as
+ * otherwise the increased number of arguments to
+ * HeapTupleSatisfiesMVCCBatch() leads to on-stack argument passing on x86-64,
+ * which causes a small regression.
+ */
+typedef struct BatchMVCCState
+{
+ HeapTupleData tuples[MaxHeapTuplesPerPage];
+ bool visible[MaxHeapTuplesPerPage];
+} BatchMVCCState;
+
+extern int HeapTupleSatisfiesMVCCBatch(Snapshot snapshot, Buffer buffer,
+ int ntups,
+ BatchMVCCState *batchmvcc,
+ OffsetNumber *vistuples_dense);
+
/*
* To avoid leaking too much knowledge about reorderbuffer implementation
* details this is implemented in reorderbuffer.c not heapam_visibility.c
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 6daf4a87dec..513a9b275a2 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -519,42 +519,86 @@ page_collect_tuples(HeapScanDesc scan, Snapshot snapshot,
BlockNumber block, int lines,
bool all_visible, bool check_serializable)
{
+ Oid relid = RelationGetRelid(scan->rs_base.rs_rd);
int ntup = 0;
- OffsetNumber lineoff;
+ int nvis = 0;
+ BatchMVCCState batchmvcc;
- for (lineoff = FirstOffsetNumber; lineoff <= lines; lineoff++)
+ /* page at a time should have been disabled otherwise */
+ Assert(IsMVCCSnapshot(snapshot));
+
+ /* first find all tuples on the page */
+ for (OffsetNumber lineoff = FirstOffsetNumber; lineoff <= lines; lineoff++)
{
ItemId lpp = PageGetItemId(page, lineoff);
- HeapTupleData loctup;
- bool valid;
+ HeapTuple tup;
- if (!ItemIdIsNormal(lpp))
+ if (unlikely(!ItemIdIsNormal(lpp)))
continue;
- loctup.t_data = (HeapTupleHeader) PageGetItem(page, lpp);
- loctup.t_len = ItemIdGetLength(lpp);
- loctup.t_tableOid = RelationGetRelid(scan->rs_base.rs_rd);
- ItemPointerSet(&(loctup.t_self), block, lineoff);
+ /*
+ * If the page is not all-visible or we need to check serializability,
+ * maintain enough state to be able to refind the tuple efficiently,
+ * without again first needing to fetch the item and then via that the
+ * tuple.
+ */
+ if (!all_visible || check_serializable)
+ {
+ tup = &batchmvcc.tuples[ntup];
+ tup->t_data = (HeapTupleHeader) PageGetItem(page, lpp);
+ tup->t_len = ItemIdGetLength(lpp);
+ tup->t_tableOid = relid;
+ ItemPointerSet(&(tup->t_self), block, lineoff);
+ }
+
+ /*
+ * If the page is all visible, these fields otherwise won't be
+ * populated in loop below.
+ */
if (all_visible)
- valid = true;
- else
- valid = HeapTupleSatisfiesVisibility(&loctup, snapshot, buffer);
-
- if (check_serializable)
- HeapCheckForSerializableConflictOut(valid, scan->rs_base.rs_rd,
- &loctup, buffer, snapshot);
-
- if (valid)
{
+ if (check_serializable)
+ {
+ batchmvcc.visible[ntup] = true;
+ }
scan->rs_vistuples[ntup] = lineoff;
- ntup++;
}
+
+ ntup++;
}
Assert(ntup <= MaxHeapTuplesPerPage);
- return ntup;
+ /*
+ * Unless the page is all visible, test visibility for all tuples one
+ * go. That is considerably more efficient than calling
+ * HeapTupleSatisfiesMVCC() one-by-one.
+ */
+ if (all_visible)
+ nvis = ntup;
+ else
+ nvis = HeapTupleSatisfiesMVCCBatch(snapshot, buffer,
+ ntup,
+ &batchmvcc,
+ scan->rs_vistuples);
+
+ /*
+ * So far we don't have batch API for testing serializabilty, so do so
+ * one-by-one.
+ */
+ if (check_serializable)
+ {
+ for (int i = 0; i < ntup; i++)
+ {
+ HeapCheckForSerializableConflictOut(batchmvcc.visible[i],
+ scan->rs_base.rs_rd,
+ &batchmvcc.tuples[i],
+ buffer, snapshot);
+ }
+ }
+
+ return nvis;
}
/*
diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c
index debf5d56b95..04284c4b2eb 100644
--- a/src/backend/access/heap/heapam_visibility.c
+++ b/src/backend/access/heap/heapam_visibility.c
@@ -1598,6 +1598,48 @@ HeapTupleSatisfiesHistoricMVCC(HeapTuple htup, Snapshot snapshot,
return true;
}
+/*
+ * Perform HeaptupleSatisfiesMVCC() on each passed in tuple. This is more
+ * efficient than doing HeapTupleSatisfiesMVCC() one-by-one.
+ *
+ * To be checked tuples are passed via BatchMVCCState->tuples. Each tuple's
+ * visibility is stored in batchmvcc->visible[]. In addition,
+ * ->vistuples_dense is set to contain the offsets of visible tuples.
+ *
+ * The reason this is more efficient than HeapTupleSatisfiesMVCC() is that it
+ * avoids a cross-translation-unit function call for each tuple. In the future
+ * it will also allow more efficient setting of hint bits.
+ *
+ * Returns the number of visible tuples.
+ */
+int
+HeapTupleSatisfiesMVCCBatch(Snapshot snapshot, Buffer buffer,
+ int ntups,
+ BatchMVCCState *batchmvcc,
+ OffsetNumber *vistuples_dense)
+{
+ int nvis = 0;
+
+ Assert(IsMVCCSnapshot(snapshot));
+
+ for (int i = 0; i < ntups; i++)
+ {
+ bool valid;
+ HeapTuple tup = &batchmvcc->tuples[i];
+
+ valid = HeapTupleSatisfiesMVCC(tup, snapshot, buffer);
+ batchmvcc->visible[i] = valid;
+
+ if (likely(valid))
+ {
+ vistuples_dense[nvis] = tup->t_self.ip_posid;
+ nvis++;
+ }
+ }
+
+ return nvis;
+}
+
/*
* HeapTupleSatisfiesVisibility
* True iff heap tuple satisfies a time qual.
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 04845d5e680..2ffdf364386 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -255,6 +255,7 @@ Barrier
BaseBackupCmd
BaseBackupTargetHandle
BaseBackupTargetType
+BatchMVCCState
BeginDirectModify_function
BeginForeignInsert_function
BeginForeignModify_function
--
2.48.1.76.g4e746b1a31.dirty
--lbbj3oq7cjvyhmbk
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
filename="v8-0006-bufmgr-Change-BufferDesc.state-to-be-a-64bit-atom.patch"
^ permalink raw reply [nested|flat] 44+ messages in thread
* problems with toast.* reloptions
@ 2025-06-19 20:20 Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2025-06-19 20:20 UTC (permalink / raw)
To: pgsql-hackers
While investigating problems caused by vacuum_rel() scribbling on its
VacuumParams argument [0], I noticed some other interesting bugs with the
toast.* reloption code. Note that the documentation for the reloptions has
the following line:
If a table parameter value is set and the equivalent toast. parameter
is not, the TOAST table will use the table's parameter value.
The problems I found are as follows:
* vacuum_rel() does not look up the main relation's reloptions when
processing a TOAST table, which is a problem for manual VACUUMs. The
aforementioned bug [0] causes you to sometimes get the expected behavior
(because the parameters are overridden before recursing to TOAST), but
fixing that bug makes that accidental behavior go away.
* For autovacuum, the main table's reloptions are only used if the TOAST
table has no reloptions set. So, if your relation has
autovacuum_vacuum_threshold and toast.vacuum_index_cleanup set, the main
relation's autovacuum_vacuum_threshold setting won't be used for the
TOAST table.
* Even when the preceding point doesn't apply, autovacuum doesn't use the
main relation's setting for some parameters (e.g., vacuum_truncate).
Instead, it leaves them uninitialized and expects vacuum_rel() to fill
them in. This is a problem because, as mentioned earlier, vacuum_rel()
doesn't consult the main relation's reloptions either.
I think we need to do something like the following to fix this:
* Teach autovacuum to combine the TOAST reloptions with the main relation's
when processing TOAST tables (with the toast.* ones winning if both are
set).
* Teach autovacuum to resolve reloptions for parameters like
vacuum_truncate instead of relying on vacuum_rel() to fill it in.
* Have vacuum_rel() send the main relation's reloptions when recursing to
the TOAST table so that we can combine them there, too.
This doesn't fix VACUUM against a TOAST table directly (e.g., VACUUM
pg_toast.pg_toast_5432), but that might not be too important because
(PROCESS_TOAST TRUE) is the main supported way to vacuum a TOAST table. If
we did want to fix that, though, I think we'd have to teach vacuum_rel() or
the relcache to look up the reloptions for the main relation.
Thoughts?
[0] https://postgr.es/m/flat/CAGRkXqTo%2BaK%3DGTy5pSc-9cy8H2F2TJvcrZ-zXEiNJj93np1UUw%40mail.gmail.com
--
nathan
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2025-06-20 02:05 Michael Paquier <michael@paquier.xyz>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 2 replies; 44+ messages in thread
From: Michael Paquier @ 2025-06-20 02:05 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: pgsql-hackers
On Thu, Jun 19, 2025 at 03:20:27PM -0500, Nathan Bossart wrote:
> While investigating problems caused by vacuum_rel() scribbling on its
> VacuumParams argument [0], I noticed some other interesting bugs with the
> toast.* reloption code. Note that the documentation for the reloptions has
> the following line:
>
> If a table parameter value is set and the equivalent toast. parameter
> is not, the TOAST table will use the table's parameter value.
>
> The problems I found are as follows:
>
> * vacuum_rel() does not look up the main relation's reloptions when
> processing a TOAST table, which is a problem for manual VACUUMs. The
> aforementioned bug [0] causes you to sometimes get the expected behavior
> (because the parameters are overridden before recursing to TOAST), but
> fixing that bug makes that accidental behavior go away.
Are you referring to the case of a VACUUM pg_toast.pg_toast_NNN? I'm
not sure that we really need to care about looking up at the parent
relation in this case. It sounds to me that the intention of this
paragraph is for the case where the TOAST table is treated as a
secondary table, not when the TOAST table is directly vacuumed.
Perhaps the wording of the docs should be improved that this does not
happen if vacuuming directly a TOAST table.
> * For autovacuum, the main table's reloptions are only used if the TOAST
> table has no reloptions set. So, if your relation has
> autovacuum_vacuum_threshold and toast.vacuum_index_cleanup set, the main
> relation's autovacuum_vacuum_threshold setting won't be used for the
> TOAST table.
[.. /me double-checks the code .. ]
So we combine the options in do_autovacuum() with the two-pass logic
to gather the relation OIDs, then apply relation_needs_vacanalyze().
That looks like an old issue, that cannot be solved as long as we rely
on the relopts to be an all-or-nothing thing when assigning the
individual values for the TOAST relation. Oops.
> * Even when the preceding point doesn't apply, autovacuum doesn't use the
> main relation's setting for some parameters (e.g., vacuum_truncate).
> Instead, it leaves them uninitialized and expects vacuum_rel() to fill
> them in. This is a problem because, as mentioned earlier, vacuum_rel()
> doesn't consult the main relation's reloptions either.
Relying on vacuum_rel() sounds like a bad idea if we can avoid that,
still perhaps that's OK as long as we don't use a pointer to the
VacuumParams and keep the updates to the value of vacuum_rel() local
inside the routine.
> I think we need to do something like the following to fix this:
>
> * Teach autovacuum to combine the TOAST reloptions with the main relation's
> when processing TOAST tables (with the toast.* ones winning if both are
> set).
>
> * Teach autovacuum to resolve reloptions for parameters like
> vacuum_truncate instead of relying on vacuum_rel() to fill it in.
These two points make sense here, yes.
> * Have vacuum_rel() send the main relation's reloptions when recursing to
> the TOAST table so that we can combine them there, too.
For the case of a manual VACUUM on the main table, where the TOAST
table is treated as a secondary citizen, that makes sense as well,
yes.
> This doesn't fix VACUUM against a TOAST table directly (e.g., VACUUM
> pg_toast.pg_toast_5432), but that might not be too important because
> (PROCESS_TOAST TRUE) is the main supported way to vacuum a TOAST table. If
> we did want to fix that, though, I think we'd have to teach vacuum_rel() or
> the relcache to look up the reloptions for the main relation.
This one does not sound that important to me for the case of manual
VACUUM case directly done on a TOAST table. If you do that, the code
kind of assumes that a TOAST table is actually a "main" relation that
has no TOAST table. That should keep the code simpler, because we
would not need to look at what the parent relation holds when deciding
which options to use in ExecVacuum(). The autovacuum case is
different, as TOAST relations are worked on as their own items rather
than being secondary relations of the main tables.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../aFTB8bs_u5WyBReK@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2025-06-20 19:12 Nathan Bossart <nathandbossart@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
1 sibling, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2025-06-20 19:12 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: pgsql-hackers
On Fri, Jun 20, 2025 at 11:05:37AM +0900, Michael Paquier wrote:
> On Thu, Jun 19, 2025 at 03:20:27PM -0500, Nathan Bossart wrote:
>> * vacuum_rel() does not look up the main relation's reloptions when
>> processing a TOAST table, which is a problem for manual VACUUMs. The
>> aforementioned bug [0] causes you to sometimes get the expected behavior
>> (because the parameters are overridden before recursing to TOAST), but
>> fixing that bug makes that accidental behavior go away.
>
> Are you referring to the case of a VACUUM pg_toast.pg_toast_NNN? I'm
> not sure that we really need to care about looking up at the parent
> relation in this case. It sounds to me that the intention of this
> paragraph is for the case where the TOAST table is treated as a
> secondary table, not when the TOAST table is directly vacuumed.
> Perhaps the wording of the docs should be improved that this does not
> happen if vacuuming directly a TOAST table.
Yeah, I was mainly thinking of a VACUUM command that recurses to the TOAST
table. Of course, it'd be nice to fix VACUUM pg_toast.pg_toast_NNN, too,
but I'm personally not too worried about that use-case.
>> This doesn't fix VACUUM against a TOAST table directly (e.g., VACUUM
>> pg_toast.pg_toast_5432), but that might not be too important because
>> (PROCESS_TOAST TRUE) is the main supported way to vacuum a TOAST table. If
>> we did want to fix that, though, I think we'd have to teach vacuum_rel() or
>> the relcache to look up the reloptions for the main relation.
>
> This one does not sound that important to me for the case of manual
> VACUUM case directly done on a TOAST table. If you do that, the code
> kind of assumes that a TOAST table is actually a "main" relation that
> has no TOAST table. That should keep the code simpler, because we
> would not need to look at what the parent relation holds when deciding
> which options to use in ExecVacuum(). The autovacuum case is
> different, as TOAST relations are worked on as their own items rather
> than being secondary relations of the main tables.
+1
--
nathan
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2025-06-22 03:45 shihao zhong <zhong950419@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: shihao zhong @ 2025-06-22 03:45 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers
> I think we need to do something like the following to fix this:
>
> * Teach autovacuum to combine the TOAST reloptions with the main relation's
> when processing TOAST tables (with the toast.* ones winning if both are
> set).
>
> * Teach autovacuum to resolve reloptions for parameters like
> vacuum_truncate instead of relying on vacuum_rel() to fill it in.
>> These two points make sense here, yes.
I investigated that this afternoon and identified two potential
implementation approaches:
1) Create functions like resolve_toast_vac_opts() and
resolve_toast_rel_opts(). These would then be used in
table_recheck_autovac(), NeedsAutoVacTableForXidWraparound(), and
do_autovacuum() after the toast table check.
2) When updating a table's relopt, also update the relopt of its
associated TOAST table if it's not already set. Similarly, when
creating a new TOAST table, it would inherit the parent's relopt.
Option 2 seems more reasonable to me, as it avoids requiring customers
to manually resolve these options, when they have different settings
for the parent and TOAST tables."
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2025-06-23 15:59 Nathan Bossart <nathandbossart@gmail.com>
parent: shihao zhong <zhong950419@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2025-06-23 15:59 UTC (permalink / raw)
To: shihao zhong <zhong950419@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers
On Sat, Jun 21, 2025 at 11:45:25PM -0400, shihao zhong wrote:
> 2) When updating a table's relopt, also update the relopt of its
> associated TOAST table if it's not already set. Similarly, when
> creating a new TOAST table, it would inherit the parent's relopt.
>
> Option 2 seems more reasonable to me, as it avoids requiring customers
> to manually resolve these options, when they have different settings
> for the parent and TOAST tables."
I like this one, but since it won't fix existing clusters, it might only be
workable for v19.
--
nathan
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2025-06-23 20:59 Nathan Bossart <nathandbossart@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
1 sibling, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2025-06-23 20:59 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: pgsql-hackers
On Fri, Jun 20, 2025 at 11:05:37AM +0900, Michael Paquier wrote:
> On Thu, Jun 19, 2025 at 03:20:27PM -0500, Nathan Bossart wrote:
>> I think we need to do something like the following to fix this:
>>
>> * Teach autovacuum to combine the TOAST reloptions with the main relation's
>> when processing TOAST tables (with the toast.* ones winning if both are
>> set).
>>
>> * Teach autovacuum to resolve reloptions for parameters like
>> vacuum_truncate instead of relying on vacuum_rel() to fill it in.
>
> These two points make sense here, yes.
>
>> * Have vacuum_rel() send the main relation's reloptions when recursing to
>> the TOAST table so that we can combine them there, too.
>
> For the case of a manual VACUUM on the main table, where the TOAST
> table is treated as a secondary citizen, that makes sense as well,
> yes.
Here is a very rough proof-of-concept patch set for this. AFAICT there are
a few options we cannot fix on the back-branches because there is no way to
tell whether it is set or has just picked up the default. On v18 and
newer, we could use isset_offset, but that doesn't exist on older versions.
(I haven't looked closely, but I'm assuming that back-patching isset_offset
isn't an option.)
I would like to explore the "option 2" from upthread [0] for v19. I think
that is a better long-term solution, and it may allow us to remove the
table_toast_map in autovacuum.
[0] https://postgr.es/m/aFl598epAdUrrv0y%40nathan
--
nathan
From a1930aa769a96c38621d7caa727c404d553b7ecc Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 23 Jun 2025 14:07:30 -0500
Subject: [PATCH v1 1/4] autovac: save all relopts instead of just avopts
---
src/backend/postmaster/autovacuum.c | 119 ++++++++++------------------
1 file changed, 43 insertions(+), 76 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 451fb90a610..f86c9fed853 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -192,8 +192,8 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions, or NULL if
+ * none */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -333,11 +333,11 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void recheck_relation_needs_vacanalyze(Oid relid, AutoVacOpts *avopts,
+static void recheck_relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
bool *dovacuum, bool *doanalyze, bool *wraparound);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
PgStat_StatTabEntry *tabentry,
int effective_multixact_freeze_max_age,
@@ -345,8 +345,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -1995,7 +1993,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
PgStat_StatTabEntry *tabentry;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2033,7 +2031,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared,
relid);
@@ -2069,7 +2067,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2095,7 +2093,7 @@ do_autovacuum(void)
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
PgStat_StatTabEntry *tabentry;
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2113,7 +2111,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2701,39 +2699,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc(sizeof(AutoVacOpts));
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2753,8 +2718,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
/* fetch the relation's relcache entry */
classTup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
@@ -2766,9 +2731,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2777,10 +2742,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- recheck_relation_needs_vacanalyze(relid, avopts, classForm,
+ recheck_relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
&dovacuum, &doanalyze, &wraparound);
@@ -2792,6 +2757,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_min_age;
int multixact_freeze_table_age;
int log_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2879,8 +2845,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2895,7 +2861,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
recheck_relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *avopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
bool *dovacuum,
@@ -2908,7 +2874,7 @@ recheck_relation_needs_vacanalyze(Oid relid,
tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared,
relid);
- relation_needs_vacanalyze(relid, avopts, classForm, tabentry,
+ relation_needs_vacanalyze(relid, relopts, classForm, tabentry,
effective_multixact_freeze_max_age,
dovacuum, doanalyze, wraparound);
@@ -2928,7 +2894,7 @@ recheck_relation_needs_vacanalyze(Oid relid,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none; tabentry is the pgstats entry, which can be
* NULL.
@@ -2962,7 +2928,7 @@ recheck_relation_needs_vacanalyze(Oid relid,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
PgStat_StatTabEntry *tabentry,
int effective_multixact_freeze_max_age,
@@ -2973,6 +2939,7 @@ relation_needs_vacanalyze(Oid relid,
{
bool force_vacuum;
bool av_enabled;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3010,45 +2977,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
/* Force vacuum if table is at risk of wraparound */
xidForceLimit = recentXid - freeze_max_age;
--
2.39.5 (Apple Git-154)
From a3bbb1fdcf6a66719af84b763861e187cc5588bd Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 23 Jun 2025 14:42:39 -0500
Subject: [PATCH v1 2/4] autovac: resolve relopts before vacuuming
---
src/backend/commands/vacuum.c | 7 ++--
src/backend/postmaster/autovacuum.c | 52 +++++++++++++++++++++--------
2 files changed, 44 insertions(+), 15 deletions(-)
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 33a33bf6b1c..0015b9ef4b0 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -421,7 +421,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/*
* Later, in vacuum_rel(), we check if a reloption override was specified.
*/
- params.max_eager_freeze_failure_rate = vacuum_max_eager_freeze_failure_rate;
+ params.max_eager_freeze_failure_rate = -1.0;
/*
* Create special memory context for cross-transaction storage.
@@ -2195,10 +2195,13 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
+ if (params->max_eager_freeze_failure_rate < 0 &&
+ rel->rd_options != NULL &&
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
params->max_eager_freeze_failure_rate =
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ else
+ params->max_eager_freeze_failure_rate = vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index f86c9fed853..3bedca971ff 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2758,6 +2758,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_min_duration;
AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
+ VacOptValue index_cleanup;
+ VacOptValue truncate;
+ double max_eager_freeze_failure_rate;
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2790,6 +2793,39 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
? avopts->multixact_freeze_table_age
: default_multixact_freeze_table_age;
+ if (relopts)
+ {
+ switch (relopts->vacuum_index_cleanup)
+ {
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ }
+ }
+ else
+ index_cleanup = VACOPTVALUE_UNSPECIFIED;
+
+ if (relopts && relopts->vacuum_truncate_set)
+ {
+ if (relopts->vacuum_truncate)
+ truncate = VACOPTVALUE_ENABLED;
+ else
+ truncate = VACOPTVALUE_DISABLED;
+ }
+ else
+ truncate = VACOPTVALUE_UNSPECIFIED;
+
+ if (relopts && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
+ else
+ max_eager_freeze_failure_rate = -1.0;
+
tab = palloc(sizeof(autovac_table));
tab->at_relid = relid;
tab->at_sharedrel = classForm->relisshared;
@@ -2806,13 +2842,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
(doanalyze ? VACOPT_ANALYZE : 0) |
(!wraparound ? VACOPT_SKIP_LOCKED : 0);
- /*
- * index_cleanup and truncate are unspecified at first in autovacuum.
- * They will be filled in with usable values using their reloptions
- * (or reloption defaults) later.
- */
- tab->at_params.index_cleanup = VACOPTVALUE_UNSPECIFIED;
- tab->at_params.truncate = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.index_cleanup = index_cleanup;
+ tab->at_params.truncate = truncate;
/* As of now, we don't support parallel vacuum for autovacuum */
tab->at_params.nworkers = -1;
tab->at_params.freeze_min_age = freeze_min_age;
@@ -2822,12 +2853,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.is_wraparound = wraparound;
tab->at_params.log_min_duration = log_min_duration;
tab->at_params.toast_parent = InvalidOid;
-
- /*
- * Later, in vacuum_rel(), we check reloptions for any
- * vacuum_max_eager_freeze_failure_rate override.
- */
- tab->at_params.max_eager_freeze_failure_rate = vacuum_max_eager_freeze_failure_rate;
+ tab->at_params.max_eager_freeze_failure_rate = max_eager_freeze_failure_rate;
tab->at_storage_param_vac_cost_limit = avopts ?
avopts->vacuum_cost_limit : 0;
tab->at_storage_param_vac_cost_delay = avopts ?
--
2.39.5 (Apple Git-154)
From 12b6404c906837c9244110095ca980ad3545bc0b Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 23 Jun 2025 15:16:38 -0500
Subject: [PATCH v1 3/4] autovac: combine reloptions correctly
---
src/backend/postmaster/autovacuum.c | 85 +++++++++++++++++++++++++++++
1 file changed, 85 insertions(+)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 3bedca971ff..0d1f4e38b58 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1873,6 +1873,73 @@ get_database_list(void)
return dblist;
}
+static void
+combine_relopts(StdRdOptions *toast_opts, const StdRdOptions *main_opts)
+{
+ AutoVacOpts *toast_avopts = &toast_opts->autovacuum;
+ const AutoVacOpts *main_avopts = &main_opts->autovacuum;
+
+ /* XXX: need isset_offset to combine "enabled" */
+
+ if (toast_avopts->vacuum_threshold == -1)
+ toast_avopts->vacuum_threshold = main_avopts->vacuum_threshold;
+
+ if (toast_avopts->vacuum_max_threshold == -2)
+ toast_avopts->vacuum_max_threshold = main_avopts->vacuum_max_threshold;
+
+ if (toast_avopts->vacuum_ins_threshold == -2)
+ toast_avopts->vacuum_ins_threshold = main_avopts->vacuum_ins_threshold;
+
+ if (toast_avopts->analyze_threshold == -1)
+ toast_avopts->analyze_threshold = main_avopts->analyze_threshold;
+
+ if (toast_avopts->vacuum_cost_limit == -1)
+ toast_avopts->vacuum_cost_limit = main_avopts->vacuum_cost_limit;
+
+ if (toast_avopts->freeze_min_age == -1)
+ toast_avopts->freeze_min_age = main_avopts->freeze_min_age;
+
+ if (toast_avopts->freeze_max_age == -1)
+ toast_avopts->freeze_max_age = main_avopts->freeze_max_age;
+
+ if (toast_avopts->freeze_table_age == -1)
+ toast_avopts->freeze_table_age = main_avopts->freeze_table_age;
+
+ if (toast_avopts->multixact_freeze_min_age == -1)
+ toast_avopts->multixact_freeze_min_age = main_avopts->multixact_freeze_min_age;
+
+ if (toast_avopts->multixact_freeze_max_age == -1)
+ toast_avopts->multixact_freeze_max_age = main_avopts->multixact_freeze_max_age;
+
+ if (toast_avopts->multixact_freeze_table_age == -1)
+ toast_avopts->multixact_freeze_table_age = main_avopts->multixact_freeze_table_age;
+
+ /* XXX: need isset_offset for log_min_duration */
+
+ if (toast_avopts->vacuum_cost_delay == -1)
+ toast_avopts->vacuum_cost_delay = main_avopts->vacuum_cost_delay;
+
+ if (toast_avopts->vacuum_scale_factor == -1)
+ toast_avopts->vacuum_scale_factor = main_avopts->vacuum_scale_factor;
+
+ if (toast_avopts->vacuum_ins_scale_factor == -1)
+ toast_avopts->vacuum_ins_scale_factor = main_avopts->vacuum_ins_scale_factor;
+
+ if (toast_avopts->analyze_scale_factor == -1)
+ toast_avopts->analyze_scale_factor = main_avopts->analyze_scale_factor;
+
+ /* XXX: need isset_offset for vacuum_index_cleanup */
+
+ if (!toast_opts->vacuum_truncate_set && main_opts->vacuum_truncate_set)
+ {
+ toast_opts->vacuum_truncate = main_opts->vacuum_truncate;
+ toast_opts->vacuum_truncate_set = true;
+ }
+
+ if (toast_opts->vacuum_max_eager_freeze_failure_rate == -1)
+ toast_opts->vacuum_max_eager_freeze_failure_rate = main_opts->vacuum_max_eager_freeze_failure_rate;
+}
+
/*
* Process a database table-by-table
*
@@ -2113,7 +2180,16 @@ do_autovacuum(void)
*/
relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
+ {
+ av_relation *hentry;
+ bool found;
+
free_relopts = true;
+
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ combine_relopts(relopts, &hentry->ar_reloptions);
+ }
else
{
av_relation *hentry;
@@ -2733,7 +2809,16 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
+ {
+ av_relation *hentry;
+ bool found;
+
free_relopts = true;
+
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ combine_relopts(relopts, &hentry->ar_reloptions);
+ }
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
--
2.39.5 (Apple Git-154)
From 724f90f4d19a2c92026adffcde8bf47967378f1e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 23 Jun 2025 15:40:28 -0500
Subject: [PATCH v1 4/4] combine relopts correctly for VACUUM commands
---
src/backend/commands/vacuum.c | 30 +++++++++++++++++++----------
src/backend/postmaster/autovacuum.c | 2 +-
src/include/commands/vacuum.h | 2 ++
3 files changed, 23 insertions(+), 11 deletions(-)
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 0015b9ef4b0..a74c3a9e2a2 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -124,7 +124,7 @@ static void vac_truncate_clog(TransactionId frozenXID,
TransactionId lastSaneFrozenXid,
MultiXactId lastSaneMinMulti);
static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
- BufferAccessStrategy bstrategy);
+ BufferAccessStrategy bstrategy, StdRdOptions *main_opts);
static double compute_parallel_delay(void);
static VacOptValue get_vacoptval_from_boolean(DefElem *def);
static bool vac_tid_reaped(ItemPointer itemptr, void *state);
@@ -634,7 +634,7 @@ vacuum(List *relations, VacuumParams *params, BufferAccessStrategy bstrategy,
if (params->options & VACOPT_VACUUM)
{
- if (!vacuum_rel(vrel->oid, vrel->relation, params, bstrategy))
+ if (!vacuum_rel(vrel->oid, vrel->relation, params, bstrategy, NULL))
continue;
}
@@ -1998,7 +1998,7 @@ vac_truncate_clog(TransactionId frozenXID,
*/
static bool
vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
- BufferAccessStrategy bstrategy)
+ BufferAccessStrategy bstrategy, StdRdOptions *main_opts)
{
LOCKMODE lmode;
Relation rel;
@@ -2008,6 +2008,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
Oid save_userid;
int save_sec_context;
int save_nestlevel;
+ StdRdOptions saved_opts;
+ StdRdOptions combined_opts;
Assert(params != NULL);
@@ -2165,6 +2167,15 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ if (rel->rd_options)
+ {
+ memcpy(&saved_opts, rel->rd_options, sizeof(StdRdOptions));
+ memcpy(&combined_opts, rel->rd_options, sizeof(StdRdOptions));
+
+ if (main_opts)
+ combine_relopts(&combined_opts, main_opts);
+ }
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2176,8 +2187,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
if (rel->rd_options == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = combined_opts.vacuum_index_cleanup;
if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
params->index_cleanup = VACOPTVALUE_AUTO;
@@ -2197,9 +2207,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
*/
if (params->max_eager_freeze_failure_rate < 0 &&
rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
+ combined_opts.vacuum_max_eager_freeze_failure_rate >= 0)
params->max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ combined_opts.vacuum_max_eager_freeze_failure_rate;
else
params->max_eager_freeze_failure_rate = vacuum_max_eager_freeze_failure_rate;
@@ -2211,9 +2221,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
{
StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
- if (opts && opts->vacuum_truncate_set)
+ if (opts && combined_opts.vacuum_truncate_set)
{
- if (opts->vacuum_truncate)
+ if (combined_opts.vacuum_truncate)
params->truncate = VACOPTVALUE_ENABLED;
else
params->truncate = VACOPTVALUE_DISABLED;
@@ -2314,7 +2324,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
toast_vacuum_params.toast_parent = relid;
- vacuum_rel(toast_relid, NULL, &toast_vacuum_params, bstrategy);
+ vacuum_rel(toast_relid, NULL, &toast_vacuum_params, bstrategy, &saved_opts);
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 0d1f4e38b58..ca399b6a92e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1873,7 +1873,7 @@ get_database_list(void)
return dblist;
}
-static void
+void
combine_relopts(StdRdOptions *toast_opts, const StdRdOptions *main_opts)
{
AutoVacOpts *toast_avopts = &toast_opts->autovacuum;
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index bc37a80dc74..928f864581b 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -24,6 +24,7 @@
#include "parser/parse_node.h"
#include "storage/buf.h"
#include "storage/lock.h"
+#include "utils/rel.h"
#include "utils/relcache.h"
/*
@@ -377,6 +378,7 @@ extern IndexBulkDeleteResult *vac_cleanup_one_index(IndexVacuumInfo *ivinfo,
/* In postmaster/autovacuum.c */
extern void AutoVacuumUpdateCostLimit(void);
extern void VacuumUpdateCosts(void);
+extern void combine_relopts(StdRdOptions *toast_opts, const StdRdOptions *main_opts);
/* in commands/vacuumparallel.c */
extern ParallelVacuumState *parallel_vacuum_init(Relation rel, Relation *indrels,
--
2.39.5 (Apple Git-154)
Attachments:
[text/plain] v1-0001-autovac-save-all-relopts-instead-of-just-avopts.patch (11.1K, ../../aFnATDLLdP0m-fGb@nathan/2-v1-0001-autovac-save-all-relopts-instead-of-just-avopts.patch)
download | inline diff:
From a1930aa769a96c38621d7caa727c404d553b7ecc Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 23 Jun 2025 14:07:30 -0500
Subject: [PATCH v1 1/4] autovac: save all relopts instead of just avopts
---
src/backend/postmaster/autovacuum.c | 119 ++++++++++------------------
1 file changed, 43 insertions(+), 76 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 451fb90a610..f86c9fed853 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -192,8 +192,8 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions, or NULL if
+ * none */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -333,11 +333,11 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void recheck_relation_needs_vacanalyze(Oid relid, AutoVacOpts *avopts,
+static void recheck_relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
bool *dovacuum, bool *doanalyze, bool *wraparound);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
PgStat_StatTabEntry *tabentry,
int effective_multixact_freeze_max_age,
@@ -345,8 +345,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -1995,7 +1993,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
PgStat_StatTabEntry *tabentry;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2033,7 +2031,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared,
relid);
@@ -2069,7 +2067,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2095,7 +2093,7 @@ do_autovacuum(void)
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
PgStat_StatTabEntry *tabentry;
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2113,7 +2111,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2701,39 +2699,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc(sizeof(AutoVacOpts));
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2753,8 +2718,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
/* fetch the relation's relcache entry */
classTup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
@@ -2766,9 +2731,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2777,10 +2742,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- recheck_relation_needs_vacanalyze(relid, avopts, classForm,
+ recheck_relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
&dovacuum, &doanalyze, &wraparound);
@@ -2792,6 +2757,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_min_age;
int multixact_freeze_table_age;
int log_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2879,8 +2845,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2895,7 +2861,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
recheck_relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *avopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
bool *dovacuum,
@@ -2908,7 +2874,7 @@ recheck_relation_needs_vacanalyze(Oid relid,
tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared,
relid);
- relation_needs_vacanalyze(relid, avopts, classForm, tabentry,
+ relation_needs_vacanalyze(relid, relopts, classForm, tabentry,
effective_multixact_freeze_max_age,
dovacuum, doanalyze, wraparound);
@@ -2928,7 +2894,7 @@ recheck_relation_needs_vacanalyze(Oid relid,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none; tabentry is the pgstats entry, which can be
* NULL.
@@ -2962,7 +2928,7 @@ recheck_relation_needs_vacanalyze(Oid relid,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
PgStat_StatTabEntry *tabentry,
int effective_multixact_freeze_max_age,
@@ -2973,6 +2939,7 @@ relation_needs_vacanalyze(Oid relid,
{
bool force_vacuum;
bool av_enabled;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3010,45 +2977,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
/* Force vacuum if table is at risk of wraparound */
xidForceLimit = recentXid - freeze_max_age;
--
2.39.5 (Apple Git-154)
[text/plain] v1-0002-autovac-resolve-relopts-before-vacuuming.patch (4.7K, ../../aFnATDLLdP0m-fGb@nathan/3-v1-0002-autovac-resolve-relopts-before-vacuuming.patch)
download | inline diff:
From a3bbb1fdcf6a66719af84b763861e187cc5588bd Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 23 Jun 2025 14:42:39 -0500
Subject: [PATCH v1 2/4] autovac: resolve relopts before vacuuming
---
src/backend/commands/vacuum.c | 7 ++--
src/backend/postmaster/autovacuum.c | 52 +++++++++++++++++++++--------
2 files changed, 44 insertions(+), 15 deletions(-)
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 33a33bf6b1c..0015b9ef4b0 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -421,7 +421,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/*
* Later, in vacuum_rel(), we check if a reloption override was specified.
*/
- params.max_eager_freeze_failure_rate = vacuum_max_eager_freeze_failure_rate;
+ params.max_eager_freeze_failure_rate = -1.0;
/*
* Create special memory context for cross-transaction storage.
@@ -2195,10 +2195,13 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
+ if (params->max_eager_freeze_failure_rate < 0 &&
+ rel->rd_options != NULL &&
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
params->max_eager_freeze_failure_rate =
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ else
+ params->max_eager_freeze_failure_rate = vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index f86c9fed853..3bedca971ff 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2758,6 +2758,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_min_duration;
AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
+ VacOptValue index_cleanup;
+ VacOptValue truncate;
+ double max_eager_freeze_failure_rate;
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2790,6 +2793,39 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
? avopts->multixact_freeze_table_age
: default_multixact_freeze_table_age;
+ if (relopts)
+ {
+ switch (relopts->vacuum_index_cleanup)
+ {
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ }
+ }
+ else
+ index_cleanup = VACOPTVALUE_UNSPECIFIED;
+
+ if (relopts && relopts->vacuum_truncate_set)
+ {
+ if (relopts->vacuum_truncate)
+ truncate = VACOPTVALUE_ENABLED;
+ else
+ truncate = VACOPTVALUE_DISABLED;
+ }
+ else
+ truncate = VACOPTVALUE_UNSPECIFIED;
+
+ if (relopts && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
+ else
+ max_eager_freeze_failure_rate = -1.0;
+
tab = palloc(sizeof(autovac_table));
tab->at_relid = relid;
tab->at_sharedrel = classForm->relisshared;
@@ -2806,13 +2842,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
(doanalyze ? VACOPT_ANALYZE : 0) |
(!wraparound ? VACOPT_SKIP_LOCKED : 0);
- /*
- * index_cleanup and truncate are unspecified at first in autovacuum.
- * They will be filled in with usable values using their reloptions
- * (or reloption defaults) later.
- */
- tab->at_params.index_cleanup = VACOPTVALUE_UNSPECIFIED;
- tab->at_params.truncate = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.index_cleanup = index_cleanup;
+ tab->at_params.truncate = truncate;
/* As of now, we don't support parallel vacuum for autovacuum */
tab->at_params.nworkers = -1;
tab->at_params.freeze_min_age = freeze_min_age;
@@ -2822,12 +2853,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.is_wraparound = wraparound;
tab->at_params.log_min_duration = log_min_duration;
tab->at_params.toast_parent = InvalidOid;
-
- /*
- * Later, in vacuum_rel(), we check reloptions for any
- * vacuum_max_eager_freeze_failure_rate override.
- */
- tab->at_params.max_eager_freeze_failure_rate = vacuum_max_eager_freeze_failure_rate;
+ tab->at_params.max_eager_freeze_failure_rate = max_eager_freeze_failure_rate;
tab->at_storage_param_vac_cost_limit = avopts ?
avopts->vacuum_cost_limit : 0;
tab->at_storage_param_vac_cost_delay = avopts ?
--
2.39.5 (Apple Git-154)
[text/plain] v1-0003-autovac-combine-reloptions-correctly.patch (4.1K, ../../aFnATDLLdP0m-fGb@nathan/4-v1-0003-autovac-combine-reloptions-correctly.patch)
download | inline diff:
From 12b6404c906837c9244110095ca980ad3545bc0b Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 23 Jun 2025 15:16:38 -0500
Subject: [PATCH v1 3/4] autovac: combine reloptions correctly
---
src/backend/postmaster/autovacuum.c | 85 +++++++++++++++++++++++++++++
1 file changed, 85 insertions(+)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 3bedca971ff..0d1f4e38b58 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1873,6 +1873,73 @@ get_database_list(void)
return dblist;
}
+static void
+combine_relopts(StdRdOptions *toast_opts, const StdRdOptions *main_opts)
+{
+ AutoVacOpts *toast_avopts = &toast_opts->autovacuum;
+ const AutoVacOpts *main_avopts = &main_opts->autovacuum;
+
+ /* XXX: need isset_offset to combine "enabled" */
+
+ if (toast_avopts->vacuum_threshold == -1)
+ toast_avopts->vacuum_threshold = main_avopts->vacuum_threshold;
+
+ if (toast_avopts->vacuum_max_threshold == -2)
+ toast_avopts->vacuum_max_threshold = main_avopts->vacuum_max_threshold;
+
+ if (toast_avopts->vacuum_ins_threshold == -2)
+ toast_avopts->vacuum_ins_threshold = main_avopts->vacuum_ins_threshold;
+
+ if (toast_avopts->analyze_threshold == -1)
+ toast_avopts->analyze_threshold = main_avopts->analyze_threshold;
+
+ if (toast_avopts->vacuum_cost_limit == -1)
+ toast_avopts->vacuum_cost_limit = main_avopts->vacuum_cost_limit;
+
+ if (toast_avopts->freeze_min_age == -1)
+ toast_avopts->freeze_min_age = main_avopts->freeze_min_age;
+
+ if (toast_avopts->freeze_max_age == -1)
+ toast_avopts->freeze_max_age = main_avopts->freeze_max_age;
+
+ if (toast_avopts->freeze_table_age == -1)
+ toast_avopts->freeze_table_age = main_avopts->freeze_table_age;
+
+ if (toast_avopts->multixact_freeze_min_age == -1)
+ toast_avopts->multixact_freeze_min_age = main_avopts->multixact_freeze_min_age;
+
+ if (toast_avopts->multixact_freeze_max_age == -1)
+ toast_avopts->multixact_freeze_max_age = main_avopts->multixact_freeze_max_age;
+
+ if (toast_avopts->multixact_freeze_table_age == -1)
+ toast_avopts->multixact_freeze_table_age = main_avopts->multixact_freeze_table_age;
+
+ /* XXX: need isset_offset for log_min_duration */
+
+ if (toast_avopts->vacuum_cost_delay == -1)
+ toast_avopts->vacuum_cost_delay = main_avopts->vacuum_cost_delay;
+
+ if (toast_avopts->vacuum_scale_factor == -1)
+ toast_avopts->vacuum_scale_factor = main_avopts->vacuum_scale_factor;
+
+ if (toast_avopts->vacuum_ins_scale_factor == -1)
+ toast_avopts->vacuum_ins_scale_factor = main_avopts->vacuum_ins_scale_factor;
+
+ if (toast_avopts->analyze_scale_factor == -1)
+ toast_avopts->analyze_scale_factor = main_avopts->analyze_scale_factor;
+
+ /* XXX: need isset_offset for vacuum_index_cleanup */
+
+ if (!toast_opts->vacuum_truncate_set && main_opts->vacuum_truncate_set)
+ {
+ toast_opts->vacuum_truncate = main_opts->vacuum_truncate;
+ toast_opts->vacuum_truncate_set = true;
+ }
+
+ if (toast_opts->vacuum_max_eager_freeze_failure_rate == -1)
+ toast_opts->vacuum_max_eager_freeze_failure_rate = main_opts->vacuum_max_eager_freeze_failure_rate;
+}
+
/*
* Process a database table-by-table
*
@@ -2113,7 +2180,16 @@ do_autovacuum(void)
*/
relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
+ {
+ av_relation *hentry;
+ bool found;
+
free_relopts = true;
+
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ combine_relopts(relopts, &hentry->ar_reloptions);
+ }
else
{
av_relation *hentry;
@@ -2733,7 +2809,16 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
+ {
+ av_relation *hentry;
+ bool found;
+
free_relopts = true;
+
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ combine_relopts(relopts, &hentry->ar_reloptions);
+ }
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
--
2.39.5 (Apple Git-154)
[text/plain] v1-0004-combine-relopts-correctly-for-VACUUM-commands.patch (5.5K, ../../aFnATDLLdP0m-fGb@nathan/5-v1-0004-combine-relopts-correctly-for-VACUUM-commands.patch)
download | inline diff:
From 724f90f4d19a2c92026adffcde8bf47967378f1e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 23 Jun 2025 15:40:28 -0500
Subject: [PATCH v1 4/4] combine relopts correctly for VACUUM commands
---
src/backend/commands/vacuum.c | 30 +++++++++++++++++++----------
src/backend/postmaster/autovacuum.c | 2 +-
src/include/commands/vacuum.h | 2 ++
3 files changed, 23 insertions(+), 11 deletions(-)
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 0015b9ef4b0..a74c3a9e2a2 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -124,7 +124,7 @@ static void vac_truncate_clog(TransactionId frozenXID,
TransactionId lastSaneFrozenXid,
MultiXactId lastSaneMinMulti);
static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
- BufferAccessStrategy bstrategy);
+ BufferAccessStrategy bstrategy, StdRdOptions *main_opts);
static double compute_parallel_delay(void);
static VacOptValue get_vacoptval_from_boolean(DefElem *def);
static bool vac_tid_reaped(ItemPointer itemptr, void *state);
@@ -634,7 +634,7 @@ vacuum(List *relations, VacuumParams *params, BufferAccessStrategy bstrategy,
if (params->options & VACOPT_VACUUM)
{
- if (!vacuum_rel(vrel->oid, vrel->relation, params, bstrategy))
+ if (!vacuum_rel(vrel->oid, vrel->relation, params, bstrategy, NULL))
continue;
}
@@ -1998,7 +1998,7 @@ vac_truncate_clog(TransactionId frozenXID,
*/
static bool
vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
- BufferAccessStrategy bstrategy)
+ BufferAccessStrategy bstrategy, StdRdOptions *main_opts)
{
LOCKMODE lmode;
Relation rel;
@@ -2008,6 +2008,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
Oid save_userid;
int save_sec_context;
int save_nestlevel;
+ StdRdOptions saved_opts;
+ StdRdOptions combined_opts;
Assert(params != NULL);
@@ -2165,6 +2167,15 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ if (rel->rd_options)
+ {
+ memcpy(&saved_opts, rel->rd_options, sizeof(StdRdOptions));
+ memcpy(&combined_opts, rel->rd_options, sizeof(StdRdOptions));
+
+ if (main_opts)
+ combine_relopts(&combined_opts, main_opts);
+ }
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2176,8 +2187,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
if (rel->rd_options == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = combined_opts.vacuum_index_cleanup;
if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
params->index_cleanup = VACOPTVALUE_AUTO;
@@ -2197,9 +2207,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
*/
if (params->max_eager_freeze_failure_rate < 0 &&
rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
+ combined_opts.vacuum_max_eager_freeze_failure_rate >= 0)
params->max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ combined_opts.vacuum_max_eager_freeze_failure_rate;
else
params->max_eager_freeze_failure_rate = vacuum_max_eager_freeze_failure_rate;
@@ -2211,9 +2221,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
{
StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
- if (opts && opts->vacuum_truncate_set)
+ if (opts && combined_opts.vacuum_truncate_set)
{
- if (opts->vacuum_truncate)
+ if (combined_opts.vacuum_truncate)
params->truncate = VACOPTVALUE_ENABLED;
else
params->truncate = VACOPTVALUE_DISABLED;
@@ -2314,7 +2324,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
toast_vacuum_params.toast_parent = relid;
- vacuum_rel(toast_relid, NULL, &toast_vacuum_params, bstrategy);
+ vacuum_rel(toast_relid, NULL, &toast_vacuum_params, bstrategy, &saved_opts);
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 0d1f4e38b58..ca399b6a92e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1873,7 +1873,7 @@ get_database_list(void)
return dblist;
}
-static void
+void
combine_relopts(StdRdOptions *toast_opts, const StdRdOptions *main_opts)
{
AutoVacOpts *toast_avopts = &toast_opts->autovacuum;
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index bc37a80dc74..928f864581b 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -24,6 +24,7 @@
#include "parser/parse_node.h"
#include "storage/buf.h"
#include "storage/lock.h"
+#include "utils/rel.h"
#include "utils/relcache.h"
/*
@@ -377,6 +378,7 @@ extern IndexBulkDeleteResult *vac_cleanup_one_index(IndexVacuumInfo *ivinfo,
/* In postmaster/autovacuum.c */
extern void AutoVacuumUpdateCostLimit(void);
extern void VacuumUpdateCosts(void);
+extern void combine_relopts(StdRdOptions *toast_opts, const StdRdOptions *main_opts);
/* in commands/vacuumparallel.c */
extern ParallelVacuumState *parallel_vacuum_init(Relation rel, Relation *indrels,
--
2.39.5 (Apple Git-154)
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2025-06-24 05:10 Michael Paquier <michael@paquier.xyz>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Michael Paquier @ 2025-06-24 05:10 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: pgsql-hackers
On Mon, Jun 23, 2025 at 03:59:56PM -0500, Nathan Bossart wrote:
> Here is a very rough proof-of-concept patch set for this. AFAICT there are
> a few options we cannot fix on the back-branches because there is no way to
> tell whether it is set or has just picked up the default. On v18 and
> newer, we could use isset_offset, but that doesn't exist on older versions.
> (I haven't looked closely, but I'm assuming that back-patching isset_offset
> isn't an option.)
Hmm. I am wondering if we need to be aggressive about this set of
changes at all in the back branches. It's been broken for a long time
without anybody really complaining about the fact that reloptions
being set or not influenced the outcome in the context of autovacuum,
so perhaps there is a good argument for keeping all that in v19. My
conservative 2c.
> I would like to explore the "option 2" from upthread [0] for v19. I think
> that is a better long-term solution, and it may allow us to remove the
> table_toast_map in autovacuum.
>
> [0] https://postgr.es/m/aFl598epAdUrrv0y%40nathan
It would be nice to have some tests here to check the state of the
options used? My best guess would be a DEBUG1 entry combined with a
scan of the logs generated and an aggressive autovacuum worker
spawn to check that the options generated are what we expect for the
relations autovacuum picks up.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../aFozXwRQu3LaDELy@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2025-06-24 16:38 Nathan Bossart <nathandbossart@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2025-06-24 16:38 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: pgsql-hackers
On Tue, Jun 24, 2025 at 02:10:55PM +0900, Michael Paquier wrote:
> On Mon, Jun 23, 2025 at 03:59:56PM -0500, Nathan Bossart wrote:
>> Here is a very rough proof-of-concept patch set for this. AFAICT there are
>> a few options we cannot fix on the back-branches because there is no way to
>> tell whether it is set or has just picked up the default. On v18 and
>> newer, we could use isset_offset, but that doesn't exist on older versions.
>> (I haven't looked closely, but I'm assuming that back-patching isset_offset
>> isn't an option.)
>
> Hmm. I am wondering if we need to be aggressive about this set of
> changes at all in the back branches. It's been broken for a long time
> without anybody really complaining about the fact that reloptions
> being set or not influenced the outcome in the context of autovacuum,
> so perhaps there is a good argument for keeping all that in v19. My
> conservative 2c.
Yeah, I'm tempted to even ask how folks feel about removing the toast.*
reloptions. Maybe there's some simple cases that work well enough, but
AFAICT any moderately-complicated setup basically doesn't work at all. In
any case, writing out this patch set has got me on the fix-on-HEAD-only
bandwagon.
>> I would like to explore the "option 2" from upthread [0] for v19. I think
>> that is a better long-term solution, and it may allow us to remove the
>> table_toast_map in autovacuum.
>
> It would be nice to have some tests here to check the state of the
> options used? My best guess would be a DEBUG1 entry combined with a
> scan of the logs generated and an aggressive autovacuum worker
> spawn to check that the options generated are what we expect for the
> relations autovacuum picks up.
Eh... I agree that's probably how we'd have to test it with the existing
tools, but it sure sounds like a recipe for a flaky test.
--
nathan
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2025-06-24 18:21 Nathan Bossart <nathandbossart@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2025-06-24 18:21 UTC (permalink / raw)
To: shihao zhong <zhong950419@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers
On Mon, Jun 23, 2025 at 10:59:51AM -0500, Nathan Bossart wrote:
> On Sat, Jun 21, 2025 at 11:45:25PM -0400, shihao zhong wrote:
>> 2) When updating a table's relopt, also update the relopt of its
>> associated TOAST table if it's not already set. Similarly, when
>> creating a new TOAST table, it would inherit the parent's relopt.
>>
>> Option 2 seems more reasonable to me, as it avoids requiring customers
>> to manually resolve these options, when they have different settings
>> for the parent and TOAST tables."
>
> I like this one, but since it won't fix existing clusters, it might only be
> workable for v19.
Actually, I think there's a problem with this approach. If we set the
reloption for both the main relation and the TOAST table, then we won't
know what to do for RESET. Take the following examples:
ALTER TABLE test SET (vacuum_truncate = false);
ALTER TABLE test RESET (vacuum_truncate);
ALTER TABLE test SET (vacuum_truncate = false);
ALTER TABLE test SET (toast.vacuum_truncate = false);
ALTER TABLE test RESET (vacuum_truncate);
After executing the commands in the first stanza, you'd expect the
vacuum_truncate reloption to be unset for both the main relation and its
TOAST table. After the second one, you'd expect it to be set for only the
TOAST table. But unless there's some way to know the source of the TOAST
table's reloption, we can't know which behavior is correct at RESET time.
--
nathan
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2025-07-02 14:44 shihao zhong <zhong950419@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: shihao zhong @ 2025-07-02 14:44 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers
>> Actually, I think there's a problem with this approach...
You're right. I forgot we can reset the table options. While we could
use a placeholder and resolve it on-demand, that seems like too much
work.
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2025-07-15 18:50 Shayon Mukherjee <shayonj@gmail.com>
parent: shihao zhong <zhong950419@gmail.com>
0 siblings, 0 replies; 44+ messages in thread
From: Shayon Mukherjee @ 2025-07-15 18:50 UTC (permalink / raw)
To: shihao zhong <zhong950419@gmail.com>; +Cc: Nathan Bossart <nathandbossart@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
On Wed, Jul 2, 2025 at 10:44 AM shihao zhong <zhong950419@gmail.com> wrote:
> >> Actually, I think there's a problem with this approach...
>
> You're right. I forgot we can reset the table options. While we could
> use a placeholder and resolve it on-demand, that seems like too much
> work.
>
>
Hi all,
I started a conversation about TOAST table vacuum truncation parameter
inheritance [1] and was pointed to this thread which I totally missed. I
recently came across the issue where `vacuum_truncate` on TOAST tables
wasn't being inherited even though the parent table had the setting, and
the documentation mentioned that it should work [2]. I see that there are a
lot of good conversations here already.
I'm curious to hear what folks think about the approach where we implement
the inheritance logic directly in `vacuum_rel()` when `params.truncate ==
VACOPTVALUE_UNSPECIFIED`. This should address the point raised upthread
about `vacuum_rel()` not looking up the main relation's reloptions when
processing a TOAST and also what I discovered in [1]. For instance it
could be something like:
1. For TOAST tables: When no explicit `toast.vacuum_truncate` is set, scan
`pg_class.reltoastrelid` to find the parent table and inherit its
`vacuum_truncate` setting.
2. For manual VACUUM: Pass the final truncate decision from main table to
TOAST table in the `toast_vacuum_params`
3. For autovacuum: The inheritance happens naturally since autovacuum calls
`vacuum_rel()` for each relation independently
This basically teaches `vacuum_rel()` to consult the main relation's
reloptions for TOAST tables, which was the core issue identified upthread.
It handles both manual VACUUM and autovacuum scenarios in one place,
without changing function signatures (which helps with potential
backporting).
The execution-layer fix should also help us avoid the parameter
contamination issues that were fixed in commit 661643dedad9 (I believe?),
since we're doing the lookup fresh each time `vacuum_rel()` is called on a
TOAST table.
I realize backporting might not be preferred given this issue has existed
for a long time. However, that is precisely why I feel like it might be
worth patching it because, at least per the docs, my understanding was that
if `vacuum_truncate` is set on the parent table then it applies to the
TOAST as well and I wonder if there are others in the same boat as well.
Would something like this focused approach (just teaching `vacuum_rel()` to
look up parent reloptions for TOAST tables) could be a reasonable fix that
complements the broader reloptions work discussed upthread for v19 and also
backportable to v13 onwards?
I am happy to help with this and split out the changes as it makes sense as
well.
Thanks
Shayon
[1]
https://www.postgresql.org/message-id/CANqtF-pLHBDdNM-DifXQqVq%2BVWA3DTYOx4%2B3m2%2BTFJ1zDOZETg%40ma...
[2]
https://www.postgresql.org/docs/current/sql-createtable.html#RELOPTION-VACUUM-TRUNCATE
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-05-01 21:41 Nathan Bossart <nathandbossart@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2026-05-01 21:41 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: pgsql-hackers
On Tue, Jun 24, 2025 at 11:38:41AM -0500, Nathan Bossart wrote:
> Yeah, I'm tempted to even ask how folks feel about removing the toast.*
> reloptions. Maybe there's some simple cases that work well enough, but
> AFAICT any moderately-complicated setup basically doesn't work at all. In
> any case, writing out this patch set has got me on the fix-on-HEAD-only
> bandwagon.
For the sake of discussion, I wrote a patch for this. I suggested removing
toast.* relopts to some colleagues recently, and nobody was aware of much
(if any) use in the field. So maybe this isn't totally
out-of-the-question...
--
nathan
From 12ec1d0e500cac6c9ba53abea2e1e3af6d6c171b Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 7 Apr 2026 15:30:53 -0500
Subject: [PATCH v2 1/1] remove toast reloptions
---
doc/src/sgml/ref/create_table.sgml | 51 ++++++++-------
src/backend/access/common/reloptions.c | 49 ++++++---------
src/backend/catalog/toasting.c | 31 ++++------
src/backend/commands/createas.c | 13 +---
src/backend/commands/repack.c | 15 +----
src/backend/commands/tablecmds.c | 19 ++----
src/backend/commands/vacuum.c | 14 +----
src/backend/postmaster/autovacuum.c | 48 +++++----------
src/backend/tcop/utility.c | 19 +-----
src/bin/pg_upgrade/check.c | 75 +++++++++++++++++++++++
src/include/access/reloptions.h | 24 +++-----
src/include/catalog/toasting.h | 6 +-
src/test/regress/expected/alter_table.out | 9 ---
src/test/regress/expected/reloptions.out | 56 ++---------------
src/test/regress/expected/vacuum.out | 8 ---
src/test/regress/sql/alter_table.sql | 4 --
src/test/regress/sql/reloptions.sql | 32 +---------
src/test/regress/sql/vacuum.sql | 8 ---
18 files changed, 177 insertions(+), 304 deletions(-)
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e342585c7f0..b8d2a657d10 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1589,14 +1589,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Storage parameters for
indexes are documented in <xref linkend="sql-createindex"/>.
The storage parameters currently
- available for tables are listed below. For many of these parameters, as
- shown, there is an additional parameter with the same name prefixed with
- <literal>toast.</literal>, which controls the behavior of the
+ available for tables are listed below. Unless otherwise noted, a table's
+ parameter value also controls the behavior of the
table's secondary <acronym>TOAST</acronym> table, if any
(see <xref linkend="storage-toast"/> for more information about TOAST).
- If a table parameter value is set and the
- equivalent <literal>toast.</literal> parameter is not, the TOAST table
- will use the table's parameter value.
Specifying these parameters for partitioned tables is not supported,
but you may specify them for individual leaf partitions.
</para>
@@ -1622,7 +1618,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
updates</link> more likely.
For a table whose entries are never updated, complete packing is the
best choice, but in heavily updated tables smaller fillfactors are
- appropriate. This parameter cannot be set for TOAST tables.
+ appropriate. This parameter does not affect the table's secondary TOAST
+ table.
</para>
</listitem>
</varlistentry>
@@ -1648,7 +1645,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Note that the default setting is often close to optimal, and
it is possible that setting this parameter could have negative
effects in some cases.
- This parameter cannot be set for TOAST tables.
+ This parameter does not affect the table's secondary TOAST table.
</para>
</listitem>
</varlistentry>
@@ -1671,7 +1668,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-enabled" xreflabel="autovacuum_enabled">
- <term><literal>autovacuum_enabled</literal>, <literal>toast.autovacuum_enabled</literal> (<type>boolean</type>)
+ <term><literal>autovacuum_enabled</literal> (<type>boolean</type>)
<indexterm>
<primary><varname>autovacuum_enabled</varname> storage parameter</primary>
</indexterm>
@@ -1696,7 +1693,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-index-cleanup" xreflabel="vacuum_index_cleanup">
- <term><literal>vacuum_index_cleanup</literal>, <literal>toast.vacuum_index_cleanup</literal> (<type>enum</type>)
+ <term><literal>vacuum_index_cleanup</literal> (<type>enum</type>)
<indexterm>
<primary><varname>vacuum_index_cleanup</varname> storage parameter</primary>
</indexterm>
@@ -1722,7 +1719,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-truncate" xreflabel="vacuum_truncate">
- <term><literal>vacuum_truncate</literal>, <literal>toast.vacuum_truncate</literal> (<type>boolean</type>)
+ <term><literal>vacuum_truncate</literal> (<type>boolean</type>)
<indexterm>
<primary><varname>vacuum_truncate</varname></primary>
<secondary>storage parameter</secondary>
@@ -1755,7 +1752,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-threshold" xreflabel="autovacuum_vacuum_threshold">
- <term><literal>autovacuum_vacuum_threshold</literal>, <literal>toast.autovacuum_vacuum_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1770,7 +1767,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-max-threshold" xreflabel="autovacuum_vacuum_max_threshold">
- <term><literal>autovacuum_vacuum_max_threshold</literal>, <literal>toast.autovacuum_vacuum_max_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_max_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_max_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1785,7 +1782,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-scale-factor" xreflabel="autovacuum_vacuum_scale_factor">
- <term><literal>autovacuum_vacuum_scale_factor</literal>, <literal>toast.autovacuum_vacuum_scale_factor</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_scale_factor</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_scale_factor</varname> </primary>
<secondary>storage parameter</secondary>
@@ -1800,7 +1797,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-insert-threshold" xreflabel="autovacuum_vacuum_insert_threshold">
- <term><literal>autovacuum_vacuum_insert_threshold</literal>, <literal>toast.autovacuum_vacuum_insert_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_insert_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_insert_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1815,7 +1812,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-insert-scale-factor" xreflabel="autovacuum_vacuum_insert_scale_factor">
- <term><literal>autovacuum_vacuum_insert_scale_factor</literal>, <literal>toast.autovacuum_vacuum_insert_scale_factor</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_insert_scale_factor</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_insert_scale_factor</varname> </primary>
<secondary>storage parameter</secondary>
@@ -1860,7 +1857,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-cost-delay" xreflabel="autovacuum_vacuum_cost_delay">
- <term><literal>autovacuum_vacuum_cost_delay</literal>, <literal>toast.autovacuum_vacuum_cost_delay</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_cost_delay</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_cost_delay</varname></primary>
<secondary>storage parameter</secondary>
@@ -1875,7 +1872,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-cost-limit" xreflabel="autovacuum_vacuum_cost_limit">
- <term><literal>autovacuum_vacuum_cost_limit</literal>, <literal>toast.autovacuum_vacuum_cost_limit</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_cost_limit</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_cost_limit</varname></primary>
<secondary>storage parameter</secondary>
@@ -1890,7 +1887,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-min-age" xreflabel="autovacuum_freeze_min_age">
- <term><literal>autovacuum_freeze_min_age</literal>, <literal>toast.autovacuum_freeze_min_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_min_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_min_age</varname> storage parameter</primary>
</indexterm>
@@ -1907,7 +1904,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-max-age" xreflabel="autovacuum_freeze_max_age">
- <term><literal>autovacuum_freeze_max_age</literal>, <literal>toast.autovacuum_freeze_max_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_max_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_max_age</varname></primary>
<secondary>storage parameter</secondary>
@@ -1924,7 +1921,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-table-age" xreflabel="autovacuum_freeze_table_age">
- <term><literal>autovacuum_freeze_table_age</literal>, <literal>toast.autovacuum_freeze_table_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_table_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_table_age</varname> storage parameter</primary>
</indexterm>
@@ -1938,7 +1935,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-min-age" xreflabel="autovacuum_multixact_freeze_min_age">
- <term><literal>autovacuum_multixact_freeze_min_age</literal>, <literal>toast.autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_min_age</varname> storage parameter</primary>
</indexterm>
@@ -1956,7 +1953,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-max-age" xreflabel="autovacuum_multixact_freeze_max_age">
- <term><literal>autovacuum_multixact_freeze_max_age</literal>, <literal>toast.autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_max_age</varname></primary>
<secondary>storage parameter</secondary>
@@ -1975,7 +1972,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-table-age" xreflabel="autovacuum_multixact_freeze_table_age">
- <term><literal>autovacuum_multixact_freeze_table_age</literal>, <literal>toast.autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_table_age</varname> storage parameter</primary>
</indexterm>
@@ -1989,7 +1986,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-log-autovacuum-min-duration" xreflabel="log_autovacuum_min_duration">
- <term><literal>log_autovacuum_min_duration</literal>, <literal>toast.log_autovacuum_min_duration</literal> (<type>integer</type>)
+ <term><literal>log_autovacuum_min_duration</literal> (<type>integer</type>)
<indexterm>
<primary><varname>log_autovacuum_min_duration</varname></primary>
<secondary>storage parameter</secondary>
@@ -2019,7 +2016,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-max-eager-freeze-failure-rate" xreflabel="vacuum_max_eager_freeze_failure_rate">
- <term><literal>vacuum_max_eager_freeze_failure_rate</literal>, <literal>toast.vacuum_max_eager_freeze_failure_rate</literal> (<type>floating point</type>)
+ <term><literal>vacuum_max_eager_freeze_failure_rate</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>vacuum_max_eager_freeze_failure_rate</varname></primary>
<secondary>storage parameter</secondary>
@@ -2044,7 +2041,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Declare the table as an additional catalog table for purposes of
logical replication. See
<xref linkend="logicaldecoding-capabilities"/> for details.
- This parameter cannot be set for TOAST tables.
+ This parameter does not affect the table's secondary TOAST table.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..50d9805f3d8 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -111,7 +111,7 @@ static relopt_bool boolRelOpts[] =
{
"autovacuum_enabled",
"Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
true
@@ -172,7 +172,7 @@ static relopt_ternary ternaryRelOpts[] =
{
"vacuum_truncate",
"Enables vacuum to truncate empty pages at the end of this table",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}
},
@@ -249,7 +249,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_threshold",
"Minimum number of tuple updates or deletes prior to vacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, INT_MAX
@@ -258,7 +258,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_max_threshold",
"Maximum number of tuple updates or deletes prior to vacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-2, -1, INT_MAX
@@ -267,7 +267,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_insert_threshold",
"Minimum number of tuple inserts prior to vacuum, or -1 to disable insert vacuums",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-2, -1, INT_MAX
@@ -285,7 +285,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_cost_limit",
"Vacuum cost amount available before napping, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 1, 10000
@@ -294,7 +294,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_min_age",
"Minimum age at which VACUUM should freeze a table row, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, 1000000000
@@ -303,7 +303,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_min_age",
"Minimum multixact age at which VACUUM should freeze a row multixact's, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, 1000000000
@@ -312,7 +312,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_max_age",
"Age at which to autovacuum a table to prevent transaction ID wraparound",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 100000, 2000000000
@@ -321,7 +321,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_max_age",
"Multixact age at which to autovacuum a table to prevent multixact wraparound",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 10000, 2000000000
@@ -330,7 +330,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_table_age",
"Age at which VACUUM should perform a full table sweep to freeze row versions",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}, -1, 0, 2000000000
},
@@ -338,7 +338,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_table_age",
"Age of multixact at which VACUUM should perform a full table sweep to freeze row versions",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}, -1, 0, 2000000000
},
@@ -346,7 +346,7 @@ static relopt_int intRelOpts[] =
{
"log_autovacuum_min_duration",
"Sets the minimum execution time above which vacuum actions by autovacuum will be logged",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, -1, INT_MAX
@@ -424,7 +424,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_cost_delay",
"Vacuum cost delay in milliseconds, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -433,7 +433,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_scale_factor",
"Number of tuple updates or deletes prior to vacuum as a fraction of reltuples",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -442,7 +442,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_insert_scale_factor",
"Number of tuple inserts prior to vacuum as a fraction of reltuples",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -460,7 +460,7 @@ static relopt_real realRelOpts[] =
{
"vacuum_max_eager_freeze_failure_rate",
"Fraction of pages in a relation vacuum can scan and fail to freeze before disabling eager scanning.",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 1.0
@@ -554,7 +554,7 @@ static relopt_enum enumRelOpts[] =
{
"vacuum_index_cleanup",
"Controls index vacuuming and index cleanup",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
@@ -2163,21 +2163,8 @@ view_reloptions(Datum reloptions, bool validate)
bytea *
heap_reloptions(char relkind, Datum reloptions, bool validate)
{
- StdRdOptions *rdopts;
-
switch (relkind)
{
- case RELKIND_TOASTVALUE:
- rdopts = (StdRdOptions *)
- default_reloptions(reloptions, validate, RELOPT_KIND_TOAST);
- if (rdopts != NULL)
- {
- /* adjust default-only parameters for TOAST relations */
- rdopts->fillfactor = 100;
- rdopts->autovacuum.analyze_threshold = -1;
- rdopts->autovacuum.analyze_scale_factor = -1;
- }
- return (bytea *) rdopts;
case RELKIND_RELATION:
case RELKIND_MATVIEW:
return default_reloptions(reloptions, validate, RELOPT_KIND_HEAP);
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 4aa52a4bd25..9f1f822702c 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -34,11 +34,11 @@
#include "utils/rel.h"
#include "utils/syscache.h"
-static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
+static void CheckAndCreateToastTable(Oid relOid,
LOCKMODE lockmode, bool check,
Oid OIDOldToast);
static bool create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
- Datum reloptions, LOCKMODE lockmode, bool check,
+ LOCKMODE lockmode, bool check,
Oid OIDOldToast);
static bool needs_toast_table(Relation rel);
@@ -48,35 +48,30 @@ static bool needs_toast_table(Relation rel);
* If the table needs a toast table, and doesn't already have one,
* then create a toast table for it.
*
- * reloptions for the toast table can be passed, too. Pass (Datum) 0
- * for default reloptions.
- *
* We expect the caller to have verified that the relation is a table and have
* already done any necessary permission checks. Callers expect this function
* to end with CommandCounterIncrement if it makes any changes.
*/
void
-AlterTableCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode)
+AlterTableCreateToastTable(Oid relOid, LOCKMODE lockmode)
{
- CheckAndCreateToastTable(relOid, reloptions, lockmode, true, InvalidOid);
+ CheckAndCreateToastTable(relOid, lockmode, true, InvalidOid);
}
void
-NewHeapCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
- Oid OIDOldToast)
+NewHeapCreateToastTable(Oid relOid, LOCKMODE lockmode, Oid OIDOldToast)
{
- CheckAndCreateToastTable(relOid, reloptions, lockmode, false, OIDOldToast);
+ CheckAndCreateToastTable(relOid, lockmode, false, OIDOldToast);
}
void
-NewRelationCreateToastTable(Oid relOid, Datum reloptions)
+NewRelationCreateToastTable(Oid relOid)
{
- CheckAndCreateToastTable(relOid, reloptions, AccessExclusiveLock, false,
- InvalidOid);
+ CheckAndCreateToastTable(relOid, AccessExclusiveLock, false, InvalidOid);
}
static void
-CheckAndCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
+CheckAndCreateToastTable(Oid relOid, LOCKMODE lockmode,
bool check, Oid OIDOldToast)
{
Relation rel;
@@ -84,7 +79,7 @@ CheckAndCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
rel = table_open(relOid, lockmode);
/* create_toast_table does all the work */
- (void) create_toast_table(rel, InvalidOid, InvalidOid, reloptions, lockmode,
+ (void) create_toast_table(rel, InvalidOid, InvalidOid, lockmode,
check, OIDOldToast);
table_close(rel, NoLock);
@@ -108,7 +103,7 @@ BootstrapToastTable(char *relName, Oid toastOid, Oid toastIndexOid)
relName);
/* create_toast_table does all the work */
- if (!create_toast_table(rel, toastOid, toastIndexOid, (Datum) 0,
+ if (!create_toast_table(rel, toastOid, toastIndexOid,
AccessExclusiveLock, false, InvalidOid))
elog(ERROR, "\"%s\" does not require a toast table",
relName);
@@ -126,7 +121,7 @@ BootstrapToastTable(char *relName, Oid toastOid, Oid toastIndexOid)
*/
static bool
create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
- Datum reloptions, LOCKMODE lockmode, bool check,
+ LOCKMODE lockmode, bool check,
Oid OIDOldToast)
{
Oid relOid = RelationGetRelid(rel);
@@ -266,7 +261,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
shared_relation,
mapped_relation,
ONCOMMIT_NOOP,
- reloptions,
+ (Datum) 0,
false,
true,
true,
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 6dbb831ca89..188e167999b 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -84,8 +84,6 @@ create_ctas_internal(List *attrList, IntoClause *into)
CreateStmt *create = makeNode(CreateStmt);
bool is_matview;
char relkind;
- Datum toast_options;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
ObjectAddress intoRelationAddr;
/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
@@ -120,16 +118,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
*/
CommandCounterIncrement();
- /* parse and validate reloptions for the toast table */
- toast_options = transformRelOptions((Datum) 0,
- create->options,
- "toast",
- validnsps,
- true, false);
-
- (void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
-
- NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+ NewRelationCreateToastTable(intoRelationAddr.objectId);
/* Create the "view" part of a materialized view. */
if (is_matview)
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 9d162957bc3..5064d8e2349 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -1201,20 +1201,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
toastid = OldHeap->rd_rel->reltoastrelid;
if (OidIsValid(toastid))
- {
- /* keep the existing toast table's reloptions, if any */
- tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid));
- if (!HeapTupleIsValid(tuple))
- elog(ERROR, "cache lookup failed for relation %u", toastid);
- reloptions = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions,
- &isNull);
- if (isNull)
- reloptions = (Datum) 0;
-
- NewHeapCreateToastTable(OIDNewHeap, reloptions, lockmode, toastid);
-
- ReleaseSysCache(tuple);
- }
+ NewHeapCreateToastTable(OIDNewHeap, lockmode, toastid);
table_close(OldHeap, NoLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d8d7969bf30..a254509cbb5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -827,7 +827,6 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
ListCell *listptr;
AttrNumber attnum;
bool partitioned;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
Oid ofTypeId;
ObjectAddress address;
LOCKMODE parentLockmode;
@@ -976,7 +975,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
/*
* Parse and validate reloptions, if any.
*/
- reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, validnsps,
+ reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, NULL,
true, false);
switch (relkind)
@@ -5433,7 +5432,7 @@ ATRewriteCatalogs(List **wqueue, LOCKMODE lockmode,
tab->relkind == RELKIND_PARTITIONED_TABLE) &&
tab->partition_constraint == NULL) ||
tab->relkind == RELKIND_MATVIEW)
- AlterTableCreateToastTable(tab->relid, (Datum) 0, lockmode);
+ AlterTableCreateToastTable(tab->relid, lockmode);
}
}
@@ -16927,7 +16926,6 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
Datum repl_val[Natts_pg_class];
bool repl_null[Natts_pg_class];
bool repl_repl[Natts_pg_class];
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
if (defList == NIL && operation != AT_ReplaceRelOptions)
return; /* nothing to do */
@@ -16960,7 +16958,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
}
/* Generate new proposed reloptions (text array) */
- newOptions = transformRelOptions(datum, defList, NULL, validnsps, false,
+ newOptions = transformRelOptions(datum, defList, NULL, NULL, false,
operation == AT_ResetRelOptions);
/* Validate */
@@ -17083,20 +17081,11 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
datum = (Datum) 0;
}
- newOptions = transformRelOptions(datum, defList, "toast", validnsps,
- false, operation == AT_ResetRelOptions);
-
- (void) heap_reloptions(RELKIND_TOASTVALUE, newOptions, true);
-
memset(repl_val, 0, sizeof(repl_val));
memset(repl_null, false, sizeof(repl_null));
memset(repl_repl, false, sizeof(repl_repl));
- if (newOptions != (Datum) 0)
- repl_val[Anum_pg_class_reloptions - 1] = newOptions;
- else
- repl_null[Anum_pg_class_reloptions - 1] = true;
-
+ repl_null[Anum_pg_class_reloptions - 1] = true;
repl_repl[Anum_pg_class_reloptions - 1] = true;
newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass),
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 99d0db82ed7..ed670b3e3f1 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2015,13 +2015,6 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
Oid save_userid;
int save_sec_context;
int save_nestlevel;
- VacuumParams toast_vacuum_params;
-
- /*
- * This function scribbles on the parameters, so make a copy early to
- * avoid affecting the TOAST table (if we do end up recursing to it).
- */
- memcpy(&toast_vacuum_params, ¶ms, sizeof(VacuumParams));
/* Begin a transaction for vacuuming this relation */
StartTransactionCommand();
@@ -2336,11 +2329,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* relation. NB: This is only safe to do because we hold a session
* lock on the main relation that prevents concurrent deletion.
*/
- toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
- toast_vacuum_params.toast_parent = relid;
+ params.options |= VACOPT_PROCESS_MAIN;
+ params.toast_parent = relid;
- vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
- isTopLevel);
+ vacuum_rel(toast_relid, NULL, params, bstrategy, isTopLevel);
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a5a8db2ff88..c0e7cbf2cb8 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2139,8 +2139,9 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
- bool free_relopts = false;
+ av_relation *hentry;
+ bool found;
+ AutoVacOpts *relopts = NULL;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2154,22 +2155,10 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
- relopts = &hentry->ar_reloptions;
- }
+ /* Use reloptions from main rel. */
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ relopts = &hentry->ar_reloptions;
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2186,10 +2175,6 @@ do_autovacuum(void)
table->score = scores.max;
tables_to_process = lappend(tables_to_process, table);
}
-
- /* Release stuff to avoid leakage */
- if (free_relopts)
- pfree(relopts);
}
table_endscan(relScan);
@@ -2826,7 +2811,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
+ AutoVacOpts *avopts = NULL;
bool free_avopts = false;
AutoVacuumScores scores;
@@ -2838,13 +2823,15 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
/*
* Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * main table reloptions.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ if (classForm->relkind != RELKIND_TOASTVALUE)
+ {
+ avopts = extract_autovac_opts(classTup, pg_class_desc);
+ if (avopts)
+ free_avopts = true;
+ }
+ else if (table_toast_map)
{
av_relation *hentry;
bool found;
@@ -3127,8 +3114,7 @@ relation_needs_vacanalyze(Oid relid,
/*
* Determine vacuum/analyze equation parameters. We have two possible
- * sources: the passed reloptions (which could be a main table or a toast
- * table), or the autovacuum GUC variables.
+ * sources: the passed reloptions or the autovacuum GUC variables.
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..d089e02c216 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1159,8 +1159,6 @@ ProcessUtilitySlow(ParseState *pstate,
if (IsA(stmt, CreateStmt))
{
CreateStmt *cstmt = (CreateStmt *) stmt;
- Datum toast_options;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
/* Remember transformed RangeVar for LIKE */
table_rv = cstmt->relation;
@@ -1180,22 +1178,7 @@ ProcessUtilitySlow(ParseState *pstate,
*/
CommandCounterIncrement();
- /*
- * parse and validate reloptions for the toast
- * table
- */
- toast_options = transformRelOptions((Datum) 0,
- cstmt->options,
- "toast",
- validnsps,
- true,
- false);
- (void) heap_reloptions(RELKIND_TOASTVALUE,
- toast_options,
- true);
-
- NewRelationCreateToastTable(address.objectId,
- toast_options);
+ NewRelationCreateToastTable(address.objectId);
}
else if (IsA(stmt, CreateForeignTableStmt))
{
diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 5a7afe62eab..f0d15f56525 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -37,6 +37,7 @@ static void check_new_cluster_subscription_configuration(void);
static void check_old_cluster_for_valid_slots(void);
static void check_old_cluster_subscription_state(void);
static void check_old_cluster_global_names(ClusterInfo *cluster);
+static void check_for_toast_reloptions(ClusterInfo *cluster);
/*
* DataTypesUsageChecks - definitions of data type checks for the old cluster
@@ -634,6 +635,9 @@ check_and_dump_old_cluster(void)
check_for_prepared_transactions(&old_cluster);
check_for_isn_and_int8_passing_mismatch(&old_cluster);
+ if (GET_MAJOR_VERSION(old_cluster.major_version) < 2000)
+ check_for_toast_reloptions(&old_cluster);
+
if (GET_MAJOR_VERSION(old_cluster.major_version) >= 1700)
{
/*
@@ -2644,3 +2648,74 @@ check_old_cluster_global_names(ClusterInfo *cluster)
else
check_ok();
}
+
+/*
+ * Callback function for processing results of query for
+ * check_for_toast_reloptions()'s UpgradeTask. If the query returned any rows
+ * (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_toast_relopts_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+ UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+ int ntups = PQntuples(res);
+ int i_nspname = PQfnumber(res, "nspname");
+ int i_relname = PQfnumber(res, "relname");
+
+ if (ntups == 0)
+ return;
+
+ if (report->file == NULL &&
+ (report->file = fopen_priv(report->path, "w")) == NULL)
+ pg_fatal("could not open file \"%s\": %m", report->path);
+
+ fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+
+ for (int rowno = 0; rowno < ntups; rowno++)
+ fprintf(report->file, " %s.%s\n",
+ PQgetvalue(res, rowno, i_nspname),
+ PQgetvalue(res, rowno, i_relname));
+}
+
+/*
+ * Verify that no storage parameters (a.k.a. reloptions) are defined for TOAST
+ * tables.
+ */
+static void
+check_for_toast_reloptions(ClusterInfo *cluster)
+{
+ UpgradeTaskReport report;
+ UpgradeTask *task = upgrade_task_create();
+ const char *query = "SELECT n.nspname, c.relname "
+ "FROM pg_catalog.pg_class c, "
+ " pg_catalog.pg_class tc, "
+ " pg_catalog.pg_namespace n "
+ "WHERE c.reltoastrelid = tc.oid AND "
+ " c.relnamespace = c.oid AND "
+ " tc.reloptions IS NOT NULL";
+
+ prep_status("Check for tables with TOAST storage parameters");
+
+ report.file = NULL;
+ snprintf(report.path, sizeof(report.path), "%s/%s",
+ log_opts.basedir,
+ "tables_with_toast_storage_parameters.txt");
+
+ upgrade_task_add_step(task, query, process_toast_relopts_check,
+ true, &report);
+ upgrade_task_run(task, cluster);
+ upgrade_task_free(task);
+
+ if (report.file)
+ {
+ fclose(report.file);
+ pg_log(PG_REPORT, "fatal");
+ pg_fatal("Your installation contains tables with TOAST storage parameters set, which is\n"
+ "not supported anymore. Consider remove the TOAST storage parameters using\n"
+ " ALTER TABLE ... RESET ( ... );\n"
+ "A list of tables with the problem is in the file:\n"
+ " %s", report.path);
+ }
+ else
+ check_ok();
+}
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..4415c1ca9b5 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -40,26 +40,22 @@ typedef enum relopt_kind
{
RELOPT_KIND_LOCAL = 0,
RELOPT_KIND_HEAP = (1 << 0),
- RELOPT_KIND_TOAST = (1 << 1),
- RELOPT_KIND_BTREE = (1 << 2),
- RELOPT_KIND_HASH = (1 << 3),
- RELOPT_KIND_GIN = (1 << 4),
- RELOPT_KIND_GIST = (1 << 5),
- RELOPT_KIND_ATTRIBUTE = (1 << 6),
- RELOPT_KIND_TABLESPACE = (1 << 7),
- RELOPT_KIND_SPGIST = (1 << 8),
- RELOPT_KIND_VIEW = (1 << 9),
- RELOPT_KIND_BRIN = (1 << 10),
- RELOPT_KIND_PARTITIONED = (1 << 11),
+ RELOPT_KIND_BTREE = (1 << 1),
+ RELOPT_KIND_HASH = (1 << 2),
+ RELOPT_KIND_GIN = (1 << 3),
+ RELOPT_KIND_GIST = (1 << 4),
+ RELOPT_KIND_ATTRIBUTE = (1 << 5),
+ RELOPT_KIND_TABLESPACE = (1 << 6),
+ RELOPT_KIND_SPGIST = (1 << 7),
+ RELOPT_KIND_VIEW = (1 << 8),
+ RELOPT_KIND_BRIN = (1 << 9),
+ RELOPT_KIND_PARTITIONED = (1 << 10),
/* if you add a new kind, make sure you update "last_default" too */
RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_PARTITIONED,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
-/* reloption namespaces allowed for heaps -- currently only TOAST */
-#define HEAP_RELOPT_NAMESPACES { "toast", NULL }
-
/* generic struct to hold shared data */
typedef struct relopt_gen
{
diff --git a/src/include/catalog/toasting.h b/src/include/catalog/toasting.h
index 0bc61a8fee9..13e5a8affcb 100644
--- a/src/include/catalog/toasting.h
+++ b/src/include/catalog/toasting.h
@@ -19,10 +19,10 @@
/*
* toasting.c prototypes
*/
-extern void NewRelationCreateToastTable(Oid relOid, Datum reloptions);
-extern void NewHeapCreateToastTable(Oid relOid, Datum reloptions,
+extern void NewRelationCreateToastTable(Oid relOid);
+extern void NewHeapCreateToastTable(Oid relOid,
LOCKMODE lockmode, Oid OIDOldToast);
-extern void AlterTableCreateToastTable(Oid relOid, Datum reloptions,
+extern void AlterTableCreateToastTable(Oid relOid,
LOCKMODE lockmode);
extern void BootstrapToastTable(char *relName,
Oid toastOid, Oid toastIndexOid);
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 6dd22be0e8d..435e6beea0f 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -2860,15 +2860,6 @@ select * from my_locks order by 1;
pg_toast | ShareUpdateExclusiveLock
(2 rows)
-commit;
-begin; alter table alterlock set (toast.autovacuum_enabled = off);
-select * from my_locks order by 1;
- relname | max_lockmode
------------+--------------------------
- alterlock | ShareUpdateExclusiveLock
- pg_toast | ShareUpdateExclusiveLock
-(2 rows)
-
commit;
begin; alter table alterlock set (autovacuum_enabled = off);
select * from my_locks order by 1;
diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index e3a974f2611..eff5fbece60 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -120,7 +120,6 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
DROP TABLE reloptions_test;
CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
WITH (vacuum_truncate=false,
- toast.vacuum_truncate=false,
autovacuum_enabled=false);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
reloptions
@@ -142,9 +141,9 @@ SELECT pg_relation_size('reloptions_test') > 0;
SELECT reloptions FROM pg_class WHERE oid =
(SELECT reltoastrelid FROM pg_class
WHERE oid = 'reloptions_test'::regclass);
- reloptions
--------------------------
- {vacuum_truncate=false}
+ reloptions
+------------
+
(1 row)
ALTER TABLE reloptions_test RESET (vacuum_truncate);
@@ -165,56 +164,11 @@ SELECT pg_relation_size('reloptions_test') = 0;
t
(1 row)
--- Test toast.* options
-DROP TABLE reloptions_test;
-CREATE TABLE reloptions_test (s VARCHAR)
- WITH (toast.autovacuum_vacuum_cost_delay = 23);
-SELECT reltoastrelid as toast_oid
- FROM pg_class WHERE oid = 'reloptions_test'::regclass \gset
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=23}
-(1 row)
-
-ALTER TABLE reloptions_test SET (toast.autovacuum_vacuum_cost_delay = 24);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=24}
-(1 row)
-
-ALTER TABLE reloptions_test RESET (toast.autovacuum_vacuum_cost_delay);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
-------------
-
-(1 row)
-
--- Fail on non-existent options in toast namespace
-CREATE TABLE reloptions_test2 (i int) WITH (toast.not_existing_option = 42);
-ERROR: unrecognized parameter "not_existing_option"
--- Mix TOAST & heap
-DROP TABLE reloptions_test;
-CREATE TABLE reloptions_test (s VARCHAR) WITH
- (toast.autovacuum_vacuum_cost_delay = 23,
- autovacuum_vacuum_cost_delay = 24, fillfactor = 40);
-SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
- reloptions
--------------------------------------------------
- {autovacuum_vacuum_cost_delay=24,fillfactor=40}
-(1 row)
-
-SELECT reloptions FROM pg_class WHERE oid = (
- SELECT reltoastrelid FROM pg_class WHERE oid = 'reloptions_test'::regclass);
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=23}
-(1 row)
-
--
-- CREATE INDEX, ALTER INDEX for btrees
--
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test (s VARCHAR);
CREATE INDEX reloptions_test_idx ON reloptions_test (s) WITH (fillfactor=30);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test_idx'::regclass;
reloptions
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index d4696bc3325..d551ef57d50 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -209,14 +209,6 @@ VACUUM no_index_cleanup;
INSERT INTO no_index_cleanup(i, t) VALUES (generate_series(31,60),
repeat('1234567890',269));
DELETE FROM no_index_cleanup WHERE i < 45;
--- Only toast index is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = off,
- toast.vacuum_index_cleanup = yes);
-VACUUM no_index_cleanup;
--- Only parent is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = true,
- toast.vacuum_index_cleanup = false);
-VACUUM no_index_cleanup;
-- Test some extra relations.
VACUUM (INDEX_CLEANUP FALSE) vaccluster;
VACUUM (INDEX_CLEANUP AUTO) vactst; -- index cleanup option is ignored if no indexes
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index f5f13bbd3e7..410465dc022 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -1784,10 +1784,6 @@ begin; alter table alterlock reset (fillfactor);
select * from my_locks order by 1;
commit;
-begin; alter table alterlock set (toast.autovacuum_enabled = off);
-select * from my_locks order by 1;
-commit;
-
begin; alter table alterlock set (autovacuum_enabled = off);
select * from my_locks order by 1;
commit;
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index 680c8bf8614..0809a2ae0e4 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -75,7 +75,6 @@ DROP TABLE reloptions_test;
CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
WITH (vacuum_truncate=false,
- toast.vacuum_truncate=false,
autovacuum_enabled=false);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
@@ -94,39 +93,12 @@ INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) reloptions_test;
SELECT pg_relation_size('reloptions_test') = 0;
--- Test toast.* options
-DROP TABLE reloptions_test;
-
-CREATE TABLE reloptions_test (s VARCHAR)
- WITH (toast.autovacuum_vacuum_cost_delay = 23);
-SELECT reltoastrelid as toast_oid
- FROM pg_class WHERE oid = 'reloptions_test'::regclass \gset
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
-ALTER TABLE reloptions_test SET (toast.autovacuum_vacuum_cost_delay = 24);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
-ALTER TABLE reloptions_test RESET (toast.autovacuum_vacuum_cost_delay);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
--- Fail on non-existent options in toast namespace
-CREATE TABLE reloptions_test2 (i int) WITH (toast.not_existing_option = 42);
-
--- Mix TOAST & heap
-DROP TABLE reloptions_test;
-
-CREATE TABLE reloptions_test (s VARCHAR) WITH
- (toast.autovacuum_vacuum_cost_delay = 23,
- autovacuum_vacuum_cost_delay = 24, fillfactor = 40);
-
-SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
-SELECT reloptions FROM pg_class WHERE oid = (
- SELECT reltoastrelid FROM pg_class WHERE oid = 'reloptions_test'::regclass);
-
--
-- CREATE INDEX, ALTER INDEX for btrees
--
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test (s VARCHAR);
CREATE INDEX reloptions_test_idx ON reloptions_test (s) WITH (fillfactor=30);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test_idx'::regclass;
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 247b8e23b23..37a84fd9145 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -175,14 +175,6 @@ VACUUM no_index_cleanup;
INSERT INTO no_index_cleanup(i, t) VALUES (generate_series(31,60),
repeat('1234567890',269));
DELETE FROM no_index_cleanup WHERE i < 45;
--- Only toast index is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = off,
- toast.vacuum_index_cleanup = yes);
-VACUUM no_index_cleanup;
--- Only parent is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = true,
- toast.vacuum_index_cleanup = false);
-VACUUM no_index_cleanup;
-- Test some extra relations.
VACUUM (INDEX_CLEANUP FALSE) vaccluster;
VACUUM (INDEX_CLEANUP AUTO) vactst; -- index cleanup option is ignored if no indexes
--
2.50.1 (Apple Git-155)
Attachments:
[text/plain] v2-0001-remove-toast-reloptions.patch (47.1K, ../../afUd-A4CvaGc9se7@nathan/2-v2-0001-remove-toast-reloptions.patch)
download | inline diff:
From 12ec1d0e500cac6c9ba53abea2e1e3af6d6c171b Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 7 Apr 2026 15:30:53 -0500
Subject: [PATCH v2 1/1] remove toast reloptions
---
doc/src/sgml/ref/create_table.sgml | 51 ++++++++-------
src/backend/access/common/reloptions.c | 49 ++++++---------
src/backend/catalog/toasting.c | 31 ++++------
src/backend/commands/createas.c | 13 +---
src/backend/commands/repack.c | 15 +----
src/backend/commands/tablecmds.c | 19 ++----
src/backend/commands/vacuum.c | 14 +----
src/backend/postmaster/autovacuum.c | 48 +++++----------
src/backend/tcop/utility.c | 19 +-----
src/bin/pg_upgrade/check.c | 75 +++++++++++++++++++++++
src/include/access/reloptions.h | 24 +++-----
src/include/catalog/toasting.h | 6 +-
src/test/regress/expected/alter_table.out | 9 ---
src/test/regress/expected/reloptions.out | 56 ++---------------
src/test/regress/expected/vacuum.out | 8 ---
src/test/regress/sql/alter_table.sql | 4 --
src/test/regress/sql/reloptions.sql | 32 +---------
src/test/regress/sql/vacuum.sql | 8 ---
18 files changed, 177 insertions(+), 304 deletions(-)
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e342585c7f0..b8d2a657d10 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1589,14 +1589,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Storage parameters for
indexes are documented in <xref linkend="sql-createindex"/>.
The storage parameters currently
- available for tables are listed below. For many of these parameters, as
- shown, there is an additional parameter with the same name prefixed with
- <literal>toast.</literal>, which controls the behavior of the
+ available for tables are listed below. Unless otherwise noted, a table's
+ parameter value also controls the behavior of the
table's secondary <acronym>TOAST</acronym> table, if any
(see <xref linkend="storage-toast"/> for more information about TOAST).
- If a table parameter value is set and the
- equivalent <literal>toast.</literal> parameter is not, the TOAST table
- will use the table's parameter value.
Specifying these parameters for partitioned tables is not supported,
but you may specify them for individual leaf partitions.
</para>
@@ -1622,7 +1618,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
updates</link> more likely.
For a table whose entries are never updated, complete packing is the
best choice, but in heavily updated tables smaller fillfactors are
- appropriate. This parameter cannot be set for TOAST tables.
+ appropriate. This parameter does not affect the table's secondary TOAST
+ table.
</para>
</listitem>
</varlistentry>
@@ -1648,7 +1645,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Note that the default setting is often close to optimal, and
it is possible that setting this parameter could have negative
effects in some cases.
- This parameter cannot be set for TOAST tables.
+ This parameter does not affect the table's secondary TOAST table.
</para>
</listitem>
</varlistentry>
@@ -1671,7 +1668,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-enabled" xreflabel="autovacuum_enabled">
- <term><literal>autovacuum_enabled</literal>, <literal>toast.autovacuum_enabled</literal> (<type>boolean</type>)
+ <term><literal>autovacuum_enabled</literal> (<type>boolean</type>)
<indexterm>
<primary><varname>autovacuum_enabled</varname> storage parameter</primary>
</indexterm>
@@ -1696,7 +1693,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-index-cleanup" xreflabel="vacuum_index_cleanup">
- <term><literal>vacuum_index_cleanup</literal>, <literal>toast.vacuum_index_cleanup</literal> (<type>enum</type>)
+ <term><literal>vacuum_index_cleanup</literal> (<type>enum</type>)
<indexterm>
<primary><varname>vacuum_index_cleanup</varname> storage parameter</primary>
</indexterm>
@@ -1722,7 +1719,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-truncate" xreflabel="vacuum_truncate">
- <term><literal>vacuum_truncate</literal>, <literal>toast.vacuum_truncate</literal> (<type>boolean</type>)
+ <term><literal>vacuum_truncate</literal> (<type>boolean</type>)
<indexterm>
<primary><varname>vacuum_truncate</varname></primary>
<secondary>storage parameter</secondary>
@@ -1755,7 +1752,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-threshold" xreflabel="autovacuum_vacuum_threshold">
- <term><literal>autovacuum_vacuum_threshold</literal>, <literal>toast.autovacuum_vacuum_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1770,7 +1767,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-max-threshold" xreflabel="autovacuum_vacuum_max_threshold">
- <term><literal>autovacuum_vacuum_max_threshold</literal>, <literal>toast.autovacuum_vacuum_max_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_max_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_max_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1785,7 +1782,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-scale-factor" xreflabel="autovacuum_vacuum_scale_factor">
- <term><literal>autovacuum_vacuum_scale_factor</literal>, <literal>toast.autovacuum_vacuum_scale_factor</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_scale_factor</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_scale_factor</varname> </primary>
<secondary>storage parameter</secondary>
@@ -1800,7 +1797,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-insert-threshold" xreflabel="autovacuum_vacuum_insert_threshold">
- <term><literal>autovacuum_vacuum_insert_threshold</literal>, <literal>toast.autovacuum_vacuum_insert_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_insert_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_insert_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1815,7 +1812,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-insert-scale-factor" xreflabel="autovacuum_vacuum_insert_scale_factor">
- <term><literal>autovacuum_vacuum_insert_scale_factor</literal>, <literal>toast.autovacuum_vacuum_insert_scale_factor</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_insert_scale_factor</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_insert_scale_factor</varname> </primary>
<secondary>storage parameter</secondary>
@@ -1860,7 +1857,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-cost-delay" xreflabel="autovacuum_vacuum_cost_delay">
- <term><literal>autovacuum_vacuum_cost_delay</literal>, <literal>toast.autovacuum_vacuum_cost_delay</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_cost_delay</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_cost_delay</varname></primary>
<secondary>storage parameter</secondary>
@@ -1875,7 +1872,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-cost-limit" xreflabel="autovacuum_vacuum_cost_limit">
- <term><literal>autovacuum_vacuum_cost_limit</literal>, <literal>toast.autovacuum_vacuum_cost_limit</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_cost_limit</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_cost_limit</varname></primary>
<secondary>storage parameter</secondary>
@@ -1890,7 +1887,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-min-age" xreflabel="autovacuum_freeze_min_age">
- <term><literal>autovacuum_freeze_min_age</literal>, <literal>toast.autovacuum_freeze_min_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_min_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_min_age</varname> storage parameter</primary>
</indexterm>
@@ -1907,7 +1904,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-max-age" xreflabel="autovacuum_freeze_max_age">
- <term><literal>autovacuum_freeze_max_age</literal>, <literal>toast.autovacuum_freeze_max_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_max_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_max_age</varname></primary>
<secondary>storage parameter</secondary>
@@ -1924,7 +1921,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-table-age" xreflabel="autovacuum_freeze_table_age">
- <term><literal>autovacuum_freeze_table_age</literal>, <literal>toast.autovacuum_freeze_table_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_table_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_table_age</varname> storage parameter</primary>
</indexterm>
@@ -1938,7 +1935,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-min-age" xreflabel="autovacuum_multixact_freeze_min_age">
- <term><literal>autovacuum_multixact_freeze_min_age</literal>, <literal>toast.autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_min_age</varname> storage parameter</primary>
</indexterm>
@@ -1956,7 +1953,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-max-age" xreflabel="autovacuum_multixact_freeze_max_age">
- <term><literal>autovacuum_multixact_freeze_max_age</literal>, <literal>toast.autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_max_age</varname></primary>
<secondary>storage parameter</secondary>
@@ -1975,7 +1972,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-table-age" xreflabel="autovacuum_multixact_freeze_table_age">
- <term><literal>autovacuum_multixact_freeze_table_age</literal>, <literal>toast.autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_table_age</varname> storage parameter</primary>
</indexterm>
@@ -1989,7 +1986,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-log-autovacuum-min-duration" xreflabel="log_autovacuum_min_duration">
- <term><literal>log_autovacuum_min_duration</literal>, <literal>toast.log_autovacuum_min_duration</literal> (<type>integer</type>)
+ <term><literal>log_autovacuum_min_duration</literal> (<type>integer</type>)
<indexterm>
<primary><varname>log_autovacuum_min_duration</varname></primary>
<secondary>storage parameter</secondary>
@@ -2019,7 +2016,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-max-eager-freeze-failure-rate" xreflabel="vacuum_max_eager_freeze_failure_rate">
- <term><literal>vacuum_max_eager_freeze_failure_rate</literal>, <literal>toast.vacuum_max_eager_freeze_failure_rate</literal> (<type>floating point</type>)
+ <term><literal>vacuum_max_eager_freeze_failure_rate</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>vacuum_max_eager_freeze_failure_rate</varname></primary>
<secondary>storage parameter</secondary>
@@ -2044,7 +2041,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Declare the table as an additional catalog table for purposes of
logical replication. See
<xref linkend="logicaldecoding-capabilities"/> for details.
- This parameter cannot be set for TOAST tables.
+ This parameter does not affect the table's secondary TOAST table.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..50d9805f3d8 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -111,7 +111,7 @@ static relopt_bool boolRelOpts[] =
{
"autovacuum_enabled",
"Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
true
@@ -172,7 +172,7 @@ static relopt_ternary ternaryRelOpts[] =
{
"vacuum_truncate",
"Enables vacuum to truncate empty pages at the end of this table",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}
},
@@ -249,7 +249,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_threshold",
"Minimum number of tuple updates or deletes prior to vacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, INT_MAX
@@ -258,7 +258,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_max_threshold",
"Maximum number of tuple updates or deletes prior to vacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-2, -1, INT_MAX
@@ -267,7 +267,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_insert_threshold",
"Minimum number of tuple inserts prior to vacuum, or -1 to disable insert vacuums",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-2, -1, INT_MAX
@@ -285,7 +285,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_cost_limit",
"Vacuum cost amount available before napping, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 1, 10000
@@ -294,7 +294,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_min_age",
"Minimum age at which VACUUM should freeze a table row, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, 1000000000
@@ -303,7 +303,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_min_age",
"Minimum multixact age at which VACUUM should freeze a row multixact's, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, 1000000000
@@ -312,7 +312,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_max_age",
"Age at which to autovacuum a table to prevent transaction ID wraparound",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 100000, 2000000000
@@ -321,7 +321,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_max_age",
"Multixact age at which to autovacuum a table to prevent multixact wraparound",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 10000, 2000000000
@@ -330,7 +330,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_table_age",
"Age at which VACUUM should perform a full table sweep to freeze row versions",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}, -1, 0, 2000000000
},
@@ -338,7 +338,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_table_age",
"Age of multixact at which VACUUM should perform a full table sweep to freeze row versions",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}, -1, 0, 2000000000
},
@@ -346,7 +346,7 @@ static relopt_int intRelOpts[] =
{
"log_autovacuum_min_duration",
"Sets the minimum execution time above which vacuum actions by autovacuum will be logged",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, -1, INT_MAX
@@ -424,7 +424,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_cost_delay",
"Vacuum cost delay in milliseconds, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -433,7 +433,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_scale_factor",
"Number of tuple updates or deletes prior to vacuum as a fraction of reltuples",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -442,7 +442,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_insert_scale_factor",
"Number of tuple inserts prior to vacuum as a fraction of reltuples",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -460,7 +460,7 @@ static relopt_real realRelOpts[] =
{
"vacuum_max_eager_freeze_failure_rate",
"Fraction of pages in a relation vacuum can scan and fail to freeze before disabling eager scanning.",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 1.0
@@ -554,7 +554,7 @@ static relopt_enum enumRelOpts[] =
{
"vacuum_index_cleanup",
"Controls index vacuuming and index cleanup",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
@@ -2163,21 +2163,8 @@ view_reloptions(Datum reloptions, bool validate)
bytea *
heap_reloptions(char relkind, Datum reloptions, bool validate)
{
- StdRdOptions *rdopts;
-
switch (relkind)
{
- case RELKIND_TOASTVALUE:
- rdopts = (StdRdOptions *)
- default_reloptions(reloptions, validate, RELOPT_KIND_TOAST);
- if (rdopts != NULL)
- {
- /* adjust default-only parameters for TOAST relations */
- rdopts->fillfactor = 100;
- rdopts->autovacuum.analyze_threshold = -1;
- rdopts->autovacuum.analyze_scale_factor = -1;
- }
- return (bytea *) rdopts;
case RELKIND_RELATION:
case RELKIND_MATVIEW:
return default_reloptions(reloptions, validate, RELOPT_KIND_HEAP);
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 4aa52a4bd25..9f1f822702c 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -34,11 +34,11 @@
#include "utils/rel.h"
#include "utils/syscache.h"
-static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
+static void CheckAndCreateToastTable(Oid relOid,
LOCKMODE lockmode, bool check,
Oid OIDOldToast);
static bool create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
- Datum reloptions, LOCKMODE lockmode, bool check,
+ LOCKMODE lockmode, bool check,
Oid OIDOldToast);
static bool needs_toast_table(Relation rel);
@@ -48,35 +48,30 @@ static bool needs_toast_table(Relation rel);
* If the table needs a toast table, and doesn't already have one,
* then create a toast table for it.
*
- * reloptions for the toast table can be passed, too. Pass (Datum) 0
- * for default reloptions.
- *
* We expect the caller to have verified that the relation is a table and have
* already done any necessary permission checks. Callers expect this function
* to end with CommandCounterIncrement if it makes any changes.
*/
void
-AlterTableCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode)
+AlterTableCreateToastTable(Oid relOid, LOCKMODE lockmode)
{
- CheckAndCreateToastTable(relOid, reloptions, lockmode, true, InvalidOid);
+ CheckAndCreateToastTable(relOid, lockmode, true, InvalidOid);
}
void
-NewHeapCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
- Oid OIDOldToast)
+NewHeapCreateToastTable(Oid relOid, LOCKMODE lockmode, Oid OIDOldToast)
{
- CheckAndCreateToastTable(relOid, reloptions, lockmode, false, OIDOldToast);
+ CheckAndCreateToastTable(relOid, lockmode, false, OIDOldToast);
}
void
-NewRelationCreateToastTable(Oid relOid, Datum reloptions)
+NewRelationCreateToastTable(Oid relOid)
{
- CheckAndCreateToastTable(relOid, reloptions, AccessExclusiveLock, false,
- InvalidOid);
+ CheckAndCreateToastTable(relOid, AccessExclusiveLock, false, InvalidOid);
}
static void
-CheckAndCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
+CheckAndCreateToastTable(Oid relOid, LOCKMODE lockmode,
bool check, Oid OIDOldToast)
{
Relation rel;
@@ -84,7 +79,7 @@ CheckAndCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
rel = table_open(relOid, lockmode);
/* create_toast_table does all the work */
- (void) create_toast_table(rel, InvalidOid, InvalidOid, reloptions, lockmode,
+ (void) create_toast_table(rel, InvalidOid, InvalidOid, lockmode,
check, OIDOldToast);
table_close(rel, NoLock);
@@ -108,7 +103,7 @@ BootstrapToastTable(char *relName, Oid toastOid, Oid toastIndexOid)
relName);
/* create_toast_table does all the work */
- if (!create_toast_table(rel, toastOid, toastIndexOid, (Datum) 0,
+ if (!create_toast_table(rel, toastOid, toastIndexOid,
AccessExclusiveLock, false, InvalidOid))
elog(ERROR, "\"%s\" does not require a toast table",
relName);
@@ -126,7 +121,7 @@ BootstrapToastTable(char *relName, Oid toastOid, Oid toastIndexOid)
*/
static bool
create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
- Datum reloptions, LOCKMODE lockmode, bool check,
+ LOCKMODE lockmode, bool check,
Oid OIDOldToast)
{
Oid relOid = RelationGetRelid(rel);
@@ -266,7 +261,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
shared_relation,
mapped_relation,
ONCOMMIT_NOOP,
- reloptions,
+ (Datum) 0,
false,
true,
true,
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 6dbb831ca89..188e167999b 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -84,8 +84,6 @@ create_ctas_internal(List *attrList, IntoClause *into)
CreateStmt *create = makeNode(CreateStmt);
bool is_matview;
char relkind;
- Datum toast_options;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
ObjectAddress intoRelationAddr;
/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
@@ -120,16 +118,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
*/
CommandCounterIncrement();
- /* parse and validate reloptions for the toast table */
- toast_options = transformRelOptions((Datum) 0,
- create->options,
- "toast",
- validnsps,
- true, false);
-
- (void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
-
- NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+ NewRelationCreateToastTable(intoRelationAddr.objectId);
/* Create the "view" part of a materialized view. */
if (is_matview)
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 9d162957bc3..5064d8e2349 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -1201,20 +1201,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
toastid = OldHeap->rd_rel->reltoastrelid;
if (OidIsValid(toastid))
- {
- /* keep the existing toast table's reloptions, if any */
- tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid));
- if (!HeapTupleIsValid(tuple))
- elog(ERROR, "cache lookup failed for relation %u", toastid);
- reloptions = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions,
- &isNull);
- if (isNull)
- reloptions = (Datum) 0;
-
- NewHeapCreateToastTable(OIDNewHeap, reloptions, lockmode, toastid);
-
- ReleaseSysCache(tuple);
- }
+ NewHeapCreateToastTable(OIDNewHeap, lockmode, toastid);
table_close(OldHeap, NoLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d8d7969bf30..a254509cbb5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -827,7 +827,6 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
ListCell *listptr;
AttrNumber attnum;
bool partitioned;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
Oid ofTypeId;
ObjectAddress address;
LOCKMODE parentLockmode;
@@ -976,7 +975,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
/*
* Parse and validate reloptions, if any.
*/
- reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, validnsps,
+ reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, NULL,
true, false);
switch (relkind)
@@ -5433,7 +5432,7 @@ ATRewriteCatalogs(List **wqueue, LOCKMODE lockmode,
tab->relkind == RELKIND_PARTITIONED_TABLE) &&
tab->partition_constraint == NULL) ||
tab->relkind == RELKIND_MATVIEW)
- AlterTableCreateToastTable(tab->relid, (Datum) 0, lockmode);
+ AlterTableCreateToastTable(tab->relid, lockmode);
}
}
@@ -16927,7 +16926,6 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
Datum repl_val[Natts_pg_class];
bool repl_null[Natts_pg_class];
bool repl_repl[Natts_pg_class];
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
if (defList == NIL && operation != AT_ReplaceRelOptions)
return; /* nothing to do */
@@ -16960,7 +16958,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
}
/* Generate new proposed reloptions (text array) */
- newOptions = transformRelOptions(datum, defList, NULL, validnsps, false,
+ newOptions = transformRelOptions(datum, defList, NULL, NULL, false,
operation == AT_ResetRelOptions);
/* Validate */
@@ -17083,20 +17081,11 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
datum = (Datum) 0;
}
- newOptions = transformRelOptions(datum, defList, "toast", validnsps,
- false, operation == AT_ResetRelOptions);
-
- (void) heap_reloptions(RELKIND_TOASTVALUE, newOptions, true);
-
memset(repl_val, 0, sizeof(repl_val));
memset(repl_null, false, sizeof(repl_null));
memset(repl_repl, false, sizeof(repl_repl));
- if (newOptions != (Datum) 0)
- repl_val[Anum_pg_class_reloptions - 1] = newOptions;
- else
- repl_null[Anum_pg_class_reloptions - 1] = true;
-
+ repl_null[Anum_pg_class_reloptions - 1] = true;
repl_repl[Anum_pg_class_reloptions - 1] = true;
newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass),
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 99d0db82ed7..ed670b3e3f1 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2015,13 +2015,6 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
Oid save_userid;
int save_sec_context;
int save_nestlevel;
- VacuumParams toast_vacuum_params;
-
- /*
- * This function scribbles on the parameters, so make a copy early to
- * avoid affecting the TOAST table (if we do end up recursing to it).
- */
- memcpy(&toast_vacuum_params, ¶ms, sizeof(VacuumParams));
/* Begin a transaction for vacuuming this relation */
StartTransactionCommand();
@@ -2336,11 +2329,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* relation. NB: This is only safe to do because we hold a session
* lock on the main relation that prevents concurrent deletion.
*/
- toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
- toast_vacuum_params.toast_parent = relid;
+ params.options |= VACOPT_PROCESS_MAIN;
+ params.toast_parent = relid;
- vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
- isTopLevel);
+ vacuum_rel(toast_relid, NULL, params, bstrategy, isTopLevel);
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a5a8db2ff88..c0e7cbf2cb8 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2139,8 +2139,9 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
- bool free_relopts = false;
+ av_relation *hentry;
+ bool found;
+ AutoVacOpts *relopts = NULL;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2154,22 +2155,10 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
- relopts = &hentry->ar_reloptions;
- }
+ /* Use reloptions from main rel. */
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ relopts = &hentry->ar_reloptions;
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2186,10 +2175,6 @@ do_autovacuum(void)
table->score = scores.max;
tables_to_process = lappend(tables_to_process, table);
}
-
- /* Release stuff to avoid leakage */
- if (free_relopts)
- pfree(relopts);
}
table_endscan(relScan);
@@ -2826,7 +2811,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
+ AutoVacOpts *avopts = NULL;
bool free_avopts = false;
AutoVacuumScores scores;
@@ -2838,13 +2823,15 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
/*
* Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * main table reloptions.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ if (classForm->relkind != RELKIND_TOASTVALUE)
+ {
+ avopts = extract_autovac_opts(classTup, pg_class_desc);
+ if (avopts)
+ free_avopts = true;
+ }
+ else if (table_toast_map)
{
av_relation *hentry;
bool found;
@@ -3127,8 +3114,7 @@ relation_needs_vacanalyze(Oid relid,
/*
* Determine vacuum/analyze equation parameters. We have two possible
- * sources: the passed reloptions (which could be a main table or a toast
- * table), or the autovacuum GUC variables.
+ * sources: the passed reloptions or the autovacuum GUC variables.
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..d089e02c216 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1159,8 +1159,6 @@ ProcessUtilitySlow(ParseState *pstate,
if (IsA(stmt, CreateStmt))
{
CreateStmt *cstmt = (CreateStmt *) stmt;
- Datum toast_options;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
/* Remember transformed RangeVar for LIKE */
table_rv = cstmt->relation;
@@ -1180,22 +1178,7 @@ ProcessUtilitySlow(ParseState *pstate,
*/
CommandCounterIncrement();
- /*
- * parse and validate reloptions for the toast
- * table
- */
- toast_options = transformRelOptions((Datum) 0,
- cstmt->options,
- "toast",
- validnsps,
- true,
- false);
- (void) heap_reloptions(RELKIND_TOASTVALUE,
- toast_options,
- true);
-
- NewRelationCreateToastTable(address.objectId,
- toast_options);
+ NewRelationCreateToastTable(address.objectId);
}
else if (IsA(stmt, CreateForeignTableStmt))
{
diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 5a7afe62eab..f0d15f56525 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -37,6 +37,7 @@ static void check_new_cluster_subscription_configuration(void);
static void check_old_cluster_for_valid_slots(void);
static void check_old_cluster_subscription_state(void);
static void check_old_cluster_global_names(ClusterInfo *cluster);
+static void check_for_toast_reloptions(ClusterInfo *cluster);
/*
* DataTypesUsageChecks - definitions of data type checks for the old cluster
@@ -634,6 +635,9 @@ check_and_dump_old_cluster(void)
check_for_prepared_transactions(&old_cluster);
check_for_isn_and_int8_passing_mismatch(&old_cluster);
+ if (GET_MAJOR_VERSION(old_cluster.major_version) < 2000)
+ check_for_toast_reloptions(&old_cluster);
+
if (GET_MAJOR_VERSION(old_cluster.major_version) >= 1700)
{
/*
@@ -2644,3 +2648,74 @@ check_old_cluster_global_names(ClusterInfo *cluster)
else
check_ok();
}
+
+/*
+ * Callback function for processing results of query for
+ * check_for_toast_reloptions()'s UpgradeTask. If the query returned any rows
+ * (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_toast_relopts_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+ UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+ int ntups = PQntuples(res);
+ int i_nspname = PQfnumber(res, "nspname");
+ int i_relname = PQfnumber(res, "relname");
+
+ if (ntups == 0)
+ return;
+
+ if (report->file == NULL &&
+ (report->file = fopen_priv(report->path, "w")) == NULL)
+ pg_fatal("could not open file \"%s\": %m", report->path);
+
+ fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+
+ for (int rowno = 0; rowno < ntups; rowno++)
+ fprintf(report->file, " %s.%s\n",
+ PQgetvalue(res, rowno, i_nspname),
+ PQgetvalue(res, rowno, i_relname));
+}
+
+/*
+ * Verify that no storage parameters (a.k.a. reloptions) are defined for TOAST
+ * tables.
+ */
+static void
+check_for_toast_reloptions(ClusterInfo *cluster)
+{
+ UpgradeTaskReport report;
+ UpgradeTask *task = upgrade_task_create();
+ const char *query = "SELECT n.nspname, c.relname "
+ "FROM pg_catalog.pg_class c, "
+ " pg_catalog.pg_class tc, "
+ " pg_catalog.pg_namespace n "
+ "WHERE c.reltoastrelid = tc.oid AND "
+ " c.relnamespace = c.oid AND "
+ " tc.reloptions IS NOT NULL";
+
+ prep_status("Check for tables with TOAST storage parameters");
+
+ report.file = NULL;
+ snprintf(report.path, sizeof(report.path), "%s/%s",
+ log_opts.basedir,
+ "tables_with_toast_storage_parameters.txt");
+
+ upgrade_task_add_step(task, query, process_toast_relopts_check,
+ true, &report);
+ upgrade_task_run(task, cluster);
+ upgrade_task_free(task);
+
+ if (report.file)
+ {
+ fclose(report.file);
+ pg_log(PG_REPORT, "fatal");
+ pg_fatal("Your installation contains tables with TOAST storage parameters set, which is\n"
+ "not supported anymore. Consider remove the TOAST storage parameters using\n"
+ " ALTER TABLE ... RESET ( ... );\n"
+ "A list of tables with the problem is in the file:\n"
+ " %s", report.path);
+ }
+ else
+ check_ok();
+}
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..4415c1ca9b5 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -40,26 +40,22 @@ typedef enum relopt_kind
{
RELOPT_KIND_LOCAL = 0,
RELOPT_KIND_HEAP = (1 << 0),
- RELOPT_KIND_TOAST = (1 << 1),
- RELOPT_KIND_BTREE = (1 << 2),
- RELOPT_KIND_HASH = (1 << 3),
- RELOPT_KIND_GIN = (1 << 4),
- RELOPT_KIND_GIST = (1 << 5),
- RELOPT_KIND_ATTRIBUTE = (1 << 6),
- RELOPT_KIND_TABLESPACE = (1 << 7),
- RELOPT_KIND_SPGIST = (1 << 8),
- RELOPT_KIND_VIEW = (1 << 9),
- RELOPT_KIND_BRIN = (1 << 10),
- RELOPT_KIND_PARTITIONED = (1 << 11),
+ RELOPT_KIND_BTREE = (1 << 1),
+ RELOPT_KIND_HASH = (1 << 2),
+ RELOPT_KIND_GIN = (1 << 3),
+ RELOPT_KIND_GIST = (1 << 4),
+ RELOPT_KIND_ATTRIBUTE = (1 << 5),
+ RELOPT_KIND_TABLESPACE = (1 << 6),
+ RELOPT_KIND_SPGIST = (1 << 7),
+ RELOPT_KIND_VIEW = (1 << 8),
+ RELOPT_KIND_BRIN = (1 << 9),
+ RELOPT_KIND_PARTITIONED = (1 << 10),
/* if you add a new kind, make sure you update "last_default" too */
RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_PARTITIONED,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
-/* reloption namespaces allowed for heaps -- currently only TOAST */
-#define HEAP_RELOPT_NAMESPACES { "toast", NULL }
-
/* generic struct to hold shared data */
typedef struct relopt_gen
{
diff --git a/src/include/catalog/toasting.h b/src/include/catalog/toasting.h
index 0bc61a8fee9..13e5a8affcb 100644
--- a/src/include/catalog/toasting.h
+++ b/src/include/catalog/toasting.h
@@ -19,10 +19,10 @@
/*
* toasting.c prototypes
*/
-extern void NewRelationCreateToastTable(Oid relOid, Datum reloptions);
-extern void NewHeapCreateToastTable(Oid relOid, Datum reloptions,
+extern void NewRelationCreateToastTable(Oid relOid);
+extern void NewHeapCreateToastTable(Oid relOid,
LOCKMODE lockmode, Oid OIDOldToast);
-extern void AlterTableCreateToastTable(Oid relOid, Datum reloptions,
+extern void AlterTableCreateToastTable(Oid relOid,
LOCKMODE lockmode);
extern void BootstrapToastTable(char *relName,
Oid toastOid, Oid toastIndexOid);
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 6dd22be0e8d..435e6beea0f 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -2860,15 +2860,6 @@ select * from my_locks order by 1;
pg_toast | ShareUpdateExclusiveLock
(2 rows)
-commit;
-begin; alter table alterlock set (toast.autovacuum_enabled = off);
-select * from my_locks order by 1;
- relname | max_lockmode
------------+--------------------------
- alterlock | ShareUpdateExclusiveLock
- pg_toast | ShareUpdateExclusiveLock
-(2 rows)
-
commit;
begin; alter table alterlock set (autovacuum_enabled = off);
select * from my_locks order by 1;
diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index e3a974f2611..eff5fbece60 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -120,7 +120,6 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
DROP TABLE reloptions_test;
CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
WITH (vacuum_truncate=false,
- toast.vacuum_truncate=false,
autovacuum_enabled=false);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
reloptions
@@ -142,9 +141,9 @@ SELECT pg_relation_size('reloptions_test') > 0;
SELECT reloptions FROM pg_class WHERE oid =
(SELECT reltoastrelid FROM pg_class
WHERE oid = 'reloptions_test'::regclass);
- reloptions
--------------------------
- {vacuum_truncate=false}
+ reloptions
+------------
+
(1 row)
ALTER TABLE reloptions_test RESET (vacuum_truncate);
@@ -165,56 +164,11 @@ SELECT pg_relation_size('reloptions_test') = 0;
t
(1 row)
--- Test toast.* options
-DROP TABLE reloptions_test;
-CREATE TABLE reloptions_test (s VARCHAR)
- WITH (toast.autovacuum_vacuum_cost_delay = 23);
-SELECT reltoastrelid as toast_oid
- FROM pg_class WHERE oid = 'reloptions_test'::regclass \gset
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=23}
-(1 row)
-
-ALTER TABLE reloptions_test SET (toast.autovacuum_vacuum_cost_delay = 24);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=24}
-(1 row)
-
-ALTER TABLE reloptions_test RESET (toast.autovacuum_vacuum_cost_delay);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
-------------
-
-(1 row)
-
--- Fail on non-existent options in toast namespace
-CREATE TABLE reloptions_test2 (i int) WITH (toast.not_existing_option = 42);
-ERROR: unrecognized parameter "not_existing_option"
--- Mix TOAST & heap
-DROP TABLE reloptions_test;
-CREATE TABLE reloptions_test (s VARCHAR) WITH
- (toast.autovacuum_vacuum_cost_delay = 23,
- autovacuum_vacuum_cost_delay = 24, fillfactor = 40);
-SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
- reloptions
--------------------------------------------------
- {autovacuum_vacuum_cost_delay=24,fillfactor=40}
-(1 row)
-
-SELECT reloptions FROM pg_class WHERE oid = (
- SELECT reltoastrelid FROM pg_class WHERE oid = 'reloptions_test'::regclass);
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=23}
-(1 row)
-
--
-- CREATE INDEX, ALTER INDEX for btrees
--
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test (s VARCHAR);
CREATE INDEX reloptions_test_idx ON reloptions_test (s) WITH (fillfactor=30);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test_idx'::regclass;
reloptions
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index d4696bc3325..d551ef57d50 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -209,14 +209,6 @@ VACUUM no_index_cleanup;
INSERT INTO no_index_cleanup(i, t) VALUES (generate_series(31,60),
repeat('1234567890',269));
DELETE FROM no_index_cleanup WHERE i < 45;
--- Only toast index is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = off,
- toast.vacuum_index_cleanup = yes);
-VACUUM no_index_cleanup;
--- Only parent is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = true,
- toast.vacuum_index_cleanup = false);
-VACUUM no_index_cleanup;
-- Test some extra relations.
VACUUM (INDEX_CLEANUP FALSE) vaccluster;
VACUUM (INDEX_CLEANUP AUTO) vactst; -- index cleanup option is ignored if no indexes
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index f5f13bbd3e7..410465dc022 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -1784,10 +1784,6 @@ begin; alter table alterlock reset (fillfactor);
select * from my_locks order by 1;
commit;
-begin; alter table alterlock set (toast.autovacuum_enabled = off);
-select * from my_locks order by 1;
-commit;
-
begin; alter table alterlock set (autovacuum_enabled = off);
select * from my_locks order by 1;
commit;
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index 680c8bf8614..0809a2ae0e4 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -75,7 +75,6 @@ DROP TABLE reloptions_test;
CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
WITH (vacuum_truncate=false,
- toast.vacuum_truncate=false,
autovacuum_enabled=false);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
@@ -94,39 +93,12 @@ INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) reloptions_test;
SELECT pg_relation_size('reloptions_test') = 0;
--- Test toast.* options
-DROP TABLE reloptions_test;
-
-CREATE TABLE reloptions_test (s VARCHAR)
- WITH (toast.autovacuum_vacuum_cost_delay = 23);
-SELECT reltoastrelid as toast_oid
- FROM pg_class WHERE oid = 'reloptions_test'::regclass \gset
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
-ALTER TABLE reloptions_test SET (toast.autovacuum_vacuum_cost_delay = 24);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
-ALTER TABLE reloptions_test RESET (toast.autovacuum_vacuum_cost_delay);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
--- Fail on non-existent options in toast namespace
-CREATE TABLE reloptions_test2 (i int) WITH (toast.not_existing_option = 42);
-
--- Mix TOAST & heap
-DROP TABLE reloptions_test;
-
-CREATE TABLE reloptions_test (s VARCHAR) WITH
- (toast.autovacuum_vacuum_cost_delay = 23,
- autovacuum_vacuum_cost_delay = 24, fillfactor = 40);
-
-SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
-SELECT reloptions FROM pg_class WHERE oid = (
- SELECT reltoastrelid FROM pg_class WHERE oid = 'reloptions_test'::regclass);
-
--
-- CREATE INDEX, ALTER INDEX for btrees
--
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test (s VARCHAR);
CREATE INDEX reloptions_test_idx ON reloptions_test (s) WITH (fillfactor=30);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test_idx'::regclass;
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 247b8e23b23..37a84fd9145 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -175,14 +175,6 @@ VACUUM no_index_cleanup;
INSERT INTO no_index_cleanup(i, t) VALUES (generate_series(31,60),
repeat('1234567890',269));
DELETE FROM no_index_cleanup WHERE i < 45;
--- Only toast index is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = off,
- toast.vacuum_index_cleanup = yes);
-VACUUM no_index_cleanup;
--- Only parent is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = true,
- toast.vacuum_index_cleanup = false);
-VACUUM no_index_cleanup;
-- Test some extra relations.
VACUUM (INDEX_CLEANUP FALSE) vaccluster;
VACUUM (INDEX_CLEANUP AUTO) vactst; -- index cleanup option is ignored if no indexes
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-05-04 15:41 Nathan Bossart <nathandbossart@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2026-05-04 15:41 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: pgsql-hackers
On Fri, May 01, 2026 at 04:41:12PM -0500, Nathan Bossart wrote:
> On Tue, Jun 24, 2025 at 11:38:41AM -0500, Nathan Bossart wrote:
>> Yeah, I'm tempted to even ask how folks feel about removing the toast.*
>> reloptions. Maybe there's some simple cases that work well enough, but
>> AFAICT any moderately-complicated setup basically doesn't work at all. In
>> any case, writing out this patch set has got me on the fix-on-HEAD-only
>> bandwagon.
>
> For the sake of discussion, I wrote a patch for this. I suggested removing
> toast.* relopts to some colleagues recently, and nobody was aware of much
> (if any) use in the field. So maybe this isn't totally
> out-of-the-question...
Apparently I forgot to test it with injection points enabled. Here's a new
patch.
--
nathan
From 6f1f1fd8158b2927793e82f207661092f1dfc65f Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 7 Apr 2026 15:30:53 -0500
Subject: [PATCH v3 1/1] remove toast reloptions
---
doc/src/sgml/ref/create_table.sgml | 51 ++++++-------
src/backend/access/common/reloptions.c | 49 +++++-------
src/backend/catalog/toasting.c | 31 ++++----
src/backend/commands/createas.c | 13 +---
src/backend/commands/repack.c | 15 +---
src/backend/commands/tablecmds.c | 19 +----
src/backend/commands/vacuum.c | 14 +---
src/backend/postmaster/autovacuum.c | 48 +++++-------
src/backend/tcop/utility.c | 19 +----
src/bin/pg_upgrade/check.c | 75 +++++++++++++++++++
src/include/access/reloptions.h | 24 +++---
src/include/catalog/toasting.h | 6 +-
.../injection_points/expected/vacuum.out | 24 +++---
.../modules/injection_points/sql/vacuum.sql | 20 ++---
src/test/regress/expected/alter_table.out | 9 ---
src/test/regress/expected/reloptions.out | 56 ++------------
src/test/regress/expected/vacuum.out | 8 --
src/test/regress/sql/alter_table.sql | 4 -
src/test/regress/sql/reloptions.sql | 32 +-------
src/test/regress/sql/vacuum.sql | 8 --
20 files changed, 199 insertions(+), 326 deletions(-)
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e342585c7f0..b8d2a657d10 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1589,14 +1589,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Storage parameters for
indexes are documented in <xref linkend="sql-createindex"/>.
The storage parameters currently
- available for tables are listed below. For many of these parameters, as
- shown, there is an additional parameter with the same name prefixed with
- <literal>toast.</literal>, which controls the behavior of the
+ available for tables are listed below. Unless otherwise noted, a table's
+ parameter value also controls the behavior of the
table's secondary <acronym>TOAST</acronym> table, if any
(see <xref linkend="storage-toast"/> for more information about TOAST).
- If a table parameter value is set and the
- equivalent <literal>toast.</literal> parameter is not, the TOAST table
- will use the table's parameter value.
Specifying these parameters for partitioned tables is not supported,
but you may specify them for individual leaf partitions.
</para>
@@ -1622,7 +1618,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
updates</link> more likely.
For a table whose entries are never updated, complete packing is the
best choice, but in heavily updated tables smaller fillfactors are
- appropriate. This parameter cannot be set for TOAST tables.
+ appropriate. This parameter does not affect the table's secondary TOAST
+ table.
</para>
</listitem>
</varlistentry>
@@ -1648,7 +1645,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Note that the default setting is often close to optimal, and
it is possible that setting this parameter could have negative
effects in some cases.
- This parameter cannot be set for TOAST tables.
+ This parameter does not affect the table's secondary TOAST table.
</para>
</listitem>
</varlistentry>
@@ -1671,7 +1668,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-enabled" xreflabel="autovacuum_enabled">
- <term><literal>autovacuum_enabled</literal>, <literal>toast.autovacuum_enabled</literal> (<type>boolean</type>)
+ <term><literal>autovacuum_enabled</literal> (<type>boolean</type>)
<indexterm>
<primary><varname>autovacuum_enabled</varname> storage parameter</primary>
</indexterm>
@@ -1696,7 +1693,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-index-cleanup" xreflabel="vacuum_index_cleanup">
- <term><literal>vacuum_index_cleanup</literal>, <literal>toast.vacuum_index_cleanup</literal> (<type>enum</type>)
+ <term><literal>vacuum_index_cleanup</literal> (<type>enum</type>)
<indexterm>
<primary><varname>vacuum_index_cleanup</varname> storage parameter</primary>
</indexterm>
@@ -1722,7 +1719,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-truncate" xreflabel="vacuum_truncate">
- <term><literal>vacuum_truncate</literal>, <literal>toast.vacuum_truncate</literal> (<type>boolean</type>)
+ <term><literal>vacuum_truncate</literal> (<type>boolean</type>)
<indexterm>
<primary><varname>vacuum_truncate</varname></primary>
<secondary>storage parameter</secondary>
@@ -1755,7 +1752,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-threshold" xreflabel="autovacuum_vacuum_threshold">
- <term><literal>autovacuum_vacuum_threshold</literal>, <literal>toast.autovacuum_vacuum_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1770,7 +1767,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-max-threshold" xreflabel="autovacuum_vacuum_max_threshold">
- <term><literal>autovacuum_vacuum_max_threshold</literal>, <literal>toast.autovacuum_vacuum_max_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_max_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_max_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1785,7 +1782,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-scale-factor" xreflabel="autovacuum_vacuum_scale_factor">
- <term><literal>autovacuum_vacuum_scale_factor</literal>, <literal>toast.autovacuum_vacuum_scale_factor</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_scale_factor</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_scale_factor</varname> </primary>
<secondary>storage parameter</secondary>
@@ -1800,7 +1797,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-insert-threshold" xreflabel="autovacuum_vacuum_insert_threshold">
- <term><literal>autovacuum_vacuum_insert_threshold</literal>, <literal>toast.autovacuum_vacuum_insert_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_insert_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_insert_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1815,7 +1812,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-insert-scale-factor" xreflabel="autovacuum_vacuum_insert_scale_factor">
- <term><literal>autovacuum_vacuum_insert_scale_factor</literal>, <literal>toast.autovacuum_vacuum_insert_scale_factor</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_insert_scale_factor</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_insert_scale_factor</varname> </primary>
<secondary>storage parameter</secondary>
@@ -1860,7 +1857,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-cost-delay" xreflabel="autovacuum_vacuum_cost_delay">
- <term><literal>autovacuum_vacuum_cost_delay</literal>, <literal>toast.autovacuum_vacuum_cost_delay</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_cost_delay</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_cost_delay</varname></primary>
<secondary>storage parameter</secondary>
@@ -1875,7 +1872,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-cost-limit" xreflabel="autovacuum_vacuum_cost_limit">
- <term><literal>autovacuum_vacuum_cost_limit</literal>, <literal>toast.autovacuum_vacuum_cost_limit</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_cost_limit</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_cost_limit</varname></primary>
<secondary>storage parameter</secondary>
@@ -1890,7 +1887,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-min-age" xreflabel="autovacuum_freeze_min_age">
- <term><literal>autovacuum_freeze_min_age</literal>, <literal>toast.autovacuum_freeze_min_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_min_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_min_age</varname> storage parameter</primary>
</indexterm>
@@ -1907,7 +1904,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-max-age" xreflabel="autovacuum_freeze_max_age">
- <term><literal>autovacuum_freeze_max_age</literal>, <literal>toast.autovacuum_freeze_max_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_max_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_max_age</varname></primary>
<secondary>storage parameter</secondary>
@@ -1924,7 +1921,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-table-age" xreflabel="autovacuum_freeze_table_age">
- <term><literal>autovacuum_freeze_table_age</literal>, <literal>toast.autovacuum_freeze_table_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_table_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_table_age</varname> storage parameter</primary>
</indexterm>
@@ -1938,7 +1935,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-min-age" xreflabel="autovacuum_multixact_freeze_min_age">
- <term><literal>autovacuum_multixact_freeze_min_age</literal>, <literal>toast.autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_min_age</varname> storage parameter</primary>
</indexterm>
@@ -1956,7 +1953,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-max-age" xreflabel="autovacuum_multixact_freeze_max_age">
- <term><literal>autovacuum_multixact_freeze_max_age</literal>, <literal>toast.autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_max_age</varname></primary>
<secondary>storage parameter</secondary>
@@ -1975,7 +1972,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-table-age" xreflabel="autovacuum_multixact_freeze_table_age">
- <term><literal>autovacuum_multixact_freeze_table_age</literal>, <literal>toast.autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_table_age</varname> storage parameter</primary>
</indexterm>
@@ -1989,7 +1986,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-log-autovacuum-min-duration" xreflabel="log_autovacuum_min_duration">
- <term><literal>log_autovacuum_min_duration</literal>, <literal>toast.log_autovacuum_min_duration</literal> (<type>integer</type>)
+ <term><literal>log_autovacuum_min_duration</literal> (<type>integer</type>)
<indexterm>
<primary><varname>log_autovacuum_min_duration</varname></primary>
<secondary>storage parameter</secondary>
@@ -2019,7 +2016,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-max-eager-freeze-failure-rate" xreflabel="vacuum_max_eager_freeze_failure_rate">
- <term><literal>vacuum_max_eager_freeze_failure_rate</literal>, <literal>toast.vacuum_max_eager_freeze_failure_rate</literal> (<type>floating point</type>)
+ <term><literal>vacuum_max_eager_freeze_failure_rate</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>vacuum_max_eager_freeze_failure_rate</varname></primary>
<secondary>storage parameter</secondary>
@@ -2044,7 +2041,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Declare the table as an additional catalog table for purposes of
logical replication. See
<xref linkend="logicaldecoding-capabilities"/> for details.
- This parameter cannot be set for TOAST tables.
+ This parameter does not affect the table's secondary TOAST table.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..50d9805f3d8 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -111,7 +111,7 @@ static relopt_bool boolRelOpts[] =
{
"autovacuum_enabled",
"Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
true
@@ -172,7 +172,7 @@ static relopt_ternary ternaryRelOpts[] =
{
"vacuum_truncate",
"Enables vacuum to truncate empty pages at the end of this table",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}
},
@@ -249,7 +249,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_threshold",
"Minimum number of tuple updates or deletes prior to vacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, INT_MAX
@@ -258,7 +258,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_max_threshold",
"Maximum number of tuple updates or deletes prior to vacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-2, -1, INT_MAX
@@ -267,7 +267,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_insert_threshold",
"Minimum number of tuple inserts prior to vacuum, or -1 to disable insert vacuums",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-2, -1, INT_MAX
@@ -285,7 +285,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_cost_limit",
"Vacuum cost amount available before napping, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 1, 10000
@@ -294,7 +294,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_min_age",
"Minimum age at which VACUUM should freeze a table row, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, 1000000000
@@ -303,7 +303,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_min_age",
"Minimum multixact age at which VACUUM should freeze a row multixact's, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, 1000000000
@@ -312,7 +312,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_max_age",
"Age at which to autovacuum a table to prevent transaction ID wraparound",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 100000, 2000000000
@@ -321,7 +321,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_max_age",
"Multixact age at which to autovacuum a table to prevent multixact wraparound",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 10000, 2000000000
@@ -330,7 +330,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_table_age",
"Age at which VACUUM should perform a full table sweep to freeze row versions",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}, -1, 0, 2000000000
},
@@ -338,7 +338,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_table_age",
"Age of multixact at which VACUUM should perform a full table sweep to freeze row versions",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}, -1, 0, 2000000000
},
@@ -346,7 +346,7 @@ static relopt_int intRelOpts[] =
{
"log_autovacuum_min_duration",
"Sets the minimum execution time above which vacuum actions by autovacuum will be logged",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, -1, INT_MAX
@@ -424,7 +424,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_cost_delay",
"Vacuum cost delay in milliseconds, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -433,7 +433,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_scale_factor",
"Number of tuple updates or deletes prior to vacuum as a fraction of reltuples",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -442,7 +442,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_insert_scale_factor",
"Number of tuple inserts prior to vacuum as a fraction of reltuples",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -460,7 +460,7 @@ static relopt_real realRelOpts[] =
{
"vacuum_max_eager_freeze_failure_rate",
"Fraction of pages in a relation vacuum can scan and fail to freeze before disabling eager scanning.",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 1.0
@@ -554,7 +554,7 @@ static relopt_enum enumRelOpts[] =
{
"vacuum_index_cleanup",
"Controls index vacuuming and index cleanup",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
@@ -2163,21 +2163,8 @@ view_reloptions(Datum reloptions, bool validate)
bytea *
heap_reloptions(char relkind, Datum reloptions, bool validate)
{
- StdRdOptions *rdopts;
-
switch (relkind)
{
- case RELKIND_TOASTVALUE:
- rdopts = (StdRdOptions *)
- default_reloptions(reloptions, validate, RELOPT_KIND_TOAST);
- if (rdopts != NULL)
- {
- /* adjust default-only parameters for TOAST relations */
- rdopts->fillfactor = 100;
- rdopts->autovacuum.analyze_threshold = -1;
- rdopts->autovacuum.analyze_scale_factor = -1;
- }
- return (bytea *) rdopts;
case RELKIND_RELATION:
case RELKIND_MATVIEW:
return default_reloptions(reloptions, validate, RELOPT_KIND_HEAP);
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 4aa52a4bd25..9f1f822702c 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -34,11 +34,11 @@
#include "utils/rel.h"
#include "utils/syscache.h"
-static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
+static void CheckAndCreateToastTable(Oid relOid,
LOCKMODE lockmode, bool check,
Oid OIDOldToast);
static bool create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
- Datum reloptions, LOCKMODE lockmode, bool check,
+ LOCKMODE lockmode, bool check,
Oid OIDOldToast);
static bool needs_toast_table(Relation rel);
@@ -48,35 +48,30 @@ static bool needs_toast_table(Relation rel);
* If the table needs a toast table, and doesn't already have one,
* then create a toast table for it.
*
- * reloptions for the toast table can be passed, too. Pass (Datum) 0
- * for default reloptions.
- *
* We expect the caller to have verified that the relation is a table and have
* already done any necessary permission checks. Callers expect this function
* to end with CommandCounterIncrement if it makes any changes.
*/
void
-AlterTableCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode)
+AlterTableCreateToastTable(Oid relOid, LOCKMODE lockmode)
{
- CheckAndCreateToastTable(relOid, reloptions, lockmode, true, InvalidOid);
+ CheckAndCreateToastTable(relOid, lockmode, true, InvalidOid);
}
void
-NewHeapCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
- Oid OIDOldToast)
+NewHeapCreateToastTable(Oid relOid, LOCKMODE lockmode, Oid OIDOldToast)
{
- CheckAndCreateToastTable(relOid, reloptions, lockmode, false, OIDOldToast);
+ CheckAndCreateToastTable(relOid, lockmode, false, OIDOldToast);
}
void
-NewRelationCreateToastTable(Oid relOid, Datum reloptions)
+NewRelationCreateToastTable(Oid relOid)
{
- CheckAndCreateToastTable(relOid, reloptions, AccessExclusiveLock, false,
- InvalidOid);
+ CheckAndCreateToastTable(relOid, AccessExclusiveLock, false, InvalidOid);
}
static void
-CheckAndCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
+CheckAndCreateToastTable(Oid relOid, LOCKMODE lockmode,
bool check, Oid OIDOldToast)
{
Relation rel;
@@ -84,7 +79,7 @@ CheckAndCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
rel = table_open(relOid, lockmode);
/* create_toast_table does all the work */
- (void) create_toast_table(rel, InvalidOid, InvalidOid, reloptions, lockmode,
+ (void) create_toast_table(rel, InvalidOid, InvalidOid, lockmode,
check, OIDOldToast);
table_close(rel, NoLock);
@@ -108,7 +103,7 @@ BootstrapToastTable(char *relName, Oid toastOid, Oid toastIndexOid)
relName);
/* create_toast_table does all the work */
- if (!create_toast_table(rel, toastOid, toastIndexOid, (Datum) 0,
+ if (!create_toast_table(rel, toastOid, toastIndexOid,
AccessExclusiveLock, false, InvalidOid))
elog(ERROR, "\"%s\" does not require a toast table",
relName);
@@ -126,7 +121,7 @@ BootstrapToastTable(char *relName, Oid toastOid, Oid toastIndexOid)
*/
static bool
create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
- Datum reloptions, LOCKMODE lockmode, bool check,
+ LOCKMODE lockmode, bool check,
Oid OIDOldToast)
{
Oid relOid = RelationGetRelid(rel);
@@ -266,7 +261,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
shared_relation,
mapped_relation,
ONCOMMIT_NOOP,
- reloptions,
+ (Datum) 0,
false,
true,
true,
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 6dbb831ca89..188e167999b 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -84,8 +84,6 @@ create_ctas_internal(List *attrList, IntoClause *into)
CreateStmt *create = makeNode(CreateStmt);
bool is_matview;
char relkind;
- Datum toast_options;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
ObjectAddress intoRelationAddr;
/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
@@ -120,16 +118,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
*/
CommandCounterIncrement();
- /* parse and validate reloptions for the toast table */
- toast_options = transformRelOptions((Datum) 0,
- create->options,
- "toast",
- validnsps,
- true, false);
-
- (void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
-
- NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+ NewRelationCreateToastTable(intoRelationAddr.objectId);
/* Create the "view" part of a materialized view. */
if (is_matview)
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 9d162957bc3..5064d8e2349 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -1201,20 +1201,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
toastid = OldHeap->rd_rel->reltoastrelid;
if (OidIsValid(toastid))
- {
- /* keep the existing toast table's reloptions, if any */
- tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid));
- if (!HeapTupleIsValid(tuple))
- elog(ERROR, "cache lookup failed for relation %u", toastid);
- reloptions = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions,
- &isNull);
- if (isNull)
- reloptions = (Datum) 0;
-
- NewHeapCreateToastTable(OIDNewHeap, reloptions, lockmode, toastid);
-
- ReleaseSysCache(tuple);
- }
+ NewHeapCreateToastTable(OIDNewHeap, lockmode, toastid);
table_close(OldHeap, NoLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d8d7969bf30..a254509cbb5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -827,7 +827,6 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
ListCell *listptr;
AttrNumber attnum;
bool partitioned;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
Oid ofTypeId;
ObjectAddress address;
LOCKMODE parentLockmode;
@@ -976,7 +975,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
/*
* Parse and validate reloptions, if any.
*/
- reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, validnsps,
+ reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, NULL,
true, false);
switch (relkind)
@@ -5433,7 +5432,7 @@ ATRewriteCatalogs(List **wqueue, LOCKMODE lockmode,
tab->relkind == RELKIND_PARTITIONED_TABLE) &&
tab->partition_constraint == NULL) ||
tab->relkind == RELKIND_MATVIEW)
- AlterTableCreateToastTable(tab->relid, (Datum) 0, lockmode);
+ AlterTableCreateToastTable(tab->relid, lockmode);
}
}
@@ -16927,7 +16926,6 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
Datum repl_val[Natts_pg_class];
bool repl_null[Natts_pg_class];
bool repl_repl[Natts_pg_class];
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
if (defList == NIL && operation != AT_ReplaceRelOptions)
return; /* nothing to do */
@@ -16960,7 +16958,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
}
/* Generate new proposed reloptions (text array) */
- newOptions = transformRelOptions(datum, defList, NULL, validnsps, false,
+ newOptions = transformRelOptions(datum, defList, NULL, NULL, false,
operation == AT_ResetRelOptions);
/* Validate */
@@ -17083,20 +17081,11 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
datum = (Datum) 0;
}
- newOptions = transformRelOptions(datum, defList, "toast", validnsps,
- false, operation == AT_ResetRelOptions);
-
- (void) heap_reloptions(RELKIND_TOASTVALUE, newOptions, true);
-
memset(repl_val, 0, sizeof(repl_val));
memset(repl_null, false, sizeof(repl_null));
memset(repl_repl, false, sizeof(repl_repl));
- if (newOptions != (Datum) 0)
- repl_val[Anum_pg_class_reloptions - 1] = newOptions;
- else
- repl_null[Anum_pg_class_reloptions - 1] = true;
-
+ repl_null[Anum_pg_class_reloptions - 1] = true;
repl_repl[Anum_pg_class_reloptions - 1] = true;
newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass),
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 99d0db82ed7..ed670b3e3f1 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2015,13 +2015,6 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
Oid save_userid;
int save_sec_context;
int save_nestlevel;
- VacuumParams toast_vacuum_params;
-
- /*
- * This function scribbles on the parameters, so make a copy early to
- * avoid affecting the TOAST table (if we do end up recursing to it).
- */
- memcpy(&toast_vacuum_params, ¶ms, sizeof(VacuumParams));
/* Begin a transaction for vacuuming this relation */
StartTransactionCommand();
@@ -2336,11 +2329,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* relation. NB: This is only safe to do because we hold a session
* lock on the main relation that prevents concurrent deletion.
*/
- toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
- toast_vacuum_params.toast_parent = relid;
+ params.options |= VACOPT_PROCESS_MAIN;
+ params.toast_parent = relid;
- vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
- isTopLevel);
+ vacuum_rel(toast_relid, NULL, params, bstrategy, isTopLevel);
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a5a8db2ff88..c0e7cbf2cb8 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2139,8 +2139,9 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
- bool free_relopts = false;
+ av_relation *hentry;
+ bool found;
+ AutoVacOpts *relopts = NULL;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2154,22 +2155,10 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
- relopts = &hentry->ar_reloptions;
- }
+ /* Use reloptions from main rel. */
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ relopts = &hentry->ar_reloptions;
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2186,10 +2175,6 @@ do_autovacuum(void)
table->score = scores.max;
tables_to_process = lappend(tables_to_process, table);
}
-
- /* Release stuff to avoid leakage */
- if (free_relopts)
- pfree(relopts);
}
table_endscan(relScan);
@@ -2826,7 +2811,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
+ AutoVacOpts *avopts = NULL;
bool free_avopts = false;
AutoVacuumScores scores;
@@ -2838,13 +2823,15 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
/*
* Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * main table reloptions.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ if (classForm->relkind != RELKIND_TOASTVALUE)
+ {
+ avopts = extract_autovac_opts(classTup, pg_class_desc);
+ if (avopts)
+ free_avopts = true;
+ }
+ else if (table_toast_map)
{
av_relation *hentry;
bool found;
@@ -3127,8 +3114,7 @@ relation_needs_vacanalyze(Oid relid,
/*
* Determine vacuum/analyze equation parameters. We have two possible
- * sources: the passed reloptions (which could be a main table or a toast
- * table), or the autovacuum GUC variables.
+ * sources: the passed reloptions or the autovacuum GUC variables.
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..d089e02c216 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1159,8 +1159,6 @@ ProcessUtilitySlow(ParseState *pstate,
if (IsA(stmt, CreateStmt))
{
CreateStmt *cstmt = (CreateStmt *) stmt;
- Datum toast_options;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
/* Remember transformed RangeVar for LIKE */
table_rv = cstmt->relation;
@@ -1180,22 +1178,7 @@ ProcessUtilitySlow(ParseState *pstate,
*/
CommandCounterIncrement();
- /*
- * parse and validate reloptions for the toast
- * table
- */
- toast_options = transformRelOptions((Datum) 0,
- cstmt->options,
- "toast",
- validnsps,
- true,
- false);
- (void) heap_reloptions(RELKIND_TOASTVALUE,
- toast_options,
- true);
-
- NewRelationCreateToastTable(address.objectId,
- toast_options);
+ NewRelationCreateToastTable(address.objectId);
}
else if (IsA(stmt, CreateForeignTableStmt))
{
diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 5a7afe62eab..f0d15f56525 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -37,6 +37,7 @@ static void check_new_cluster_subscription_configuration(void);
static void check_old_cluster_for_valid_slots(void);
static void check_old_cluster_subscription_state(void);
static void check_old_cluster_global_names(ClusterInfo *cluster);
+static void check_for_toast_reloptions(ClusterInfo *cluster);
/*
* DataTypesUsageChecks - definitions of data type checks for the old cluster
@@ -634,6 +635,9 @@ check_and_dump_old_cluster(void)
check_for_prepared_transactions(&old_cluster);
check_for_isn_and_int8_passing_mismatch(&old_cluster);
+ if (GET_MAJOR_VERSION(old_cluster.major_version) < 2000)
+ check_for_toast_reloptions(&old_cluster);
+
if (GET_MAJOR_VERSION(old_cluster.major_version) >= 1700)
{
/*
@@ -2644,3 +2648,74 @@ check_old_cluster_global_names(ClusterInfo *cluster)
else
check_ok();
}
+
+/*
+ * Callback function for processing results of query for
+ * check_for_toast_reloptions()'s UpgradeTask. If the query returned any rows
+ * (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_toast_relopts_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+ UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+ int ntups = PQntuples(res);
+ int i_nspname = PQfnumber(res, "nspname");
+ int i_relname = PQfnumber(res, "relname");
+
+ if (ntups == 0)
+ return;
+
+ if (report->file == NULL &&
+ (report->file = fopen_priv(report->path, "w")) == NULL)
+ pg_fatal("could not open file \"%s\": %m", report->path);
+
+ fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+
+ for (int rowno = 0; rowno < ntups; rowno++)
+ fprintf(report->file, " %s.%s\n",
+ PQgetvalue(res, rowno, i_nspname),
+ PQgetvalue(res, rowno, i_relname));
+}
+
+/*
+ * Verify that no storage parameters (a.k.a. reloptions) are defined for TOAST
+ * tables.
+ */
+static void
+check_for_toast_reloptions(ClusterInfo *cluster)
+{
+ UpgradeTaskReport report;
+ UpgradeTask *task = upgrade_task_create();
+ const char *query = "SELECT n.nspname, c.relname "
+ "FROM pg_catalog.pg_class c, "
+ " pg_catalog.pg_class tc, "
+ " pg_catalog.pg_namespace n "
+ "WHERE c.reltoastrelid = tc.oid AND "
+ " c.relnamespace = c.oid AND "
+ " tc.reloptions IS NOT NULL";
+
+ prep_status("Check for tables with TOAST storage parameters");
+
+ report.file = NULL;
+ snprintf(report.path, sizeof(report.path), "%s/%s",
+ log_opts.basedir,
+ "tables_with_toast_storage_parameters.txt");
+
+ upgrade_task_add_step(task, query, process_toast_relopts_check,
+ true, &report);
+ upgrade_task_run(task, cluster);
+ upgrade_task_free(task);
+
+ if (report.file)
+ {
+ fclose(report.file);
+ pg_log(PG_REPORT, "fatal");
+ pg_fatal("Your installation contains tables with TOAST storage parameters set, which is\n"
+ "not supported anymore. Consider remove the TOAST storage parameters using\n"
+ " ALTER TABLE ... RESET ( ... );\n"
+ "A list of tables with the problem is in the file:\n"
+ " %s", report.path);
+ }
+ else
+ check_ok();
+}
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..4415c1ca9b5 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -40,26 +40,22 @@ typedef enum relopt_kind
{
RELOPT_KIND_LOCAL = 0,
RELOPT_KIND_HEAP = (1 << 0),
- RELOPT_KIND_TOAST = (1 << 1),
- RELOPT_KIND_BTREE = (1 << 2),
- RELOPT_KIND_HASH = (1 << 3),
- RELOPT_KIND_GIN = (1 << 4),
- RELOPT_KIND_GIST = (1 << 5),
- RELOPT_KIND_ATTRIBUTE = (1 << 6),
- RELOPT_KIND_TABLESPACE = (1 << 7),
- RELOPT_KIND_SPGIST = (1 << 8),
- RELOPT_KIND_VIEW = (1 << 9),
- RELOPT_KIND_BRIN = (1 << 10),
- RELOPT_KIND_PARTITIONED = (1 << 11),
+ RELOPT_KIND_BTREE = (1 << 1),
+ RELOPT_KIND_HASH = (1 << 2),
+ RELOPT_KIND_GIN = (1 << 3),
+ RELOPT_KIND_GIST = (1 << 4),
+ RELOPT_KIND_ATTRIBUTE = (1 << 5),
+ RELOPT_KIND_TABLESPACE = (1 << 6),
+ RELOPT_KIND_SPGIST = (1 << 7),
+ RELOPT_KIND_VIEW = (1 << 8),
+ RELOPT_KIND_BRIN = (1 << 9),
+ RELOPT_KIND_PARTITIONED = (1 << 10),
/* if you add a new kind, make sure you update "last_default" too */
RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_PARTITIONED,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
-/* reloption namespaces allowed for heaps -- currently only TOAST */
-#define HEAP_RELOPT_NAMESPACES { "toast", NULL }
-
/* generic struct to hold shared data */
typedef struct relopt_gen
{
diff --git a/src/include/catalog/toasting.h b/src/include/catalog/toasting.h
index 0bc61a8fee9..13e5a8affcb 100644
--- a/src/include/catalog/toasting.h
+++ b/src/include/catalog/toasting.h
@@ -19,10 +19,10 @@
/*
* toasting.c prototypes
*/
-extern void NewRelationCreateToastTable(Oid relOid, Datum reloptions);
-extern void NewHeapCreateToastTable(Oid relOid, Datum reloptions,
+extern void NewRelationCreateToastTable(Oid relOid);
+extern void NewHeapCreateToastTable(Oid relOid,
LOCKMODE lockmode, Oid OIDOldToast);
-extern void AlterTableCreateToastTable(Oid relOid, Datum reloptions,
+extern void AlterTableCreateToastTable(Oid relOid,
LOCKMODE lockmode);
extern void BootstrapToastTable(char *relName,
Oid toastOid, Oid toastIndexOid);
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..dcfa729d05e 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -43,29 +43,29 @@ SELECT injection_points_attach('vacuum-truncate-enabled', 'notice');
(1 row)
-- Check state of index_cleanup and truncate in VACUUM.
-CREATE TABLE vac_tab_on_toast_off(i int, j text) WITH
+CREATE TABLE vac_tab_on_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=true, toast.vacuum_index_cleanup=false,
- vacuum_truncate=true, toast.vacuum_truncate=false);
-CREATE TABLE vac_tab_off_toast_on(i int, j text) WITH
+ vacuum_index_cleanup=true,
+ vacuum_truncate=true);
+CREATE TABLE vac_tab_off_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=false, toast.vacuum_index_cleanup=true,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false);
-- Multiple relations should use their options in isolation.
-VACUUM vac_tab_on_toast_off, vac_tab_off_toast_on;
+VACUUM vac_tab_on_toast, vac_tab_off_toast;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-enabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-enabled
NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-disabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-disabled
-NOTICE: notice triggered for injection point vacuum-index-cleanup-enabled
-NOTICE: notice triggered for injection point vacuum-truncate-enabled
-- Check "auto" case of index_cleanup and "truncate" controlled by
-- its GUC.
CREATE TABLE vac_tab_auto(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=auto, toast.vacuum_index_cleanup=auto);
+ vacuum_index_cleanup=auto);
SET vacuum_truncate = false;
VACUUM vac_tab_auto;
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
@@ -80,8 +80,8 @@ NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
DROP TABLE vac_tab_auto;
-DROP TABLE vac_tab_on_toast_off;
-DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_on_toast;
+DROP TABLE vac_tab_off_toast;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..ee7a2e0566e 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -11,22 +11,22 @@ SELECT injection_points_attach('vacuum-truncate-disabled', 'notice');
SELECT injection_points_attach('vacuum-truncate-enabled', 'notice');
-- Check state of index_cleanup and truncate in VACUUM.
-CREATE TABLE vac_tab_on_toast_off(i int, j text) WITH
+CREATE TABLE vac_tab_on_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=true, toast.vacuum_index_cleanup=false,
- vacuum_truncate=true, toast.vacuum_truncate=false);
-CREATE TABLE vac_tab_off_toast_on(i int, j text) WITH
+ vacuum_index_cleanup=true,
+ vacuum_truncate=true);
+CREATE TABLE vac_tab_off_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=false, toast.vacuum_index_cleanup=true,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false);
-- Multiple relations should use their options in isolation.
-VACUUM vac_tab_on_toast_off, vac_tab_off_toast_on;
+VACUUM vac_tab_on_toast, vac_tab_off_toast;
-- Check "auto" case of index_cleanup and "truncate" controlled by
-- its GUC.
CREATE TABLE vac_tab_auto(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=auto, toast.vacuum_index_cleanup=auto);
+ vacuum_index_cleanup=auto);
SET vacuum_truncate = false;
VACUUM vac_tab_auto;
SET vacuum_truncate = true;
@@ -34,8 +34,8 @@ VACUUM vac_tab_auto;
RESET vacuum_truncate;
DROP TABLE vac_tab_auto;
-DROP TABLE vac_tab_on_toast_off;
-DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_on_toast;
+DROP TABLE vac_tab_off_toast;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 6dd22be0e8d..435e6beea0f 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -2860,15 +2860,6 @@ select * from my_locks order by 1;
pg_toast | ShareUpdateExclusiveLock
(2 rows)
-commit;
-begin; alter table alterlock set (toast.autovacuum_enabled = off);
-select * from my_locks order by 1;
- relname | max_lockmode
------------+--------------------------
- alterlock | ShareUpdateExclusiveLock
- pg_toast | ShareUpdateExclusiveLock
-(2 rows)
-
commit;
begin; alter table alterlock set (autovacuum_enabled = off);
select * from my_locks order by 1;
diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index e3a974f2611..eff5fbece60 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -120,7 +120,6 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
DROP TABLE reloptions_test;
CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
WITH (vacuum_truncate=false,
- toast.vacuum_truncate=false,
autovacuum_enabled=false);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
reloptions
@@ -142,9 +141,9 @@ SELECT pg_relation_size('reloptions_test') > 0;
SELECT reloptions FROM pg_class WHERE oid =
(SELECT reltoastrelid FROM pg_class
WHERE oid = 'reloptions_test'::regclass);
- reloptions
--------------------------
- {vacuum_truncate=false}
+ reloptions
+------------
+
(1 row)
ALTER TABLE reloptions_test RESET (vacuum_truncate);
@@ -165,56 +164,11 @@ SELECT pg_relation_size('reloptions_test') = 0;
t
(1 row)
--- Test toast.* options
-DROP TABLE reloptions_test;
-CREATE TABLE reloptions_test (s VARCHAR)
- WITH (toast.autovacuum_vacuum_cost_delay = 23);
-SELECT reltoastrelid as toast_oid
- FROM pg_class WHERE oid = 'reloptions_test'::regclass \gset
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=23}
-(1 row)
-
-ALTER TABLE reloptions_test SET (toast.autovacuum_vacuum_cost_delay = 24);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=24}
-(1 row)
-
-ALTER TABLE reloptions_test RESET (toast.autovacuum_vacuum_cost_delay);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
-------------
-
-(1 row)
-
--- Fail on non-existent options in toast namespace
-CREATE TABLE reloptions_test2 (i int) WITH (toast.not_existing_option = 42);
-ERROR: unrecognized parameter "not_existing_option"
--- Mix TOAST & heap
-DROP TABLE reloptions_test;
-CREATE TABLE reloptions_test (s VARCHAR) WITH
- (toast.autovacuum_vacuum_cost_delay = 23,
- autovacuum_vacuum_cost_delay = 24, fillfactor = 40);
-SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
- reloptions
--------------------------------------------------
- {autovacuum_vacuum_cost_delay=24,fillfactor=40}
-(1 row)
-
-SELECT reloptions FROM pg_class WHERE oid = (
- SELECT reltoastrelid FROM pg_class WHERE oid = 'reloptions_test'::regclass);
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=23}
-(1 row)
-
--
-- CREATE INDEX, ALTER INDEX for btrees
--
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test (s VARCHAR);
CREATE INDEX reloptions_test_idx ON reloptions_test (s) WITH (fillfactor=30);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test_idx'::regclass;
reloptions
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index d4696bc3325..d551ef57d50 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -209,14 +209,6 @@ VACUUM no_index_cleanup;
INSERT INTO no_index_cleanup(i, t) VALUES (generate_series(31,60),
repeat('1234567890',269));
DELETE FROM no_index_cleanup WHERE i < 45;
--- Only toast index is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = off,
- toast.vacuum_index_cleanup = yes);
-VACUUM no_index_cleanup;
--- Only parent is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = true,
- toast.vacuum_index_cleanup = false);
-VACUUM no_index_cleanup;
-- Test some extra relations.
VACUUM (INDEX_CLEANUP FALSE) vaccluster;
VACUUM (INDEX_CLEANUP AUTO) vactst; -- index cleanup option is ignored if no indexes
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index f5f13bbd3e7..410465dc022 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -1784,10 +1784,6 @@ begin; alter table alterlock reset (fillfactor);
select * from my_locks order by 1;
commit;
-begin; alter table alterlock set (toast.autovacuum_enabled = off);
-select * from my_locks order by 1;
-commit;
-
begin; alter table alterlock set (autovacuum_enabled = off);
select * from my_locks order by 1;
commit;
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index 680c8bf8614..0809a2ae0e4 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -75,7 +75,6 @@ DROP TABLE reloptions_test;
CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
WITH (vacuum_truncate=false,
- toast.vacuum_truncate=false,
autovacuum_enabled=false);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
@@ -94,39 +93,12 @@ INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) reloptions_test;
SELECT pg_relation_size('reloptions_test') = 0;
--- Test toast.* options
-DROP TABLE reloptions_test;
-
-CREATE TABLE reloptions_test (s VARCHAR)
- WITH (toast.autovacuum_vacuum_cost_delay = 23);
-SELECT reltoastrelid as toast_oid
- FROM pg_class WHERE oid = 'reloptions_test'::regclass \gset
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
-ALTER TABLE reloptions_test SET (toast.autovacuum_vacuum_cost_delay = 24);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
-ALTER TABLE reloptions_test RESET (toast.autovacuum_vacuum_cost_delay);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
--- Fail on non-existent options in toast namespace
-CREATE TABLE reloptions_test2 (i int) WITH (toast.not_existing_option = 42);
-
--- Mix TOAST & heap
-DROP TABLE reloptions_test;
-
-CREATE TABLE reloptions_test (s VARCHAR) WITH
- (toast.autovacuum_vacuum_cost_delay = 23,
- autovacuum_vacuum_cost_delay = 24, fillfactor = 40);
-
-SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
-SELECT reloptions FROM pg_class WHERE oid = (
- SELECT reltoastrelid FROM pg_class WHERE oid = 'reloptions_test'::regclass);
-
--
-- CREATE INDEX, ALTER INDEX for btrees
--
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test (s VARCHAR);
CREATE INDEX reloptions_test_idx ON reloptions_test (s) WITH (fillfactor=30);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test_idx'::regclass;
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 247b8e23b23..37a84fd9145 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -175,14 +175,6 @@ VACUUM no_index_cleanup;
INSERT INTO no_index_cleanup(i, t) VALUES (generate_series(31,60),
repeat('1234567890',269));
DELETE FROM no_index_cleanup WHERE i < 45;
--- Only toast index is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = off,
- toast.vacuum_index_cleanup = yes);
-VACUUM no_index_cleanup;
--- Only parent is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = true,
- toast.vacuum_index_cleanup = false);
-VACUUM no_index_cleanup;
-- Test some extra relations.
VACUUM (INDEX_CLEANUP FALSE) vaccluster;
VACUUM (INDEX_CLEANUP AUTO) vactst; -- index cleanup option is ignored if no indexes
--
2.50.1 (Apple Git-155)
Attachments:
[text/plain] v3-0001-remove-toast-reloptions.patch (51.8K, ../../afi-G3xIntN-pUjR@nathan/2-v3-0001-remove-toast-reloptions.patch)
download | inline diff:
From 6f1f1fd8158b2927793e82f207661092f1dfc65f Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 7 Apr 2026 15:30:53 -0500
Subject: [PATCH v3 1/1] remove toast reloptions
---
doc/src/sgml/ref/create_table.sgml | 51 ++++++-------
src/backend/access/common/reloptions.c | 49 +++++-------
src/backend/catalog/toasting.c | 31 ++++----
src/backend/commands/createas.c | 13 +---
src/backend/commands/repack.c | 15 +---
src/backend/commands/tablecmds.c | 19 +----
src/backend/commands/vacuum.c | 14 +---
src/backend/postmaster/autovacuum.c | 48 +++++-------
src/backend/tcop/utility.c | 19 +----
src/bin/pg_upgrade/check.c | 75 +++++++++++++++++++
src/include/access/reloptions.h | 24 +++---
src/include/catalog/toasting.h | 6 +-
.../injection_points/expected/vacuum.out | 24 +++---
.../modules/injection_points/sql/vacuum.sql | 20 ++---
src/test/regress/expected/alter_table.out | 9 ---
src/test/regress/expected/reloptions.out | 56 ++------------
src/test/regress/expected/vacuum.out | 8 --
src/test/regress/sql/alter_table.sql | 4 -
src/test/regress/sql/reloptions.sql | 32 +-------
src/test/regress/sql/vacuum.sql | 8 --
20 files changed, 199 insertions(+), 326 deletions(-)
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e342585c7f0..b8d2a657d10 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1589,14 +1589,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Storage parameters for
indexes are documented in <xref linkend="sql-createindex"/>.
The storage parameters currently
- available for tables are listed below. For many of these parameters, as
- shown, there is an additional parameter with the same name prefixed with
- <literal>toast.</literal>, which controls the behavior of the
+ available for tables are listed below. Unless otherwise noted, a table's
+ parameter value also controls the behavior of the
table's secondary <acronym>TOAST</acronym> table, if any
(see <xref linkend="storage-toast"/> for more information about TOAST).
- If a table parameter value is set and the
- equivalent <literal>toast.</literal> parameter is not, the TOAST table
- will use the table's parameter value.
Specifying these parameters for partitioned tables is not supported,
but you may specify them for individual leaf partitions.
</para>
@@ -1622,7 +1618,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
updates</link> more likely.
For a table whose entries are never updated, complete packing is the
best choice, but in heavily updated tables smaller fillfactors are
- appropriate. This parameter cannot be set for TOAST tables.
+ appropriate. This parameter does not affect the table's secondary TOAST
+ table.
</para>
</listitem>
</varlistentry>
@@ -1648,7 +1645,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Note that the default setting is often close to optimal, and
it is possible that setting this parameter could have negative
effects in some cases.
- This parameter cannot be set for TOAST tables.
+ This parameter does not affect the table's secondary TOAST table.
</para>
</listitem>
</varlistentry>
@@ -1671,7 +1668,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-enabled" xreflabel="autovacuum_enabled">
- <term><literal>autovacuum_enabled</literal>, <literal>toast.autovacuum_enabled</literal> (<type>boolean</type>)
+ <term><literal>autovacuum_enabled</literal> (<type>boolean</type>)
<indexterm>
<primary><varname>autovacuum_enabled</varname> storage parameter</primary>
</indexterm>
@@ -1696,7 +1693,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-index-cleanup" xreflabel="vacuum_index_cleanup">
- <term><literal>vacuum_index_cleanup</literal>, <literal>toast.vacuum_index_cleanup</literal> (<type>enum</type>)
+ <term><literal>vacuum_index_cleanup</literal> (<type>enum</type>)
<indexterm>
<primary><varname>vacuum_index_cleanup</varname> storage parameter</primary>
</indexterm>
@@ -1722,7 +1719,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-truncate" xreflabel="vacuum_truncate">
- <term><literal>vacuum_truncate</literal>, <literal>toast.vacuum_truncate</literal> (<type>boolean</type>)
+ <term><literal>vacuum_truncate</literal> (<type>boolean</type>)
<indexterm>
<primary><varname>vacuum_truncate</varname></primary>
<secondary>storage parameter</secondary>
@@ -1755,7 +1752,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-threshold" xreflabel="autovacuum_vacuum_threshold">
- <term><literal>autovacuum_vacuum_threshold</literal>, <literal>toast.autovacuum_vacuum_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1770,7 +1767,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-max-threshold" xreflabel="autovacuum_vacuum_max_threshold">
- <term><literal>autovacuum_vacuum_max_threshold</literal>, <literal>toast.autovacuum_vacuum_max_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_max_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_max_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1785,7 +1782,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-scale-factor" xreflabel="autovacuum_vacuum_scale_factor">
- <term><literal>autovacuum_vacuum_scale_factor</literal>, <literal>toast.autovacuum_vacuum_scale_factor</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_scale_factor</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_scale_factor</varname> </primary>
<secondary>storage parameter</secondary>
@@ -1800,7 +1797,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-insert-threshold" xreflabel="autovacuum_vacuum_insert_threshold">
- <term><literal>autovacuum_vacuum_insert_threshold</literal>, <literal>toast.autovacuum_vacuum_insert_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_insert_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_insert_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1815,7 +1812,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-insert-scale-factor" xreflabel="autovacuum_vacuum_insert_scale_factor">
- <term><literal>autovacuum_vacuum_insert_scale_factor</literal>, <literal>toast.autovacuum_vacuum_insert_scale_factor</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_insert_scale_factor</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_insert_scale_factor</varname> </primary>
<secondary>storage parameter</secondary>
@@ -1860,7 +1857,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-cost-delay" xreflabel="autovacuum_vacuum_cost_delay">
- <term><literal>autovacuum_vacuum_cost_delay</literal>, <literal>toast.autovacuum_vacuum_cost_delay</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_cost_delay</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_cost_delay</varname></primary>
<secondary>storage parameter</secondary>
@@ -1875,7 +1872,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-cost-limit" xreflabel="autovacuum_vacuum_cost_limit">
- <term><literal>autovacuum_vacuum_cost_limit</literal>, <literal>toast.autovacuum_vacuum_cost_limit</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_cost_limit</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_cost_limit</varname></primary>
<secondary>storage parameter</secondary>
@@ -1890,7 +1887,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-min-age" xreflabel="autovacuum_freeze_min_age">
- <term><literal>autovacuum_freeze_min_age</literal>, <literal>toast.autovacuum_freeze_min_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_min_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_min_age</varname> storage parameter</primary>
</indexterm>
@@ -1907,7 +1904,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-max-age" xreflabel="autovacuum_freeze_max_age">
- <term><literal>autovacuum_freeze_max_age</literal>, <literal>toast.autovacuum_freeze_max_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_max_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_max_age</varname></primary>
<secondary>storage parameter</secondary>
@@ -1924,7 +1921,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-table-age" xreflabel="autovacuum_freeze_table_age">
- <term><literal>autovacuum_freeze_table_age</literal>, <literal>toast.autovacuum_freeze_table_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_table_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_table_age</varname> storage parameter</primary>
</indexterm>
@@ -1938,7 +1935,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-min-age" xreflabel="autovacuum_multixact_freeze_min_age">
- <term><literal>autovacuum_multixact_freeze_min_age</literal>, <literal>toast.autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_min_age</varname> storage parameter</primary>
</indexterm>
@@ -1956,7 +1953,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-max-age" xreflabel="autovacuum_multixact_freeze_max_age">
- <term><literal>autovacuum_multixact_freeze_max_age</literal>, <literal>toast.autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_max_age</varname></primary>
<secondary>storage parameter</secondary>
@@ -1975,7 +1972,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-table-age" xreflabel="autovacuum_multixact_freeze_table_age">
- <term><literal>autovacuum_multixact_freeze_table_age</literal>, <literal>toast.autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_table_age</varname> storage parameter</primary>
</indexterm>
@@ -1989,7 +1986,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-log-autovacuum-min-duration" xreflabel="log_autovacuum_min_duration">
- <term><literal>log_autovacuum_min_duration</literal>, <literal>toast.log_autovacuum_min_duration</literal> (<type>integer</type>)
+ <term><literal>log_autovacuum_min_duration</literal> (<type>integer</type>)
<indexterm>
<primary><varname>log_autovacuum_min_duration</varname></primary>
<secondary>storage parameter</secondary>
@@ -2019,7 +2016,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-max-eager-freeze-failure-rate" xreflabel="vacuum_max_eager_freeze_failure_rate">
- <term><literal>vacuum_max_eager_freeze_failure_rate</literal>, <literal>toast.vacuum_max_eager_freeze_failure_rate</literal> (<type>floating point</type>)
+ <term><literal>vacuum_max_eager_freeze_failure_rate</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>vacuum_max_eager_freeze_failure_rate</varname></primary>
<secondary>storage parameter</secondary>
@@ -2044,7 +2041,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Declare the table as an additional catalog table for purposes of
logical replication. See
<xref linkend="logicaldecoding-capabilities"/> for details.
- This parameter cannot be set for TOAST tables.
+ This parameter does not affect the table's secondary TOAST table.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..50d9805f3d8 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -111,7 +111,7 @@ static relopt_bool boolRelOpts[] =
{
"autovacuum_enabled",
"Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
true
@@ -172,7 +172,7 @@ static relopt_ternary ternaryRelOpts[] =
{
"vacuum_truncate",
"Enables vacuum to truncate empty pages at the end of this table",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}
},
@@ -249,7 +249,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_threshold",
"Minimum number of tuple updates or deletes prior to vacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, INT_MAX
@@ -258,7 +258,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_max_threshold",
"Maximum number of tuple updates or deletes prior to vacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-2, -1, INT_MAX
@@ -267,7 +267,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_insert_threshold",
"Minimum number of tuple inserts prior to vacuum, or -1 to disable insert vacuums",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-2, -1, INT_MAX
@@ -285,7 +285,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_cost_limit",
"Vacuum cost amount available before napping, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 1, 10000
@@ -294,7 +294,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_min_age",
"Minimum age at which VACUUM should freeze a table row, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, 1000000000
@@ -303,7 +303,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_min_age",
"Minimum multixact age at which VACUUM should freeze a row multixact's, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, 1000000000
@@ -312,7 +312,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_max_age",
"Age at which to autovacuum a table to prevent transaction ID wraparound",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 100000, 2000000000
@@ -321,7 +321,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_max_age",
"Multixact age at which to autovacuum a table to prevent multixact wraparound",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 10000, 2000000000
@@ -330,7 +330,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_table_age",
"Age at which VACUUM should perform a full table sweep to freeze row versions",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}, -1, 0, 2000000000
},
@@ -338,7 +338,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_table_age",
"Age of multixact at which VACUUM should perform a full table sweep to freeze row versions",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}, -1, 0, 2000000000
},
@@ -346,7 +346,7 @@ static relopt_int intRelOpts[] =
{
"log_autovacuum_min_duration",
"Sets the minimum execution time above which vacuum actions by autovacuum will be logged",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, -1, INT_MAX
@@ -424,7 +424,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_cost_delay",
"Vacuum cost delay in milliseconds, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -433,7 +433,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_scale_factor",
"Number of tuple updates or deletes prior to vacuum as a fraction of reltuples",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -442,7 +442,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_insert_scale_factor",
"Number of tuple inserts prior to vacuum as a fraction of reltuples",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -460,7 +460,7 @@ static relopt_real realRelOpts[] =
{
"vacuum_max_eager_freeze_failure_rate",
"Fraction of pages in a relation vacuum can scan and fail to freeze before disabling eager scanning.",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 1.0
@@ -554,7 +554,7 @@ static relopt_enum enumRelOpts[] =
{
"vacuum_index_cleanup",
"Controls index vacuuming and index cleanup",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
@@ -2163,21 +2163,8 @@ view_reloptions(Datum reloptions, bool validate)
bytea *
heap_reloptions(char relkind, Datum reloptions, bool validate)
{
- StdRdOptions *rdopts;
-
switch (relkind)
{
- case RELKIND_TOASTVALUE:
- rdopts = (StdRdOptions *)
- default_reloptions(reloptions, validate, RELOPT_KIND_TOAST);
- if (rdopts != NULL)
- {
- /* adjust default-only parameters for TOAST relations */
- rdopts->fillfactor = 100;
- rdopts->autovacuum.analyze_threshold = -1;
- rdopts->autovacuum.analyze_scale_factor = -1;
- }
- return (bytea *) rdopts;
case RELKIND_RELATION:
case RELKIND_MATVIEW:
return default_reloptions(reloptions, validate, RELOPT_KIND_HEAP);
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 4aa52a4bd25..9f1f822702c 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -34,11 +34,11 @@
#include "utils/rel.h"
#include "utils/syscache.h"
-static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
+static void CheckAndCreateToastTable(Oid relOid,
LOCKMODE lockmode, bool check,
Oid OIDOldToast);
static bool create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
- Datum reloptions, LOCKMODE lockmode, bool check,
+ LOCKMODE lockmode, bool check,
Oid OIDOldToast);
static bool needs_toast_table(Relation rel);
@@ -48,35 +48,30 @@ static bool needs_toast_table(Relation rel);
* If the table needs a toast table, and doesn't already have one,
* then create a toast table for it.
*
- * reloptions for the toast table can be passed, too. Pass (Datum) 0
- * for default reloptions.
- *
* We expect the caller to have verified that the relation is a table and have
* already done any necessary permission checks. Callers expect this function
* to end with CommandCounterIncrement if it makes any changes.
*/
void
-AlterTableCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode)
+AlterTableCreateToastTable(Oid relOid, LOCKMODE lockmode)
{
- CheckAndCreateToastTable(relOid, reloptions, lockmode, true, InvalidOid);
+ CheckAndCreateToastTable(relOid, lockmode, true, InvalidOid);
}
void
-NewHeapCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
- Oid OIDOldToast)
+NewHeapCreateToastTable(Oid relOid, LOCKMODE lockmode, Oid OIDOldToast)
{
- CheckAndCreateToastTable(relOid, reloptions, lockmode, false, OIDOldToast);
+ CheckAndCreateToastTable(relOid, lockmode, false, OIDOldToast);
}
void
-NewRelationCreateToastTable(Oid relOid, Datum reloptions)
+NewRelationCreateToastTable(Oid relOid)
{
- CheckAndCreateToastTable(relOid, reloptions, AccessExclusiveLock, false,
- InvalidOid);
+ CheckAndCreateToastTable(relOid, AccessExclusiveLock, false, InvalidOid);
}
static void
-CheckAndCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
+CheckAndCreateToastTable(Oid relOid, LOCKMODE lockmode,
bool check, Oid OIDOldToast)
{
Relation rel;
@@ -84,7 +79,7 @@ CheckAndCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
rel = table_open(relOid, lockmode);
/* create_toast_table does all the work */
- (void) create_toast_table(rel, InvalidOid, InvalidOid, reloptions, lockmode,
+ (void) create_toast_table(rel, InvalidOid, InvalidOid, lockmode,
check, OIDOldToast);
table_close(rel, NoLock);
@@ -108,7 +103,7 @@ BootstrapToastTable(char *relName, Oid toastOid, Oid toastIndexOid)
relName);
/* create_toast_table does all the work */
- if (!create_toast_table(rel, toastOid, toastIndexOid, (Datum) 0,
+ if (!create_toast_table(rel, toastOid, toastIndexOid,
AccessExclusiveLock, false, InvalidOid))
elog(ERROR, "\"%s\" does not require a toast table",
relName);
@@ -126,7 +121,7 @@ BootstrapToastTable(char *relName, Oid toastOid, Oid toastIndexOid)
*/
static bool
create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
- Datum reloptions, LOCKMODE lockmode, bool check,
+ LOCKMODE lockmode, bool check,
Oid OIDOldToast)
{
Oid relOid = RelationGetRelid(rel);
@@ -266,7 +261,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
shared_relation,
mapped_relation,
ONCOMMIT_NOOP,
- reloptions,
+ (Datum) 0,
false,
true,
true,
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 6dbb831ca89..188e167999b 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -84,8 +84,6 @@ create_ctas_internal(List *attrList, IntoClause *into)
CreateStmt *create = makeNode(CreateStmt);
bool is_matview;
char relkind;
- Datum toast_options;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
ObjectAddress intoRelationAddr;
/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
@@ -120,16 +118,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
*/
CommandCounterIncrement();
- /* parse and validate reloptions for the toast table */
- toast_options = transformRelOptions((Datum) 0,
- create->options,
- "toast",
- validnsps,
- true, false);
-
- (void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
-
- NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+ NewRelationCreateToastTable(intoRelationAddr.objectId);
/* Create the "view" part of a materialized view. */
if (is_matview)
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 9d162957bc3..5064d8e2349 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -1201,20 +1201,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
toastid = OldHeap->rd_rel->reltoastrelid;
if (OidIsValid(toastid))
- {
- /* keep the existing toast table's reloptions, if any */
- tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid));
- if (!HeapTupleIsValid(tuple))
- elog(ERROR, "cache lookup failed for relation %u", toastid);
- reloptions = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions,
- &isNull);
- if (isNull)
- reloptions = (Datum) 0;
-
- NewHeapCreateToastTable(OIDNewHeap, reloptions, lockmode, toastid);
-
- ReleaseSysCache(tuple);
- }
+ NewHeapCreateToastTable(OIDNewHeap, lockmode, toastid);
table_close(OldHeap, NoLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d8d7969bf30..a254509cbb5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -827,7 +827,6 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
ListCell *listptr;
AttrNumber attnum;
bool partitioned;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
Oid ofTypeId;
ObjectAddress address;
LOCKMODE parentLockmode;
@@ -976,7 +975,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
/*
* Parse and validate reloptions, if any.
*/
- reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, validnsps,
+ reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, NULL,
true, false);
switch (relkind)
@@ -5433,7 +5432,7 @@ ATRewriteCatalogs(List **wqueue, LOCKMODE lockmode,
tab->relkind == RELKIND_PARTITIONED_TABLE) &&
tab->partition_constraint == NULL) ||
tab->relkind == RELKIND_MATVIEW)
- AlterTableCreateToastTable(tab->relid, (Datum) 0, lockmode);
+ AlterTableCreateToastTable(tab->relid, lockmode);
}
}
@@ -16927,7 +16926,6 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
Datum repl_val[Natts_pg_class];
bool repl_null[Natts_pg_class];
bool repl_repl[Natts_pg_class];
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
if (defList == NIL && operation != AT_ReplaceRelOptions)
return; /* nothing to do */
@@ -16960,7 +16958,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
}
/* Generate new proposed reloptions (text array) */
- newOptions = transformRelOptions(datum, defList, NULL, validnsps, false,
+ newOptions = transformRelOptions(datum, defList, NULL, NULL, false,
operation == AT_ResetRelOptions);
/* Validate */
@@ -17083,20 +17081,11 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
datum = (Datum) 0;
}
- newOptions = transformRelOptions(datum, defList, "toast", validnsps,
- false, operation == AT_ResetRelOptions);
-
- (void) heap_reloptions(RELKIND_TOASTVALUE, newOptions, true);
-
memset(repl_val, 0, sizeof(repl_val));
memset(repl_null, false, sizeof(repl_null));
memset(repl_repl, false, sizeof(repl_repl));
- if (newOptions != (Datum) 0)
- repl_val[Anum_pg_class_reloptions - 1] = newOptions;
- else
- repl_null[Anum_pg_class_reloptions - 1] = true;
-
+ repl_null[Anum_pg_class_reloptions - 1] = true;
repl_repl[Anum_pg_class_reloptions - 1] = true;
newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass),
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 99d0db82ed7..ed670b3e3f1 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2015,13 +2015,6 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
Oid save_userid;
int save_sec_context;
int save_nestlevel;
- VacuumParams toast_vacuum_params;
-
- /*
- * This function scribbles on the parameters, so make a copy early to
- * avoid affecting the TOAST table (if we do end up recursing to it).
- */
- memcpy(&toast_vacuum_params, ¶ms, sizeof(VacuumParams));
/* Begin a transaction for vacuuming this relation */
StartTransactionCommand();
@@ -2336,11 +2329,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* relation. NB: This is only safe to do because we hold a session
* lock on the main relation that prevents concurrent deletion.
*/
- toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
- toast_vacuum_params.toast_parent = relid;
+ params.options |= VACOPT_PROCESS_MAIN;
+ params.toast_parent = relid;
- vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
- isTopLevel);
+ vacuum_rel(toast_relid, NULL, params, bstrategy, isTopLevel);
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a5a8db2ff88..c0e7cbf2cb8 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2139,8 +2139,9 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
- bool free_relopts = false;
+ av_relation *hentry;
+ bool found;
+ AutoVacOpts *relopts = NULL;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2154,22 +2155,10 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
- relopts = &hentry->ar_reloptions;
- }
+ /* Use reloptions from main rel. */
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ relopts = &hentry->ar_reloptions;
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2186,10 +2175,6 @@ do_autovacuum(void)
table->score = scores.max;
tables_to_process = lappend(tables_to_process, table);
}
-
- /* Release stuff to avoid leakage */
- if (free_relopts)
- pfree(relopts);
}
table_endscan(relScan);
@@ -2826,7 +2811,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
+ AutoVacOpts *avopts = NULL;
bool free_avopts = false;
AutoVacuumScores scores;
@@ -2838,13 +2823,15 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
/*
* Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * main table reloptions.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ if (classForm->relkind != RELKIND_TOASTVALUE)
+ {
+ avopts = extract_autovac_opts(classTup, pg_class_desc);
+ if (avopts)
+ free_avopts = true;
+ }
+ else if (table_toast_map)
{
av_relation *hentry;
bool found;
@@ -3127,8 +3114,7 @@ relation_needs_vacanalyze(Oid relid,
/*
* Determine vacuum/analyze equation parameters. We have two possible
- * sources: the passed reloptions (which could be a main table or a toast
- * table), or the autovacuum GUC variables.
+ * sources: the passed reloptions or the autovacuum GUC variables.
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..d089e02c216 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1159,8 +1159,6 @@ ProcessUtilitySlow(ParseState *pstate,
if (IsA(stmt, CreateStmt))
{
CreateStmt *cstmt = (CreateStmt *) stmt;
- Datum toast_options;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
/* Remember transformed RangeVar for LIKE */
table_rv = cstmt->relation;
@@ -1180,22 +1178,7 @@ ProcessUtilitySlow(ParseState *pstate,
*/
CommandCounterIncrement();
- /*
- * parse and validate reloptions for the toast
- * table
- */
- toast_options = transformRelOptions((Datum) 0,
- cstmt->options,
- "toast",
- validnsps,
- true,
- false);
- (void) heap_reloptions(RELKIND_TOASTVALUE,
- toast_options,
- true);
-
- NewRelationCreateToastTable(address.objectId,
- toast_options);
+ NewRelationCreateToastTable(address.objectId);
}
else if (IsA(stmt, CreateForeignTableStmt))
{
diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 5a7afe62eab..f0d15f56525 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -37,6 +37,7 @@ static void check_new_cluster_subscription_configuration(void);
static void check_old_cluster_for_valid_slots(void);
static void check_old_cluster_subscription_state(void);
static void check_old_cluster_global_names(ClusterInfo *cluster);
+static void check_for_toast_reloptions(ClusterInfo *cluster);
/*
* DataTypesUsageChecks - definitions of data type checks for the old cluster
@@ -634,6 +635,9 @@ check_and_dump_old_cluster(void)
check_for_prepared_transactions(&old_cluster);
check_for_isn_and_int8_passing_mismatch(&old_cluster);
+ if (GET_MAJOR_VERSION(old_cluster.major_version) < 2000)
+ check_for_toast_reloptions(&old_cluster);
+
if (GET_MAJOR_VERSION(old_cluster.major_version) >= 1700)
{
/*
@@ -2644,3 +2648,74 @@ check_old_cluster_global_names(ClusterInfo *cluster)
else
check_ok();
}
+
+/*
+ * Callback function for processing results of query for
+ * check_for_toast_reloptions()'s UpgradeTask. If the query returned any rows
+ * (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_toast_relopts_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+ UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+ int ntups = PQntuples(res);
+ int i_nspname = PQfnumber(res, "nspname");
+ int i_relname = PQfnumber(res, "relname");
+
+ if (ntups == 0)
+ return;
+
+ if (report->file == NULL &&
+ (report->file = fopen_priv(report->path, "w")) == NULL)
+ pg_fatal("could not open file \"%s\": %m", report->path);
+
+ fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+
+ for (int rowno = 0; rowno < ntups; rowno++)
+ fprintf(report->file, " %s.%s\n",
+ PQgetvalue(res, rowno, i_nspname),
+ PQgetvalue(res, rowno, i_relname));
+}
+
+/*
+ * Verify that no storage parameters (a.k.a. reloptions) are defined for TOAST
+ * tables.
+ */
+static void
+check_for_toast_reloptions(ClusterInfo *cluster)
+{
+ UpgradeTaskReport report;
+ UpgradeTask *task = upgrade_task_create();
+ const char *query = "SELECT n.nspname, c.relname "
+ "FROM pg_catalog.pg_class c, "
+ " pg_catalog.pg_class tc, "
+ " pg_catalog.pg_namespace n "
+ "WHERE c.reltoastrelid = tc.oid AND "
+ " c.relnamespace = c.oid AND "
+ " tc.reloptions IS NOT NULL";
+
+ prep_status("Check for tables with TOAST storage parameters");
+
+ report.file = NULL;
+ snprintf(report.path, sizeof(report.path), "%s/%s",
+ log_opts.basedir,
+ "tables_with_toast_storage_parameters.txt");
+
+ upgrade_task_add_step(task, query, process_toast_relopts_check,
+ true, &report);
+ upgrade_task_run(task, cluster);
+ upgrade_task_free(task);
+
+ if (report.file)
+ {
+ fclose(report.file);
+ pg_log(PG_REPORT, "fatal");
+ pg_fatal("Your installation contains tables with TOAST storage parameters set, which is\n"
+ "not supported anymore. Consider remove the TOAST storage parameters using\n"
+ " ALTER TABLE ... RESET ( ... );\n"
+ "A list of tables with the problem is in the file:\n"
+ " %s", report.path);
+ }
+ else
+ check_ok();
+}
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..4415c1ca9b5 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -40,26 +40,22 @@ typedef enum relopt_kind
{
RELOPT_KIND_LOCAL = 0,
RELOPT_KIND_HEAP = (1 << 0),
- RELOPT_KIND_TOAST = (1 << 1),
- RELOPT_KIND_BTREE = (1 << 2),
- RELOPT_KIND_HASH = (1 << 3),
- RELOPT_KIND_GIN = (1 << 4),
- RELOPT_KIND_GIST = (1 << 5),
- RELOPT_KIND_ATTRIBUTE = (1 << 6),
- RELOPT_KIND_TABLESPACE = (1 << 7),
- RELOPT_KIND_SPGIST = (1 << 8),
- RELOPT_KIND_VIEW = (1 << 9),
- RELOPT_KIND_BRIN = (1 << 10),
- RELOPT_KIND_PARTITIONED = (1 << 11),
+ RELOPT_KIND_BTREE = (1 << 1),
+ RELOPT_KIND_HASH = (1 << 2),
+ RELOPT_KIND_GIN = (1 << 3),
+ RELOPT_KIND_GIST = (1 << 4),
+ RELOPT_KIND_ATTRIBUTE = (1 << 5),
+ RELOPT_KIND_TABLESPACE = (1 << 6),
+ RELOPT_KIND_SPGIST = (1 << 7),
+ RELOPT_KIND_VIEW = (1 << 8),
+ RELOPT_KIND_BRIN = (1 << 9),
+ RELOPT_KIND_PARTITIONED = (1 << 10),
/* if you add a new kind, make sure you update "last_default" too */
RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_PARTITIONED,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
-/* reloption namespaces allowed for heaps -- currently only TOAST */
-#define HEAP_RELOPT_NAMESPACES { "toast", NULL }
-
/* generic struct to hold shared data */
typedef struct relopt_gen
{
diff --git a/src/include/catalog/toasting.h b/src/include/catalog/toasting.h
index 0bc61a8fee9..13e5a8affcb 100644
--- a/src/include/catalog/toasting.h
+++ b/src/include/catalog/toasting.h
@@ -19,10 +19,10 @@
/*
* toasting.c prototypes
*/
-extern void NewRelationCreateToastTable(Oid relOid, Datum reloptions);
-extern void NewHeapCreateToastTable(Oid relOid, Datum reloptions,
+extern void NewRelationCreateToastTable(Oid relOid);
+extern void NewHeapCreateToastTable(Oid relOid,
LOCKMODE lockmode, Oid OIDOldToast);
-extern void AlterTableCreateToastTable(Oid relOid, Datum reloptions,
+extern void AlterTableCreateToastTable(Oid relOid,
LOCKMODE lockmode);
extern void BootstrapToastTable(char *relName,
Oid toastOid, Oid toastIndexOid);
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..dcfa729d05e 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -43,29 +43,29 @@ SELECT injection_points_attach('vacuum-truncate-enabled', 'notice');
(1 row)
-- Check state of index_cleanup and truncate in VACUUM.
-CREATE TABLE vac_tab_on_toast_off(i int, j text) WITH
+CREATE TABLE vac_tab_on_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=true, toast.vacuum_index_cleanup=false,
- vacuum_truncate=true, toast.vacuum_truncate=false);
-CREATE TABLE vac_tab_off_toast_on(i int, j text) WITH
+ vacuum_index_cleanup=true,
+ vacuum_truncate=true);
+CREATE TABLE vac_tab_off_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=false, toast.vacuum_index_cleanup=true,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false);
-- Multiple relations should use their options in isolation.
-VACUUM vac_tab_on_toast_off, vac_tab_off_toast_on;
+VACUUM vac_tab_on_toast, vac_tab_off_toast;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-enabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-enabled
NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-disabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-disabled
-NOTICE: notice triggered for injection point vacuum-index-cleanup-enabled
-NOTICE: notice triggered for injection point vacuum-truncate-enabled
-- Check "auto" case of index_cleanup and "truncate" controlled by
-- its GUC.
CREATE TABLE vac_tab_auto(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=auto, toast.vacuum_index_cleanup=auto);
+ vacuum_index_cleanup=auto);
SET vacuum_truncate = false;
VACUUM vac_tab_auto;
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
@@ -80,8 +80,8 @@ NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
DROP TABLE vac_tab_auto;
-DROP TABLE vac_tab_on_toast_off;
-DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_on_toast;
+DROP TABLE vac_tab_off_toast;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..ee7a2e0566e 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -11,22 +11,22 @@ SELECT injection_points_attach('vacuum-truncate-disabled', 'notice');
SELECT injection_points_attach('vacuum-truncate-enabled', 'notice');
-- Check state of index_cleanup and truncate in VACUUM.
-CREATE TABLE vac_tab_on_toast_off(i int, j text) WITH
+CREATE TABLE vac_tab_on_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=true, toast.vacuum_index_cleanup=false,
- vacuum_truncate=true, toast.vacuum_truncate=false);
-CREATE TABLE vac_tab_off_toast_on(i int, j text) WITH
+ vacuum_index_cleanup=true,
+ vacuum_truncate=true);
+CREATE TABLE vac_tab_off_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=false, toast.vacuum_index_cleanup=true,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false);
-- Multiple relations should use their options in isolation.
-VACUUM vac_tab_on_toast_off, vac_tab_off_toast_on;
+VACUUM vac_tab_on_toast, vac_tab_off_toast;
-- Check "auto" case of index_cleanup and "truncate" controlled by
-- its GUC.
CREATE TABLE vac_tab_auto(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=auto, toast.vacuum_index_cleanup=auto);
+ vacuum_index_cleanup=auto);
SET vacuum_truncate = false;
VACUUM vac_tab_auto;
SET vacuum_truncate = true;
@@ -34,8 +34,8 @@ VACUUM vac_tab_auto;
RESET vacuum_truncate;
DROP TABLE vac_tab_auto;
-DROP TABLE vac_tab_on_toast_off;
-DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_on_toast;
+DROP TABLE vac_tab_off_toast;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 6dd22be0e8d..435e6beea0f 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -2860,15 +2860,6 @@ select * from my_locks order by 1;
pg_toast | ShareUpdateExclusiveLock
(2 rows)
-commit;
-begin; alter table alterlock set (toast.autovacuum_enabled = off);
-select * from my_locks order by 1;
- relname | max_lockmode
------------+--------------------------
- alterlock | ShareUpdateExclusiveLock
- pg_toast | ShareUpdateExclusiveLock
-(2 rows)
-
commit;
begin; alter table alterlock set (autovacuum_enabled = off);
select * from my_locks order by 1;
diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index e3a974f2611..eff5fbece60 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -120,7 +120,6 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
DROP TABLE reloptions_test;
CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
WITH (vacuum_truncate=false,
- toast.vacuum_truncate=false,
autovacuum_enabled=false);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
reloptions
@@ -142,9 +141,9 @@ SELECT pg_relation_size('reloptions_test') > 0;
SELECT reloptions FROM pg_class WHERE oid =
(SELECT reltoastrelid FROM pg_class
WHERE oid = 'reloptions_test'::regclass);
- reloptions
--------------------------
- {vacuum_truncate=false}
+ reloptions
+------------
+
(1 row)
ALTER TABLE reloptions_test RESET (vacuum_truncate);
@@ -165,56 +164,11 @@ SELECT pg_relation_size('reloptions_test') = 0;
t
(1 row)
--- Test toast.* options
-DROP TABLE reloptions_test;
-CREATE TABLE reloptions_test (s VARCHAR)
- WITH (toast.autovacuum_vacuum_cost_delay = 23);
-SELECT reltoastrelid as toast_oid
- FROM pg_class WHERE oid = 'reloptions_test'::regclass \gset
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=23}
-(1 row)
-
-ALTER TABLE reloptions_test SET (toast.autovacuum_vacuum_cost_delay = 24);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=24}
-(1 row)
-
-ALTER TABLE reloptions_test RESET (toast.autovacuum_vacuum_cost_delay);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
-------------
-
-(1 row)
-
--- Fail on non-existent options in toast namespace
-CREATE TABLE reloptions_test2 (i int) WITH (toast.not_existing_option = 42);
-ERROR: unrecognized parameter "not_existing_option"
--- Mix TOAST & heap
-DROP TABLE reloptions_test;
-CREATE TABLE reloptions_test (s VARCHAR) WITH
- (toast.autovacuum_vacuum_cost_delay = 23,
- autovacuum_vacuum_cost_delay = 24, fillfactor = 40);
-SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
- reloptions
--------------------------------------------------
- {autovacuum_vacuum_cost_delay=24,fillfactor=40}
-(1 row)
-
-SELECT reloptions FROM pg_class WHERE oid = (
- SELECT reltoastrelid FROM pg_class WHERE oid = 'reloptions_test'::regclass);
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=23}
-(1 row)
-
--
-- CREATE INDEX, ALTER INDEX for btrees
--
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test (s VARCHAR);
CREATE INDEX reloptions_test_idx ON reloptions_test (s) WITH (fillfactor=30);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test_idx'::regclass;
reloptions
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index d4696bc3325..d551ef57d50 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -209,14 +209,6 @@ VACUUM no_index_cleanup;
INSERT INTO no_index_cleanup(i, t) VALUES (generate_series(31,60),
repeat('1234567890',269));
DELETE FROM no_index_cleanup WHERE i < 45;
--- Only toast index is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = off,
- toast.vacuum_index_cleanup = yes);
-VACUUM no_index_cleanup;
--- Only parent is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = true,
- toast.vacuum_index_cleanup = false);
-VACUUM no_index_cleanup;
-- Test some extra relations.
VACUUM (INDEX_CLEANUP FALSE) vaccluster;
VACUUM (INDEX_CLEANUP AUTO) vactst; -- index cleanup option is ignored if no indexes
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index f5f13bbd3e7..410465dc022 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -1784,10 +1784,6 @@ begin; alter table alterlock reset (fillfactor);
select * from my_locks order by 1;
commit;
-begin; alter table alterlock set (toast.autovacuum_enabled = off);
-select * from my_locks order by 1;
-commit;
-
begin; alter table alterlock set (autovacuum_enabled = off);
select * from my_locks order by 1;
commit;
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index 680c8bf8614..0809a2ae0e4 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -75,7 +75,6 @@ DROP TABLE reloptions_test;
CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
WITH (vacuum_truncate=false,
- toast.vacuum_truncate=false,
autovacuum_enabled=false);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
@@ -94,39 +93,12 @@ INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) reloptions_test;
SELECT pg_relation_size('reloptions_test') = 0;
--- Test toast.* options
-DROP TABLE reloptions_test;
-
-CREATE TABLE reloptions_test (s VARCHAR)
- WITH (toast.autovacuum_vacuum_cost_delay = 23);
-SELECT reltoastrelid as toast_oid
- FROM pg_class WHERE oid = 'reloptions_test'::regclass \gset
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
-ALTER TABLE reloptions_test SET (toast.autovacuum_vacuum_cost_delay = 24);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
-ALTER TABLE reloptions_test RESET (toast.autovacuum_vacuum_cost_delay);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
--- Fail on non-existent options in toast namespace
-CREATE TABLE reloptions_test2 (i int) WITH (toast.not_existing_option = 42);
-
--- Mix TOAST & heap
-DROP TABLE reloptions_test;
-
-CREATE TABLE reloptions_test (s VARCHAR) WITH
- (toast.autovacuum_vacuum_cost_delay = 23,
- autovacuum_vacuum_cost_delay = 24, fillfactor = 40);
-
-SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
-SELECT reloptions FROM pg_class WHERE oid = (
- SELECT reltoastrelid FROM pg_class WHERE oid = 'reloptions_test'::regclass);
-
--
-- CREATE INDEX, ALTER INDEX for btrees
--
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test (s VARCHAR);
CREATE INDEX reloptions_test_idx ON reloptions_test (s) WITH (fillfactor=30);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test_idx'::regclass;
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 247b8e23b23..37a84fd9145 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -175,14 +175,6 @@ VACUUM no_index_cleanup;
INSERT INTO no_index_cleanup(i, t) VALUES (generate_series(31,60),
repeat('1234567890',269));
DELETE FROM no_index_cleanup WHERE i < 45;
--- Only toast index is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = off,
- toast.vacuum_index_cleanup = yes);
-VACUUM no_index_cleanup;
--- Only parent is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = true,
- toast.vacuum_index_cleanup = false);
-VACUUM no_index_cleanup;
-- Test some extra relations.
VACUUM (INDEX_CLEANUP FALSE) vaccluster;
VACUUM (INDEX_CLEANUP AUTO) vactst; -- index cleanup option is ignored if no indexes
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-05-06 21:31 Nathan Bossart <nathandbossart@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2026-05-06 21:31 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: pgsql-hackers
On Mon, May 04, 2026 at 10:41:15AM -0500, Nathan Bossart wrote:
> Apparently I forgot to test it with injection points enabled. Here's a new
> patch.
v4 fixes a small bug in pg_upgrade's new check query.
--
nathan
From 0d8df9cbcd0dbf21815d95c6422f3a216a7e15ce Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 7 Apr 2026 15:30:53 -0500
Subject: [PATCH v4 1/1] remove toast reloptions
---
doc/src/sgml/ref/create_table.sgml | 51 ++++++-------
src/backend/access/common/reloptions.c | 49 +++++-------
src/backend/catalog/toasting.c | 31 ++++----
src/backend/commands/createas.c | 13 +---
src/backend/commands/repack.c | 15 +---
src/backend/commands/tablecmds.c | 19 +----
src/backend/commands/vacuum.c | 14 +---
src/backend/postmaster/autovacuum.c | 48 +++++-------
src/backend/tcop/utility.c | 19 +----
src/bin/pg_upgrade/check.c | 75 +++++++++++++++++++
src/include/access/reloptions.h | 24 +++---
src/include/catalog/toasting.h | 6 +-
.../injection_points/expected/vacuum.out | 24 +++---
.../modules/injection_points/sql/vacuum.sql | 20 ++---
src/test/regress/expected/alter_table.out | 9 ---
src/test/regress/expected/reloptions.out | 56 ++------------
src/test/regress/expected/vacuum.out | 8 --
src/test/regress/sql/alter_table.sql | 4 -
src/test/regress/sql/reloptions.sql | 32 +-------
src/test/regress/sql/vacuum.sql | 8 --
20 files changed, 199 insertions(+), 326 deletions(-)
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e342585c7f0..b8d2a657d10 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1589,14 +1589,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Storage parameters for
indexes are documented in <xref linkend="sql-createindex"/>.
The storage parameters currently
- available for tables are listed below. For many of these parameters, as
- shown, there is an additional parameter with the same name prefixed with
- <literal>toast.</literal>, which controls the behavior of the
+ available for tables are listed below. Unless otherwise noted, a table's
+ parameter value also controls the behavior of the
table's secondary <acronym>TOAST</acronym> table, if any
(see <xref linkend="storage-toast"/> for more information about TOAST).
- If a table parameter value is set and the
- equivalent <literal>toast.</literal> parameter is not, the TOAST table
- will use the table's parameter value.
Specifying these parameters for partitioned tables is not supported,
but you may specify them for individual leaf partitions.
</para>
@@ -1622,7 +1618,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
updates</link> more likely.
For a table whose entries are never updated, complete packing is the
best choice, but in heavily updated tables smaller fillfactors are
- appropriate. This parameter cannot be set for TOAST tables.
+ appropriate. This parameter does not affect the table's secondary TOAST
+ table.
</para>
</listitem>
</varlistentry>
@@ -1648,7 +1645,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Note that the default setting is often close to optimal, and
it is possible that setting this parameter could have negative
effects in some cases.
- This parameter cannot be set for TOAST tables.
+ This parameter does not affect the table's secondary TOAST table.
</para>
</listitem>
</varlistentry>
@@ -1671,7 +1668,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-enabled" xreflabel="autovacuum_enabled">
- <term><literal>autovacuum_enabled</literal>, <literal>toast.autovacuum_enabled</literal> (<type>boolean</type>)
+ <term><literal>autovacuum_enabled</literal> (<type>boolean</type>)
<indexterm>
<primary><varname>autovacuum_enabled</varname> storage parameter</primary>
</indexterm>
@@ -1696,7 +1693,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-index-cleanup" xreflabel="vacuum_index_cleanup">
- <term><literal>vacuum_index_cleanup</literal>, <literal>toast.vacuum_index_cleanup</literal> (<type>enum</type>)
+ <term><literal>vacuum_index_cleanup</literal> (<type>enum</type>)
<indexterm>
<primary><varname>vacuum_index_cleanup</varname> storage parameter</primary>
</indexterm>
@@ -1722,7 +1719,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-truncate" xreflabel="vacuum_truncate">
- <term><literal>vacuum_truncate</literal>, <literal>toast.vacuum_truncate</literal> (<type>boolean</type>)
+ <term><literal>vacuum_truncate</literal> (<type>boolean</type>)
<indexterm>
<primary><varname>vacuum_truncate</varname></primary>
<secondary>storage parameter</secondary>
@@ -1755,7 +1752,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-threshold" xreflabel="autovacuum_vacuum_threshold">
- <term><literal>autovacuum_vacuum_threshold</literal>, <literal>toast.autovacuum_vacuum_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1770,7 +1767,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-max-threshold" xreflabel="autovacuum_vacuum_max_threshold">
- <term><literal>autovacuum_vacuum_max_threshold</literal>, <literal>toast.autovacuum_vacuum_max_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_max_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_max_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1785,7 +1782,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-scale-factor" xreflabel="autovacuum_vacuum_scale_factor">
- <term><literal>autovacuum_vacuum_scale_factor</literal>, <literal>toast.autovacuum_vacuum_scale_factor</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_scale_factor</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_scale_factor</varname> </primary>
<secondary>storage parameter</secondary>
@@ -1800,7 +1797,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-insert-threshold" xreflabel="autovacuum_vacuum_insert_threshold">
- <term><literal>autovacuum_vacuum_insert_threshold</literal>, <literal>toast.autovacuum_vacuum_insert_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_insert_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_insert_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1815,7 +1812,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-insert-scale-factor" xreflabel="autovacuum_vacuum_insert_scale_factor">
- <term><literal>autovacuum_vacuum_insert_scale_factor</literal>, <literal>toast.autovacuum_vacuum_insert_scale_factor</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_insert_scale_factor</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_insert_scale_factor</varname> </primary>
<secondary>storage parameter</secondary>
@@ -1860,7 +1857,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-cost-delay" xreflabel="autovacuum_vacuum_cost_delay">
- <term><literal>autovacuum_vacuum_cost_delay</literal>, <literal>toast.autovacuum_vacuum_cost_delay</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_cost_delay</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_cost_delay</varname></primary>
<secondary>storage parameter</secondary>
@@ -1875,7 +1872,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-cost-limit" xreflabel="autovacuum_vacuum_cost_limit">
- <term><literal>autovacuum_vacuum_cost_limit</literal>, <literal>toast.autovacuum_vacuum_cost_limit</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_cost_limit</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_cost_limit</varname></primary>
<secondary>storage parameter</secondary>
@@ -1890,7 +1887,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-min-age" xreflabel="autovacuum_freeze_min_age">
- <term><literal>autovacuum_freeze_min_age</literal>, <literal>toast.autovacuum_freeze_min_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_min_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_min_age</varname> storage parameter</primary>
</indexterm>
@@ -1907,7 +1904,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-max-age" xreflabel="autovacuum_freeze_max_age">
- <term><literal>autovacuum_freeze_max_age</literal>, <literal>toast.autovacuum_freeze_max_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_max_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_max_age</varname></primary>
<secondary>storage parameter</secondary>
@@ -1924,7 +1921,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-table-age" xreflabel="autovacuum_freeze_table_age">
- <term><literal>autovacuum_freeze_table_age</literal>, <literal>toast.autovacuum_freeze_table_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_table_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_table_age</varname> storage parameter</primary>
</indexterm>
@@ -1938,7 +1935,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-min-age" xreflabel="autovacuum_multixact_freeze_min_age">
- <term><literal>autovacuum_multixact_freeze_min_age</literal>, <literal>toast.autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_min_age</varname> storage parameter</primary>
</indexterm>
@@ -1956,7 +1953,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-max-age" xreflabel="autovacuum_multixact_freeze_max_age">
- <term><literal>autovacuum_multixact_freeze_max_age</literal>, <literal>toast.autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_max_age</varname></primary>
<secondary>storage parameter</secondary>
@@ -1975,7 +1972,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-table-age" xreflabel="autovacuum_multixact_freeze_table_age">
- <term><literal>autovacuum_multixact_freeze_table_age</literal>, <literal>toast.autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_table_age</varname> storage parameter</primary>
</indexterm>
@@ -1989,7 +1986,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-log-autovacuum-min-duration" xreflabel="log_autovacuum_min_duration">
- <term><literal>log_autovacuum_min_duration</literal>, <literal>toast.log_autovacuum_min_duration</literal> (<type>integer</type>)
+ <term><literal>log_autovacuum_min_duration</literal> (<type>integer</type>)
<indexterm>
<primary><varname>log_autovacuum_min_duration</varname></primary>
<secondary>storage parameter</secondary>
@@ -2019,7 +2016,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-max-eager-freeze-failure-rate" xreflabel="vacuum_max_eager_freeze_failure_rate">
- <term><literal>vacuum_max_eager_freeze_failure_rate</literal>, <literal>toast.vacuum_max_eager_freeze_failure_rate</literal> (<type>floating point</type>)
+ <term><literal>vacuum_max_eager_freeze_failure_rate</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>vacuum_max_eager_freeze_failure_rate</varname></primary>
<secondary>storage parameter</secondary>
@@ -2044,7 +2041,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Declare the table as an additional catalog table for purposes of
logical replication. See
<xref linkend="logicaldecoding-capabilities"/> for details.
- This parameter cannot be set for TOAST tables.
+ This parameter does not affect the table's secondary TOAST table.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..50d9805f3d8 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -111,7 +111,7 @@ static relopt_bool boolRelOpts[] =
{
"autovacuum_enabled",
"Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
true
@@ -172,7 +172,7 @@ static relopt_ternary ternaryRelOpts[] =
{
"vacuum_truncate",
"Enables vacuum to truncate empty pages at the end of this table",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}
},
@@ -249,7 +249,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_threshold",
"Minimum number of tuple updates or deletes prior to vacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, INT_MAX
@@ -258,7 +258,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_max_threshold",
"Maximum number of tuple updates or deletes prior to vacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-2, -1, INT_MAX
@@ -267,7 +267,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_insert_threshold",
"Minimum number of tuple inserts prior to vacuum, or -1 to disable insert vacuums",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-2, -1, INT_MAX
@@ -285,7 +285,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_cost_limit",
"Vacuum cost amount available before napping, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 1, 10000
@@ -294,7 +294,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_min_age",
"Minimum age at which VACUUM should freeze a table row, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, 1000000000
@@ -303,7 +303,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_min_age",
"Minimum multixact age at which VACUUM should freeze a row multixact's, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, 1000000000
@@ -312,7 +312,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_max_age",
"Age at which to autovacuum a table to prevent transaction ID wraparound",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 100000, 2000000000
@@ -321,7 +321,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_max_age",
"Multixact age at which to autovacuum a table to prevent multixact wraparound",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 10000, 2000000000
@@ -330,7 +330,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_table_age",
"Age at which VACUUM should perform a full table sweep to freeze row versions",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}, -1, 0, 2000000000
},
@@ -338,7 +338,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_table_age",
"Age of multixact at which VACUUM should perform a full table sweep to freeze row versions",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}, -1, 0, 2000000000
},
@@ -346,7 +346,7 @@ static relopt_int intRelOpts[] =
{
"log_autovacuum_min_duration",
"Sets the minimum execution time above which vacuum actions by autovacuum will be logged",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, -1, INT_MAX
@@ -424,7 +424,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_cost_delay",
"Vacuum cost delay in milliseconds, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -433,7 +433,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_scale_factor",
"Number of tuple updates or deletes prior to vacuum as a fraction of reltuples",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -442,7 +442,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_insert_scale_factor",
"Number of tuple inserts prior to vacuum as a fraction of reltuples",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -460,7 +460,7 @@ static relopt_real realRelOpts[] =
{
"vacuum_max_eager_freeze_failure_rate",
"Fraction of pages in a relation vacuum can scan and fail to freeze before disabling eager scanning.",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 1.0
@@ -554,7 +554,7 @@ static relopt_enum enumRelOpts[] =
{
"vacuum_index_cleanup",
"Controls index vacuuming and index cleanup",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
@@ -2163,21 +2163,8 @@ view_reloptions(Datum reloptions, bool validate)
bytea *
heap_reloptions(char relkind, Datum reloptions, bool validate)
{
- StdRdOptions *rdopts;
-
switch (relkind)
{
- case RELKIND_TOASTVALUE:
- rdopts = (StdRdOptions *)
- default_reloptions(reloptions, validate, RELOPT_KIND_TOAST);
- if (rdopts != NULL)
- {
- /* adjust default-only parameters for TOAST relations */
- rdopts->fillfactor = 100;
- rdopts->autovacuum.analyze_threshold = -1;
- rdopts->autovacuum.analyze_scale_factor = -1;
- }
- return (bytea *) rdopts;
case RELKIND_RELATION:
case RELKIND_MATVIEW:
return default_reloptions(reloptions, validate, RELOPT_KIND_HEAP);
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 4aa52a4bd25..9f1f822702c 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -34,11 +34,11 @@
#include "utils/rel.h"
#include "utils/syscache.h"
-static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
+static void CheckAndCreateToastTable(Oid relOid,
LOCKMODE lockmode, bool check,
Oid OIDOldToast);
static bool create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
- Datum reloptions, LOCKMODE lockmode, bool check,
+ LOCKMODE lockmode, bool check,
Oid OIDOldToast);
static bool needs_toast_table(Relation rel);
@@ -48,35 +48,30 @@ static bool needs_toast_table(Relation rel);
* If the table needs a toast table, and doesn't already have one,
* then create a toast table for it.
*
- * reloptions for the toast table can be passed, too. Pass (Datum) 0
- * for default reloptions.
- *
* We expect the caller to have verified that the relation is a table and have
* already done any necessary permission checks. Callers expect this function
* to end with CommandCounterIncrement if it makes any changes.
*/
void
-AlterTableCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode)
+AlterTableCreateToastTable(Oid relOid, LOCKMODE lockmode)
{
- CheckAndCreateToastTable(relOid, reloptions, lockmode, true, InvalidOid);
+ CheckAndCreateToastTable(relOid, lockmode, true, InvalidOid);
}
void
-NewHeapCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
- Oid OIDOldToast)
+NewHeapCreateToastTable(Oid relOid, LOCKMODE lockmode, Oid OIDOldToast)
{
- CheckAndCreateToastTable(relOid, reloptions, lockmode, false, OIDOldToast);
+ CheckAndCreateToastTable(relOid, lockmode, false, OIDOldToast);
}
void
-NewRelationCreateToastTable(Oid relOid, Datum reloptions)
+NewRelationCreateToastTable(Oid relOid)
{
- CheckAndCreateToastTable(relOid, reloptions, AccessExclusiveLock, false,
- InvalidOid);
+ CheckAndCreateToastTable(relOid, AccessExclusiveLock, false, InvalidOid);
}
static void
-CheckAndCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
+CheckAndCreateToastTable(Oid relOid, LOCKMODE lockmode,
bool check, Oid OIDOldToast)
{
Relation rel;
@@ -84,7 +79,7 @@ CheckAndCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
rel = table_open(relOid, lockmode);
/* create_toast_table does all the work */
- (void) create_toast_table(rel, InvalidOid, InvalidOid, reloptions, lockmode,
+ (void) create_toast_table(rel, InvalidOid, InvalidOid, lockmode,
check, OIDOldToast);
table_close(rel, NoLock);
@@ -108,7 +103,7 @@ BootstrapToastTable(char *relName, Oid toastOid, Oid toastIndexOid)
relName);
/* create_toast_table does all the work */
- if (!create_toast_table(rel, toastOid, toastIndexOid, (Datum) 0,
+ if (!create_toast_table(rel, toastOid, toastIndexOid,
AccessExclusiveLock, false, InvalidOid))
elog(ERROR, "\"%s\" does not require a toast table",
relName);
@@ -126,7 +121,7 @@ BootstrapToastTable(char *relName, Oid toastOid, Oid toastIndexOid)
*/
static bool
create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
- Datum reloptions, LOCKMODE lockmode, bool check,
+ LOCKMODE lockmode, bool check,
Oid OIDOldToast)
{
Oid relOid = RelationGetRelid(rel);
@@ -266,7 +261,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
shared_relation,
mapped_relation,
ONCOMMIT_NOOP,
- reloptions,
+ (Datum) 0,
false,
true,
true,
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 6dbb831ca89..188e167999b 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -84,8 +84,6 @@ create_ctas_internal(List *attrList, IntoClause *into)
CreateStmt *create = makeNode(CreateStmt);
bool is_matview;
char relkind;
- Datum toast_options;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
ObjectAddress intoRelationAddr;
/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
@@ -120,16 +118,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
*/
CommandCounterIncrement();
- /* parse and validate reloptions for the toast table */
- toast_options = transformRelOptions((Datum) 0,
- create->options,
- "toast",
- validnsps,
- true, false);
-
- (void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
-
- NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+ NewRelationCreateToastTable(intoRelationAddr.objectId);
/* Create the "view" part of a materialized view. */
if (is_matview)
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 9a199dd9bfb..3b80d331815 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -1201,20 +1201,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
toastid = OldHeap->rd_rel->reltoastrelid;
if (OidIsValid(toastid))
- {
- /* keep the existing toast table's reloptions, if any */
- tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid));
- if (!HeapTupleIsValid(tuple))
- elog(ERROR, "cache lookup failed for relation %u", toastid);
- reloptions = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions,
- &isNull);
- if (isNull)
- reloptions = (Datum) 0;
-
- NewHeapCreateToastTable(OIDNewHeap, reloptions, lockmode, toastid);
-
- ReleaseSysCache(tuple);
- }
+ NewHeapCreateToastTable(OIDNewHeap, lockmode, toastid);
table_close(OldHeap, NoLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d8d7969bf30..a254509cbb5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -827,7 +827,6 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
ListCell *listptr;
AttrNumber attnum;
bool partitioned;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
Oid ofTypeId;
ObjectAddress address;
LOCKMODE parentLockmode;
@@ -976,7 +975,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
/*
* Parse and validate reloptions, if any.
*/
- reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, validnsps,
+ reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, NULL,
true, false);
switch (relkind)
@@ -5433,7 +5432,7 @@ ATRewriteCatalogs(List **wqueue, LOCKMODE lockmode,
tab->relkind == RELKIND_PARTITIONED_TABLE) &&
tab->partition_constraint == NULL) ||
tab->relkind == RELKIND_MATVIEW)
- AlterTableCreateToastTable(tab->relid, (Datum) 0, lockmode);
+ AlterTableCreateToastTable(tab->relid, lockmode);
}
}
@@ -16927,7 +16926,6 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
Datum repl_val[Natts_pg_class];
bool repl_null[Natts_pg_class];
bool repl_repl[Natts_pg_class];
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
if (defList == NIL && operation != AT_ReplaceRelOptions)
return; /* nothing to do */
@@ -16960,7 +16958,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
}
/* Generate new proposed reloptions (text array) */
- newOptions = transformRelOptions(datum, defList, NULL, validnsps, false,
+ newOptions = transformRelOptions(datum, defList, NULL, NULL, false,
operation == AT_ResetRelOptions);
/* Validate */
@@ -17083,20 +17081,11 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
datum = (Datum) 0;
}
- newOptions = transformRelOptions(datum, defList, "toast", validnsps,
- false, operation == AT_ResetRelOptions);
-
- (void) heap_reloptions(RELKIND_TOASTVALUE, newOptions, true);
-
memset(repl_val, 0, sizeof(repl_val));
memset(repl_null, false, sizeof(repl_null));
memset(repl_repl, false, sizeof(repl_repl));
- if (newOptions != (Datum) 0)
- repl_val[Anum_pg_class_reloptions - 1] = newOptions;
- else
- repl_null[Anum_pg_class_reloptions - 1] = true;
-
+ repl_null[Anum_pg_class_reloptions - 1] = true;
repl_repl[Anum_pg_class_reloptions - 1] = true;
newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass),
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index a4abb29cf64..2b44afdbf25 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2020,13 +2020,6 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
Oid save_userid;
int save_sec_context;
int save_nestlevel;
- VacuumParams toast_vacuum_params;
-
- /*
- * This function scribbles on the parameters, so make a copy early to
- * avoid affecting the TOAST table (if we do end up recursing to it).
- */
- memcpy(&toast_vacuum_params, ¶ms, sizeof(VacuumParams));
/* Begin a transaction for vacuuming this relation */
StartTransactionCommand();
@@ -2341,11 +2334,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* relation. NB: This is only safe to do because we hold a session
* lock on the main relation that prevents concurrent deletion.
*/
- toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
- toast_vacuum_params.toast_parent = relid;
+ params.options |= VACOPT_PROCESS_MAIN;
+ params.toast_parent = relid;
- vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
- isTopLevel);
+ vacuum_rel(toast_relid, NULL, params, bstrategy, isTopLevel);
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a5a8db2ff88..c0e7cbf2cb8 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2139,8 +2139,9 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
- bool free_relopts = false;
+ av_relation *hentry;
+ bool found;
+ AutoVacOpts *relopts = NULL;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2154,22 +2155,10 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
- relopts = &hentry->ar_reloptions;
- }
+ /* Use reloptions from main rel. */
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ relopts = &hentry->ar_reloptions;
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2186,10 +2175,6 @@ do_autovacuum(void)
table->score = scores.max;
tables_to_process = lappend(tables_to_process, table);
}
-
- /* Release stuff to avoid leakage */
- if (free_relopts)
- pfree(relopts);
}
table_endscan(relScan);
@@ -2826,7 +2811,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
+ AutoVacOpts *avopts = NULL;
bool free_avopts = false;
AutoVacuumScores scores;
@@ -2838,13 +2823,15 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
/*
* Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * main table reloptions.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ if (classForm->relkind != RELKIND_TOASTVALUE)
+ {
+ avopts = extract_autovac_opts(classTup, pg_class_desc);
+ if (avopts)
+ free_avopts = true;
+ }
+ else if (table_toast_map)
{
av_relation *hentry;
bool found;
@@ -3127,8 +3114,7 @@ relation_needs_vacanalyze(Oid relid,
/*
* Determine vacuum/analyze equation parameters. We have two possible
- * sources: the passed reloptions (which could be a main table or a toast
- * table), or the autovacuum GUC variables.
+ * sources: the passed reloptions or the autovacuum GUC variables.
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..d089e02c216 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1159,8 +1159,6 @@ ProcessUtilitySlow(ParseState *pstate,
if (IsA(stmt, CreateStmt))
{
CreateStmt *cstmt = (CreateStmt *) stmt;
- Datum toast_options;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
/* Remember transformed RangeVar for LIKE */
table_rv = cstmt->relation;
@@ -1180,22 +1178,7 @@ ProcessUtilitySlow(ParseState *pstate,
*/
CommandCounterIncrement();
- /*
- * parse and validate reloptions for the toast
- * table
- */
- toast_options = transformRelOptions((Datum) 0,
- cstmt->options,
- "toast",
- validnsps,
- true,
- false);
- (void) heap_reloptions(RELKIND_TOASTVALUE,
- toast_options,
- true);
-
- NewRelationCreateToastTable(address.objectId,
- toast_options);
+ NewRelationCreateToastTable(address.objectId);
}
else if (IsA(stmt, CreateForeignTableStmt))
{
diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 5a7afe62eab..18d49a6fc17 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -37,6 +37,7 @@ static void check_new_cluster_subscription_configuration(void);
static void check_old_cluster_for_valid_slots(void);
static void check_old_cluster_subscription_state(void);
static void check_old_cluster_global_names(ClusterInfo *cluster);
+static void check_for_toast_reloptions(ClusterInfo *cluster);
/*
* DataTypesUsageChecks - definitions of data type checks for the old cluster
@@ -634,6 +635,9 @@ check_and_dump_old_cluster(void)
check_for_prepared_transactions(&old_cluster);
check_for_isn_and_int8_passing_mismatch(&old_cluster);
+ if (GET_MAJOR_VERSION(old_cluster.major_version) < 2000)
+ check_for_toast_reloptions(&old_cluster);
+
if (GET_MAJOR_VERSION(old_cluster.major_version) >= 1700)
{
/*
@@ -2644,3 +2648,74 @@ check_old_cluster_global_names(ClusterInfo *cluster)
else
check_ok();
}
+
+/*
+ * Callback function for processing results of query for
+ * check_for_toast_reloptions()'s UpgradeTask. If the query returned any rows
+ * (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_toast_relopts_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+ UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+ int ntups = PQntuples(res);
+ int i_nspname = PQfnumber(res, "nspname");
+ int i_relname = PQfnumber(res, "relname");
+
+ if (ntups == 0)
+ return;
+
+ if (report->file == NULL &&
+ (report->file = fopen_priv(report->path, "w")) == NULL)
+ pg_fatal("could not open file \"%s\": %m", report->path);
+
+ fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+
+ for (int rowno = 0; rowno < ntups; rowno++)
+ fprintf(report->file, " %s.%s\n",
+ PQgetvalue(res, rowno, i_nspname),
+ PQgetvalue(res, rowno, i_relname));
+}
+
+/*
+ * Verify that no storage parameters (a.k.a. reloptions) are defined for TOAST
+ * tables.
+ */
+static void
+check_for_toast_reloptions(ClusterInfo *cluster)
+{
+ UpgradeTaskReport report;
+ UpgradeTask *task = upgrade_task_create();
+ const char *query = "SELECT n.nspname, c.relname "
+ "FROM pg_catalog.pg_class c, "
+ " pg_catalog.pg_class tc, "
+ " pg_catalog.pg_namespace n "
+ "WHERE c.reltoastrelid = tc.oid AND "
+ " c.relnamespace = n.oid AND "
+ " tc.reloptions IS NOT NULL";
+
+ prep_status("Check for tables with TOAST storage parameters");
+
+ report.file = NULL;
+ snprintf(report.path, sizeof(report.path), "%s/%s",
+ log_opts.basedir,
+ "tables_with_toast_storage_parameters.txt");
+
+ upgrade_task_add_step(task, query, process_toast_relopts_check,
+ true, &report);
+ upgrade_task_run(task, cluster);
+ upgrade_task_free(task);
+
+ if (report.file)
+ {
+ fclose(report.file);
+ pg_log(PG_REPORT, "fatal");
+ pg_fatal("Your installation contains tables with TOAST storage parameters set, which is\n"
+ "not supported anymore. Consider remove the TOAST storage parameters using\n"
+ " ALTER TABLE ... RESET ( ... );\n"
+ "A list of tables with the problem is in the file:\n"
+ " %s", report.path);
+ }
+ else
+ check_ok();
+}
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..4415c1ca9b5 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -40,26 +40,22 @@ typedef enum relopt_kind
{
RELOPT_KIND_LOCAL = 0,
RELOPT_KIND_HEAP = (1 << 0),
- RELOPT_KIND_TOAST = (1 << 1),
- RELOPT_KIND_BTREE = (1 << 2),
- RELOPT_KIND_HASH = (1 << 3),
- RELOPT_KIND_GIN = (1 << 4),
- RELOPT_KIND_GIST = (1 << 5),
- RELOPT_KIND_ATTRIBUTE = (1 << 6),
- RELOPT_KIND_TABLESPACE = (1 << 7),
- RELOPT_KIND_SPGIST = (1 << 8),
- RELOPT_KIND_VIEW = (1 << 9),
- RELOPT_KIND_BRIN = (1 << 10),
- RELOPT_KIND_PARTITIONED = (1 << 11),
+ RELOPT_KIND_BTREE = (1 << 1),
+ RELOPT_KIND_HASH = (1 << 2),
+ RELOPT_KIND_GIN = (1 << 3),
+ RELOPT_KIND_GIST = (1 << 4),
+ RELOPT_KIND_ATTRIBUTE = (1 << 5),
+ RELOPT_KIND_TABLESPACE = (1 << 6),
+ RELOPT_KIND_SPGIST = (1 << 7),
+ RELOPT_KIND_VIEW = (1 << 8),
+ RELOPT_KIND_BRIN = (1 << 9),
+ RELOPT_KIND_PARTITIONED = (1 << 10),
/* if you add a new kind, make sure you update "last_default" too */
RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_PARTITIONED,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
-/* reloption namespaces allowed for heaps -- currently only TOAST */
-#define HEAP_RELOPT_NAMESPACES { "toast", NULL }
-
/* generic struct to hold shared data */
typedef struct relopt_gen
{
diff --git a/src/include/catalog/toasting.h b/src/include/catalog/toasting.h
index 0bc61a8fee9..13e5a8affcb 100644
--- a/src/include/catalog/toasting.h
+++ b/src/include/catalog/toasting.h
@@ -19,10 +19,10 @@
/*
* toasting.c prototypes
*/
-extern void NewRelationCreateToastTable(Oid relOid, Datum reloptions);
-extern void NewHeapCreateToastTable(Oid relOid, Datum reloptions,
+extern void NewRelationCreateToastTable(Oid relOid);
+extern void NewHeapCreateToastTable(Oid relOid,
LOCKMODE lockmode, Oid OIDOldToast);
-extern void AlterTableCreateToastTable(Oid relOid, Datum reloptions,
+extern void AlterTableCreateToastTable(Oid relOid,
LOCKMODE lockmode);
extern void BootstrapToastTable(char *relName,
Oid toastOid, Oid toastIndexOid);
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..dcfa729d05e 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -43,29 +43,29 @@ SELECT injection_points_attach('vacuum-truncate-enabled', 'notice');
(1 row)
-- Check state of index_cleanup and truncate in VACUUM.
-CREATE TABLE vac_tab_on_toast_off(i int, j text) WITH
+CREATE TABLE vac_tab_on_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=true, toast.vacuum_index_cleanup=false,
- vacuum_truncate=true, toast.vacuum_truncate=false);
-CREATE TABLE vac_tab_off_toast_on(i int, j text) WITH
+ vacuum_index_cleanup=true,
+ vacuum_truncate=true);
+CREATE TABLE vac_tab_off_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=false, toast.vacuum_index_cleanup=true,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false);
-- Multiple relations should use their options in isolation.
-VACUUM vac_tab_on_toast_off, vac_tab_off_toast_on;
+VACUUM vac_tab_on_toast, vac_tab_off_toast;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-enabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-enabled
NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-disabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-disabled
-NOTICE: notice triggered for injection point vacuum-index-cleanup-enabled
-NOTICE: notice triggered for injection point vacuum-truncate-enabled
-- Check "auto" case of index_cleanup and "truncate" controlled by
-- its GUC.
CREATE TABLE vac_tab_auto(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=auto, toast.vacuum_index_cleanup=auto);
+ vacuum_index_cleanup=auto);
SET vacuum_truncate = false;
VACUUM vac_tab_auto;
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
@@ -80,8 +80,8 @@ NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
DROP TABLE vac_tab_auto;
-DROP TABLE vac_tab_on_toast_off;
-DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_on_toast;
+DROP TABLE vac_tab_off_toast;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..ee7a2e0566e 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -11,22 +11,22 @@ SELECT injection_points_attach('vacuum-truncate-disabled', 'notice');
SELECT injection_points_attach('vacuum-truncate-enabled', 'notice');
-- Check state of index_cleanup and truncate in VACUUM.
-CREATE TABLE vac_tab_on_toast_off(i int, j text) WITH
+CREATE TABLE vac_tab_on_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=true, toast.vacuum_index_cleanup=false,
- vacuum_truncate=true, toast.vacuum_truncate=false);
-CREATE TABLE vac_tab_off_toast_on(i int, j text) WITH
+ vacuum_index_cleanup=true,
+ vacuum_truncate=true);
+CREATE TABLE vac_tab_off_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=false, toast.vacuum_index_cleanup=true,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false);
-- Multiple relations should use their options in isolation.
-VACUUM vac_tab_on_toast_off, vac_tab_off_toast_on;
+VACUUM vac_tab_on_toast, vac_tab_off_toast;
-- Check "auto" case of index_cleanup and "truncate" controlled by
-- its GUC.
CREATE TABLE vac_tab_auto(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=auto, toast.vacuum_index_cleanup=auto);
+ vacuum_index_cleanup=auto);
SET vacuum_truncate = false;
VACUUM vac_tab_auto;
SET vacuum_truncate = true;
@@ -34,8 +34,8 @@ VACUUM vac_tab_auto;
RESET vacuum_truncate;
DROP TABLE vac_tab_auto;
-DROP TABLE vac_tab_on_toast_off;
-DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_on_toast;
+DROP TABLE vac_tab_off_toast;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 6dd22be0e8d..435e6beea0f 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -2860,15 +2860,6 @@ select * from my_locks order by 1;
pg_toast | ShareUpdateExclusiveLock
(2 rows)
-commit;
-begin; alter table alterlock set (toast.autovacuum_enabled = off);
-select * from my_locks order by 1;
- relname | max_lockmode
------------+--------------------------
- alterlock | ShareUpdateExclusiveLock
- pg_toast | ShareUpdateExclusiveLock
-(2 rows)
-
commit;
begin; alter table alterlock set (autovacuum_enabled = off);
select * from my_locks order by 1;
diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index e3a974f2611..eff5fbece60 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -120,7 +120,6 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
DROP TABLE reloptions_test;
CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
WITH (vacuum_truncate=false,
- toast.vacuum_truncate=false,
autovacuum_enabled=false);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
reloptions
@@ -142,9 +141,9 @@ SELECT pg_relation_size('reloptions_test') > 0;
SELECT reloptions FROM pg_class WHERE oid =
(SELECT reltoastrelid FROM pg_class
WHERE oid = 'reloptions_test'::regclass);
- reloptions
--------------------------
- {vacuum_truncate=false}
+ reloptions
+------------
+
(1 row)
ALTER TABLE reloptions_test RESET (vacuum_truncate);
@@ -165,56 +164,11 @@ SELECT pg_relation_size('reloptions_test') = 0;
t
(1 row)
--- Test toast.* options
-DROP TABLE reloptions_test;
-CREATE TABLE reloptions_test (s VARCHAR)
- WITH (toast.autovacuum_vacuum_cost_delay = 23);
-SELECT reltoastrelid as toast_oid
- FROM pg_class WHERE oid = 'reloptions_test'::regclass \gset
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=23}
-(1 row)
-
-ALTER TABLE reloptions_test SET (toast.autovacuum_vacuum_cost_delay = 24);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=24}
-(1 row)
-
-ALTER TABLE reloptions_test RESET (toast.autovacuum_vacuum_cost_delay);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
-------------
-
-(1 row)
-
--- Fail on non-existent options in toast namespace
-CREATE TABLE reloptions_test2 (i int) WITH (toast.not_existing_option = 42);
-ERROR: unrecognized parameter "not_existing_option"
--- Mix TOAST & heap
-DROP TABLE reloptions_test;
-CREATE TABLE reloptions_test (s VARCHAR) WITH
- (toast.autovacuum_vacuum_cost_delay = 23,
- autovacuum_vacuum_cost_delay = 24, fillfactor = 40);
-SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
- reloptions
--------------------------------------------------
- {autovacuum_vacuum_cost_delay=24,fillfactor=40}
-(1 row)
-
-SELECT reloptions FROM pg_class WHERE oid = (
- SELECT reltoastrelid FROM pg_class WHERE oid = 'reloptions_test'::regclass);
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=23}
-(1 row)
-
--
-- CREATE INDEX, ALTER INDEX for btrees
--
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test (s VARCHAR);
CREATE INDEX reloptions_test_idx ON reloptions_test (s) WITH (fillfactor=30);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test_idx'::regclass;
reloptions
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index d4696bc3325..d551ef57d50 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -209,14 +209,6 @@ VACUUM no_index_cleanup;
INSERT INTO no_index_cleanup(i, t) VALUES (generate_series(31,60),
repeat('1234567890',269));
DELETE FROM no_index_cleanup WHERE i < 45;
--- Only toast index is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = off,
- toast.vacuum_index_cleanup = yes);
-VACUUM no_index_cleanup;
--- Only parent is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = true,
- toast.vacuum_index_cleanup = false);
-VACUUM no_index_cleanup;
-- Test some extra relations.
VACUUM (INDEX_CLEANUP FALSE) vaccluster;
VACUUM (INDEX_CLEANUP AUTO) vactst; -- index cleanup option is ignored if no indexes
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index f5f13bbd3e7..410465dc022 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -1784,10 +1784,6 @@ begin; alter table alterlock reset (fillfactor);
select * from my_locks order by 1;
commit;
-begin; alter table alterlock set (toast.autovacuum_enabled = off);
-select * from my_locks order by 1;
-commit;
-
begin; alter table alterlock set (autovacuum_enabled = off);
select * from my_locks order by 1;
commit;
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index 680c8bf8614..0809a2ae0e4 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -75,7 +75,6 @@ DROP TABLE reloptions_test;
CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
WITH (vacuum_truncate=false,
- toast.vacuum_truncate=false,
autovacuum_enabled=false);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
@@ -94,39 +93,12 @@ INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) reloptions_test;
SELECT pg_relation_size('reloptions_test') = 0;
--- Test toast.* options
-DROP TABLE reloptions_test;
-
-CREATE TABLE reloptions_test (s VARCHAR)
- WITH (toast.autovacuum_vacuum_cost_delay = 23);
-SELECT reltoastrelid as toast_oid
- FROM pg_class WHERE oid = 'reloptions_test'::regclass \gset
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
-ALTER TABLE reloptions_test SET (toast.autovacuum_vacuum_cost_delay = 24);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
-ALTER TABLE reloptions_test RESET (toast.autovacuum_vacuum_cost_delay);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
--- Fail on non-existent options in toast namespace
-CREATE TABLE reloptions_test2 (i int) WITH (toast.not_existing_option = 42);
-
--- Mix TOAST & heap
-DROP TABLE reloptions_test;
-
-CREATE TABLE reloptions_test (s VARCHAR) WITH
- (toast.autovacuum_vacuum_cost_delay = 23,
- autovacuum_vacuum_cost_delay = 24, fillfactor = 40);
-
-SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
-SELECT reloptions FROM pg_class WHERE oid = (
- SELECT reltoastrelid FROM pg_class WHERE oid = 'reloptions_test'::regclass);
-
--
-- CREATE INDEX, ALTER INDEX for btrees
--
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test (s VARCHAR);
CREATE INDEX reloptions_test_idx ON reloptions_test (s) WITH (fillfactor=30);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test_idx'::regclass;
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 247b8e23b23..37a84fd9145 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -175,14 +175,6 @@ VACUUM no_index_cleanup;
INSERT INTO no_index_cleanup(i, t) VALUES (generate_series(31,60),
repeat('1234567890',269));
DELETE FROM no_index_cleanup WHERE i < 45;
--- Only toast index is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = off,
- toast.vacuum_index_cleanup = yes);
-VACUUM no_index_cleanup;
--- Only parent is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = true,
- toast.vacuum_index_cleanup = false);
-VACUUM no_index_cleanup;
-- Test some extra relations.
VACUUM (INDEX_CLEANUP FALSE) vaccluster;
VACUUM (INDEX_CLEANUP AUTO) vactst; -- index cleanup option is ignored if no indexes
--
2.50.1 (Apple Git-155)
Attachments:
[text/plain] v4-0001-remove-toast-reloptions.patch (51.8K, ../../afuzQuX17qpVugnY@nathan/2-v4-0001-remove-toast-reloptions.patch)
download | inline diff:
From 0d8df9cbcd0dbf21815d95c6422f3a216a7e15ce Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 7 Apr 2026 15:30:53 -0500
Subject: [PATCH v4 1/1] remove toast reloptions
---
doc/src/sgml/ref/create_table.sgml | 51 ++++++-------
src/backend/access/common/reloptions.c | 49 +++++-------
src/backend/catalog/toasting.c | 31 ++++----
src/backend/commands/createas.c | 13 +---
src/backend/commands/repack.c | 15 +---
src/backend/commands/tablecmds.c | 19 +----
src/backend/commands/vacuum.c | 14 +---
src/backend/postmaster/autovacuum.c | 48 +++++-------
src/backend/tcop/utility.c | 19 +----
src/bin/pg_upgrade/check.c | 75 +++++++++++++++++++
src/include/access/reloptions.h | 24 +++---
src/include/catalog/toasting.h | 6 +-
.../injection_points/expected/vacuum.out | 24 +++---
.../modules/injection_points/sql/vacuum.sql | 20 ++---
src/test/regress/expected/alter_table.out | 9 ---
src/test/regress/expected/reloptions.out | 56 ++------------
src/test/regress/expected/vacuum.out | 8 --
src/test/regress/sql/alter_table.sql | 4 -
src/test/regress/sql/reloptions.sql | 32 +-------
src/test/regress/sql/vacuum.sql | 8 --
20 files changed, 199 insertions(+), 326 deletions(-)
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index e342585c7f0..b8d2a657d10 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1589,14 +1589,10 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Storage parameters for
indexes are documented in <xref linkend="sql-createindex"/>.
The storage parameters currently
- available for tables are listed below. For many of these parameters, as
- shown, there is an additional parameter with the same name prefixed with
- <literal>toast.</literal>, which controls the behavior of the
+ available for tables are listed below. Unless otherwise noted, a table's
+ parameter value also controls the behavior of the
table's secondary <acronym>TOAST</acronym> table, if any
(see <xref linkend="storage-toast"/> for more information about TOAST).
- If a table parameter value is set and the
- equivalent <literal>toast.</literal> parameter is not, the TOAST table
- will use the table's parameter value.
Specifying these parameters for partitioned tables is not supported,
but you may specify them for individual leaf partitions.
</para>
@@ -1622,7 +1618,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
updates</link> more likely.
For a table whose entries are never updated, complete packing is the
best choice, but in heavily updated tables smaller fillfactors are
- appropriate. This parameter cannot be set for TOAST tables.
+ appropriate. This parameter does not affect the table's secondary TOAST
+ table.
</para>
</listitem>
</varlistentry>
@@ -1648,7 +1645,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Note that the default setting is often close to optimal, and
it is possible that setting this parameter could have negative
effects in some cases.
- This parameter cannot be set for TOAST tables.
+ This parameter does not affect the table's secondary TOAST table.
</para>
</listitem>
</varlistentry>
@@ -1671,7 +1668,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-enabled" xreflabel="autovacuum_enabled">
- <term><literal>autovacuum_enabled</literal>, <literal>toast.autovacuum_enabled</literal> (<type>boolean</type>)
+ <term><literal>autovacuum_enabled</literal> (<type>boolean</type>)
<indexterm>
<primary><varname>autovacuum_enabled</varname> storage parameter</primary>
</indexterm>
@@ -1696,7 +1693,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-index-cleanup" xreflabel="vacuum_index_cleanup">
- <term><literal>vacuum_index_cleanup</literal>, <literal>toast.vacuum_index_cleanup</literal> (<type>enum</type>)
+ <term><literal>vacuum_index_cleanup</literal> (<type>enum</type>)
<indexterm>
<primary><varname>vacuum_index_cleanup</varname> storage parameter</primary>
</indexterm>
@@ -1722,7 +1719,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-truncate" xreflabel="vacuum_truncate">
- <term><literal>vacuum_truncate</literal>, <literal>toast.vacuum_truncate</literal> (<type>boolean</type>)
+ <term><literal>vacuum_truncate</literal> (<type>boolean</type>)
<indexterm>
<primary><varname>vacuum_truncate</varname></primary>
<secondary>storage parameter</secondary>
@@ -1755,7 +1752,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-threshold" xreflabel="autovacuum_vacuum_threshold">
- <term><literal>autovacuum_vacuum_threshold</literal>, <literal>toast.autovacuum_vacuum_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1770,7 +1767,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-max-threshold" xreflabel="autovacuum_vacuum_max_threshold">
- <term><literal>autovacuum_vacuum_max_threshold</literal>, <literal>toast.autovacuum_vacuum_max_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_max_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_max_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1785,7 +1782,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-scale-factor" xreflabel="autovacuum_vacuum_scale_factor">
- <term><literal>autovacuum_vacuum_scale_factor</literal>, <literal>toast.autovacuum_vacuum_scale_factor</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_scale_factor</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_scale_factor</varname> </primary>
<secondary>storage parameter</secondary>
@@ -1800,7 +1797,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-insert-threshold" xreflabel="autovacuum_vacuum_insert_threshold">
- <term><literal>autovacuum_vacuum_insert_threshold</literal>, <literal>toast.autovacuum_vacuum_insert_threshold</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_insert_threshold</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_insert_threshold</varname></primary>
<secondary>storage parameter</secondary>
@@ -1815,7 +1812,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-insert-scale-factor" xreflabel="autovacuum_vacuum_insert_scale_factor">
- <term><literal>autovacuum_vacuum_insert_scale_factor</literal>, <literal>toast.autovacuum_vacuum_insert_scale_factor</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_insert_scale_factor</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_insert_scale_factor</varname> </primary>
<secondary>storage parameter</secondary>
@@ -1860,7 +1857,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-cost-delay" xreflabel="autovacuum_vacuum_cost_delay">
- <term><literal>autovacuum_vacuum_cost_delay</literal>, <literal>toast.autovacuum_vacuum_cost_delay</literal> (<type>floating point</type>)
+ <term><literal>autovacuum_vacuum_cost_delay</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_cost_delay</varname></primary>
<secondary>storage parameter</secondary>
@@ -1875,7 +1872,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-vacuum-cost-limit" xreflabel="autovacuum_vacuum_cost_limit">
- <term><literal>autovacuum_vacuum_cost_limit</literal>, <literal>toast.autovacuum_vacuum_cost_limit</literal> (<type>integer</type>)
+ <term><literal>autovacuum_vacuum_cost_limit</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_vacuum_cost_limit</varname></primary>
<secondary>storage parameter</secondary>
@@ -1890,7 +1887,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-min-age" xreflabel="autovacuum_freeze_min_age">
- <term><literal>autovacuum_freeze_min_age</literal>, <literal>toast.autovacuum_freeze_min_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_min_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_min_age</varname> storage parameter</primary>
</indexterm>
@@ -1907,7 +1904,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-max-age" xreflabel="autovacuum_freeze_max_age">
- <term><literal>autovacuum_freeze_max_age</literal>, <literal>toast.autovacuum_freeze_max_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_max_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_max_age</varname></primary>
<secondary>storage parameter</secondary>
@@ -1924,7 +1921,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-freeze-table-age" xreflabel="autovacuum_freeze_table_age">
- <term><literal>autovacuum_freeze_table_age</literal>, <literal>toast.autovacuum_freeze_table_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_freeze_table_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_freeze_table_age</varname> storage parameter</primary>
</indexterm>
@@ -1938,7 +1935,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-min-age" xreflabel="autovacuum_multixact_freeze_min_age">
- <term><literal>autovacuum_multixact_freeze_min_age</literal>, <literal>toast.autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_min_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_min_age</varname> storage parameter</primary>
</indexterm>
@@ -1956,7 +1953,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-max-age" xreflabel="autovacuum_multixact_freeze_max_age">
- <term><literal>autovacuum_multixact_freeze_max_age</literal>, <literal>toast.autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_max_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_max_age</varname></primary>
<secondary>storage parameter</secondary>
@@ -1975,7 +1972,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-autovacuum-multixact-freeze-table-age" xreflabel="autovacuum_multixact_freeze_table_age">
- <term><literal>autovacuum_multixact_freeze_table_age</literal>, <literal>toast.autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)
+ <term><literal>autovacuum_multixact_freeze_table_age</literal> (<type>integer</type>)
<indexterm>
<primary><varname>autovacuum_multixact_freeze_table_age</varname> storage parameter</primary>
</indexterm>
@@ -1989,7 +1986,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-log-autovacuum-min-duration" xreflabel="log_autovacuum_min_duration">
- <term><literal>log_autovacuum_min_duration</literal>, <literal>toast.log_autovacuum_min_duration</literal> (<type>integer</type>)
+ <term><literal>log_autovacuum_min_duration</literal> (<type>integer</type>)
<indexterm>
<primary><varname>log_autovacuum_min_duration</varname></primary>
<secondary>storage parameter</secondary>
@@ -2019,7 +2016,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="reloption-vacuum-max-eager-freeze-failure-rate" xreflabel="vacuum_max_eager_freeze_failure_rate">
- <term><literal>vacuum_max_eager_freeze_failure_rate</literal>, <literal>toast.vacuum_max_eager_freeze_failure_rate</literal> (<type>floating point</type>)
+ <term><literal>vacuum_max_eager_freeze_failure_rate</literal> (<type>floating point</type>)
<indexterm>
<primary><varname>vacuum_max_eager_freeze_failure_rate</varname></primary>
<secondary>storage parameter</secondary>
@@ -2044,7 +2041,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Declare the table as an additional catalog table for purposes of
logical replication. See
<xref linkend="logicaldecoding-capabilities"/> for details.
- This parameter cannot be set for TOAST tables.
+ This parameter does not affect the table's secondary TOAST table.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..50d9805f3d8 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -111,7 +111,7 @@ static relopt_bool boolRelOpts[] =
{
"autovacuum_enabled",
"Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
true
@@ -172,7 +172,7 @@ static relopt_ternary ternaryRelOpts[] =
{
"vacuum_truncate",
"Enables vacuum to truncate empty pages at the end of this table",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}
},
@@ -249,7 +249,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_threshold",
"Minimum number of tuple updates or deletes prior to vacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, INT_MAX
@@ -258,7 +258,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_max_threshold",
"Maximum number of tuple updates or deletes prior to vacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-2, -1, INT_MAX
@@ -267,7 +267,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_insert_threshold",
"Minimum number of tuple inserts prior to vacuum, or -1 to disable insert vacuums",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-2, -1, INT_MAX
@@ -285,7 +285,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_vacuum_cost_limit",
"Vacuum cost amount available before napping, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 1, 10000
@@ -294,7 +294,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_min_age",
"Minimum age at which VACUUM should freeze a table row, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, 1000000000
@@ -303,7 +303,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_min_age",
"Minimum multixact age at which VACUUM should freeze a row multixact's, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0, 1000000000
@@ -312,7 +312,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_max_age",
"Age at which to autovacuum a table to prevent transaction ID wraparound",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 100000, 2000000000
@@ -321,7 +321,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_max_age",
"Multixact age at which to autovacuum a table to prevent multixact wraparound",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 10000, 2000000000
@@ -330,7 +330,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_freeze_table_age",
"Age at which VACUUM should perform a full table sweep to freeze row versions",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}, -1, 0, 2000000000
},
@@ -338,7 +338,7 @@ static relopt_int intRelOpts[] =
{
"autovacuum_multixact_freeze_table_age",
"Age of multixact at which VACUUM should perform a full table sweep to freeze row versions",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
}, -1, 0, 2000000000
},
@@ -346,7 +346,7 @@ static relopt_int intRelOpts[] =
{
"log_autovacuum_min_duration",
"Sets the minimum execution time above which vacuum actions by autovacuum will be logged",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, -1, INT_MAX
@@ -424,7 +424,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_cost_delay",
"Vacuum cost delay in milliseconds, for autovacuum",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -433,7 +433,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_scale_factor",
"Number of tuple updates or deletes prior to vacuum as a fraction of reltuples",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -442,7 +442,7 @@ static relopt_real realRelOpts[] =
{
"autovacuum_vacuum_insert_scale_factor",
"Number of tuple inserts prior to vacuum as a fraction of reltuples",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 100.0
@@ -460,7 +460,7 @@ static relopt_real realRelOpts[] =
{
"vacuum_max_eager_freeze_failure_rate",
"Fraction of pages in a relation vacuum can scan and fail to freeze before disabling eager scanning.",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
-1, 0.0, 1.0
@@ -554,7 +554,7 @@ static relopt_enum enumRelOpts[] =
{
"vacuum_index_cleanup",
"Controls index vacuuming and index cleanup",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ RELOPT_KIND_HEAP,
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
@@ -2163,21 +2163,8 @@ view_reloptions(Datum reloptions, bool validate)
bytea *
heap_reloptions(char relkind, Datum reloptions, bool validate)
{
- StdRdOptions *rdopts;
-
switch (relkind)
{
- case RELKIND_TOASTVALUE:
- rdopts = (StdRdOptions *)
- default_reloptions(reloptions, validate, RELOPT_KIND_TOAST);
- if (rdopts != NULL)
- {
- /* adjust default-only parameters for TOAST relations */
- rdopts->fillfactor = 100;
- rdopts->autovacuum.analyze_threshold = -1;
- rdopts->autovacuum.analyze_scale_factor = -1;
- }
- return (bytea *) rdopts;
case RELKIND_RELATION:
case RELKIND_MATVIEW:
return default_reloptions(reloptions, validate, RELOPT_KIND_HEAP);
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 4aa52a4bd25..9f1f822702c 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -34,11 +34,11 @@
#include "utils/rel.h"
#include "utils/syscache.h"
-static void CheckAndCreateToastTable(Oid relOid, Datum reloptions,
+static void CheckAndCreateToastTable(Oid relOid,
LOCKMODE lockmode, bool check,
Oid OIDOldToast);
static bool create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
- Datum reloptions, LOCKMODE lockmode, bool check,
+ LOCKMODE lockmode, bool check,
Oid OIDOldToast);
static bool needs_toast_table(Relation rel);
@@ -48,35 +48,30 @@ static bool needs_toast_table(Relation rel);
* If the table needs a toast table, and doesn't already have one,
* then create a toast table for it.
*
- * reloptions for the toast table can be passed, too. Pass (Datum) 0
- * for default reloptions.
- *
* We expect the caller to have verified that the relation is a table and have
* already done any necessary permission checks. Callers expect this function
* to end with CommandCounterIncrement if it makes any changes.
*/
void
-AlterTableCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode)
+AlterTableCreateToastTable(Oid relOid, LOCKMODE lockmode)
{
- CheckAndCreateToastTable(relOid, reloptions, lockmode, true, InvalidOid);
+ CheckAndCreateToastTable(relOid, lockmode, true, InvalidOid);
}
void
-NewHeapCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
- Oid OIDOldToast)
+NewHeapCreateToastTable(Oid relOid, LOCKMODE lockmode, Oid OIDOldToast)
{
- CheckAndCreateToastTable(relOid, reloptions, lockmode, false, OIDOldToast);
+ CheckAndCreateToastTable(relOid, lockmode, false, OIDOldToast);
}
void
-NewRelationCreateToastTable(Oid relOid, Datum reloptions)
+NewRelationCreateToastTable(Oid relOid)
{
- CheckAndCreateToastTable(relOid, reloptions, AccessExclusiveLock, false,
- InvalidOid);
+ CheckAndCreateToastTable(relOid, AccessExclusiveLock, false, InvalidOid);
}
static void
-CheckAndCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
+CheckAndCreateToastTable(Oid relOid, LOCKMODE lockmode,
bool check, Oid OIDOldToast)
{
Relation rel;
@@ -84,7 +79,7 @@ CheckAndCreateToastTable(Oid relOid, Datum reloptions, LOCKMODE lockmode,
rel = table_open(relOid, lockmode);
/* create_toast_table does all the work */
- (void) create_toast_table(rel, InvalidOid, InvalidOid, reloptions, lockmode,
+ (void) create_toast_table(rel, InvalidOid, InvalidOid, lockmode,
check, OIDOldToast);
table_close(rel, NoLock);
@@ -108,7 +103,7 @@ BootstrapToastTable(char *relName, Oid toastOid, Oid toastIndexOid)
relName);
/* create_toast_table does all the work */
- if (!create_toast_table(rel, toastOid, toastIndexOid, (Datum) 0,
+ if (!create_toast_table(rel, toastOid, toastIndexOid,
AccessExclusiveLock, false, InvalidOid))
elog(ERROR, "\"%s\" does not require a toast table",
relName);
@@ -126,7 +121,7 @@ BootstrapToastTable(char *relName, Oid toastOid, Oid toastIndexOid)
*/
static bool
create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
- Datum reloptions, LOCKMODE lockmode, bool check,
+ LOCKMODE lockmode, bool check,
Oid OIDOldToast)
{
Oid relOid = RelationGetRelid(rel);
@@ -266,7 +261,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
shared_relation,
mapped_relation,
ONCOMMIT_NOOP,
- reloptions,
+ (Datum) 0,
false,
true,
true,
diff --git a/src/backend/commands/createas.c b/src/backend/commands/createas.c
index 6dbb831ca89..188e167999b 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -84,8 +84,6 @@ create_ctas_internal(List *attrList, IntoClause *into)
CreateStmt *create = makeNode(CreateStmt);
bool is_matview;
char relkind;
- Datum toast_options;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
ObjectAddress intoRelationAddr;
/* This code supports both CREATE TABLE AS and CREATE MATERIALIZED VIEW */
@@ -120,16 +118,7 @@ create_ctas_internal(List *attrList, IntoClause *into)
*/
CommandCounterIncrement();
- /* parse and validate reloptions for the toast table */
- toast_options = transformRelOptions((Datum) 0,
- create->options,
- "toast",
- validnsps,
- true, false);
-
- (void) heap_reloptions(RELKIND_TOASTVALUE, toast_options, true);
-
- NewRelationCreateToastTable(intoRelationAddr.objectId, toast_options);
+ NewRelationCreateToastTable(intoRelationAddr.objectId);
/* Create the "view" part of a materialized view. */
if (is_matview)
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 9a199dd9bfb..3b80d331815 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -1201,20 +1201,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
*/
toastid = OldHeap->rd_rel->reltoastrelid;
if (OidIsValid(toastid))
- {
- /* keep the existing toast table's reloptions, if any */
- tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(toastid));
- if (!HeapTupleIsValid(tuple))
- elog(ERROR, "cache lookup failed for relation %u", toastid);
- reloptions = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions,
- &isNull);
- if (isNull)
- reloptions = (Datum) 0;
-
- NewHeapCreateToastTable(OIDNewHeap, reloptions, lockmode, toastid);
-
- ReleaseSysCache(tuple);
- }
+ NewHeapCreateToastTable(OIDNewHeap, lockmode, toastid);
table_close(OldHeap, NoLock);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d8d7969bf30..a254509cbb5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -827,7 +827,6 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
ListCell *listptr;
AttrNumber attnum;
bool partitioned;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
Oid ofTypeId;
ObjectAddress address;
LOCKMODE parentLockmode;
@@ -976,7 +975,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
/*
* Parse and validate reloptions, if any.
*/
- reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, validnsps,
+ reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, NULL,
true, false);
switch (relkind)
@@ -5433,7 +5432,7 @@ ATRewriteCatalogs(List **wqueue, LOCKMODE lockmode,
tab->relkind == RELKIND_PARTITIONED_TABLE) &&
tab->partition_constraint == NULL) ||
tab->relkind == RELKIND_MATVIEW)
- AlterTableCreateToastTable(tab->relid, (Datum) 0, lockmode);
+ AlterTableCreateToastTable(tab->relid, lockmode);
}
}
@@ -16927,7 +16926,6 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
Datum repl_val[Natts_pg_class];
bool repl_null[Natts_pg_class];
bool repl_repl[Natts_pg_class];
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
if (defList == NIL && operation != AT_ReplaceRelOptions)
return; /* nothing to do */
@@ -16960,7 +16958,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
}
/* Generate new proposed reloptions (text array) */
- newOptions = transformRelOptions(datum, defList, NULL, validnsps, false,
+ newOptions = transformRelOptions(datum, defList, NULL, NULL, false,
operation == AT_ResetRelOptions);
/* Validate */
@@ -17083,20 +17081,11 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
datum = (Datum) 0;
}
- newOptions = transformRelOptions(datum, defList, "toast", validnsps,
- false, operation == AT_ResetRelOptions);
-
- (void) heap_reloptions(RELKIND_TOASTVALUE, newOptions, true);
-
memset(repl_val, 0, sizeof(repl_val));
memset(repl_null, false, sizeof(repl_null));
memset(repl_repl, false, sizeof(repl_repl));
- if (newOptions != (Datum) 0)
- repl_val[Anum_pg_class_reloptions - 1] = newOptions;
- else
- repl_null[Anum_pg_class_reloptions - 1] = true;
-
+ repl_null[Anum_pg_class_reloptions - 1] = true;
repl_repl[Anum_pg_class_reloptions - 1] = true;
newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass),
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index a4abb29cf64..2b44afdbf25 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2020,13 +2020,6 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
Oid save_userid;
int save_sec_context;
int save_nestlevel;
- VacuumParams toast_vacuum_params;
-
- /*
- * This function scribbles on the parameters, so make a copy early to
- * avoid affecting the TOAST table (if we do end up recursing to it).
- */
- memcpy(&toast_vacuum_params, ¶ms, sizeof(VacuumParams));
/* Begin a transaction for vacuuming this relation */
StartTransactionCommand();
@@ -2341,11 +2334,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* relation. NB: This is only safe to do because we hold a session
* lock on the main relation that prevents concurrent deletion.
*/
- toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
- toast_vacuum_params.toast_parent = relid;
+ params.options |= VACOPT_PROCESS_MAIN;
+ params.toast_parent = relid;
- vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
- isTopLevel);
+ vacuum_rel(toast_relid, NULL, params, bstrategy, isTopLevel);
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a5a8db2ff88..c0e7cbf2cb8 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2139,8 +2139,9 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
- bool free_relopts = false;
+ av_relation *hentry;
+ bool found;
+ AutoVacOpts *relopts = NULL;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2154,22 +2155,10 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
- relopts = &hentry->ar_reloptions;
- }
+ /* Use reloptions from main rel. */
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ relopts = &hentry->ar_reloptions;
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2186,10 +2175,6 @@ do_autovacuum(void)
table->score = scores.max;
tables_to_process = lappend(tables_to_process, table);
}
-
- /* Release stuff to avoid leakage */
- if (free_relopts)
- pfree(relopts);
}
table_endscan(relScan);
@@ -2826,7 +2811,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
+ AutoVacOpts *avopts = NULL;
bool free_avopts = false;
AutoVacuumScores scores;
@@ -2838,13 +2823,15 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
/*
* Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * main table reloptions.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ if (classForm->relkind != RELKIND_TOASTVALUE)
+ {
+ avopts = extract_autovac_opts(classTup, pg_class_desc);
+ if (avopts)
+ free_avopts = true;
+ }
+ else if (table_toast_map)
{
av_relation *hentry;
bool found;
@@ -3127,8 +3114,7 @@ relation_needs_vacanalyze(Oid relid,
/*
* Determine vacuum/analyze equation parameters. We have two possible
- * sources: the passed reloptions (which could be a main table or a toast
- * table), or the autovacuum GUC variables.
+ * sources: the passed reloptions or the autovacuum GUC variables.
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..d089e02c216 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1159,8 +1159,6 @@ ProcessUtilitySlow(ParseState *pstate,
if (IsA(stmt, CreateStmt))
{
CreateStmt *cstmt = (CreateStmt *) stmt;
- Datum toast_options;
- const char *const validnsps[] = HEAP_RELOPT_NAMESPACES;
/* Remember transformed RangeVar for LIKE */
table_rv = cstmt->relation;
@@ -1180,22 +1178,7 @@ ProcessUtilitySlow(ParseState *pstate,
*/
CommandCounterIncrement();
- /*
- * parse and validate reloptions for the toast
- * table
- */
- toast_options = transformRelOptions((Datum) 0,
- cstmt->options,
- "toast",
- validnsps,
- true,
- false);
- (void) heap_reloptions(RELKIND_TOASTVALUE,
- toast_options,
- true);
-
- NewRelationCreateToastTable(address.objectId,
- toast_options);
+ NewRelationCreateToastTable(address.objectId);
}
else if (IsA(stmt, CreateForeignTableStmt))
{
diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 5a7afe62eab..18d49a6fc17 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -37,6 +37,7 @@ static void check_new_cluster_subscription_configuration(void);
static void check_old_cluster_for_valid_slots(void);
static void check_old_cluster_subscription_state(void);
static void check_old_cluster_global_names(ClusterInfo *cluster);
+static void check_for_toast_reloptions(ClusterInfo *cluster);
/*
* DataTypesUsageChecks - definitions of data type checks for the old cluster
@@ -634,6 +635,9 @@ check_and_dump_old_cluster(void)
check_for_prepared_transactions(&old_cluster);
check_for_isn_and_int8_passing_mismatch(&old_cluster);
+ if (GET_MAJOR_VERSION(old_cluster.major_version) < 2000)
+ check_for_toast_reloptions(&old_cluster);
+
if (GET_MAJOR_VERSION(old_cluster.major_version) >= 1700)
{
/*
@@ -2644,3 +2648,74 @@ check_old_cluster_global_names(ClusterInfo *cluster)
else
check_ok();
}
+
+/*
+ * Callback function for processing results of query for
+ * check_for_toast_reloptions()'s UpgradeTask. If the query returned any rows
+ * (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_toast_relopts_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+ UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+ int ntups = PQntuples(res);
+ int i_nspname = PQfnumber(res, "nspname");
+ int i_relname = PQfnumber(res, "relname");
+
+ if (ntups == 0)
+ return;
+
+ if (report->file == NULL &&
+ (report->file = fopen_priv(report->path, "w")) == NULL)
+ pg_fatal("could not open file \"%s\": %m", report->path);
+
+ fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+
+ for (int rowno = 0; rowno < ntups; rowno++)
+ fprintf(report->file, " %s.%s\n",
+ PQgetvalue(res, rowno, i_nspname),
+ PQgetvalue(res, rowno, i_relname));
+}
+
+/*
+ * Verify that no storage parameters (a.k.a. reloptions) are defined for TOAST
+ * tables.
+ */
+static void
+check_for_toast_reloptions(ClusterInfo *cluster)
+{
+ UpgradeTaskReport report;
+ UpgradeTask *task = upgrade_task_create();
+ const char *query = "SELECT n.nspname, c.relname "
+ "FROM pg_catalog.pg_class c, "
+ " pg_catalog.pg_class tc, "
+ " pg_catalog.pg_namespace n "
+ "WHERE c.reltoastrelid = tc.oid AND "
+ " c.relnamespace = n.oid AND "
+ " tc.reloptions IS NOT NULL";
+
+ prep_status("Check for tables with TOAST storage parameters");
+
+ report.file = NULL;
+ snprintf(report.path, sizeof(report.path), "%s/%s",
+ log_opts.basedir,
+ "tables_with_toast_storage_parameters.txt");
+
+ upgrade_task_add_step(task, query, process_toast_relopts_check,
+ true, &report);
+ upgrade_task_run(task, cluster);
+ upgrade_task_free(task);
+
+ if (report.file)
+ {
+ fclose(report.file);
+ pg_log(PG_REPORT, "fatal");
+ pg_fatal("Your installation contains tables with TOAST storage parameters set, which is\n"
+ "not supported anymore. Consider remove the TOAST storage parameters using\n"
+ " ALTER TABLE ... RESET ( ... );\n"
+ "A list of tables with the problem is in the file:\n"
+ " %s", report.path);
+ }
+ else
+ check_ok();
+}
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..4415c1ca9b5 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -40,26 +40,22 @@ typedef enum relopt_kind
{
RELOPT_KIND_LOCAL = 0,
RELOPT_KIND_HEAP = (1 << 0),
- RELOPT_KIND_TOAST = (1 << 1),
- RELOPT_KIND_BTREE = (1 << 2),
- RELOPT_KIND_HASH = (1 << 3),
- RELOPT_KIND_GIN = (1 << 4),
- RELOPT_KIND_GIST = (1 << 5),
- RELOPT_KIND_ATTRIBUTE = (1 << 6),
- RELOPT_KIND_TABLESPACE = (1 << 7),
- RELOPT_KIND_SPGIST = (1 << 8),
- RELOPT_KIND_VIEW = (1 << 9),
- RELOPT_KIND_BRIN = (1 << 10),
- RELOPT_KIND_PARTITIONED = (1 << 11),
+ RELOPT_KIND_BTREE = (1 << 1),
+ RELOPT_KIND_HASH = (1 << 2),
+ RELOPT_KIND_GIN = (1 << 3),
+ RELOPT_KIND_GIST = (1 << 4),
+ RELOPT_KIND_ATTRIBUTE = (1 << 5),
+ RELOPT_KIND_TABLESPACE = (1 << 6),
+ RELOPT_KIND_SPGIST = (1 << 7),
+ RELOPT_KIND_VIEW = (1 << 8),
+ RELOPT_KIND_BRIN = (1 << 9),
+ RELOPT_KIND_PARTITIONED = (1 << 10),
/* if you add a new kind, make sure you update "last_default" too */
RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_PARTITIONED,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
-/* reloption namespaces allowed for heaps -- currently only TOAST */
-#define HEAP_RELOPT_NAMESPACES { "toast", NULL }
-
/* generic struct to hold shared data */
typedef struct relopt_gen
{
diff --git a/src/include/catalog/toasting.h b/src/include/catalog/toasting.h
index 0bc61a8fee9..13e5a8affcb 100644
--- a/src/include/catalog/toasting.h
+++ b/src/include/catalog/toasting.h
@@ -19,10 +19,10 @@
/*
* toasting.c prototypes
*/
-extern void NewRelationCreateToastTable(Oid relOid, Datum reloptions);
-extern void NewHeapCreateToastTable(Oid relOid, Datum reloptions,
+extern void NewRelationCreateToastTable(Oid relOid);
+extern void NewHeapCreateToastTable(Oid relOid,
LOCKMODE lockmode, Oid OIDOldToast);
-extern void AlterTableCreateToastTable(Oid relOid, Datum reloptions,
+extern void AlterTableCreateToastTable(Oid relOid,
LOCKMODE lockmode);
extern void BootstrapToastTable(char *relName,
Oid toastOid, Oid toastIndexOid);
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..dcfa729d05e 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -43,29 +43,29 @@ SELECT injection_points_attach('vacuum-truncate-enabled', 'notice');
(1 row)
-- Check state of index_cleanup and truncate in VACUUM.
-CREATE TABLE vac_tab_on_toast_off(i int, j text) WITH
+CREATE TABLE vac_tab_on_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=true, toast.vacuum_index_cleanup=false,
- vacuum_truncate=true, toast.vacuum_truncate=false);
-CREATE TABLE vac_tab_off_toast_on(i int, j text) WITH
+ vacuum_index_cleanup=true,
+ vacuum_truncate=true);
+CREATE TABLE vac_tab_off_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=false, toast.vacuum_index_cleanup=true,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false);
-- Multiple relations should use their options in isolation.
-VACUUM vac_tab_on_toast_off, vac_tab_off_toast_on;
+VACUUM vac_tab_on_toast, vac_tab_off_toast;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-enabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-enabled
NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-disabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-disabled
-NOTICE: notice triggered for injection point vacuum-index-cleanup-enabled
-NOTICE: notice triggered for injection point vacuum-truncate-enabled
-- Check "auto" case of index_cleanup and "truncate" controlled by
-- its GUC.
CREATE TABLE vac_tab_auto(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=auto, toast.vacuum_index_cleanup=auto);
+ vacuum_index_cleanup=auto);
SET vacuum_truncate = false;
VACUUM vac_tab_auto;
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
@@ -80,8 +80,8 @@ NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
DROP TABLE vac_tab_auto;
-DROP TABLE vac_tab_on_toast_off;
-DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_on_toast;
+DROP TABLE vac_tab_off_toast;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..ee7a2e0566e 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -11,22 +11,22 @@ SELECT injection_points_attach('vacuum-truncate-disabled', 'notice');
SELECT injection_points_attach('vacuum-truncate-enabled', 'notice');
-- Check state of index_cleanup and truncate in VACUUM.
-CREATE TABLE vac_tab_on_toast_off(i int, j text) WITH
+CREATE TABLE vac_tab_on_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=true, toast.vacuum_index_cleanup=false,
- vacuum_truncate=true, toast.vacuum_truncate=false);
-CREATE TABLE vac_tab_off_toast_on(i int, j text) WITH
+ vacuum_index_cleanup=true,
+ vacuum_truncate=true);
+CREATE TABLE vac_tab_off_toast(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=false, toast.vacuum_index_cleanup=true,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false);
-- Multiple relations should use their options in isolation.
-VACUUM vac_tab_on_toast_off, vac_tab_off_toast_on;
+VACUUM vac_tab_on_toast, vac_tab_off_toast;
-- Check "auto" case of index_cleanup and "truncate" controlled by
-- its GUC.
CREATE TABLE vac_tab_auto(i int, j text) WITH
(autovacuum_enabled=false,
- vacuum_index_cleanup=auto, toast.vacuum_index_cleanup=auto);
+ vacuum_index_cleanup=auto);
SET vacuum_truncate = false;
VACUUM vac_tab_auto;
SET vacuum_truncate = true;
@@ -34,8 +34,8 @@ VACUUM vac_tab_auto;
RESET vacuum_truncate;
DROP TABLE vac_tab_auto;
-DROP TABLE vac_tab_on_toast_off;
-DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_on_toast;
+DROP TABLE vac_tab_off_toast;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
diff --git a/src/test/regress/expected/alter_table.out b/src/test/regress/expected/alter_table.out
index 6dd22be0e8d..435e6beea0f 100644
--- a/src/test/regress/expected/alter_table.out
+++ b/src/test/regress/expected/alter_table.out
@@ -2860,15 +2860,6 @@ select * from my_locks order by 1;
pg_toast | ShareUpdateExclusiveLock
(2 rows)
-commit;
-begin; alter table alterlock set (toast.autovacuum_enabled = off);
-select * from my_locks order by 1;
- relname | max_lockmode
------------+--------------------------
- alterlock | ShareUpdateExclusiveLock
- pg_toast | ShareUpdateExclusiveLock
-(2 rows)
-
commit;
begin; alter table alterlock set (autovacuum_enabled = off);
select * from my_locks order by 1;
diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index e3a974f2611..eff5fbece60 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -120,7 +120,6 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
DROP TABLE reloptions_test;
CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
WITH (vacuum_truncate=false,
- toast.vacuum_truncate=false,
autovacuum_enabled=false);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
reloptions
@@ -142,9 +141,9 @@ SELECT pg_relation_size('reloptions_test') > 0;
SELECT reloptions FROM pg_class WHERE oid =
(SELECT reltoastrelid FROM pg_class
WHERE oid = 'reloptions_test'::regclass);
- reloptions
--------------------------
- {vacuum_truncate=false}
+ reloptions
+------------
+
(1 row)
ALTER TABLE reloptions_test RESET (vacuum_truncate);
@@ -165,56 +164,11 @@ SELECT pg_relation_size('reloptions_test') = 0;
t
(1 row)
--- Test toast.* options
-DROP TABLE reloptions_test;
-CREATE TABLE reloptions_test (s VARCHAR)
- WITH (toast.autovacuum_vacuum_cost_delay = 23);
-SELECT reltoastrelid as toast_oid
- FROM pg_class WHERE oid = 'reloptions_test'::regclass \gset
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=23}
-(1 row)
-
-ALTER TABLE reloptions_test SET (toast.autovacuum_vacuum_cost_delay = 24);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=24}
-(1 row)
-
-ALTER TABLE reloptions_test RESET (toast.autovacuum_vacuum_cost_delay);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
- reloptions
-------------
-
-(1 row)
-
--- Fail on non-existent options in toast namespace
-CREATE TABLE reloptions_test2 (i int) WITH (toast.not_existing_option = 42);
-ERROR: unrecognized parameter "not_existing_option"
--- Mix TOAST & heap
-DROP TABLE reloptions_test;
-CREATE TABLE reloptions_test (s VARCHAR) WITH
- (toast.autovacuum_vacuum_cost_delay = 23,
- autovacuum_vacuum_cost_delay = 24, fillfactor = 40);
-SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
- reloptions
--------------------------------------------------
- {autovacuum_vacuum_cost_delay=24,fillfactor=40}
-(1 row)
-
-SELECT reloptions FROM pg_class WHERE oid = (
- SELECT reltoastrelid FROM pg_class WHERE oid = 'reloptions_test'::regclass);
- reloptions
------------------------------------
- {autovacuum_vacuum_cost_delay=23}
-(1 row)
-
--
-- CREATE INDEX, ALTER INDEX for btrees
--
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test (s VARCHAR);
CREATE INDEX reloptions_test_idx ON reloptions_test (s) WITH (fillfactor=30);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test_idx'::regclass;
reloptions
diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out
index d4696bc3325..d551ef57d50 100644
--- a/src/test/regress/expected/vacuum.out
+++ b/src/test/regress/expected/vacuum.out
@@ -209,14 +209,6 @@ VACUUM no_index_cleanup;
INSERT INTO no_index_cleanup(i, t) VALUES (generate_series(31,60),
repeat('1234567890',269));
DELETE FROM no_index_cleanup WHERE i < 45;
--- Only toast index is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = off,
- toast.vacuum_index_cleanup = yes);
-VACUUM no_index_cleanup;
--- Only parent is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = true,
- toast.vacuum_index_cleanup = false);
-VACUUM no_index_cleanup;
-- Test some extra relations.
VACUUM (INDEX_CLEANUP FALSE) vaccluster;
VACUUM (INDEX_CLEANUP AUTO) vactst; -- index cleanup option is ignored if no indexes
diff --git a/src/test/regress/sql/alter_table.sql b/src/test/regress/sql/alter_table.sql
index f5f13bbd3e7..410465dc022 100644
--- a/src/test/regress/sql/alter_table.sql
+++ b/src/test/regress/sql/alter_table.sql
@@ -1784,10 +1784,6 @@ begin; alter table alterlock reset (fillfactor);
select * from my_locks order by 1;
commit;
-begin; alter table alterlock set (toast.autovacuum_enabled = off);
-select * from my_locks order by 1;
-commit;
-
begin; alter table alterlock set (autovacuum_enabled = off);
select * from my_locks order by 1;
commit;
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index 680c8bf8614..0809a2ae0e4 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -75,7 +75,6 @@ DROP TABLE reloptions_test;
CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
WITH (vacuum_truncate=false,
- toast.vacuum_truncate=false,
autovacuum_enabled=false);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
@@ -94,39 +93,12 @@ INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) reloptions_test;
SELECT pg_relation_size('reloptions_test') = 0;
--- Test toast.* options
-DROP TABLE reloptions_test;
-
-CREATE TABLE reloptions_test (s VARCHAR)
- WITH (toast.autovacuum_vacuum_cost_delay = 23);
-SELECT reltoastrelid as toast_oid
- FROM pg_class WHERE oid = 'reloptions_test'::regclass \gset
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
-ALTER TABLE reloptions_test SET (toast.autovacuum_vacuum_cost_delay = 24);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
-ALTER TABLE reloptions_test RESET (toast.autovacuum_vacuum_cost_delay);
-SELECT reloptions FROM pg_class WHERE oid = :toast_oid;
-
--- Fail on non-existent options in toast namespace
-CREATE TABLE reloptions_test2 (i int) WITH (toast.not_existing_option = 42);
-
--- Mix TOAST & heap
-DROP TABLE reloptions_test;
-
-CREATE TABLE reloptions_test (s VARCHAR) WITH
- (toast.autovacuum_vacuum_cost_delay = 23,
- autovacuum_vacuum_cost_delay = 24, fillfactor = 40);
-
-SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
-SELECT reloptions FROM pg_class WHERE oid = (
- SELECT reltoastrelid FROM pg_class WHERE oid = 'reloptions_test'::regclass);
-
--
-- CREATE INDEX, ALTER INDEX for btrees
--
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test (s VARCHAR);
CREATE INDEX reloptions_test_idx ON reloptions_test (s) WITH (fillfactor=30);
SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test_idx'::regclass;
diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql
index 247b8e23b23..37a84fd9145 100644
--- a/src/test/regress/sql/vacuum.sql
+++ b/src/test/regress/sql/vacuum.sql
@@ -175,14 +175,6 @@ VACUUM no_index_cleanup;
INSERT INTO no_index_cleanup(i, t) VALUES (generate_series(31,60),
repeat('1234567890',269));
DELETE FROM no_index_cleanup WHERE i < 45;
--- Only toast index is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = off,
- toast.vacuum_index_cleanup = yes);
-VACUUM no_index_cleanup;
--- Only parent is cleaned up.
-ALTER TABLE no_index_cleanup SET (vacuum_index_cleanup = true,
- toast.vacuum_index_cleanup = false);
-VACUUM no_index_cleanup;
-- Test some extra relations.
VACUUM (INDEX_CLEANUP FALSE) vaccluster;
VACUUM (INDEX_CLEANUP AUTO) vactst; -- index cleanup option is ignored if no indexes
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-05-26 05:55 solai v <solai.cdac@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: solai v @ 2026-05-26 05:55 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers
Hi,
I tested the TOAST reloptions behavior locally on current HEAD and was
able to reproduce the inheritance and RESET scenarios discussed in the
thread.
From my testing, explicit toast.reloptions behave independently from
parent reloptions. After setting both parent and TOAST vacuum_truncate
values, RESET(vacuum_truncate) on the parent table cleared only the
parent reloption while the explicit TOAST reloption remained intact.
I also tried applying the v1-0004 patch in a clean worktree, but most
hunks in vacuum.c and autovacuum.c no longer apply cleanly against
current HEAD due to code drift. From inspecting the rejected hunks, it
looks like the patch approach was to dynamically combine parent and
TOAST reloptions during VACUUM/autovacuum execution instead of copying
inherited reloptions directly into the TOAST relation.
Regards,
solai
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-05-26 14:23 Nathan Bossart <nathandbossart@gmail.com>
parent: solai v <solai.cdac@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2026-05-26 14:23 UTC (permalink / raw)
To: solai v <solai.cdac@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers
On Tue, May 26, 2026 at 11:25:39AM +0530, solai v wrote:
> I tested the TOAST reloptions behavior locally on current HEAD and was
> able to reproduce the inheritance and RESET scenarios discussed in the
> thread.
> From my testing, explicit toast.reloptions behave independently from
> parent reloptions. After setting both parent and TOAST vacuum_truncate
> values, RESET(vacuum_truncate) on the parent table cleared only the
> parent reloption while the explicit TOAST reloption remained intact.
> I also tried applying the v1-0004 patch in a clean worktree, but most
> hunks in vacuum.c and autovacuum.c no longer apply cleanly against
> current HEAD due to code drift. From inspecting the rejected hunks, it
> looks like the patch approach was to dynamically combine parent and
> TOAST reloptions during VACUUM/autovacuum execution instead of copying
> inherited reloptions directly into the TOAST relation.
I humbly encourage you to read the rest of the thread. In particular, I'm
curious whether anyone would object to removing the TOAST reloptions.
--
nathan
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-06-02 09:42 solai v <solai.cdac@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: solai v @ 2026-06-02 09:42 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; pgsql-hackers
Hi,
> I humbly encourage you to read the rest of the thread. In particular, I'm
> curious whether anyone would object to removing the TOAST reloptions.
I tested the remove-toast-reloptions.patch on current HEAD.
The patch applied and built cleanly .I verified that TOAST reloptions
are no longer accepted:
ALTER TABLE toat_test SET (toast.vacuum_truncate=false):
ALTER TABLE toast_test SET(toast.autovacuum_enabled = false);
Both returned:
ERROR:unrecognized parameter namespace "toast"
Regular reloptions still work as expected:
ALTER TABLE toast_test SET (vacuum_truncate=false);
The patch behaved as expected in my testing
Regards
solai
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-06-02 12:31 Nikita Malakhov <hukutoc@gmail.com>
parent: solai v <solai.cdac@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nikita Malakhov @ 2026-06-02 12:31 UTC (permalink / raw)
To: solai v <solai.cdac@gmail.com>; +Cc: Nathan Bossart <nathandbossart@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
Hi hackers!
I strongly object to removing TOAST reloptions.
As anyone here knows, TOAST tables are subject to bloating, and in my
opinion
we'll eventually come to multi TOAST tables per relation.
I've tested such an approach some time ago, it does not create any problems
and provides additional possibilities like ability to vacuum separate TOAST
only partly influencing overall work with parent relation.
On Tue, Jun 2, 2026 at 12:42 PM solai v <solai.cdac@gmail.com> wrote:
> Hi,
> > I humbly encourage you to read the rest of the thread. In particular,
> I'm
> > curious whether anyone would object to removing the TOAST reloptions.
> I tested the remove-toast-reloptions.patch on current HEAD.
> The patch applied and built cleanly .I verified that TOAST reloptions
> are no longer accepted:
> ALTER TABLE toat_test SET (toast.vacuum_truncate=false):
> ALTER TABLE toast_test SET(toast.autovacuum_enabled = false);
> Both returned:
> ERROR:unrecognized parameter namespace "toast"
> Regular reloptions still work as expected:
> ALTER TABLE toast_test SET (vacuum_truncate=false);
> The patch behaved as expected in my testing
>
> Regards
> solai
>
>
>
--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-06-02 14:08 Nathan Bossart <nathandbossart@gmail.com>
parent: Nikita Malakhov <hukutoc@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2026-06-02 14:08 UTC (permalink / raw)
To: Nikita Malakhov <hukutoc@gmail.com>; +Cc: solai v <solai.cdac@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
On Tue, Jun 02, 2026 at 03:31:40PM +0300, Nikita Malakhov wrote:
> I strongly object to removing TOAST reloptions.
FWIW I am working on a patch to actually fix the bugs instead of removing
the TOAST relopts. Will hopefully have something to share in the near
future.
--
nathan
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-06-03 19:27 Nikita Malakhov <hukutoc@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nikita Malakhov @ 2026-06-03 19:27 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: solai v <solai.cdac@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
Hi,
Nathan, sorry if there is any misunderstanding, I answered Solai V
against removing TOAST reloptions. Glad you're working on this subject.
Cheers!
On Tue, Jun 2, 2026 at 5:08 PM Nathan Bossart <nathandbossart@gmail.com>
wrote:
> On Tue, Jun 02, 2026 at 03:31:40PM +0300, Nikita Malakhov wrote:
> > I strongly object to removing TOAST reloptions.
>
> FWIW I am working on a patch to actually fix the bugs instead of removing
> the TOAST relopts. Will hopefully have something to share in the near
> future.
>
> --
> nathan
>
--
Regards,
Nikita Malakhov
Postgres Professional
The Russian Postgres Company
https://postgrespro.ru/
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-06-09 18:31 Nathan Bossart <nathandbossart@gmail.com>
parent: Nikita Malakhov <hukutoc@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2026-06-09 18:31 UTC (permalink / raw)
To: Nikita Malakhov <hukutoc@gmail.com>; +Cc: solai v <solai.cdac@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
Okay, here is a new patch set that aims to actually fix the issues, not
just remove the TOAST reloptions. I followed roughly the approach I
originally suggested in my first post: autovacuum merges the relopts, and
VACUUM passes them to the TOAST table when recursing. As previously
mentioned, vacuuming a TOAST table directly isn't fixed, but I think that's
okay. Our main supported way to VACUUM a TOAST table is to use "VACUUM
(PROCESS_MAIN false) main_table".
Something else this patch makes worse is that we remain oblivious to
concurrent storage parameter changes on the main table. That is, if
someone changes a relopt during a long-running vacuum on the main table,
we might use a stale relopt value when we process the TOAST table. To fix
that, I suspect we'd need to do more lookups, which I was hoping to avoid.
But this doesn't seem like a pressing issue, and AFAICT this stuff has been
broken for a very long time, so IMHO it's not worth the additional effort.
--
nathan
From 9ae7499234c20796f3846952d33419c567847370 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:35:34 -0500
Subject: [PATCH v5 1/4] Remove extract_autovac_opts().
extract_autovac_opts() returned a palloc'd copy of only the
AutoVacOpts portion of a relation's reloptions. Upcoming work
needs the rest of the StdRdOptions as well, so the callers must
keep the whole struct around.
Remove the helper and have the callers obtain reloptions from
extractRelOptions() directly. av_relation now caches a
StdRdOptions instead of an AutoVacOpts, and
relation_needs_vacanalyze() takes a StdRdOptions and extracts the
autovacuum portion itself. This is preparatory refactoring with no
change in behavior.
---
src/backend/postmaster/autovacuum.c | 122 ++++++++++------------------
1 file changed, 44 insertions(+), 78 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a5a8db2ff88..203a146b1c0 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -202,8 +202,7 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -381,7 +380,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -390,8 +389,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2035,7 +2032,7 @@ do_autovacuum(void)
while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2074,7 +2071,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2116,7 +2113,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2139,7 +2136,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2158,7 +2155,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2774,39 +2771,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc_object(AutoVacOpts);
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2826,8 +2790,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2840,9 +2804,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2851,10 +2815,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- relation_needs_vacanalyze(relid, avopts, classForm,
+ relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
DEBUG3,
&dovacuum, &doanalyze, &wraparound,
@@ -2869,6 +2833,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_vacuum_min_duration;
int log_analyze_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2980,8 +2945,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2993,7 +2958,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none.
*
@@ -3067,7 +3032,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -3081,6 +3046,7 @@ relation_needs_vacanalyze(Oid relid,
bool force_vacuum;
bool av_enabled;
bool may_free = false;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3132,45 +3098,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
@@ -3661,7 +3627,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
- AutoVacOpts *avopts;
+ StdRdOptions *relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3677,14 +3643,14 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
- relation_needs_vacanalyze(form->oid, avopts, form,
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
- pfree(avopts);
+ if (relopts)
+ pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
vals[1] = Float8GetDatum(scores.max);
--
2.50.1 (Apple Git-155)
From 5f9a8cfb16bc1d83119d7273d6ba9bf9d3630c71 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:50:36 -0500
Subject: [PATCH v5 2/4] Make autovacuum_enabled a ternary reloption.
This commit reimplements autovacuum_enabled as a ternary, using the
support added in commit 4d6a66f675 and following the example of
vacuum_truncate. This changes only the internal representation: an
unset value still behaves as enabled, and the option accepts the
same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 19 +++++++++----------
src/backend/catalog/index.c | 3 ++-
src/backend/postmaster/autovacuum.c | 2 +-
src/include/utils/rel.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..79834126f2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -107,15 +107,6 @@ static relopt_bool boolRelOpts[] =
},
false
},
- {
- {
- "autovacuum_enabled",
- "Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
- ShareUpdateExclusiveLock
- },
- true
- },
{
{
"user_catalog_table",
@@ -168,6 +159,14 @@ static relopt_bool boolRelOpts[] =
static relopt_ternary ternaryRelOpts[] =
{
+ {
+ {
+ "autovacuum_enabled",
+ "Enables autovacuum in this relation",
+ RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ ShareUpdateExclusiveLock
+ }
+ },
{
{
"vacuum_truncate",
@@ -1976,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
static const relopt_parse_elt tab[] = {
{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_BOOL,
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
{"autovacuum_parallel_workers", RELOPT_TYPE_INT,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 9407c357f27..c9f32728902 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2873,7 +2873,8 @@ index_update_stats(Relation rel,
{
StdRdOptions *options = (StdRdOptions *) rel->rd_options;
- if (options != NULL && !options->autovacuum.enabled)
+ if (options != NULL &&
+ options->autovacuum.enabled == PG_TERNARY_FALSE)
update_stats = false;
}
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 203a146b1c0..aa2aca8fc4b 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3136,7 +3136,7 @@ relation_needs_vacanalyze(Oid relid,
? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index fa07ebf8ff7..f0824b6899a 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -310,7 +310,7 @@ typedef struct ForeignKeyCacheInfo
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
{
- bool enabled;
+ pg_ternary enabled;
int autovacuum_parallel_workers;
int vacuum_threshold;
--
2.50.1 (Apple Git-155)
From c4540d163b6b901d409416a7c55dabb174778978 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 15:27:58 -0500
Subject: [PATCH v5 3/4] Add an "unset" value for vacuum_index_cleanup.
This commit adds a new value to StdRdOptIndexCleanup to distinguish
whether it is explicitly set, similar to ViewOptCheckOption's
VIEW_OPTION_CHECK_OPTION_NOT_SET. This changes only the internal
representation; an unset value still defaults to AUTO, and the
option accepts the same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 3 ++-
src/backend/commands/vacuum.c | 23 ++++++++++++++---------
src/include/utils/rel.h | 1 +
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79834126f2f..58eb72e2339 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -517,6 +517,7 @@ static relopt_real realRelOpts[] =
/* values from StdRdOptIndexCleanup */
static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
{
+ /* no value for NOT_SET */
{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
@@ -557,7 +558,7 @@ static relopt_enum enumRelOpts[] =
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
},
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index a4abb29cf64..4ee1f913b64 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2191,20 +2191,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
StdRdOptIndexCleanup vacuum_index_cleanup;
if (rel->rd_options == NULL)
- vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+ vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
vacuum_index_cleanup =
((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
- if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
- params.index_cleanup = VACOPTVALUE_AUTO;
- else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
- params.index_cleanup = VACOPTVALUE_ENABLED;
- else
+ switch (vacuum_index_cleanup)
{
- Assert(vacuum_index_cleanup ==
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
- params.index_cleanup = VACOPTVALUE_DISABLED;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ params.index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ params.index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
}
}
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index f0824b6899a..f1b96b1099d 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,6 +338,7 @@ typedef enum StdRdOptIndexCleanup
STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
} StdRdOptIndexCleanup;
typedef struct StdRdOptions
--
2.50.1 (Apple Git-155)
From c7f6f3ddd622e9936a64aaa26de4ffadf8506405 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 9 Jun 2026 11:53:12 -0500
Subject: [PATCH v5 4/4] Fix VACUUM and autovacuum handling of TOAST storage
parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Unfortunately, current reality does not match this description.
Neither VACUUM nor autovacuum consults the main table's
non-autovacuum storage parameters, and autovacuum only consults the
main table's autovacuum-related storage parameters if the TOAST
table lacks any. One silver lining is that all currently-supported
TOAST storage parameters are related to vacuum, whose code paths
already access the main table's parameters. This means that our
solution needn't involve more lookups; we just need to propagate
them correctly.
To fix, this commit teaches autovacuum to combine the TOAST storage
parameters with the main table's (with the toast.* ones winning if
both are set), and it teaches VACUUM to send down the main table's
parameters when recursing to a TOAST table. This doesn't fix
VACUUM against a TOAST table directly (e.g., VACUUM
pg_toast.pg_toast_5432), but that's probably okay because it's not
the main supported way to vacuum a TOAST table (see VACUUM's
PROCESS_MAIN and PROCESS_TOAST options).
An existing shortcoming that this patch only makes worse is that
autovacuum/VACUUM remain oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/commands/vacuum.c | 36 +++-
src/backend/postmaster/autovacuum.c | 187 ++++++++++++++++--
src/include/commands/vacuum.h | 8 +
src/include/utils/rel.h | 3 +
.../injection_points/expected/vacuum.out | 11 ++
.../modules/injection_points/sql/vacuum.sql | 8 +
6 files changed, 234 insertions(+), 19 deletions(-)
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 4ee1f913b64..a2f77b349b2 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -187,6 +187,9 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_index_cleanup = VACOPTVALUE_UNSPECIFIED;
+ params.main_truncate = VACOPTVALUE_UNSPECIFIED;
+ params.main_max_eager_freeze_failure_rate = -1.0;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2184,7 +2187,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
- * specified in VACUUM command, or when running in an autovacuum worker
+ * specified in VACUUM command, or when running in an autovacuum worker. A
+ * TOAST table with no setting of its own inherits the main table's value.
*/
if (params.index_cleanup == VACOPTVALUE_UNSPECIFIED)
{
@@ -2200,15 +2204,21 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
params.index_cleanup = VACOPTVALUE_ENABLED;
+ toast_vacuum_params.main_index_cleanup = VACOPTVALUE_ENABLED;
break;
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
params.index_cleanup = VACOPTVALUE_DISABLED;
+ toast_vacuum_params.main_index_cleanup = VACOPTVALUE_DISABLED;
break;
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
params.index_cleanup = VACOPTVALUE_AUTO;
+ toast_vacuum_params.main_index_cleanup = VACOPTVALUE_AUTO;
break;
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
- params.index_cleanup = VACOPTVALUE_AUTO;
+ if (params.main_index_cleanup != VACOPTVALUE_UNSPECIFIED)
+ params.index_cleanup = params.main_index_cleanup;
+ else
+ params.index_cleanup = VACOPTVALUE_AUTO;
break;
}
}
@@ -2224,16 +2234,26 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
/*
* Check if the vacuum_max_eager_freeze_failure_rate table storage
- * parameter was specified. This overrides the GUC value.
+ * parameter was specified. This overrides the GUC value. A TOAST table
+ * with no setting of its own inherits the main table's value.
*/
if (rel->rd_options != NULL &&
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
+ {
params.max_eager_freeze_failure_rate =
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ toast_vacuum_params.main_max_eager_freeze_failure_rate =
+ ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ }
+ else if (params.main_max_eager_freeze_failure_rate >= 0.0)
+ params.max_eager_freeze_failure_rate =
+ params.main_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
- * specified in VACUUM command, or when running in an autovacuum worker
+ * specified in VACUUM command, or when running in an autovacuum worker. A
+ * TOAST table with no setting of its own inherits the main table's value
+ * before falling back to the GUC.
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
@@ -2242,10 +2262,18 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
{
if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ {
params.truncate = VACOPTVALUE_ENABLED;
+ toast_vacuum_params.main_truncate = VACOPTVALUE_ENABLED;
+ }
else
+ {
params.truncate = VACOPTVALUE_DISABLED;
+ toast_vacuum_params.main_truncate = VACOPTVALUE_DISABLED;
+ }
}
+ else if (params.main_truncate != VACOPTVALUE_UNSPECIFIED)
+ params.truncate = params.main_truncate;
else if (vacuum_truncate)
params.truncate = VACOPTVALUE_ENABLED;
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index aa2aca8fc4b..50c7f3d171d 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -396,6 +396,8 @@ static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
static void avl_sigusr2_handler(SIGNAL_ARGS);
static bool av_worker_available(void);
static void check_av_worker_gucs(void);
+static StdRdOptions *merge_autovac_opts(StdRdOptions *toast_opts,
+ StdRdOptions *main_opts);
@@ -2142,6 +2144,8 @@ do_autovacuum(void)
bool doanalyze;
bool wraparound;
AutoVacuumScores scores;
+ av_relation *hentry;
+ bool found;
/*
* We cannot safely process other backends' temp tables, so skip 'em.
@@ -2152,21 +2156,19 @@ do_autovacuum(void)
relid = classForm->oid;
/*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
+ * fetch reloptions -- merge any unset options from the main rel
+ *
+ * Note that we don't bother merging the non-autovacuum relopts here
+ * because they do not impact our choice of whether to process the
+ * table.
*/
relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
- relopts = &hentry->ar_reloptions;
- }
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ relopts = merge_autovac_opts(relopts, &hentry->ar_reloptions);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2791,6 +2793,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
autovac_table *tab = NULL;
bool wraparound;
StdRdOptions *relopts;
+ StdRdOptions *main_relopts = NULL;
bool free_relopts = false;
AutoVacuumScores scores;
@@ -2801,21 +2804,25 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+
+ if (classForm->relkind == RELKIND_TOASTVALUE &&
+ table_toast_map != NULL)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- relopts = &hentry->ar_reloptions;
+ {
+ main_relopts = &hentry->ar_reloptions;
+ relopts = merge_autovac_opts(relopts, main_relopts);
+ }
}
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2902,6 +2909,48 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ /*
+ * For TOAST tables, provide fallbacks for options that are not part
+ * of AutoVacOpts (and thus are not handled by merge_autovac_opts()).
+ */
+ tab->at_params.main_index_cleanup = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.main_truncate = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.main_max_eager_freeze_failure_rate = -1.0;
+
+ if (main_relopts != NULL)
+ {
+ switch (main_relopts->vacuum_index_cleanup)
+ {
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ break;
+ }
+
+ switch (main_relopts->vacuum_truncate)
+ {
+ case PG_TERNARY_TRUE:
+ tab->at_params.main_truncate = VACOPTVALUE_ENABLED;
+ break;
+ case PG_TERNARY_FALSE:
+ tab->at_params.main_truncate = VACOPTVALUE_DISABLED;
+ break;
+ case PG_TERNARY_UNSET:
+ break;
+ }
+
+ if (main_relopts->vacuum_max_eager_freeze_failure_rate >= 0.0)
+ tab->at_params.main_max_eager_freeze_failure_rate =
+ main_relopts->vacuum_max_eager_freeze_failure_rate;
+ }
+
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
if (avopts)
@@ -3670,3 +3719,111 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
return (Datum) 0;
}
+
+/*
+ * Combine a TOAST table's autovacuum options with the main table's. Any
+ * option the TOAST table leaves unset is taken from the main table's options.
+ * Either argument may be NULL. If both are NULL, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * NB: This function destructively modifies toast_opts!
+ */
+static StdRdOptions *
+merge_autovac_opts(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ /* ternary fields */
+ static const int ternary_offsets[] = {
+ offsetof(AutoVacOpts, enabled),
+ };
+
+ /* integer fields whose unset sentinel is -1 */
+ static const int int_offsets_1[] = {
+ offsetof(AutoVacOpts, autovacuum_parallel_workers),
+ offsetof(AutoVacOpts, vacuum_threshold),
+ offsetof(AutoVacOpts, analyze_threshold),
+ offsetof(AutoVacOpts, vacuum_cost_limit),
+ offsetof(AutoVacOpts, freeze_min_age),
+ offsetof(AutoVacOpts, freeze_max_age),
+ offsetof(AutoVacOpts, freeze_table_age),
+ offsetof(AutoVacOpts, multixact_freeze_min_age),
+ offsetof(AutoVacOpts, multixact_freeze_max_age),
+ offsetof(AutoVacOpts, multixact_freeze_table_age),
+ offsetof(AutoVacOpts, log_vacuum_min_duration),
+ offsetof(AutoVacOpts, log_analyze_min_duration),
+ };
+
+ /* integer fields whose unset sentinel is -2 */
+ static const int int_offsets_2[] = {
+ offsetof(AutoVacOpts, vacuum_max_threshold),
+ offsetof(AutoVacOpts, vacuum_ins_threshold),
+ };
+
+ /* float fields */
+ static const int float_offsets[] = {
+ offsetof(AutoVacOpts, vacuum_cost_delay),
+ offsetof(AutoVacOpts, vacuum_scale_factor),
+ offsetof(AutoVacOpts, vacuum_ins_scale_factor),
+ offsetof(AutoVacOpts, analyze_scale_factor),
+ };
+
+ AutoVacOpts *toast_avopts;
+ AutoVacOpts *main_avopts;
+
+ if (toast_opts == NULL)
+ return main_opts;
+ if (main_opts == NULL)
+ return toast_opts;
+
+ toast_avopts = &toast_opts->autovacuum;
+ main_avopts = &main_opts->autovacuum;
+
+ for (int i = 0; i < lengthof(ternary_offsets); i++)
+ {
+ pg_ternary *toast_opt;
+ pg_ternary *main_opt;
+
+ toast_opt = (pg_ternary *) ((char *) toast_avopts + ternary_offsets[i]);
+ main_opt = (pg_ternary *) ((char *) main_avopts + ternary_offsets[i]);
+
+ if (*toast_opt == PG_TERNARY_UNSET)
+ *toast_opt = *main_opt;
+ }
+
+ for (int i = 0; i < lengthof(int_offsets_1); i++)
+ {
+ int *toast_opt;
+ int *main_opt;
+
+ toast_opt = (int *) ((char *) toast_avopts + int_offsets_1[i]);
+ main_opt = (int *) ((char *) main_avopts + int_offsets_1[i]);
+
+ if (*toast_opt == -1)
+ *toast_opt = *main_opt;
+ }
+
+ for (int i = 0; i < lengthof(int_offsets_2); i++)
+ {
+ int *toast_opt;
+ int *main_opt;
+
+ toast_opt = (int *) ((char *) toast_avopts + int_offsets_2[i]);
+ main_opt = (int *) ((char *) main_avopts + int_offsets_2[i]);
+
+ if (*toast_opt == -2)
+ *toast_opt = *main_opt;
+ }
+
+ for (int i = 0; i < lengthof(float_offsets); i++)
+ {
+ double *toast_opt;
+ double *main_opt;
+
+ toast_opt = (double *) ((char *) toast_avopts + float_offsets[i]);
+ main_opt = (double *) ((char *) main_avopts + float_offsets[i]);
+
+ if (*toast_opt == -1.0)
+ *toast_opt = *main_opt;
+ }
+
+ return toast_opts;
+}
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9cea36d..335d1516a7e 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,14 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * Main table fallback values for TOAST tables to inherit when they have
+ * no setting of their own.
+ */
+ VacOptValue main_index_cleanup;
+ VacOptValue main_truncate;
+ double main_max_eager_freeze_failure_rate;
} VacuumParams;
/*
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index f1b96b1099d..9b55c601e94 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -306,6 +306,9 @@ typedef struct ForeignKeyCacheInfo
* RelationGetFillFactor() and RelationGetTargetPageFreeSpace() can only
* be applied to relations that use this format or a superset for
* private options data.
+ *
+ * NB: When adding a new member, be sure to update merge_autovac_opts() and/or
+ * table_recheck_autovac() as necessary!
*/
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..0f0790a8584 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,6 +79,17 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- TOAST table inherits main table's resolved values
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
+DROP TABLE vac_tab_toast_inherit;
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..11a371e4ff7 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,6 +33,14 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- TOAST table inherits main table's resolved values
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+DROP TABLE vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
--
2.50.1 (Apple Git-155)
Attachments:
[text/plain] v5-0001-Remove-extract_autovac_opts.patch (11.5K, ../../aihb_hMloqVDpbtw@nathan/2-v5-0001-Remove-extract_autovac_opts.patch)
download | inline diff:
From 9ae7499234c20796f3846952d33419c567847370 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:35:34 -0500
Subject: [PATCH v5 1/4] Remove extract_autovac_opts().
extract_autovac_opts() returned a palloc'd copy of only the
AutoVacOpts portion of a relation's reloptions. Upcoming work
needs the rest of the StdRdOptions as well, so the callers must
keep the whole struct around.
Remove the helper and have the callers obtain reloptions from
extractRelOptions() directly. av_relation now caches a
StdRdOptions instead of an AutoVacOpts, and
relation_needs_vacanalyze() takes a StdRdOptions and extracts the
autovacuum portion itself. This is preparatory refactoring with no
change in behavior.
---
src/backend/postmaster/autovacuum.c | 122 ++++++++++------------------
1 file changed, 44 insertions(+), 78 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a5a8db2ff88..203a146b1c0 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -202,8 +202,7 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -381,7 +380,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -390,8 +389,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2035,7 +2032,7 @@ do_autovacuum(void)
while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2074,7 +2071,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2116,7 +2113,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2139,7 +2136,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2158,7 +2155,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2774,39 +2771,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc_object(AutoVacOpts);
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2826,8 +2790,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2840,9 +2804,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2851,10 +2815,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- relation_needs_vacanalyze(relid, avopts, classForm,
+ relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
DEBUG3,
&dovacuum, &doanalyze, &wraparound,
@@ -2869,6 +2833,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_vacuum_min_duration;
int log_analyze_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2980,8 +2945,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2993,7 +2958,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none.
*
@@ -3067,7 +3032,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -3081,6 +3046,7 @@ relation_needs_vacanalyze(Oid relid,
bool force_vacuum;
bool av_enabled;
bool may_free = false;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3132,45 +3098,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
@@ -3661,7 +3627,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
- AutoVacOpts *avopts;
+ StdRdOptions *relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3677,14 +3643,14 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
- relation_needs_vacanalyze(form->oid, avopts, form,
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
- pfree(avopts);
+ if (relopts)
+ pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
vals[1] = Float8GetDatum(scores.max);
--
2.50.1 (Apple Git-155)
[text/plain] v5-0002-Make-autovacuum_enabled-a-ternary-reloption.patch (3.6K, ../../aihb_hMloqVDpbtw@nathan/3-v5-0002-Make-autovacuum_enabled-a-ternary-reloption.patch)
download | inline diff:
From 5f9a8cfb16bc1d83119d7273d6ba9bf9d3630c71 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:50:36 -0500
Subject: [PATCH v5 2/4] Make autovacuum_enabled a ternary reloption.
This commit reimplements autovacuum_enabled as a ternary, using the
support added in commit 4d6a66f675 and following the example of
vacuum_truncate. This changes only the internal representation: an
unset value still behaves as enabled, and the option accepts the
same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 19 +++++++++----------
src/backend/catalog/index.c | 3 ++-
src/backend/postmaster/autovacuum.c | 2 +-
src/include/utils/rel.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..79834126f2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -107,15 +107,6 @@ static relopt_bool boolRelOpts[] =
},
false
},
- {
- {
- "autovacuum_enabled",
- "Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
- ShareUpdateExclusiveLock
- },
- true
- },
{
{
"user_catalog_table",
@@ -168,6 +159,14 @@ static relopt_bool boolRelOpts[] =
static relopt_ternary ternaryRelOpts[] =
{
+ {
+ {
+ "autovacuum_enabled",
+ "Enables autovacuum in this relation",
+ RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ ShareUpdateExclusiveLock
+ }
+ },
{
{
"vacuum_truncate",
@@ -1976,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
static const relopt_parse_elt tab[] = {
{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_BOOL,
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
{"autovacuum_parallel_workers", RELOPT_TYPE_INT,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 9407c357f27..c9f32728902 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2873,7 +2873,8 @@ index_update_stats(Relation rel,
{
StdRdOptions *options = (StdRdOptions *) rel->rd_options;
- if (options != NULL && !options->autovacuum.enabled)
+ if (options != NULL &&
+ options->autovacuum.enabled == PG_TERNARY_FALSE)
update_stats = false;
}
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 203a146b1c0..aa2aca8fc4b 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3136,7 +3136,7 @@ relation_needs_vacanalyze(Oid relid,
? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index fa07ebf8ff7..f0824b6899a 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -310,7 +310,7 @@ typedef struct ForeignKeyCacheInfo
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
{
- bool enabled;
+ pg_ternary enabled;
int autovacuum_parallel_workers;
int vacuum_threshold;
--
2.50.1 (Apple Git-155)
[text/plain] v5-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch (3.5K, ../../aihb_hMloqVDpbtw@nathan/4-v5-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch)
download | inline diff:
From c4540d163b6b901d409416a7c55dabb174778978 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 15:27:58 -0500
Subject: [PATCH v5 3/4] Add an "unset" value for vacuum_index_cleanup.
This commit adds a new value to StdRdOptIndexCleanup to distinguish
whether it is explicitly set, similar to ViewOptCheckOption's
VIEW_OPTION_CHECK_OPTION_NOT_SET. This changes only the internal
representation; an unset value still defaults to AUTO, and the
option accepts the same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 3 ++-
src/backend/commands/vacuum.c | 23 ++++++++++++++---------
src/include/utils/rel.h | 1 +
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79834126f2f..58eb72e2339 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -517,6 +517,7 @@ static relopt_real realRelOpts[] =
/* values from StdRdOptIndexCleanup */
static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
{
+ /* no value for NOT_SET */
{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
@@ -557,7 +558,7 @@ static relopt_enum enumRelOpts[] =
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
},
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index a4abb29cf64..4ee1f913b64 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2191,20 +2191,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
StdRdOptIndexCleanup vacuum_index_cleanup;
if (rel->rd_options == NULL)
- vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+ vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
vacuum_index_cleanup =
((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
- if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
- params.index_cleanup = VACOPTVALUE_AUTO;
- else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
- params.index_cleanup = VACOPTVALUE_ENABLED;
- else
+ switch (vacuum_index_cleanup)
{
- Assert(vacuum_index_cleanup ==
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
- params.index_cleanup = VACOPTVALUE_DISABLED;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ params.index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ params.index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
}
}
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index f0824b6899a..f1b96b1099d 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,6 +338,7 @@ typedef enum StdRdOptIndexCleanup
STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
} StdRdOptIndexCleanup;
typedef struct StdRdOptions
--
2.50.1 (Apple Git-155)
[text/plain] v5-0004-Fix-VACUUM-and-autovacuum-handling-of-TOAST-stora.patch (17.1K, ../../aihb_hMloqVDpbtw@nathan/5-v5-0004-Fix-VACUUM-and-autovacuum-handling-of-TOAST-stora.patch)
download | inline diff:
From c7f6f3ddd622e9936a64aaa26de4ffadf8506405 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 9 Jun 2026 11:53:12 -0500
Subject: [PATCH v5 4/4] Fix VACUUM and autovacuum handling of TOAST storage
parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Unfortunately, current reality does not match this description.
Neither VACUUM nor autovacuum consults the main table's
non-autovacuum storage parameters, and autovacuum only consults the
main table's autovacuum-related storage parameters if the TOAST
table lacks any. One silver lining is that all currently-supported
TOAST storage parameters are related to vacuum, whose code paths
already access the main table's parameters. This means that our
solution needn't involve more lookups; we just need to propagate
them correctly.
To fix, this commit teaches autovacuum to combine the TOAST storage
parameters with the main table's (with the toast.* ones winning if
both are set), and it teaches VACUUM to send down the main table's
parameters when recursing to a TOAST table. This doesn't fix
VACUUM against a TOAST table directly (e.g., VACUUM
pg_toast.pg_toast_5432), but that's probably okay because it's not
the main supported way to vacuum a TOAST table (see VACUUM's
PROCESS_MAIN and PROCESS_TOAST options).
An existing shortcoming that this patch only makes worse is that
autovacuum/VACUUM remain oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/commands/vacuum.c | 36 +++-
src/backend/postmaster/autovacuum.c | 187 ++++++++++++++++--
src/include/commands/vacuum.h | 8 +
src/include/utils/rel.h | 3 +
.../injection_points/expected/vacuum.out | 11 ++
.../modules/injection_points/sql/vacuum.sql | 8 +
6 files changed, 234 insertions(+), 19 deletions(-)
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 4ee1f913b64..a2f77b349b2 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -187,6 +187,9 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_index_cleanup = VACOPTVALUE_UNSPECIFIED;
+ params.main_truncate = VACOPTVALUE_UNSPECIFIED;
+ params.main_max_eager_freeze_failure_rate = -1.0;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2184,7 +2187,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
- * specified in VACUUM command, or when running in an autovacuum worker
+ * specified in VACUUM command, or when running in an autovacuum worker. A
+ * TOAST table with no setting of its own inherits the main table's value.
*/
if (params.index_cleanup == VACOPTVALUE_UNSPECIFIED)
{
@@ -2200,15 +2204,21 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
params.index_cleanup = VACOPTVALUE_ENABLED;
+ toast_vacuum_params.main_index_cleanup = VACOPTVALUE_ENABLED;
break;
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
params.index_cleanup = VACOPTVALUE_DISABLED;
+ toast_vacuum_params.main_index_cleanup = VACOPTVALUE_DISABLED;
break;
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
params.index_cleanup = VACOPTVALUE_AUTO;
+ toast_vacuum_params.main_index_cleanup = VACOPTVALUE_AUTO;
break;
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
- params.index_cleanup = VACOPTVALUE_AUTO;
+ if (params.main_index_cleanup != VACOPTVALUE_UNSPECIFIED)
+ params.index_cleanup = params.main_index_cleanup;
+ else
+ params.index_cleanup = VACOPTVALUE_AUTO;
break;
}
}
@@ -2224,16 +2234,26 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
/*
* Check if the vacuum_max_eager_freeze_failure_rate table storage
- * parameter was specified. This overrides the GUC value.
+ * parameter was specified. This overrides the GUC value. A TOAST table
+ * with no setting of its own inherits the main table's value.
*/
if (rel->rd_options != NULL &&
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
+ {
params.max_eager_freeze_failure_rate =
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ toast_vacuum_params.main_max_eager_freeze_failure_rate =
+ ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ }
+ else if (params.main_max_eager_freeze_failure_rate >= 0.0)
+ params.max_eager_freeze_failure_rate =
+ params.main_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
- * specified in VACUUM command, or when running in an autovacuum worker
+ * specified in VACUUM command, or when running in an autovacuum worker. A
+ * TOAST table with no setting of its own inherits the main table's value
+ * before falling back to the GUC.
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
@@ -2242,10 +2262,18 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
{
if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ {
params.truncate = VACOPTVALUE_ENABLED;
+ toast_vacuum_params.main_truncate = VACOPTVALUE_ENABLED;
+ }
else
+ {
params.truncate = VACOPTVALUE_DISABLED;
+ toast_vacuum_params.main_truncate = VACOPTVALUE_DISABLED;
+ }
}
+ else if (params.main_truncate != VACOPTVALUE_UNSPECIFIED)
+ params.truncate = params.main_truncate;
else if (vacuum_truncate)
params.truncate = VACOPTVALUE_ENABLED;
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index aa2aca8fc4b..50c7f3d171d 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -396,6 +396,8 @@ static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
static void avl_sigusr2_handler(SIGNAL_ARGS);
static bool av_worker_available(void);
static void check_av_worker_gucs(void);
+static StdRdOptions *merge_autovac_opts(StdRdOptions *toast_opts,
+ StdRdOptions *main_opts);
@@ -2142,6 +2144,8 @@ do_autovacuum(void)
bool doanalyze;
bool wraparound;
AutoVacuumScores scores;
+ av_relation *hentry;
+ bool found;
/*
* We cannot safely process other backends' temp tables, so skip 'em.
@@ -2152,21 +2156,19 @@ do_autovacuum(void)
relid = classForm->oid;
/*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
+ * fetch reloptions -- merge any unset options from the main rel
+ *
+ * Note that we don't bother merging the non-autovacuum relopts here
+ * because they do not impact our choice of whether to process the
+ * table.
*/
relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
- relopts = &hentry->ar_reloptions;
- }
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ relopts = merge_autovac_opts(relopts, &hentry->ar_reloptions);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2791,6 +2793,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
autovac_table *tab = NULL;
bool wraparound;
StdRdOptions *relopts;
+ StdRdOptions *main_relopts = NULL;
bool free_relopts = false;
AutoVacuumScores scores;
@@ -2801,21 +2804,25 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+
+ if (classForm->relkind == RELKIND_TOASTVALUE &&
+ table_toast_map != NULL)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- relopts = &hentry->ar_reloptions;
+ {
+ main_relopts = &hentry->ar_reloptions;
+ relopts = merge_autovac_opts(relopts, main_relopts);
+ }
}
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2902,6 +2909,48 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ /*
+ * For TOAST tables, provide fallbacks for options that are not part
+ * of AutoVacOpts (and thus are not handled by merge_autovac_opts()).
+ */
+ tab->at_params.main_index_cleanup = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.main_truncate = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.main_max_eager_freeze_failure_rate = -1.0;
+
+ if (main_relopts != NULL)
+ {
+ switch (main_relopts->vacuum_index_cleanup)
+ {
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ break;
+ }
+
+ switch (main_relopts->vacuum_truncate)
+ {
+ case PG_TERNARY_TRUE:
+ tab->at_params.main_truncate = VACOPTVALUE_ENABLED;
+ break;
+ case PG_TERNARY_FALSE:
+ tab->at_params.main_truncate = VACOPTVALUE_DISABLED;
+ break;
+ case PG_TERNARY_UNSET:
+ break;
+ }
+
+ if (main_relopts->vacuum_max_eager_freeze_failure_rate >= 0.0)
+ tab->at_params.main_max_eager_freeze_failure_rate =
+ main_relopts->vacuum_max_eager_freeze_failure_rate;
+ }
+
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
if (avopts)
@@ -3670,3 +3719,111 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
return (Datum) 0;
}
+
+/*
+ * Combine a TOAST table's autovacuum options with the main table's. Any
+ * option the TOAST table leaves unset is taken from the main table's options.
+ * Either argument may be NULL. If both are NULL, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * NB: This function destructively modifies toast_opts!
+ */
+static StdRdOptions *
+merge_autovac_opts(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ /* ternary fields */
+ static const int ternary_offsets[] = {
+ offsetof(AutoVacOpts, enabled),
+ };
+
+ /* integer fields whose unset sentinel is -1 */
+ static const int int_offsets_1[] = {
+ offsetof(AutoVacOpts, autovacuum_parallel_workers),
+ offsetof(AutoVacOpts, vacuum_threshold),
+ offsetof(AutoVacOpts, analyze_threshold),
+ offsetof(AutoVacOpts, vacuum_cost_limit),
+ offsetof(AutoVacOpts, freeze_min_age),
+ offsetof(AutoVacOpts, freeze_max_age),
+ offsetof(AutoVacOpts, freeze_table_age),
+ offsetof(AutoVacOpts, multixact_freeze_min_age),
+ offsetof(AutoVacOpts, multixact_freeze_max_age),
+ offsetof(AutoVacOpts, multixact_freeze_table_age),
+ offsetof(AutoVacOpts, log_vacuum_min_duration),
+ offsetof(AutoVacOpts, log_analyze_min_duration),
+ };
+
+ /* integer fields whose unset sentinel is -2 */
+ static const int int_offsets_2[] = {
+ offsetof(AutoVacOpts, vacuum_max_threshold),
+ offsetof(AutoVacOpts, vacuum_ins_threshold),
+ };
+
+ /* float fields */
+ static const int float_offsets[] = {
+ offsetof(AutoVacOpts, vacuum_cost_delay),
+ offsetof(AutoVacOpts, vacuum_scale_factor),
+ offsetof(AutoVacOpts, vacuum_ins_scale_factor),
+ offsetof(AutoVacOpts, analyze_scale_factor),
+ };
+
+ AutoVacOpts *toast_avopts;
+ AutoVacOpts *main_avopts;
+
+ if (toast_opts == NULL)
+ return main_opts;
+ if (main_opts == NULL)
+ return toast_opts;
+
+ toast_avopts = &toast_opts->autovacuum;
+ main_avopts = &main_opts->autovacuum;
+
+ for (int i = 0; i < lengthof(ternary_offsets); i++)
+ {
+ pg_ternary *toast_opt;
+ pg_ternary *main_opt;
+
+ toast_opt = (pg_ternary *) ((char *) toast_avopts + ternary_offsets[i]);
+ main_opt = (pg_ternary *) ((char *) main_avopts + ternary_offsets[i]);
+
+ if (*toast_opt == PG_TERNARY_UNSET)
+ *toast_opt = *main_opt;
+ }
+
+ for (int i = 0; i < lengthof(int_offsets_1); i++)
+ {
+ int *toast_opt;
+ int *main_opt;
+
+ toast_opt = (int *) ((char *) toast_avopts + int_offsets_1[i]);
+ main_opt = (int *) ((char *) main_avopts + int_offsets_1[i]);
+
+ if (*toast_opt == -1)
+ *toast_opt = *main_opt;
+ }
+
+ for (int i = 0; i < lengthof(int_offsets_2); i++)
+ {
+ int *toast_opt;
+ int *main_opt;
+
+ toast_opt = (int *) ((char *) toast_avopts + int_offsets_2[i]);
+ main_opt = (int *) ((char *) main_avopts + int_offsets_2[i]);
+
+ if (*toast_opt == -2)
+ *toast_opt = *main_opt;
+ }
+
+ for (int i = 0; i < lengthof(float_offsets); i++)
+ {
+ double *toast_opt;
+ double *main_opt;
+
+ toast_opt = (double *) ((char *) toast_avopts + float_offsets[i]);
+ main_opt = (double *) ((char *) main_avopts + float_offsets[i]);
+
+ if (*toast_opt == -1.0)
+ *toast_opt = *main_opt;
+ }
+
+ return toast_opts;
+}
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9cea36d..335d1516a7e 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,14 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * Main table fallback values for TOAST tables to inherit when they have
+ * no setting of their own.
+ */
+ VacOptValue main_index_cleanup;
+ VacOptValue main_truncate;
+ double main_max_eager_freeze_failure_rate;
} VacuumParams;
/*
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index f1b96b1099d..9b55c601e94 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -306,6 +306,9 @@ typedef struct ForeignKeyCacheInfo
* RelationGetFillFactor() and RelationGetTargetPageFreeSpace() can only
* be applied to relations that use this format or a superset for
* private options data.
+ *
+ * NB: When adding a new member, be sure to update merge_autovac_opts() and/or
+ * table_recheck_autovac() as necessary!
*/
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..0f0790a8584 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,6 +79,17 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- TOAST table inherits main table's resolved values
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
+DROP TABLE vac_tab_toast_inherit;
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..11a371e4ff7 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,6 +33,14 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- TOAST table inherits main table's resolved values
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+DROP TABLE vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-06-10 11:41 solai v <solai.cdac@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: solai v @ 2026-06-10 11:41 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: Nikita Malakhov <hukutoc@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
Hi,
I tested the v5 patch series on the current HEAD.
The patches applied and built successfully. I verified that TOAST
reloptions are still accepted and stored correctly. I also tested
parent and TOAST reloption combinations, RESET behavior, and VACUUM
processing.
For example, with:
ALTER TABLE toast_test2 SET (vacuum_truncate = false);
ALTER TABLE toast_test2 SET (toast.vacuum_truncate = true);
resetting the parent reloption preserved the explicit TOAST reloption
as expected.
I also verified that VACUUM (PROCESS_MAIN false) correctly processes
only the associated TOAST table.
I did not observe any issues during testing.
Regards,
solai
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-06 21:38 Nathan Bossart <nathandbossart@gmail.com>
parent: solai v <solai.cdac@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2026-08-06 21:38 UTC (permalink / raw)
To: solai v <solai.cdac@gmail.com>; +Cc: Nikita Malakhov <hukutoc@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
On Wed, Jun 10, 2026 at 05:11:36PM +0530, solai v wrote:
> I did not observe any issues during testing.
Thanks for looking. Here is a new patch set with some small fixes.
--
nathan
From dea49b2bdb905e0688ace601826e9208244f4393 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:35:34 -0500
Subject: [PATCH v6 1/4] Remove extract_autovac_opts().
extract_autovac_opts() returned a palloc'd copy of only the
AutoVacOpts portion of a relation's reloptions. Upcoming work
needs the rest of the StdRdOptions as well, so the callers must
keep the whole struct around.
Remove the helper and have the callers obtain reloptions from
extractRelOptions() directly. av_relation now caches a
StdRdOptions instead of an AutoVacOpts, and
relation_needs_vacanalyze() takes a StdRdOptions and extracts the
autovacuum portion itself. This is preparatory refactoring with no
change in behavior.
---
src/backend/postmaster/autovacuum.c | 122 ++++++++++------------------
1 file changed, 44 insertions(+), 78 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index ee202a4b47e..db1f5d0e575 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -202,8 +202,7 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -379,7 +378,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -388,8 +387,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2037,7 +2034,7 @@ do_autovacuum(void)
while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2076,7 +2073,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2118,7 +2115,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2141,7 +2138,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2160,7 +2157,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2776,39 +2773,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc_object(AutoVacOpts);
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2828,8 +2792,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2842,9 +2806,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2853,10 +2817,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- relation_needs_vacanalyze(relid, avopts, classForm,
+ relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
DEBUG3,
&dovacuum, &doanalyze, &wraparound,
@@ -2871,6 +2835,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_vacuum_min_duration;
int log_analyze_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2982,8 +2947,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2995,7 +2960,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none.
*
@@ -3072,7 +3037,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -3086,6 +3051,7 @@ relation_needs_vacanalyze(Oid relid,
bool force_vacuum;
bool av_enabled;
bool may_free = false;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3137,45 +3103,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
@@ -3668,7 +3634,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
- AutoVacOpts *avopts;
+ StdRdOptions *relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3684,14 +3650,14 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
- relation_needs_vacanalyze(form->oid, avopts, form,
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
- pfree(avopts);
+ if (relopts)
+ pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
vals[1] = Float8GetDatum(scores.max);
--
2.50.1 (Apple Git-155)
From 21c53bbe52be14e29e5c3ccea9c1bb24804ece58 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:50:36 -0500
Subject: [PATCH v6 2/4] Make autovacuum_enabled a ternary reloption.
This commit reimplements autovacuum_enabled as a ternary, using the
support added in commit 4d6a66f675 and following the example of
vacuum_truncate. This changes only the internal representation: an
unset value still behaves as enabled, and the option accepts the
same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 19 +++++++++----------
src/backend/catalog/index.c | 3 ++-
src/backend/postmaster/autovacuum.c | 2 +-
src/include/utils/rel.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..79834126f2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -107,15 +107,6 @@ static relopt_bool boolRelOpts[] =
},
false
},
- {
- {
- "autovacuum_enabled",
- "Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
- ShareUpdateExclusiveLock
- },
- true
- },
{
{
"user_catalog_table",
@@ -168,6 +159,14 @@ static relopt_bool boolRelOpts[] =
static relopt_ternary ternaryRelOpts[] =
{
+ {
+ {
+ "autovacuum_enabled",
+ "Enables autovacuum in this relation",
+ RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ ShareUpdateExclusiveLock
+ }
+ },
{
{
"vacuum_truncate",
@@ -1976,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
static const relopt_parse_elt tab[] = {
{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_BOOL,
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
{"autovacuum_parallel_workers", RELOPT_TYPE_INT,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 7c1b94407a9..1cd1368984e 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2897,7 +2897,8 @@ index_update_stats(Relation rel,
{
StdRdOptions *options = (StdRdOptions *) rel->rd_options;
- if (options != NULL && !options->autovacuum.enabled)
+ if (options != NULL &&
+ options->autovacuum.enabled == PG_TERNARY_FALSE)
update_stats = false;
}
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index db1f5d0e575..c8a1b66ca2e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3141,7 +3141,7 @@ relation_needs_vacanalyze(Oid relid,
? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 89c159b133f..2ee98e9c6cc 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -310,7 +310,7 @@ typedef struct ForeignKeyCacheInfo
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
{
- bool enabled;
+ pg_ternary enabled;
int autovacuum_parallel_workers;
int vacuum_threshold;
--
2.50.1 (Apple Git-155)
From 7fd1c7d4fdb1763b44105d3ce2a2bf0712f1fa2e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 15:27:58 -0500
Subject: [PATCH v6 3/4] Add an "unset" value for vacuum_index_cleanup.
This commit adds a new value to StdRdOptIndexCleanup to distinguish
whether it is explicitly set, similar to ViewOptCheckOption's
VIEW_OPTION_CHECK_OPTION_NOT_SET. This changes only the internal
representation; an unset value still defaults to AUTO, and the
option accepts the same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 3 ++-
src/backend/commands/vacuum.c | 23 ++++++++++++++---------
src/include/utils/rel.h | 1 +
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79834126f2f..58eb72e2339 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -517,6 +517,7 @@ static relopt_real realRelOpts[] =
/* values from StdRdOptIndexCleanup */
static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
{
+ /* no value for NOT_SET */
{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
@@ -557,7 +558,7 @@ static relopt_enum enumRelOpts[] =
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
},
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 31f9824899c..52116c02b59 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2210,20 +2210,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
StdRdOptIndexCleanup vacuum_index_cleanup;
if (rel->rd_options == NULL)
- vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+ vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
vacuum_index_cleanup =
((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
- if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
- params.index_cleanup = VACOPTVALUE_AUTO;
- else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
- params.index_cleanup = VACOPTVALUE_ENABLED;
- else
+ switch (vacuum_index_cleanup)
{
- Assert(vacuum_index_cleanup ==
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
- params.index_cleanup = VACOPTVALUE_DISABLED;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ params.index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ params.index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
}
}
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 2ee98e9c6cc..01b6e1a872c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,6 +338,7 @@ typedef enum StdRdOptIndexCleanup
STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
} StdRdOptIndexCleanup;
typedef struct StdRdOptions
--
2.50.1 (Apple Git-155)
From 8f0361e9f7b418a573a0166b34ed66151e9a87a9 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Thu, 6 Aug 2026 15:19:54 -0500
Subject: [PATCH v6 4/4] Fix VACUUM and autovacuum handling of TOAST storage
parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Unfortunately, current reality does not match this description.
Neither VACUUM nor autovacuum consults the main table's
non-autovacuum storage parameters, and autovacuum only consults the
main table's autovacuum-related storage parameters if the TOAST
table lacks any. One silver lining is that all currently-supported
TOAST storage parameters are related to vacuum, whose code paths
already access the main table's parameters. This means that our
solution needn't involve more lookups; we just need to propagate
them correctly.
To fix, this commit teaches autovacuum to combine the TOAST storage
parameters with the main table's (with the toast.* ones winning if
both are set), and it teaches VACUUM to send down the main table's
parameters when recursing to a TOAST table. This doesn't fix
VACUUM against a TOAST table directly (e.g., VACUUM
pg_toast.pg_toast_5432), but that's probably okay because it's not
the main supported way to vacuum a TOAST table (see VACUUM's
PROCESS_MAIN and PROCESS_TOAST options).
An existing shortcoming that this patch only makes worse is that
autovacuum/VACUUM remain oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/access/common/reloptions.c | 2 +-
src/backend/commands/vacuum.c | 36 ++-
src/backend/postmaster/autovacuum.c | 266 ++++++++++++++++--
src/include/commands/vacuum.h | 8 +
src/include/utils/rel.h | 3 +
.../injection_points/expected/vacuum.out | 11 +
.../modules/injection_points/sql/vacuum.sql | 8 +
7 files changed, 306 insertions(+), 28 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 58eb72e2339..94e9152cbab 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
},
{
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 52116c02b59..6a8df38ca13 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -187,6 +187,9 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_index_cleanup = VACOPTVALUE_UNSPECIFIED;
+ params.main_truncate = VACOPTVALUE_UNSPECIFIED;
+ params.main_max_eager_freeze_failure_rate = -1.0;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2203,7 +2206,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
- * specified in VACUUM command, or when running in an autovacuum worker
+ * specified in VACUUM command, or when running in an autovacuum worker. A
+ * TOAST table with no setting of its own inherits the main table's value.
*/
if (params.index_cleanup == VACOPTVALUE_UNSPECIFIED)
{
@@ -2219,15 +2223,21 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
params.index_cleanup = VACOPTVALUE_ENABLED;
+ toast_vacuum_params.main_index_cleanup = VACOPTVALUE_ENABLED;
break;
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
params.index_cleanup = VACOPTVALUE_DISABLED;
+ toast_vacuum_params.main_index_cleanup = VACOPTVALUE_DISABLED;
break;
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
params.index_cleanup = VACOPTVALUE_AUTO;
+ toast_vacuum_params.main_index_cleanup = VACOPTVALUE_AUTO;
break;
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
- params.index_cleanup = VACOPTVALUE_AUTO;
+ if (params.main_index_cleanup != VACOPTVALUE_UNSPECIFIED)
+ params.index_cleanup = params.main_index_cleanup;
+ else
+ params.index_cleanup = VACOPTVALUE_AUTO;
break;
}
}
@@ -2243,16 +2253,26 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
/*
* Check if the vacuum_max_eager_freeze_failure_rate table storage
- * parameter was specified. This overrides the GUC value.
+ * parameter was specified. This overrides the GUC value. A TOAST table
+ * with no setting of its own inherits the main table's value.
*/
if (rel->rd_options != NULL &&
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
+ {
params.max_eager_freeze_failure_rate =
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ toast_vacuum_params.main_max_eager_freeze_failure_rate =
+ ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ }
+ else if (params.main_max_eager_freeze_failure_rate >= 0.0)
+ params.max_eager_freeze_failure_rate =
+ params.main_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
- * specified in VACUUM command, or when running in an autovacuum worker
+ * specified in VACUUM command, or when running in an autovacuum worker. A
+ * TOAST table with no setting of its own inherits the main table's value
+ * before falling back to the GUC.
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
@@ -2261,10 +2281,18 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
{
if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ {
params.truncate = VACOPTVALUE_ENABLED;
+ toast_vacuum_params.main_truncate = VACOPTVALUE_ENABLED;
+ }
else
+ {
params.truncate = VACOPTVALUE_DISABLED;
+ toast_vacuum_params.main_truncate = VACOPTVALUE_DISABLED;
+ }
}
+ else if (params.main_truncate != VACOPTVALUE_UNSPECIFIED)
+ params.truncate = params.main_truncate;
else if (vacuum_truncate)
params.truncate = VACOPTVALUE_ENABLED;
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index c8a1b66ca2e..7b72a262ca5 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -394,6 +394,8 @@ static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
static void avl_sigusr2_handler(SIGNAL_ARGS);
static bool av_worker_available(void);
static void check_av_worker_gucs(void);
+static StdRdOptions *merge_autovac_opts(StdRdOptions *toast_opts,
+ StdRdOptions *main_opts);
@@ -2017,9 +2019,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2144,6 +2146,8 @@ do_autovacuum(void)
bool doanalyze;
bool wraparound;
AutoVacuumScores scores;
+ av_relation *hentry;
+ bool found;
/*
* We cannot safely process other backends' temp tables, so skip 'em.
@@ -2154,21 +2158,19 @@ do_autovacuum(void)
relid = classForm->oid;
/*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
+ * fetch reloptions -- merge any unset options from the main rel
+ *
+ * Note that we don't bother merging the non-autovacuum relopts here
+ * because they do not impact our choice of whether to process the
+ * table.
*/
relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
- relopts = &hentry->ar_reloptions;
- }
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ relopts = merge_autovac_opts(relopts, &hentry->ar_reloptions);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2793,6 +2795,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
autovac_table *tab = NULL;
bool wraparound;
StdRdOptions *relopts;
+ StdRdOptions *main_relopts = NULL;
bool free_relopts = false;
AutoVacuumScores scores;
@@ -2803,21 +2806,25 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+
+ if (classForm->relkind == RELKIND_TOASTVALUE &&
+ table_toast_map != NULL)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- relopts = &hentry->ar_reloptions;
+ {
+ main_relopts = &hentry->ar_reloptions;
+ relopts = merge_autovac_opts(relopts, main_relopts);
+ }
}
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2904,6 +2911,48 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ /*
+ * For TOAST tables, provide fallbacks for options that are not part
+ * of AutoVacOpts (and thus are not handled by merge_autovac_opts()).
+ */
+ tab->at_params.main_index_cleanup = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.main_truncate = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.main_max_eager_freeze_failure_rate = -1.0;
+
+ if (main_relopts != NULL)
+ {
+ switch (main_relopts->vacuum_index_cleanup)
+ {
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ break;
+ }
+
+ switch (main_relopts->vacuum_truncate)
+ {
+ case PG_TERNARY_TRUE:
+ tab->at_params.main_truncate = VACOPTVALUE_ENABLED;
+ break;
+ case PG_TERNARY_FALSE:
+ tab->at_params.main_truncate = VACOPTVALUE_DISABLED;
+ break;
+ case PG_TERNARY_UNSET:
+ break;
+ }
+
+ if (main_relopts->vacuum_max_eager_freeze_failure_rate >= 0.0)
+ tab->at_params.main_max_eager_freeze_failure_rate =
+ main_relopts->vacuum_max_eager_freeze_failure_rate;
+ }
+
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
if (avopts)
@@ -2960,9 +3009,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the StdRdOptions options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * relopts is a pointer to the StdRdOptions options (the relation's own for a
+ * plain table, or those merged with the main table's for a TOAST table), NULL
+ * if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3619,6 +3668,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
Relation rel;
TableScanDesc scan;
HeapTuple tup;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
InitMaterializedSRF(fcinfo, 0);
@@ -3628,13 +3679,64 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS);
+
rel = table_open(RelationRelationId, AccessShareLock);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found);
+
+ /* hash_search already filled in the key */
+ hentry->ar_relid = form->oid;
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parent tables' reloptions, we can generate the
+ * results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ bool free_relopts = false;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3651,12 +3753,25 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
continue;
relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (relopts)
+ free_relopts = true;
+ if (form->relkind == RELKIND_TOASTVALUE)
+ {
+ av_relation *hentry;
+ bool found;
+
+ hentry = hash_search(table_toast_map, &form->oid,
+ HASH_FIND, &found);
+ if (found)
+ relopts = merge_autovac_opts(relopts, &hentry->ar_reloptions);
+ }
+
relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (relopts)
+ if (free_relopts)
pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
@@ -3674,6 +3789,111 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
+
+/*
+ * Combine a TOAST table's relevant autovacuum options with the main table's.
+ * Any option the TOAST table leaves unset is taken from the main table's
+ * options. Either argument may be NULL. If both are NULL, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * NB: This function destructively modifies toast_opts!
+ */
+static StdRdOptions *
+merge_autovac_opts(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ /* ternary fields */
+ static const int ternary_offsets[] = {
+ offsetof(AutoVacOpts, enabled),
+ };
+
+ /* integer fields whose unset sentinel is -1 */
+ static const int int_offsets_1[] = {
+ offsetof(AutoVacOpts, vacuum_threshold),
+ offsetof(AutoVacOpts, vacuum_cost_limit),
+ offsetof(AutoVacOpts, freeze_min_age),
+ offsetof(AutoVacOpts, freeze_max_age),
+ offsetof(AutoVacOpts, freeze_table_age),
+ offsetof(AutoVacOpts, multixact_freeze_min_age),
+ offsetof(AutoVacOpts, multixact_freeze_max_age),
+ offsetof(AutoVacOpts, multixact_freeze_table_age),
+ };
+
+ /* integer fields whose unset sentinel is -2 */
+ static const int int_offsets_2[] = {
+ offsetof(AutoVacOpts, vacuum_max_threshold),
+ offsetof(AutoVacOpts, vacuum_ins_threshold),
+ offsetof(AutoVacOpts, log_vacuum_min_duration),
+ };
+
+ /* float fields */
+ static const int float_offsets[] = {
+ offsetof(AutoVacOpts, vacuum_cost_delay),
+ offsetof(AutoVacOpts, vacuum_scale_factor),
+ offsetof(AutoVacOpts, vacuum_ins_scale_factor),
+ };
+
+ AutoVacOpts *toast_avopts;
+ AutoVacOpts *main_avopts;
+
+ if (toast_opts == NULL)
+ return main_opts;
+ if (main_opts == NULL)
+ return toast_opts;
+
+ toast_avopts = &toast_opts->autovacuum;
+ main_avopts = &main_opts->autovacuum;
+
+ for (int i = 0; i < lengthof(ternary_offsets); i++)
+ {
+ pg_ternary *toast_opt;
+ pg_ternary *main_opt;
+
+ toast_opt = (pg_ternary *) ((char *) toast_avopts + ternary_offsets[i]);
+ main_opt = (pg_ternary *) ((char *) main_avopts + ternary_offsets[i]);
+
+ if (*toast_opt == PG_TERNARY_UNSET)
+ *toast_opt = *main_opt;
+ }
+
+ for (int i = 0; i < lengthof(int_offsets_1); i++)
+ {
+ int *toast_opt;
+ int *main_opt;
+
+ toast_opt = (int *) ((char *) toast_avopts + int_offsets_1[i]);
+ main_opt = (int *) ((char *) main_avopts + int_offsets_1[i]);
+
+ if (*toast_opt == -1)
+ *toast_opt = *main_opt;
+ }
+
+ for (int i = 0; i < lengthof(int_offsets_2); i++)
+ {
+ int *toast_opt;
+ int *main_opt;
+
+ toast_opt = (int *) ((char *) toast_avopts + int_offsets_2[i]);
+ main_opt = (int *) ((char *) main_avopts + int_offsets_2[i]);
+
+ if (*toast_opt == -2)
+ *toast_opt = *main_opt;
+ }
+
+ for (int i = 0; i < lengthof(float_offsets); i++)
+ {
+ double *toast_opt;
+ double *main_opt;
+
+ toast_opt = (double *) ((char *) toast_avopts + float_offsets[i]);
+ main_opt = (double *) ((char *) main_avopts + float_offsets[i]);
+
+ if (*toast_opt == -1.0)
+ *toast_opt = *main_opt;
+ }
+
+ return toast_opts;
+}
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..fdfceb38396 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,14 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * Main table fallback values for TOAST tables to inherit when they have
+ * no setting of their own.
+ */
+ VacOptValue main_index_cleanup;
+ VacOptValue main_truncate;
+ double main_max_eager_freeze_failure_rate;
} VacuumParams;
/*
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 01b6e1a872c..4750ea8d864 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -306,6 +306,9 @@ typedef struct ForeignKeyCacheInfo
* RelationGetFillFactor() and RelationGetTargetPageFreeSpace() can only
* be applied to relations that use this format or a superset for
* private options data.
+ *
+ * NB: When adding a new member, be sure to update merge_autovac_opts() and/or
+ * table_recheck_autovac() as necessary!
*/
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..0f0790a8584 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,6 +79,17 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- TOAST table inherits main table's resolved values
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
+DROP TABLE vac_tab_toast_inherit;
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..11a371e4ff7 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,6 +33,14 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- TOAST table inherits main table's resolved values
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+DROP TABLE vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
--
2.50.1 (Apple Git-155)
Attachments:
[text/plain] v6-0001-Remove-extract_autovac_opts.patch (11.5K, ../../anT-1AlQKmXtcfrP@nathan/2-v6-0001-Remove-extract_autovac_opts.patch)
download | inline diff:
From dea49b2bdb905e0688ace601826e9208244f4393 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:35:34 -0500
Subject: [PATCH v6 1/4] Remove extract_autovac_opts().
extract_autovac_opts() returned a palloc'd copy of only the
AutoVacOpts portion of a relation's reloptions. Upcoming work
needs the rest of the StdRdOptions as well, so the callers must
keep the whole struct around.
Remove the helper and have the callers obtain reloptions from
extractRelOptions() directly. av_relation now caches a
StdRdOptions instead of an AutoVacOpts, and
relation_needs_vacanalyze() takes a StdRdOptions and extracts the
autovacuum portion itself. This is preparatory refactoring with no
change in behavior.
---
src/backend/postmaster/autovacuum.c | 122 ++++++++++------------------
1 file changed, 44 insertions(+), 78 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index ee202a4b47e..db1f5d0e575 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -202,8 +202,7 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -379,7 +378,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -388,8 +387,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2037,7 +2034,7 @@ do_autovacuum(void)
while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2076,7 +2073,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2118,7 +2115,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2141,7 +2138,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2160,7 +2157,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2776,39 +2773,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc_object(AutoVacOpts);
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2828,8 +2792,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2842,9 +2806,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2853,10 +2817,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- relation_needs_vacanalyze(relid, avopts, classForm,
+ relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
DEBUG3,
&dovacuum, &doanalyze, &wraparound,
@@ -2871,6 +2835,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_vacuum_min_duration;
int log_analyze_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2982,8 +2947,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2995,7 +2960,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none.
*
@@ -3072,7 +3037,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -3086,6 +3051,7 @@ relation_needs_vacanalyze(Oid relid,
bool force_vacuum;
bool av_enabled;
bool may_free = false;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3137,45 +3103,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
@@ -3668,7 +3634,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
- AutoVacOpts *avopts;
+ StdRdOptions *relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3684,14 +3650,14 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
- relation_needs_vacanalyze(form->oid, avopts, form,
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
- pfree(avopts);
+ if (relopts)
+ pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
vals[1] = Float8GetDatum(scores.max);
--
2.50.1 (Apple Git-155)
[text/plain] v6-0002-Make-autovacuum_enabled-a-ternary-reloption.patch (3.6K, ../../anT-1AlQKmXtcfrP@nathan/3-v6-0002-Make-autovacuum_enabled-a-ternary-reloption.patch)
download | inline diff:
From 21c53bbe52be14e29e5c3ccea9c1bb24804ece58 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:50:36 -0500
Subject: [PATCH v6 2/4] Make autovacuum_enabled a ternary reloption.
This commit reimplements autovacuum_enabled as a ternary, using the
support added in commit 4d6a66f675 and following the example of
vacuum_truncate. This changes only the internal representation: an
unset value still behaves as enabled, and the option accepts the
same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 19 +++++++++----------
src/backend/catalog/index.c | 3 ++-
src/backend/postmaster/autovacuum.c | 2 +-
src/include/utils/rel.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..79834126f2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -107,15 +107,6 @@ static relopt_bool boolRelOpts[] =
},
false
},
- {
- {
- "autovacuum_enabled",
- "Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
- ShareUpdateExclusiveLock
- },
- true
- },
{
{
"user_catalog_table",
@@ -168,6 +159,14 @@ static relopt_bool boolRelOpts[] =
static relopt_ternary ternaryRelOpts[] =
{
+ {
+ {
+ "autovacuum_enabled",
+ "Enables autovacuum in this relation",
+ RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ ShareUpdateExclusiveLock
+ }
+ },
{
{
"vacuum_truncate",
@@ -1976,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
static const relopt_parse_elt tab[] = {
{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_BOOL,
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
{"autovacuum_parallel_workers", RELOPT_TYPE_INT,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 7c1b94407a9..1cd1368984e 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2897,7 +2897,8 @@ index_update_stats(Relation rel,
{
StdRdOptions *options = (StdRdOptions *) rel->rd_options;
- if (options != NULL && !options->autovacuum.enabled)
+ if (options != NULL &&
+ options->autovacuum.enabled == PG_TERNARY_FALSE)
update_stats = false;
}
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index db1f5d0e575..c8a1b66ca2e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3141,7 +3141,7 @@ relation_needs_vacanalyze(Oid relid,
? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 89c159b133f..2ee98e9c6cc 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -310,7 +310,7 @@ typedef struct ForeignKeyCacheInfo
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
{
- bool enabled;
+ pg_ternary enabled;
int autovacuum_parallel_workers;
int vacuum_threshold;
--
2.50.1 (Apple Git-155)
[text/plain] v6-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch (3.5K, ../../anT-1AlQKmXtcfrP@nathan/4-v6-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch)
download | inline diff:
From 7fd1c7d4fdb1763b44105d3ce2a2bf0712f1fa2e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 15:27:58 -0500
Subject: [PATCH v6 3/4] Add an "unset" value for vacuum_index_cleanup.
This commit adds a new value to StdRdOptIndexCleanup to distinguish
whether it is explicitly set, similar to ViewOptCheckOption's
VIEW_OPTION_CHECK_OPTION_NOT_SET. This changes only the internal
representation; an unset value still defaults to AUTO, and the
option accepts the same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 3 ++-
src/backend/commands/vacuum.c | 23 ++++++++++++++---------
src/include/utils/rel.h | 1 +
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79834126f2f..58eb72e2339 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -517,6 +517,7 @@ static relopt_real realRelOpts[] =
/* values from StdRdOptIndexCleanup */
static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
{
+ /* no value for NOT_SET */
{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
@@ -557,7 +558,7 @@ static relopt_enum enumRelOpts[] =
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
},
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 31f9824899c..52116c02b59 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2210,20 +2210,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
StdRdOptIndexCleanup vacuum_index_cleanup;
if (rel->rd_options == NULL)
- vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+ vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
vacuum_index_cleanup =
((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
- if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
- params.index_cleanup = VACOPTVALUE_AUTO;
- else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
- params.index_cleanup = VACOPTVALUE_ENABLED;
- else
+ switch (vacuum_index_cleanup)
{
- Assert(vacuum_index_cleanup ==
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
- params.index_cleanup = VACOPTVALUE_DISABLED;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ params.index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ params.index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
}
}
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 2ee98e9c6cc..01b6e1a872c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,6 +338,7 @@ typedef enum StdRdOptIndexCleanup
STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
} StdRdOptIndexCleanup;
typedef struct StdRdOptions
--
2.50.1 (Apple Git-155)
[text/plain] v6-0004-Fix-VACUUM-and-autovacuum-handling-of-TOAST-stora.patch (21.8K, ../../anT-1AlQKmXtcfrP@nathan/5-v6-0004-Fix-VACUUM-and-autovacuum-handling-of-TOAST-stora.patch)
download | inline diff:
From 8f0361e9f7b418a573a0166b34ed66151e9a87a9 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Thu, 6 Aug 2026 15:19:54 -0500
Subject: [PATCH v6 4/4] Fix VACUUM and autovacuum handling of TOAST storage
parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Unfortunately, current reality does not match this description.
Neither VACUUM nor autovacuum consults the main table's
non-autovacuum storage parameters, and autovacuum only consults the
main table's autovacuum-related storage parameters if the TOAST
table lacks any. One silver lining is that all currently-supported
TOAST storage parameters are related to vacuum, whose code paths
already access the main table's parameters. This means that our
solution needn't involve more lookups; we just need to propagate
them correctly.
To fix, this commit teaches autovacuum to combine the TOAST storage
parameters with the main table's (with the toast.* ones winning if
both are set), and it teaches VACUUM to send down the main table's
parameters when recursing to a TOAST table. This doesn't fix
VACUUM against a TOAST table directly (e.g., VACUUM
pg_toast.pg_toast_5432), but that's probably okay because it's not
the main supported way to vacuum a TOAST table (see VACUUM's
PROCESS_MAIN and PROCESS_TOAST options).
An existing shortcoming that this patch only makes worse is that
autovacuum/VACUUM remain oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/access/common/reloptions.c | 2 +-
src/backend/commands/vacuum.c | 36 ++-
src/backend/postmaster/autovacuum.c | 266 ++++++++++++++++--
src/include/commands/vacuum.h | 8 +
src/include/utils/rel.h | 3 +
.../injection_points/expected/vacuum.out | 11 +
.../modules/injection_points/sql/vacuum.sql | 8 +
7 files changed, 306 insertions(+), 28 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 58eb72e2339..94e9152cbab 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
},
{
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 52116c02b59..6a8df38ca13 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -187,6 +187,9 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_index_cleanup = VACOPTVALUE_UNSPECIFIED;
+ params.main_truncate = VACOPTVALUE_UNSPECIFIED;
+ params.main_max_eager_freeze_failure_rate = -1.0;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2203,7 +2206,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
- * specified in VACUUM command, or when running in an autovacuum worker
+ * specified in VACUUM command, or when running in an autovacuum worker. A
+ * TOAST table with no setting of its own inherits the main table's value.
*/
if (params.index_cleanup == VACOPTVALUE_UNSPECIFIED)
{
@@ -2219,15 +2223,21 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
params.index_cleanup = VACOPTVALUE_ENABLED;
+ toast_vacuum_params.main_index_cleanup = VACOPTVALUE_ENABLED;
break;
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
params.index_cleanup = VACOPTVALUE_DISABLED;
+ toast_vacuum_params.main_index_cleanup = VACOPTVALUE_DISABLED;
break;
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
params.index_cleanup = VACOPTVALUE_AUTO;
+ toast_vacuum_params.main_index_cleanup = VACOPTVALUE_AUTO;
break;
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
- params.index_cleanup = VACOPTVALUE_AUTO;
+ if (params.main_index_cleanup != VACOPTVALUE_UNSPECIFIED)
+ params.index_cleanup = params.main_index_cleanup;
+ else
+ params.index_cleanup = VACOPTVALUE_AUTO;
break;
}
}
@@ -2243,16 +2253,26 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
/*
* Check if the vacuum_max_eager_freeze_failure_rate table storage
- * parameter was specified. This overrides the GUC value.
+ * parameter was specified. This overrides the GUC value. A TOAST table
+ * with no setting of its own inherits the main table's value.
*/
if (rel->rd_options != NULL &&
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
+ {
params.max_eager_freeze_failure_rate =
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ toast_vacuum_params.main_max_eager_freeze_failure_rate =
+ ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ }
+ else if (params.main_max_eager_freeze_failure_rate >= 0.0)
+ params.max_eager_freeze_failure_rate =
+ params.main_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
- * specified in VACUUM command, or when running in an autovacuum worker
+ * specified in VACUUM command, or when running in an autovacuum worker. A
+ * TOAST table with no setting of its own inherits the main table's value
+ * before falling back to the GUC.
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
@@ -2261,10 +2281,18 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
{
if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ {
params.truncate = VACOPTVALUE_ENABLED;
+ toast_vacuum_params.main_truncate = VACOPTVALUE_ENABLED;
+ }
else
+ {
params.truncate = VACOPTVALUE_DISABLED;
+ toast_vacuum_params.main_truncate = VACOPTVALUE_DISABLED;
+ }
}
+ else if (params.main_truncate != VACOPTVALUE_UNSPECIFIED)
+ params.truncate = params.main_truncate;
else if (vacuum_truncate)
params.truncate = VACOPTVALUE_ENABLED;
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index c8a1b66ca2e..7b72a262ca5 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -394,6 +394,8 @@ static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
static void avl_sigusr2_handler(SIGNAL_ARGS);
static bool av_worker_available(void);
static void check_av_worker_gucs(void);
+static StdRdOptions *merge_autovac_opts(StdRdOptions *toast_opts,
+ StdRdOptions *main_opts);
@@ -2017,9 +2019,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2144,6 +2146,8 @@ do_autovacuum(void)
bool doanalyze;
bool wraparound;
AutoVacuumScores scores;
+ av_relation *hentry;
+ bool found;
/*
* We cannot safely process other backends' temp tables, so skip 'em.
@@ -2154,21 +2158,19 @@ do_autovacuum(void)
relid = classForm->oid;
/*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
+ * fetch reloptions -- merge any unset options from the main rel
+ *
+ * Note that we don't bother merging the non-autovacuum relopts here
+ * because they do not impact our choice of whether to process the
+ * table.
*/
relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
- relopts = &hentry->ar_reloptions;
- }
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ relopts = merge_autovac_opts(relopts, &hentry->ar_reloptions);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2793,6 +2795,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
autovac_table *tab = NULL;
bool wraparound;
StdRdOptions *relopts;
+ StdRdOptions *main_relopts = NULL;
bool free_relopts = false;
AutoVacuumScores scores;
@@ -2803,21 +2806,25 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+
+ if (classForm->relkind == RELKIND_TOASTVALUE &&
+ table_toast_map != NULL)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- relopts = &hentry->ar_reloptions;
+ {
+ main_relopts = &hentry->ar_reloptions;
+ relopts = merge_autovac_opts(relopts, main_relopts);
+ }
}
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2904,6 +2911,48 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ /*
+ * For TOAST tables, provide fallbacks for options that are not part
+ * of AutoVacOpts (and thus are not handled by merge_autovac_opts()).
+ */
+ tab->at_params.main_index_cleanup = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.main_truncate = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.main_max_eager_freeze_failure_rate = -1.0;
+
+ if (main_relopts != NULL)
+ {
+ switch (main_relopts->vacuum_index_cleanup)
+ {
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ break;
+ }
+
+ switch (main_relopts->vacuum_truncate)
+ {
+ case PG_TERNARY_TRUE:
+ tab->at_params.main_truncate = VACOPTVALUE_ENABLED;
+ break;
+ case PG_TERNARY_FALSE:
+ tab->at_params.main_truncate = VACOPTVALUE_DISABLED;
+ break;
+ case PG_TERNARY_UNSET:
+ break;
+ }
+
+ if (main_relopts->vacuum_max_eager_freeze_failure_rate >= 0.0)
+ tab->at_params.main_max_eager_freeze_failure_rate =
+ main_relopts->vacuum_max_eager_freeze_failure_rate;
+ }
+
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
if (avopts)
@@ -2960,9 +3009,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the StdRdOptions options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * relopts is a pointer to the StdRdOptions options (the relation's own for a
+ * plain table, or those merged with the main table's for a TOAST table), NULL
+ * if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3619,6 +3668,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
Relation rel;
TableScanDesc scan;
HeapTuple tup;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
InitMaterializedSRF(fcinfo, 0);
@@ -3628,13 +3679,64 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS);
+
rel = table_open(RelationRelationId, AccessShareLock);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found);
+
+ /* hash_search already filled in the key */
+ hentry->ar_relid = form->oid;
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parent tables' reloptions, we can generate the
+ * results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ bool free_relopts = false;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3651,12 +3753,25 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
continue;
relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (relopts)
+ free_relopts = true;
+ if (form->relkind == RELKIND_TOASTVALUE)
+ {
+ av_relation *hentry;
+ bool found;
+
+ hentry = hash_search(table_toast_map, &form->oid,
+ HASH_FIND, &found);
+ if (found)
+ relopts = merge_autovac_opts(relopts, &hentry->ar_reloptions);
+ }
+
relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (relopts)
+ if (free_relopts)
pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
@@ -3674,6 +3789,111 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
+
+/*
+ * Combine a TOAST table's relevant autovacuum options with the main table's.
+ * Any option the TOAST table leaves unset is taken from the main table's
+ * options. Either argument may be NULL. If both are NULL, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * NB: This function destructively modifies toast_opts!
+ */
+static StdRdOptions *
+merge_autovac_opts(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ /* ternary fields */
+ static const int ternary_offsets[] = {
+ offsetof(AutoVacOpts, enabled),
+ };
+
+ /* integer fields whose unset sentinel is -1 */
+ static const int int_offsets_1[] = {
+ offsetof(AutoVacOpts, vacuum_threshold),
+ offsetof(AutoVacOpts, vacuum_cost_limit),
+ offsetof(AutoVacOpts, freeze_min_age),
+ offsetof(AutoVacOpts, freeze_max_age),
+ offsetof(AutoVacOpts, freeze_table_age),
+ offsetof(AutoVacOpts, multixact_freeze_min_age),
+ offsetof(AutoVacOpts, multixact_freeze_max_age),
+ offsetof(AutoVacOpts, multixact_freeze_table_age),
+ };
+
+ /* integer fields whose unset sentinel is -2 */
+ static const int int_offsets_2[] = {
+ offsetof(AutoVacOpts, vacuum_max_threshold),
+ offsetof(AutoVacOpts, vacuum_ins_threshold),
+ offsetof(AutoVacOpts, log_vacuum_min_duration),
+ };
+
+ /* float fields */
+ static const int float_offsets[] = {
+ offsetof(AutoVacOpts, vacuum_cost_delay),
+ offsetof(AutoVacOpts, vacuum_scale_factor),
+ offsetof(AutoVacOpts, vacuum_ins_scale_factor),
+ };
+
+ AutoVacOpts *toast_avopts;
+ AutoVacOpts *main_avopts;
+
+ if (toast_opts == NULL)
+ return main_opts;
+ if (main_opts == NULL)
+ return toast_opts;
+
+ toast_avopts = &toast_opts->autovacuum;
+ main_avopts = &main_opts->autovacuum;
+
+ for (int i = 0; i < lengthof(ternary_offsets); i++)
+ {
+ pg_ternary *toast_opt;
+ pg_ternary *main_opt;
+
+ toast_opt = (pg_ternary *) ((char *) toast_avopts + ternary_offsets[i]);
+ main_opt = (pg_ternary *) ((char *) main_avopts + ternary_offsets[i]);
+
+ if (*toast_opt == PG_TERNARY_UNSET)
+ *toast_opt = *main_opt;
+ }
+
+ for (int i = 0; i < lengthof(int_offsets_1); i++)
+ {
+ int *toast_opt;
+ int *main_opt;
+
+ toast_opt = (int *) ((char *) toast_avopts + int_offsets_1[i]);
+ main_opt = (int *) ((char *) main_avopts + int_offsets_1[i]);
+
+ if (*toast_opt == -1)
+ *toast_opt = *main_opt;
+ }
+
+ for (int i = 0; i < lengthof(int_offsets_2); i++)
+ {
+ int *toast_opt;
+ int *main_opt;
+
+ toast_opt = (int *) ((char *) toast_avopts + int_offsets_2[i]);
+ main_opt = (int *) ((char *) main_avopts + int_offsets_2[i]);
+
+ if (*toast_opt == -2)
+ *toast_opt = *main_opt;
+ }
+
+ for (int i = 0; i < lengthof(float_offsets); i++)
+ {
+ double *toast_opt;
+ double *main_opt;
+
+ toast_opt = (double *) ((char *) toast_avopts + float_offsets[i]);
+ main_opt = (double *) ((char *) main_avopts + float_offsets[i]);
+
+ if (*toast_opt == -1.0)
+ *toast_opt = *main_opt;
+ }
+
+ return toast_opts;
+}
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..fdfceb38396 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,14 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * Main table fallback values for TOAST tables to inherit when they have
+ * no setting of their own.
+ */
+ VacOptValue main_index_cleanup;
+ VacOptValue main_truncate;
+ double main_max_eager_freeze_failure_rate;
} VacuumParams;
/*
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 01b6e1a872c..4750ea8d864 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -306,6 +306,9 @@ typedef struct ForeignKeyCacheInfo
* RelationGetFillFactor() and RelationGetTargetPageFreeSpace() can only
* be applied to relations that use this format or a superset for
* private options data.
+ *
+ * NB: When adding a new member, be sure to update merge_autovac_opts() and/or
+ * table_recheck_autovac() as necessary!
*/
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..0f0790a8584 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,6 +79,17 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- TOAST table inherits main table's resolved values
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
+DROP TABLE vac_tab_toast_inherit;
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..11a371e4ff7 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,6 +33,14 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- TOAST table inherits main table's resolved values
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+DROP TABLE vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-09 13:37 Greg Burd <greg@burd.me>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 2 replies; 44+ messages in thread
From: Greg Burd @ 2026-08-09 13:37 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; solai v <solai.cdac@gmail.com>; +Cc: Nikita Malakhov <hukutoc@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
On Fri, Aug 7, 2026, at 12:50 PM, Nathan Bossart wrote:
> On Thu, Aug 06, 2026 at 04:38:28PM -0500, Nathan Bossart wrote:
>> Thanks for looking. Here is a new patch set with some small fixes.
>
> Sorry for the noise. I found some other small bugs and found another small
> cleanup opportunity.
Hey Nathan,
First, thanks for taking on this subtle and confusing aspect of heap/TOAST/reloptions interactions. I agree with your diagnosis and your approach and I think this would be a solid step in the right direction. I applied your v7 patch set and ran the test world, works as advertised.
Am I misunderstanding this? It seems to me that making autovacuum_enabled a ternary and then merging it means a heap table with autovacuum_enabled=false and some toast.* option set now stops autovacuuming the TOAST table.
In v7-0002 the option moves from bool to ternary:
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ typedef struct AutoVacOpts
- bool enabled;
+ pg_ternary enabled;
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ relation_needs_vacanalyze
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
and in v7-0005 the merge fills an unset TOAST value from the main table:
+ /* ternary fields */
+ static const int ternary_offsets[] = {
+ offsetof(AutoVacOpts, enabled),
+ };
...
+ for (int i = 0; i < lengthof(ternary_offsets); i++)
+ {
+ pg_ternary *toast_opt;
+ pg_ternary *main_opt;
+
+ toast_opt = (pg_ternary *) ((char *) toast_avopts + ternary_offsets[i]);
+ main_opt = (pg_ternary *) ((char *) main_avopts + ternary_offsets[i]);
+
+ if (*toast_opt == PG_TERNARY_UNSET)
+ *toast_opt = *main_opt;
+ }
So main enabled=PG_TERNARY_FALSE + toast unset -> toast enabled becomes PG_TERNARY_FALSE -> av_enabled is false. Today's all-or-nothing bug leaves that TOAST table getting vacuumed. I agree the new behavior matches the documented contract, but it is a behavior change for the person who disabled autovac on a table they vacuum by hand and never thought about the TOAST side. Wraparound is still forced, but ordinary dead-tuple bloat on the TOAST relation is now on them. So, maybe a line in the commit message and in the CREATE TABLE docs to make that more explicit would help people avoid making that mistake in practice?
In merge_autovac_opts() the four offset arrays keyed by "which sentinel means unset", is that duplicating knowledge that already lives in the relopt tables in reloptions.c?
+ /* integer fields whose unset sentinel is -1 */
+ static const int int_offsets_1[] = {
+ offsetof(AutoVacOpts, vacuum_threshold),
+ offsetof(AutoVacOpts, vacuum_cost_limit),
+ offsetof(AutoVacOpts, freeze_min_age),
+ offsetof(AutoVacOpts, freeze_max_age),
+ offsetof(AutoVacOpts, freeze_table_age),
+ offsetof(AutoVacOpts, multixact_freeze_min_age),
+ offsetof(AutoVacOpts, multixact_freeze_max_age),
+ offsetof(AutoVacOpts, multixact_freeze_table_age),
+ };
+
+ /* integer fields whose unset sentinel is -2 */
+ static const int int_offsets_2[] = {
+ offsetof(AutoVacOpts, vacuum_max_threshold),
+ offsetof(AutoVacOpts, vacuum_ins_threshold),
+ offsetof(AutoVacOpts, log_vacuum_min_duration),
+ };
Those -1/-2 sentinels are the very defaults declared over in reloptions.c this same patch even moves one of them from -1 to -2 there:
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
so the "which sentinel means unset for field X" fact now lives in two places, kept in agreement only by the NB comment added in rel.h:
+ * NB: When adding a new member, be sure to update merge_autovac_opts() and/or
+ * table_recheck_autovac() as necessary!
Add an AutoVacOpts field, or change a field's default sentinel, and forget to update the matching array here, and the merge silently keeps the TOAST table's default instead of inheriting, nothing fails to compile and no test goes red. Can this be driven off the relopt metadata (relopt_parse_elt already knows each option's type and default) instead of the hand-maintained offset arrays?
On testing: the coverage doesn't touch the risky code. There's one injection-point case, and it's manual VACUUM only, index_cleanup/truncate only:
+-- TOAST table inherits main table's resolved values
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+DROP TABLE vac_tab_toast_inherit;
Nothing exercises the autovacuum decision path, autovacuum_enabled inheritance, or any of the numeric AutoVacOpts that merge_autovac_opts() actually resolves which is precisely the code I'm worried about above. FWIW the pg_stat_get_autovacuum_scores() SRF that 0005 extends looks like it could drive a deterministic test of the autovac path (compute the decision without spawning a worker), which sidesteps the flakiness worry raised upthread.
In summary, solid work and I hope it lands. Just a few small issues to clean up.
best.
-greg
> --
> nathan
>
> Attachments:
> * v7-0001-Remove-extract_autovac_opts.patch
> * v7-0002-Make-autovacuum_enabled-a-ternary-reloption.patch
> * v7-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch
> * v7-0004-Simplify-autovacuum-s-TOAST-to-main-relation-relo.patch
> * v7-0005-Fix-VACUUM-and-autovacuum-handling-of-TOAST-stora.patch
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-10 06:08 solai v <solai.cdac@gmail.com>
parent: Greg Burd <greg@burd.me>
1 sibling, 0 replies; 44+ messages in thread
From: solai v @ 2026-08-10 06:08 UTC (permalink / raw)
To: Greg Burd <greg@burd.me>; +Cc: Nathan Bossart <nathandbossart@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
Hi,
I tested the v7 patch series on the current HEAD. The patches applied
cleanly, and both the build and make check-world passed.
I also tested the TOAST and autovacuum reloptions, including
vacuum_truncate, autovacuum_enabled, and the numeric autovacuum
options. The parent/TOAST settings and RESET behavior worked as
expected.
VACUUM (VERBOSE) also completed successfully for both the main and TOAST tables.
Overall, the patch worked as expected in my testing.
Regards,
solai
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-10 16:30 Nathan Bossart <nathandbossart@gmail.com>
parent: Greg Burd <greg@burd.me>
1 sibling, 2 replies; 44+ messages in thread
From: Nathan Bossart @ 2026-08-10 16:30 UTC (permalink / raw)
To: Greg Burd <greg@burd.me>; +Cc: solai v <solai.cdac@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
On Sun, Aug 09, 2026 at 09:37:49AM -0400, Greg Burd wrote:
> Am I misunderstanding this? It seems to me that making autovacuum_enabled
> a ternary and then merging it means a heap table with
> autovacuum_enabled=false and some toast.* option set now stops
> autovacuuming the TOAST table.
>
> [...]
>
> So main enabled=PG_TERNARY_FALSE + toast unset -> toast enabled becomes
> PG_TERNARY_FALSE -> av_enabled is false. Today's all-or-nothing bug
> leaves that TOAST table getting vacuumed. I agree the new behavior
> matches the documented contract, but it is a behavior change for the
> person who disabled autovac on a table they vacuum by hand and never
> thought about the TOAST side. Wraparound is still forced, but ordinary
> dead-tuple bloat on the TOAST relation is now on them. So, maybe a line
> in the commit message and in the CREATE TABLE docs to make that more
> explicit would help people avoid making that mistake in practice?
Eh... I don't see much reason to worry about making relopts work how
they're documented. I mean, that's the whole point of this patch. You
could make roughly the same argument about every other reloption with a
corresponding TOAST setting. From asking around, I get the idea that
setting toast.* relopts is pretty rare, anyway.
Perhaps there's an argument for improving the docs to make this behavior a
little more apparent, but I think we can take care of that separately.
> In merge_autovac_opts() the four offset arrays keyed by "which sentinel
> means unset", is that duplicating knowledge that already lives in the
> relopt tables in reloptions.c?
>
> [...]
>
> Add an AutoVacOpts field, or change a field's default sentinel, and
> forget to update the matching array here, and the merge silently keeps
> the TOAST table's default instead of inheriting, nothing fails to compile
> and no test goes red. Can this be driven off the relopt metadata
> (relopt_parse_elt already knows each option's type and default) instead
> of the hand-maintained offset arrays?
I'm looking into this. Since this is almost certainly a master-only change
at this point, it seems reasonable to spend some more time on making this
stuff less fragile.
> On testing: the coverage doesn't touch the risky code. There's one
> injection-point case, and it's manual VACUUM only, index_cleanup/truncate
> only:
>
> +-- TOAST table inherits main table's resolved values
> +CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
> + (autovacuum_enabled=false,
> + vacuum_index_cleanup=false,
> + vacuum_truncate=false, toast.vacuum_truncate=true);
> +VACUUM vac_tab_toast_inherit;
> +DROP TABLE vac_tab_toast_inherit;
>
> Nothing exercises the autovacuum decision path, autovacuum_enabled
> inheritance, or any of the numeric AutoVacOpts that merge_autovac_opts()
> actually resolves which is precisely the code I'm worried about above.
> FWIW the pg_stat_get_autovacuum_scores() SRF that 0005 extends looks like
> it could drive a deterministic test of the autovac path (compute the
> decision without spawning a worker), which sidesteps the flakiness worry
> raised upthread.
Will add some more coverage.
> In summary, solid work and I hope it lands. Just a few small issues to
> clean up.
Thanks for reviewing.
--
nathan
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-11 14:53 Sami Imseih <samimseih@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
1 sibling, 1 reply; 44+ messages in thread
From: Sami Imseih @ 2026-08-11 14:53 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: Greg Burd <greg@burd.me>; solai v <solai.cdac@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
Hi,
I looked these patches while looking at potential cases for injection point
conditional strings [0], which could allow for better autovacuum testing,
particularly for the case being discussed here.
As far as the patches go, No comments on 0001-0004, just two nits on 0005.
1/ Instead of setting toast_vacuum_params.* inside each branch, it reads
cleaner to set toast_vacuum_params.* after params.* is done, and this
way you only set toast_vacuum_params.*once. For example, in
index_cleanup:
@@ -2223,15 +2223,12 @@ vacuum_rel(Oid relid, RangeVar *relation,
VacuumParams params,
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
params.index_cleanup = VACOPTVALUE_ENABLED;
- toast_vacuum_params.main_index_cleanup = VACOPTVALUE_ENABLED;
break;
...
}
+
+ /* A TOAST table inherits the main relation's value. */
+ toast_vacuum_params.main_index_cleanup = params.index_cleanup;
}
The same applies to main_max_eager_freeze_failure_rate and main_truncate.
2/ A couple of comment tweaks in merge_autovac_opts()
```
@@ -3791,8 +3791,11 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
* The arrays below cover only the options that can be set on a TOAST table,
* grouped by the value that means "unset". Both of those facts come from
* reloptions.c, so they must be kept in sync with the entries here.
+ * Analyze options are intentionally excluded because TOAST tables are never
+ * analyzed, and autovacuum_parallel_workers is excluded because it can't be
+ * set on a TOAST table.
*
- * NB: This function destructively modifies toast_opts!
+ * NB: This modifies toast_opts in place.
*/
static StdRdOptions *
merge_autovac_opts(StdRdOptions *toast_opts, StdRdOptions *main_opts)
@@ -3821,7 +3824,7 @@ merge_autovac_opts(StdRdOptions *toast_opts,
StdRdOptions *main_opts)
offsetof(AutoVacOpts, log_vacuum_min_duration),
};
- /* float fields */
+ /* float fields whose unset sentinel is -1.0 */
static const int float_offsets[] = {
offsetof(AutoVacOpts, vacuum_cost_delay),
offsetof(AutoVacOpts, vacuum_scale_factor),
```
[0] https://www.postgresql.org/message-id/CAA5RZ0sfRTST8RQaoVXfEnVRchRtAP_YhFFUVcm6dC6v1TAChw%40mail.gma...
--
Sami Imseih
Amazon Web Services (AWS)
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-11 21:43 Nathan Bossart <nathandbossart@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
1 sibling, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2026-08-11 21:43 UTC (permalink / raw)
To: Greg Burd <greg@burd.me>; +Cc: solai v <solai.cdac@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
On Mon, Aug 10, 2026 at 11:30:17AM -0500, Nathan Bossart wrote:
> On Sun, Aug 09, 2026 at 09:37:49AM -0400, Greg Burd wrote:
>> In merge_autovac_opts() the four offset arrays keyed by "which sentinel
>> means unset", is that duplicating knowledge that already lives in the
>> relopt tables in reloptions.c?
>>
>> [...]
>>
>> Add an AutoVacOpts field, or change a field's default sentinel, and
>> forget to update the matching array here, and the merge silently keeps
>> the TOAST table's default instead of inheriting, nothing fails to compile
>> and no test goes red. Can this be driven off the relopt metadata
>> (relopt_parse_elt already knows each option's type and default) instead
>> of the hand-maintained offset arrays?
>
> I'm looking into this. Since this is almost certainly a master-only change
> at this point, it seems reasonable to spend some more time on making this
> stuff less fragile.
Here is a new patch set with an attempt at the above. The new 0005
contains the log_autovacuum_min_duration default change along with an
assertion that all TOAST storage parameters have unsettable defaults. 0006
is just some prerequisite refactoring for 0007. And 0007 is the fix with
the merge done based on the main reloption list instead of new arrays.
>> Nothing exercises the autovacuum decision path, autovacuum_enabled
>> inheritance, or any of the numeric AutoVacOpts that merge_autovac_opts()
>> actually resolves which is precisely the code I'm worried about above.
>> FWIW the pg_stat_get_autovacuum_scores() SRF that 0005 extends looks like
>> it could drive a deterministic test of the autovac path (compute the
>> decision without spawning a worker), which sidesteps the flakiness worry
>> raised upthread.
>
> Will add some more coverage.
Done.
--
nathan
From d26b57020009e22f0e969f0357def265066e191c Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:35:34 -0500
Subject: [PATCH v8 1/7] Remove extract_autovac_opts().
extract_autovac_opts() returned a palloc'd copy of only the
AutoVacOpts portion of a relation's reloptions. Upcoming work
needs the rest of the StdRdOptions as well, so the callers must
keep the whole struct around.
Remove the helper and have the callers obtain reloptions from
extractRelOptions() directly. av_relation now caches a
StdRdOptions instead of an AutoVacOpts, and
relation_needs_vacanalyze() takes a StdRdOptions and extracts the
autovacuum portion itself. This is preparatory refactoring with no
change in behavior.
---
src/backend/postmaster/autovacuum.c | 122 ++++++++++------------------
1 file changed, 44 insertions(+), 78 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index ee202a4b47e..db1f5d0e575 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -202,8 +202,7 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -379,7 +378,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -388,8 +387,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2037,7 +2034,7 @@ do_autovacuum(void)
while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2076,7 +2073,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2118,7 +2115,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2141,7 +2138,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2160,7 +2157,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2776,39 +2773,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc_object(AutoVacOpts);
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2828,8 +2792,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2842,9 +2806,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2853,10 +2817,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- relation_needs_vacanalyze(relid, avopts, classForm,
+ relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
DEBUG3,
&dovacuum, &doanalyze, &wraparound,
@@ -2871,6 +2835,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_vacuum_min_duration;
int log_analyze_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2982,8 +2947,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2995,7 +2960,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none.
*
@@ -3072,7 +3037,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -3086,6 +3051,7 @@ relation_needs_vacanalyze(Oid relid,
bool force_vacuum;
bool av_enabled;
bool may_free = false;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3137,45 +3103,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
@@ -3668,7 +3634,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
- AutoVacOpts *avopts;
+ StdRdOptions *relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3684,14 +3650,14 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
- relation_needs_vacanalyze(form->oid, avopts, form,
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
- pfree(avopts);
+ if (relopts)
+ pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
vals[1] = Float8GetDatum(scores.max);
--
2.50.1 (Apple Git-155)
From 4a3e2a8b50cd2c67b896f4c0853fae201e3f146f Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:50:36 -0500
Subject: [PATCH v8 2/7] Make autovacuum_enabled a ternary reloption.
This commit reimplements autovacuum_enabled as a ternary, using the
support added in commit 4d6a66f675 and following the example of
vacuum_truncate. This changes only the internal representation: an
unset value still behaves as enabled, and the option accepts the
same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 19 +++++++++----------
src/backend/catalog/index.c | 3 ++-
src/backend/postmaster/autovacuum.c | 2 +-
src/include/utils/rel.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..79834126f2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -107,15 +107,6 @@ static relopt_bool boolRelOpts[] =
},
false
},
- {
- {
- "autovacuum_enabled",
- "Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
- ShareUpdateExclusiveLock
- },
- true
- },
{
{
"user_catalog_table",
@@ -168,6 +159,14 @@ static relopt_bool boolRelOpts[] =
static relopt_ternary ternaryRelOpts[] =
{
+ {
+ {
+ "autovacuum_enabled",
+ "Enables autovacuum in this relation",
+ RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ ShareUpdateExclusiveLock
+ }
+ },
{
{
"vacuum_truncate",
@@ -1976,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
static const relopt_parse_elt tab[] = {
{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_BOOL,
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
{"autovacuum_parallel_workers", RELOPT_TYPE_INT,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 4c5da7e5db0..ec21b83b6b8 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2900,7 +2900,8 @@ index_update_stats(Relation rel,
{
StdRdOptions *options = (StdRdOptions *) rel->rd_options;
- if (options != NULL && !options->autovacuum.enabled)
+ if (options != NULL &&
+ options->autovacuum.enabled == PG_TERNARY_FALSE)
update_stats = false;
}
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index db1f5d0e575..c8a1b66ca2e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3141,7 +3141,7 @@ relation_needs_vacanalyze(Oid relid,
? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 89c159b133f..2ee98e9c6cc 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -310,7 +310,7 @@ typedef struct ForeignKeyCacheInfo
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
{
- bool enabled;
+ pg_ternary enabled;
int autovacuum_parallel_workers;
int vacuum_threshold;
--
2.50.1 (Apple Git-155)
From c602fb70ed014073312f0b7b601028ca24f9b4c5 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 15:27:58 -0500
Subject: [PATCH v8 3/7] Add an "unset" value for vacuum_index_cleanup.
This commit adds a new value to StdRdOptIndexCleanup to distinguish
whether it is explicitly set, similar to ViewOptCheckOption's
VIEW_OPTION_CHECK_OPTION_NOT_SET. This changes only the internal
representation; an unset value still defaults to AUTO, and the
option accepts the same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 3 ++-
src/backend/commands/vacuum.c | 23 ++++++++++++++---------
src/include/utils/rel.h | 1 +
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79834126f2f..58eb72e2339 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -517,6 +517,7 @@ static relopt_real realRelOpts[] =
/* values from StdRdOptIndexCleanup */
static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
{
+ /* no value for NOT_SET */
{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
@@ -557,7 +558,7 @@ static relopt_enum enumRelOpts[] =
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
},
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 31f9824899c..52116c02b59 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2210,20 +2210,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
StdRdOptIndexCleanup vacuum_index_cleanup;
if (rel->rd_options == NULL)
- vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+ vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
vacuum_index_cleanup =
((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
- if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
- params.index_cleanup = VACOPTVALUE_AUTO;
- else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
- params.index_cleanup = VACOPTVALUE_ENABLED;
- else
+ switch (vacuum_index_cleanup)
{
- Assert(vacuum_index_cleanup ==
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
- params.index_cleanup = VACOPTVALUE_DISABLED;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ params.index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ params.index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
}
}
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 2ee98e9c6cc..01b6e1a872c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,6 +338,7 @@ typedef enum StdRdOptIndexCleanup
STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
} StdRdOptIndexCleanup;
typedef struct StdRdOptions
--
2.50.1 (Apple Git-155)
From 04510ea61ad52f49f8486a64d4fc95c4bd35d0ab Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 7 Aug 2026 10:55:34 -0500
Subject: [PATCH v8 4/7] Simplify autovacuum's TOAST-to-main-relation
reloptions map.
do_autovacuum() adds an entry to this map for every relation that
has a TOAST table, and uses a flag to mark the entries that have no
reloptions to pass down. The unconditional entry made sense back
when the payload was the main relation's OID: commit 7d4c9a5793
added the map so that the TOAST pass could find the parent, whose
pg_autovacuum row supplied the settings for a TOAST table that had
none of its own. Commit 834a6da4f7 replaced that lookup by copying
the parent's reloptions into the entry, which left the OID unread
and called for the flag, since a by-value AutoVacOpts cannot say
"not set".
An entry is now worth creating only when there is something to
inherit, so skip the relations that have no reloptions and let a
successful lookup speak for itself. Two relations cannot share a
TOAST table, and each pass sees a single catalog snapshot, so an
insertion can never find an existing entry; assert that rather than
quietly ignoring it.
While at it, remove two more leftovers of that same conversion:
ar_relid, unread ever since, and a NULL test on table_toast_map in
table_recheck_autovac(), which used to carry the relkind test for
get_pg_autovacuum_tuple_relid() and has had no NULL to catch since
that function went away.
---
src/backend/postmaster/autovacuum.c | 26 +++++++-------------------
1 file changed, 7 insertions(+), 19 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index c8a1b66ca2e..32efe967890 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -200,8 +200,6 @@ typedef struct avw_dbase
typedef struct av_relation
{
Oid ar_toastrelid; /* hash key - must be first */
- Oid ar_relid;
- bool ar_hasrelopts;
StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
@@ -2097,7 +2095,7 @@ do_autovacuum(void)
* this whether or not the table is going to be vacuumed, because we
* don't automatically vacuum toast tables along the parent table.
*/
- if (OidIsValid(classForm->reltoastrelid))
+ if (OidIsValid(classForm->reltoastrelid) && relopts)
{
av_relation *hentry;
bool found;
@@ -2105,19 +2103,10 @@ do_autovacuum(void)
hentry = hash_search(table_toast_map,
&classForm->reltoastrelid,
HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
- if (!found)
- {
- /* hash_search already filled in the key */
- hentry->ar_relid = relid;
- hentry->ar_hasrelopts = false;
- if (relopts != NULL)
- {
- hentry->ar_hasrelopts = true;
- memcpy(&hentry->ar_reloptions, relopts,
- sizeof(StdRdOptions));
- }
- }
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
}
/* Release stuff to avoid per-relation leakage */
@@ -2166,7 +2155,7 @@ do_autovacuum(void)
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
@@ -2809,14 +2798,13 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ else if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
--
2.50.1 (Apple Git-155)
From c0327f11b5b4e1ace51e7d411aef87a176df72c5 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:55:44 -0500
Subject: [PATCH v8 5/7] Give TOAST storage parameters unsettable defaults.
This is preparatory work for a follow-up commit that will fill in a
TOAST table's unset storage parameters from its main table's. For
that, it must be possible to tell an option nobody set from one the
user set to the value that option happens to default to. The
parsed form of a relation's options has nowhere to record which
ones were specified, so an unset option is simply one still holding
its declared default.
Require, then, that any option a TOAST table accepts default to a
value the user cannot set. Ternaries already comply, having no
default at all, as does vacuum_index_cleanup, whose "not set"
member has no spelling. Among the numeric ones only
log_autovacuum_min_duration was in violation, defaulting to -1 with
a minimum of -1, so give it -2 instead, matching
autovacuum_vacuum_max_threshold and
autovacuum_vacuum_insert_threshold. Users won't notice; -1 already
behaved exactly as leaving the option unset does, and it still
does.
An assertion in initialize_reloptions() enforces both of the rules
the follow-up commit will rely on: that the default is unsettable,
and that anything settable on a TOAST table is settable on a heap,
since the inherited value is read from a main table's options at
the same offset. Note that this rules out bool and string options,
neither of which can express "unset".
---
src/backend/access/common/reloptions.c | 78 +++++++++++++++++++++++++-
src/backend/postmaster/autovacuum.c | 2 +-
2 files changed, 78 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 58eb72e2339..02bc8213def 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
},
{
{
@@ -613,6 +613,78 @@ static void parse_one_reloption(relopt_value *option, char *text_str,
((option).isset ? strlen((option).string_val) : \
((relopt_string *) (option).gen)->default_len)
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Verify that every option a TOAST table accepts defaults to a value the user
+ * cannot set. Nothing records which options were specified, so an option
+ * still holding its default is the only way to recognize one that was never
+ * set, and that is how a TOAST table tells which values it should take from
+ * its main table.
+ */
+static void
+assert_toast_defaults_unsettable(void)
+{
+ for (int i = 0; relOpts[i]; i++)
+ {
+ relopt_gen *gen = relOpts[i];
+
+ if ((gen->kinds & RELOPT_KIND_TOAST) == 0)
+ continue;
+
+ /*
+ * A TOAST table's value is filled in from its main table's at the
+ * same offset in the same struct, so the option must be settable on a
+ * heap too.
+ */
+ Assert((gen->kinds & RELOPT_KIND_HEAP) != 0);
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+
+ /*
+ * Ternaries carry no default, and parse_one_reloption() can
+ * only produce true or false, so PG_TERNARY_UNSET is already
+ * beyond a user's reach.
+ */
+ break;
+
+ case RELOPT_TYPE_INT:
+ {
+ relopt_int *optint = (relopt_int *) gen;
+
+ Assert(optint->default_val < optint->min ||
+ optint->default_val > optint->max);
+ break;
+ }
+
+ case RELOPT_TYPE_REAL:
+ {
+ relopt_real *optreal = (relopt_real *) gen;
+
+ Assert(optreal->default_val < optreal->min ||
+ optreal->default_val > optreal->max);
+ break;
+ }
+
+ case RELOPT_TYPE_ENUM:
+ {
+ relopt_enum *optenum = (relopt_enum *) gen;
+
+ for (relopt_enum_elt_def *elt = optenum->members;
+ elt->string_val; elt++)
+ Assert(elt->symbol_val != optenum->default_val);
+ break;
+ }
+
+ default:
+ /* Neither bools nor strings can express "unset". */
+ Assert(false);
+ }
+ }
+}
+#endif /* USE_ASSERT_CHECKING */
+
/*
* initialize_reloptions
* initialization routine, must be called before parsing
@@ -730,6 +802,10 @@ initialize_reloptions(void)
/* flag the work is complete */
need_initialization = false;
+
+#ifdef USE_ASSERT_CHECKING
+ assert_toast_defaults_unsettable();
+#endif
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 32efe967890..a99f7108636 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2832,7 +2832,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* defaults, autovacuum's own first and plain vacuum second.
*/
- /* -1 in autovac setting means use log_autovacuum_min_duration */
+ /* a negative autovac setting means use log_autovacuum_min_duration */
log_vacuum_min_duration = (avopts && avopts->log_vacuum_min_duration >= 0)
? avopts->log_vacuum_min_duration
: Log_autovacuum_min_duration;
--
2.50.1 (Apple Git-155)
From 86346bdce4bd1182520348c40151a695da8b53e2 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:56:02 -0500
Subject: [PATCH v8 6/7] Move the StdRdOptions parse table to file scope.
This is preparatory work for a follow-up commit that will walk the
table from another function. Nothing changes but the indentation
of its entries.
---
src/backend/access/common/reloptions.c | 115 +++++++++++++------------
1 file changed, 60 insertions(+), 55 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 02bc8213def..4548eb02676 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2044,69 +2044,74 @@ fillRelOptions(void *rdopts, Size basesize,
}
+/*
+ * Parse table for StdRdOptions, which is shared by the RELOPT_KIND_HEAP and
+ * RELOPT_KIND_TOAST kinds.
+ */
+static const relopt_parse_elt stdRdOptionsTab[] = {
+ {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
+ {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
+ {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
+ {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
+ {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
+ {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
+ {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
+ {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
+ {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
+ {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
+ {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
+ {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
+ {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
+ {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
+ {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
+ {"toast_tuple_target", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, toast_tuple_target)},
+ {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
+ {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
+ {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
+ {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"user_catalog_table", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, user_catalog_table)},
+ {"parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, parallel_workers)},
+ {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+ offsetof(StdRdOptions, vacuum_index_cleanup)},
+ {"vacuum_truncate", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, vacuum_truncate)},
+ {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
+};
+
/*
* Option parser for anything that uses StdRdOptions.
*/
bytea *
default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
- static const relopt_parse_elt tab[] = {
- {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
- {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
- {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
- {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
- {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
- {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
- {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
- {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
- {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
- {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
- {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
- {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
- {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
- {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
- {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
- {"toast_tuple_target", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, toast_tuple_target)},
- {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
- {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
- {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
- {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
- {"user_catalog_table", RELOPT_TYPE_BOOL,
- offsetof(StdRdOptions, user_catalog_table)},
- {"parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, parallel_workers)},
- {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
- offsetof(StdRdOptions, vacuum_index_cleanup)},
- {"vacuum_truncate", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, vacuum_truncate)},
- {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
- };
-
return (bytea *) build_reloptions(reloptions, validate, kind,
sizeof(StdRdOptions),
- tab, lengthof(tab));
+ stdRdOptionsTab,
+ lengthof(stdRdOptionsTab));
}
/*
--
2.50.1 (Apple Git-155)
From a3f9114efaa75412eeaaede9120b280d46ec3a49 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 11 Aug 2026 15:43:15 -0500
Subject: [PATCH v8 7/7] Fix VACUUM and autovacuum handling of TOAST storage
parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Unfortunately, current reality does not match this description.
Neither VACUUM nor autovacuum consults the main table's
non-autovacuum storage parameters, and autovacuum only consults the
main table's autovacuum-related storage parameters if the TOAST
table lacks any. One silver lining is that all currently-supported
TOAST storage parameters are related to vacuum, whose code paths
already access the main table's parameters. This means that our
solution needn't involve more lookups; we just need to propagate
them correctly.
To fix, this commit teaches autovacuum to combine the TOAST storage
parameters with the main table's (with the toast.* ones winning if
both are set), and it teaches VACUUM to send down the main table's
parameters when recursing to a TOAST table. This doesn't fix
VACUUM against a TOAST table directly (e.g., VACUUM
pg_toast.pg_toast_5432), but that's probably okay because it's not
the main supported way to vacuum a TOAST table (see VACUUM's
PROCESS_MAIN and PROCESS_TOAST options).
An existing shortcoming that this patch only makes worse is that
autovacuum/VACUUM remain oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/access/common/reloptions.c | 93 ++++++++++
src/backend/commands/vacuum.c | 30 +++-
src/backend/postmaster/autovacuum.c | 159 +++++++++++++++---
src/include/access/reloptions.h | 2 +
src/include/commands/vacuum.h | 8 +
.../injection_points/expected/vacuum.out | 29 ++++
.../modules/injection_points/sql/vacuum.sql | 16 ++
7 files changed, 311 insertions(+), 26 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 4548eb02676..60f1dc7ddd5 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2114,6 +2114,99 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
lengthof(stdRdOptionsTab));
}
+/*
+ * find_reloption
+ * Look up a reloption of the given kind by name.
+ *
+ * Returns NULL if no such option can be set on relations of that kind. Note
+ * that names are unique only within a kind; "fillfactor", for example, is
+ * declared separately for heaps and for each index access method.
+ */
+static relopt_gen *
+find_reloption(const char *name, relopt_kind kind)
+{
+ if (need_initialization)
+ initialize_reloptions();
+
+ for (int i = 0; relOpts[i]; i++)
+ {
+ if ((relOpts[i]->kinds & kind) != 0 &&
+ strcmp(relOpts[i]->name, name) == 0)
+ return relOpts[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * merge_toast_reloptions
+ * Fill in a TOAST table's unset options from its main table's.
+ *
+ * Any option that may be set on a TOAST table but was not is taken from
+ * main_opts. Either argument may be NULL; if both are, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * An option counts as unset while it still holds the default declared for it
+ * above, which works because nothing a TOAST table accepts has a default the
+ * user could also set (see assert_toast_defaults_unsettable()).
+ *
+ * NB: This destructively modifies toast_opts, and what it returns may be
+ * either argument, so the caller must know which of the two it owns.
+ */
+StdRdOptions *
+merge_toast_reloptions(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ if (toast_opts == NULL)
+ return main_opts;
+ if (main_opts == NULL)
+ return toast_opts;
+
+ for (int i = 0; i < lengthof(stdRdOptionsTab); i++)
+ {
+ const relopt_parse_elt *elem = &stdRdOptionsTab[i];
+ relopt_gen *gen;
+ char *toast_val;
+ char *main_val;
+
+ /* Skip anything that cannot be set on a TOAST table. */
+ gen = find_reloption(elem->optname, RELOPT_KIND_TOAST);
+ if (gen == NULL)
+ continue;
+
+ toast_val = (char *) toast_opts + elem->offset;
+ main_val = (char *) main_opts + elem->offset;
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+ if (*(pg_ternary *) toast_val == PG_TERNARY_UNSET)
+ *(pg_ternary *) toast_val = *(pg_ternary *) main_val;
+ break;
+
+ case RELOPT_TYPE_INT:
+ if (*(int *) toast_val == ((relopt_int *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ case RELOPT_TYPE_REAL:
+ if (*(double *) toast_val == ((relopt_real *) gen)->default_val)
+ *(double *) toast_val = *(double *) main_val;
+ break;
+
+ case RELOPT_TYPE_ENUM:
+ if (*(int *) toast_val == ((relopt_enum *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ default:
+ elog(ERROR, "reloption \"%s\" has a type a TOAST table cannot inherit",
+ elem->optname);
+ }
+ }
+
+ return toast_opts;
+}
+
/*
* build_reloptions
*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 52116c02b59..3af66006586 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -187,6 +187,9 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_index_cleanup = VACOPTVALUE_UNSPECIFIED;
+ params.main_truncate = VACOPTVALUE_UNSPECIFIED;
+ params.main_max_eager_freeze_failure_rate = -1.0;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2203,7 +2206,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
- * specified in VACUUM command, or when running in an autovacuum worker
+ * specified in VACUUM command, or when running in an autovacuum worker. A
+ * TOAST table with no setting of its own inherits the main table's value.
*/
if (params.index_cleanup == VACOPTVALUE_UNSPECIFIED)
{
@@ -2227,11 +2231,16 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
params.index_cleanup = VACOPTVALUE_AUTO;
break;
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
- params.index_cleanup = VACOPTVALUE_AUTO;
+ if (params.main_index_cleanup != VACOPTVALUE_UNSPECIFIED)
+ params.index_cleanup = params.main_index_cleanup;
+ else
+ params.index_cleanup = VACOPTVALUE_AUTO;
break;
}
}
+ toast_vacuum_params.main_index_cleanup = params.index_cleanup;
+
#ifdef USE_INJECTION_POINTS
if (params.index_cleanup == VACOPTVALUE_AUTO)
INJECTION_POINT("vacuum-index-cleanup-auto", NULL);
@@ -2243,16 +2252,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
/*
* Check if the vacuum_max_eager_freeze_failure_rate table storage
- * parameter was specified. This overrides the GUC value.
+ * parameter was specified. This overrides the GUC value. A TOAST table
+ * with no setting of its own inherits the main table's value.
*/
if (rel->rd_options != NULL &&
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
params.max_eager_freeze_failure_rate =
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ else if (params.main_max_eager_freeze_failure_rate >= 0.0)
+ params.max_eager_freeze_failure_rate =
+ params.main_max_eager_freeze_failure_rate;
+
+ toast_vacuum_params.main_max_eager_freeze_failure_rate =
+ params.max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
- * specified in VACUUM command, or when running in an autovacuum worker
+ * specified in VACUUM command, or when running in an autovacuum worker. A
+ * TOAST table with no setting of its own inherits the main table's value
+ * before falling back to the GUC.
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
@@ -2265,12 +2283,16 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
else
params.truncate = VACOPTVALUE_DISABLED;
}
+ else if (params.main_truncate != VACOPTVALUE_UNSPECIFIED)
+ params.truncate = params.main_truncate;
else if (vacuum_truncate)
params.truncate = VACOPTVALUE_ENABLED;
else
params.truncate = VACOPTVALUE_DISABLED;
}
+ toast_vacuum_params.main_truncate = params.truncate;
+
#ifdef USE_INJECTION_POINTS
if (params.truncate == VACOPTVALUE_AUTO)
INJECTION_POINT("vacuum-truncate-auto", NULL);
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a99f7108636..881e07ecf9e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2015,9 +2015,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2133,6 +2133,8 @@ do_autovacuum(void)
bool doanalyze;
bool wraparound;
AutoVacuumScores scores;
+ av_relation *hentry;
+ bool found;
/*
* We cannot safely process other backends' temp tables, so skip 'em.
@@ -2143,21 +2145,19 @@ do_autovacuum(void)
relid = classForm->oid;
/*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
+ * fetch reloptions -- merge any unset options from the main rel
+ *
+ * Note that this fills in the storage parameters only VACUUM
+ * consults, too. That does no harm here; whether we process the
+ * table depends on the autovacuum parameters alone.
*/
relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found)
+ relopts = merge_toast_reloptions(relopts, &hentry->ar_reloptions);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2782,6 +2782,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
autovac_table *tab = NULL;
bool wraparound;
StdRdOptions *relopts;
+ StdRdOptions *main_relopts = NULL;
bool free_relopts = false;
AutoVacuumScores scores;
@@ -2792,20 +2793,24 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE)
+
+ if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found)
- relopts = &hentry->ar_reloptions;
+ {
+ main_relopts = &hentry->ar_reloptions;
+ relopts = merge_toast_reloptions(relopts, main_relopts);
+ }
}
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2892,6 +2897,49 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ /*
+ * For TOAST tables, provide fallbacks for the options that
+ * vacuum_rel() resolves from the relation's own reloptions, which do
+ * not have the main table's values merged in.
+ */
+ tab->at_params.main_index_cleanup = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.main_truncate = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.main_max_eager_freeze_failure_rate = -1.0;
+
+ if (main_relopts != NULL)
+ {
+ switch (main_relopts->vacuum_index_cleanup)
+ {
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ break;
+ }
+
+ switch (main_relopts->vacuum_truncate)
+ {
+ case PG_TERNARY_TRUE:
+ tab->at_params.main_truncate = VACOPTVALUE_ENABLED;
+ break;
+ case PG_TERNARY_FALSE:
+ tab->at_params.main_truncate = VACOPTVALUE_DISABLED;
+ break;
+ case PG_TERNARY_UNSET:
+ break;
+ }
+
+ if (main_relopts->vacuum_max_eager_freeze_failure_rate >= 0.0)
+ tab->at_params.main_max_eager_freeze_failure_rate =
+ main_relopts->vacuum_max_eager_freeze_failure_rate;
+ }
+
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
if (avopts)
@@ -2948,9 +2996,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the StdRdOptions options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * relopts is a pointer to the StdRdOptions options (the relation's own for a
+ * plain table, or those merged with the main table's for a TOAST table), NULL
+ * if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3607,6 +3655,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
Relation rel;
TableScanDesc scan;
HeapTuple tup;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
InitMaterializedSRF(fcinfo, 0);
@@ -3616,13 +3666,64 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
rel = table_open(RelationRelationId, AccessShareLock);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parent tables' reloptions, we can generate the
+ * results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ bool free_relopts = false;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3639,12 +3740,25 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
continue;
relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (relopts)
+ free_relopts = true;
+ if (form->relkind == RELKIND_TOASTVALUE)
+ {
+ av_relation *hentry;
+ bool found;
+
+ hentry = hash_search(table_toast_map, &form->oid,
+ HASH_FIND, &found);
+ if (found)
+ relopts = merge_toast_reloptions(relopts, &hentry->ar_reloptions);
+ }
+
relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (relopts)
+ if (free_relopts)
pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
@@ -3662,6 +3776,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..6c599382f02 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -247,6 +247,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options,
extern bytea *default_reloptions(Datum reloptions, bool validate,
relopt_kind kind);
+extern struct StdRdOptions *merge_toast_reloptions(struct StdRdOptions *toast_opts,
+ struct StdRdOptions *main_opts);
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..fdfceb38396 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,14 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * Main table fallback values for TOAST tables to inherit when they have
+ * no setting of their own.
+ */
+ VacOptValue main_index_cleanup;
+ VacOptValue main_truncate;
+ double main_max_eager_freeze_failure_rate;
} VacuumParams;
/*
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..1f9acc73594 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,9 +79,38 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- TOAST table inherits main table's resolved values
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
+ over
+------
+ t
+(1 row)
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..085f64337ad 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,9 +33,25 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- TOAST table inherits main table's resolved values
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
--
2.50.1 (Apple Git-155)
Attachments:
[text/plain] v8-0001-Remove-extract_autovac_opts.patch (11.5K, ../../anuXd35G8WLTUaBR@nathan/2-v8-0001-Remove-extract_autovac_opts.patch)
download | inline diff:
From d26b57020009e22f0e969f0357def265066e191c Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:35:34 -0500
Subject: [PATCH v8 1/7] Remove extract_autovac_opts().
extract_autovac_opts() returned a palloc'd copy of only the
AutoVacOpts portion of a relation's reloptions. Upcoming work
needs the rest of the StdRdOptions as well, so the callers must
keep the whole struct around.
Remove the helper and have the callers obtain reloptions from
extractRelOptions() directly. av_relation now caches a
StdRdOptions instead of an AutoVacOpts, and
relation_needs_vacanalyze() takes a StdRdOptions and extracts the
autovacuum portion itself. This is preparatory refactoring with no
change in behavior.
---
src/backend/postmaster/autovacuum.c | 122 ++++++++++------------------
1 file changed, 44 insertions(+), 78 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index ee202a4b47e..db1f5d0e575 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -202,8 +202,7 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -379,7 +378,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -388,8 +387,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2037,7 +2034,7 @@ do_autovacuum(void)
while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2076,7 +2073,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2118,7 +2115,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2141,7 +2138,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2160,7 +2157,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2776,39 +2773,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc_object(AutoVacOpts);
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2828,8 +2792,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2842,9 +2806,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2853,10 +2817,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- relation_needs_vacanalyze(relid, avopts, classForm,
+ relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
DEBUG3,
&dovacuum, &doanalyze, &wraparound,
@@ -2871,6 +2835,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_vacuum_min_duration;
int log_analyze_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2982,8 +2947,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2995,7 +2960,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none.
*
@@ -3072,7 +3037,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -3086,6 +3051,7 @@ relation_needs_vacanalyze(Oid relid,
bool force_vacuum;
bool av_enabled;
bool may_free = false;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3137,45 +3103,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
@@ -3668,7 +3634,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
- AutoVacOpts *avopts;
+ StdRdOptions *relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3684,14 +3650,14 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
- relation_needs_vacanalyze(form->oid, avopts, form,
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
- pfree(avopts);
+ if (relopts)
+ pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
vals[1] = Float8GetDatum(scores.max);
--
2.50.1 (Apple Git-155)
[text/plain] v8-0002-Make-autovacuum_enabled-a-ternary-reloption.patch (3.6K, ../../anuXd35G8WLTUaBR@nathan/3-v8-0002-Make-autovacuum_enabled-a-ternary-reloption.patch)
download | inline diff:
From 4a3e2a8b50cd2c67b896f4c0853fae201e3f146f Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:50:36 -0500
Subject: [PATCH v8 2/7] Make autovacuum_enabled a ternary reloption.
This commit reimplements autovacuum_enabled as a ternary, using the
support added in commit 4d6a66f675 and following the example of
vacuum_truncate. This changes only the internal representation: an
unset value still behaves as enabled, and the option accepts the
same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 19 +++++++++----------
src/backend/catalog/index.c | 3 ++-
src/backend/postmaster/autovacuum.c | 2 +-
src/include/utils/rel.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..79834126f2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -107,15 +107,6 @@ static relopt_bool boolRelOpts[] =
},
false
},
- {
- {
- "autovacuum_enabled",
- "Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
- ShareUpdateExclusiveLock
- },
- true
- },
{
{
"user_catalog_table",
@@ -168,6 +159,14 @@ static relopt_bool boolRelOpts[] =
static relopt_ternary ternaryRelOpts[] =
{
+ {
+ {
+ "autovacuum_enabled",
+ "Enables autovacuum in this relation",
+ RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ ShareUpdateExclusiveLock
+ }
+ },
{
{
"vacuum_truncate",
@@ -1976,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
static const relopt_parse_elt tab[] = {
{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_BOOL,
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
{"autovacuum_parallel_workers", RELOPT_TYPE_INT,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 4c5da7e5db0..ec21b83b6b8 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2900,7 +2900,8 @@ index_update_stats(Relation rel,
{
StdRdOptions *options = (StdRdOptions *) rel->rd_options;
- if (options != NULL && !options->autovacuum.enabled)
+ if (options != NULL &&
+ options->autovacuum.enabled == PG_TERNARY_FALSE)
update_stats = false;
}
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index db1f5d0e575..c8a1b66ca2e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3141,7 +3141,7 @@ relation_needs_vacanalyze(Oid relid,
? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 89c159b133f..2ee98e9c6cc 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -310,7 +310,7 @@ typedef struct ForeignKeyCacheInfo
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
{
- bool enabled;
+ pg_ternary enabled;
int autovacuum_parallel_workers;
int vacuum_threshold;
--
2.50.1 (Apple Git-155)
[text/plain] v8-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch (3.5K, ../../anuXd35G8WLTUaBR@nathan/4-v8-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch)
download | inline diff:
From c602fb70ed014073312f0b7b601028ca24f9b4c5 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 15:27:58 -0500
Subject: [PATCH v8 3/7] Add an "unset" value for vacuum_index_cleanup.
This commit adds a new value to StdRdOptIndexCleanup to distinguish
whether it is explicitly set, similar to ViewOptCheckOption's
VIEW_OPTION_CHECK_OPTION_NOT_SET. This changes only the internal
representation; an unset value still defaults to AUTO, and the
option accepts the same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 3 ++-
src/backend/commands/vacuum.c | 23 ++++++++++++++---------
src/include/utils/rel.h | 1 +
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79834126f2f..58eb72e2339 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -517,6 +517,7 @@ static relopt_real realRelOpts[] =
/* values from StdRdOptIndexCleanup */
static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
{
+ /* no value for NOT_SET */
{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
@@ -557,7 +558,7 @@ static relopt_enum enumRelOpts[] =
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
},
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 31f9824899c..52116c02b59 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2210,20 +2210,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
StdRdOptIndexCleanup vacuum_index_cleanup;
if (rel->rd_options == NULL)
- vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+ vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
vacuum_index_cleanup =
((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
- if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
- params.index_cleanup = VACOPTVALUE_AUTO;
- else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
- params.index_cleanup = VACOPTVALUE_ENABLED;
- else
+ switch (vacuum_index_cleanup)
{
- Assert(vacuum_index_cleanup ==
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
- params.index_cleanup = VACOPTVALUE_DISABLED;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ params.index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ params.index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
}
}
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 2ee98e9c6cc..01b6e1a872c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,6 +338,7 @@ typedef enum StdRdOptIndexCleanup
STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
} StdRdOptIndexCleanup;
typedef struct StdRdOptions
--
2.50.1 (Apple Git-155)
[text/plain] v8-0004-Simplify-autovacuum-s-TOAST-to-main-relation-relo.patch (3.8K, ../../anuXd35G8WLTUaBR@nathan/5-v8-0004-Simplify-autovacuum-s-TOAST-to-main-relation-relo.patch)
download | inline diff:
From 04510ea61ad52f49f8486a64d4fc95c4bd35d0ab Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 7 Aug 2026 10:55:34 -0500
Subject: [PATCH v8 4/7] Simplify autovacuum's TOAST-to-main-relation
reloptions map.
do_autovacuum() adds an entry to this map for every relation that
has a TOAST table, and uses a flag to mark the entries that have no
reloptions to pass down. The unconditional entry made sense back
when the payload was the main relation's OID: commit 7d4c9a5793
added the map so that the TOAST pass could find the parent, whose
pg_autovacuum row supplied the settings for a TOAST table that had
none of its own. Commit 834a6da4f7 replaced that lookup by copying
the parent's reloptions into the entry, which left the OID unread
and called for the flag, since a by-value AutoVacOpts cannot say
"not set".
An entry is now worth creating only when there is something to
inherit, so skip the relations that have no reloptions and let a
successful lookup speak for itself. Two relations cannot share a
TOAST table, and each pass sees a single catalog snapshot, so an
insertion can never find an existing entry; assert that rather than
quietly ignoring it.
While at it, remove two more leftovers of that same conversion:
ar_relid, unread ever since, and a NULL test on table_toast_map in
table_recheck_autovac(), which used to carry the relkind test for
get_pg_autovacuum_tuple_relid() and has had no NULL to catch since
that function went away.
---
src/backend/postmaster/autovacuum.c | 26 +++++++-------------------
1 file changed, 7 insertions(+), 19 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index c8a1b66ca2e..32efe967890 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -200,8 +200,6 @@ typedef struct avw_dbase
typedef struct av_relation
{
Oid ar_toastrelid; /* hash key - must be first */
- Oid ar_relid;
- bool ar_hasrelopts;
StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
@@ -2097,7 +2095,7 @@ do_autovacuum(void)
* this whether or not the table is going to be vacuumed, because we
* don't automatically vacuum toast tables along the parent table.
*/
- if (OidIsValid(classForm->reltoastrelid))
+ if (OidIsValid(classForm->reltoastrelid) && relopts)
{
av_relation *hentry;
bool found;
@@ -2105,19 +2103,10 @@ do_autovacuum(void)
hentry = hash_search(table_toast_map,
&classForm->reltoastrelid,
HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
- if (!found)
- {
- /* hash_search already filled in the key */
- hentry->ar_relid = relid;
- hentry->ar_hasrelopts = false;
- if (relopts != NULL)
- {
- hentry->ar_hasrelopts = true;
- memcpy(&hentry->ar_reloptions, relopts,
- sizeof(StdRdOptions));
- }
- }
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
}
/* Release stuff to avoid per-relation leakage */
@@ -2166,7 +2155,7 @@ do_autovacuum(void)
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
@@ -2809,14 +2798,13 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ else if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
--
2.50.1 (Apple Git-155)
[text/plain] v8-0005-Give-TOAST-storage-parameters-unsettable-defaults.patch (5.0K, ../../anuXd35G8WLTUaBR@nathan/6-v8-0005-Give-TOAST-storage-parameters-unsettable-defaults.patch)
download | inline diff:
From c0327f11b5b4e1ace51e7d411aef87a176df72c5 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:55:44 -0500
Subject: [PATCH v8 5/7] Give TOAST storage parameters unsettable defaults.
This is preparatory work for a follow-up commit that will fill in a
TOAST table's unset storage parameters from its main table's. For
that, it must be possible to tell an option nobody set from one the
user set to the value that option happens to default to. The
parsed form of a relation's options has nowhere to record which
ones were specified, so an unset option is simply one still holding
its declared default.
Require, then, that any option a TOAST table accepts default to a
value the user cannot set. Ternaries already comply, having no
default at all, as does vacuum_index_cleanup, whose "not set"
member has no spelling. Among the numeric ones only
log_autovacuum_min_duration was in violation, defaulting to -1 with
a minimum of -1, so give it -2 instead, matching
autovacuum_vacuum_max_threshold and
autovacuum_vacuum_insert_threshold. Users won't notice; -1 already
behaved exactly as leaving the option unset does, and it still
does.
An assertion in initialize_reloptions() enforces both of the rules
the follow-up commit will rely on: that the default is unsettable,
and that anything settable on a TOAST table is settable on a heap,
since the inherited value is read from a main table's options at
the same offset. Note that this rules out bool and string options,
neither of which can express "unset".
---
src/backend/access/common/reloptions.c | 78 +++++++++++++++++++++++++-
src/backend/postmaster/autovacuum.c | 2 +-
2 files changed, 78 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 58eb72e2339..02bc8213def 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
},
{
{
@@ -613,6 +613,78 @@ static void parse_one_reloption(relopt_value *option, char *text_str,
((option).isset ? strlen((option).string_val) : \
((relopt_string *) (option).gen)->default_len)
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Verify that every option a TOAST table accepts defaults to a value the user
+ * cannot set. Nothing records which options were specified, so an option
+ * still holding its default is the only way to recognize one that was never
+ * set, and that is how a TOAST table tells which values it should take from
+ * its main table.
+ */
+static void
+assert_toast_defaults_unsettable(void)
+{
+ for (int i = 0; relOpts[i]; i++)
+ {
+ relopt_gen *gen = relOpts[i];
+
+ if ((gen->kinds & RELOPT_KIND_TOAST) == 0)
+ continue;
+
+ /*
+ * A TOAST table's value is filled in from its main table's at the
+ * same offset in the same struct, so the option must be settable on a
+ * heap too.
+ */
+ Assert((gen->kinds & RELOPT_KIND_HEAP) != 0);
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+
+ /*
+ * Ternaries carry no default, and parse_one_reloption() can
+ * only produce true or false, so PG_TERNARY_UNSET is already
+ * beyond a user's reach.
+ */
+ break;
+
+ case RELOPT_TYPE_INT:
+ {
+ relopt_int *optint = (relopt_int *) gen;
+
+ Assert(optint->default_val < optint->min ||
+ optint->default_val > optint->max);
+ break;
+ }
+
+ case RELOPT_TYPE_REAL:
+ {
+ relopt_real *optreal = (relopt_real *) gen;
+
+ Assert(optreal->default_val < optreal->min ||
+ optreal->default_val > optreal->max);
+ break;
+ }
+
+ case RELOPT_TYPE_ENUM:
+ {
+ relopt_enum *optenum = (relopt_enum *) gen;
+
+ for (relopt_enum_elt_def *elt = optenum->members;
+ elt->string_val; elt++)
+ Assert(elt->symbol_val != optenum->default_val);
+ break;
+ }
+
+ default:
+ /* Neither bools nor strings can express "unset". */
+ Assert(false);
+ }
+ }
+}
+#endif /* USE_ASSERT_CHECKING */
+
/*
* initialize_reloptions
* initialization routine, must be called before parsing
@@ -730,6 +802,10 @@ initialize_reloptions(void)
/* flag the work is complete */
need_initialization = false;
+
+#ifdef USE_ASSERT_CHECKING
+ assert_toast_defaults_unsettable();
+#endif
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 32efe967890..a99f7108636 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2832,7 +2832,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* defaults, autovacuum's own first and plain vacuum second.
*/
- /* -1 in autovac setting means use log_autovacuum_min_duration */
+ /* a negative autovac setting means use log_autovacuum_min_duration */
log_vacuum_min_duration = (avopts && avopts->log_vacuum_min_duration >= 0)
? avopts->log_vacuum_min_duration
: Log_autovacuum_min_duration;
--
2.50.1 (Apple Git-155)
[text/plain] v8-0006-Move-the-StdRdOptions-parse-table-to-file-scope.patch (7.8K, ../../anuXd35G8WLTUaBR@nathan/7-v8-0006-Move-the-StdRdOptions-parse-table-to-file-scope.patch)
download | inline diff:
From 86346bdce4bd1182520348c40151a695da8b53e2 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:56:02 -0500
Subject: [PATCH v8 6/7] Move the StdRdOptions parse table to file scope.
This is preparatory work for a follow-up commit that will walk the
table from another function. Nothing changes but the indentation
of its entries.
---
src/backend/access/common/reloptions.c | 115 +++++++++++++------------
1 file changed, 60 insertions(+), 55 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 02bc8213def..4548eb02676 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2044,69 +2044,74 @@ fillRelOptions(void *rdopts, Size basesize,
}
+/*
+ * Parse table for StdRdOptions, which is shared by the RELOPT_KIND_HEAP and
+ * RELOPT_KIND_TOAST kinds.
+ */
+static const relopt_parse_elt stdRdOptionsTab[] = {
+ {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
+ {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
+ {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
+ {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
+ {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
+ {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
+ {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
+ {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
+ {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
+ {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
+ {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
+ {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
+ {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
+ {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
+ {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
+ {"toast_tuple_target", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, toast_tuple_target)},
+ {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
+ {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
+ {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
+ {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"user_catalog_table", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, user_catalog_table)},
+ {"parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, parallel_workers)},
+ {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+ offsetof(StdRdOptions, vacuum_index_cleanup)},
+ {"vacuum_truncate", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, vacuum_truncate)},
+ {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
+};
+
/*
* Option parser for anything that uses StdRdOptions.
*/
bytea *
default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
- static const relopt_parse_elt tab[] = {
- {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
- {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
- {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
- {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
- {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
- {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
- {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
- {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
- {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
- {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
- {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
- {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
- {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
- {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
- {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
- {"toast_tuple_target", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, toast_tuple_target)},
- {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
- {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
- {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
- {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
- {"user_catalog_table", RELOPT_TYPE_BOOL,
- offsetof(StdRdOptions, user_catalog_table)},
- {"parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, parallel_workers)},
- {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
- offsetof(StdRdOptions, vacuum_index_cleanup)},
- {"vacuum_truncate", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, vacuum_truncate)},
- {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
- };
-
return (bytea *) build_reloptions(reloptions, validate, kind,
sizeof(StdRdOptions),
- tab, lengthof(tab));
+ stdRdOptionsTab,
+ lengthof(stdRdOptionsTab));
}
/*
--
2.50.1 (Apple Git-155)
[text/plain] v8-0007-Fix-VACUUM-and-autovacuum-handling-of-TOAST-stora.patch (22.1K, ../../anuXd35G8WLTUaBR@nathan/8-v8-0007-Fix-VACUUM-and-autovacuum-handling-of-TOAST-stora.patch)
download | inline diff:
From a3f9114efaa75412eeaaede9120b280d46ec3a49 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Tue, 11 Aug 2026 15:43:15 -0500
Subject: [PATCH v8 7/7] Fix VACUUM and autovacuum handling of TOAST storage
parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Unfortunately, current reality does not match this description.
Neither VACUUM nor autovacuum consults the main table's
non-autovacuum storage parameters, and autovacuum only consults the
main table's autovacuum-related storage parameters if the TOAST
table lacks any. One silver lining is that all currently-supported
TOAST storage parameters are related to vacuum, whose code paths
already access the main table's parameters. This means that our
solution needn't involve more lookups; we just need to propagate
them correctly.
To fix, this commit teaches autovacuum to combine the TOAST storage
parameters with the main table's (with the toast.* ones winning if
both are set), and it teaches VACUUM to send down the main table's
parameters when recursing to a TOAST table. This doesn't fix
VACUUM against a TOAST table directly (e.g., VACUUM
pg_toast.pg_toast_5432), but that's probably okay because it's not
the main supported way to vacuum a TOAST table (see VACUUM's
PROCESS_MAIN and PROCESS_TOAST options).
An existing shortcoming that this patch only makes worse is that
autovacuum/VACUUM remain oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/access/common/reloptions.c | 93 ++++++++++
src/backend/commands/vacuum.c | 30 +++-
src/backend/postmaster/autovacuum.c | 159 +++++++++++++++---
src/include/access/reloptions.h | 2 +
src/include/commands/vacuum.h | 8 +
.../injection_points/expected/vacuum.out | 29 ++++
.../modules/injection_points/sql/vacuum.sql | 16 ++
7 files changed, 311 insertions(+), 26 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 4548eb02676..60f1dc7ddd5 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2114,6 +2114,99 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
lengthof(stdRdOptionsTab));
}
+/*
+ * find_reloption
+ * Look up a reloption of the given kind by name.
+ *
+ * Returns NULL if no such option can be set on relations of that kind. Note
+ * that names are unique only within a kind; "fillfactor", for example, is
+ * declared separately for heaps and for each index access method.
+ */
+static relopt_gen *
+find_reloption(const char *name, relopt_kind kind)
+{
+ if (need_initialization)
+ initialize_reloptions();
+
+ for (int i = 0; relOpts[i]; i++)
+ {
+ if ((relOpts[i]->kinds & kind) != 0 &&
+ strcmp(relOpts[i]->name, name) == 0)
+ return relOpts[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * merge_toast_reloptions
+ * Fill in a TOAST table's unset options from its main table's.
+ *
+ * Any option that may be set on a TOAST table but was not is taken from
+ * main_opts. Either argument may be NULL; if both are, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * An option counts as unset while it still holds the default declared for it
+ * above, which works because nothing a TOAST table accepts has a default the
+ * user could also set (see assert_toast_defaults_unsettable()).
+ *
+ * NB: This destructively modifies toast_opts, and what it returns may be
+ * either argument, so the caller must know which of the two it owns.
+ */
+StdRdOptions *
+merge_toast_reloptions(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ if (toast_opts == NULL)
+ return main_opts;
+ if (main_opts == NULL)
+ return toast_opts;
+
+ for (int i = 0; i < lengthof(stdRdOptionsTab); i++)
+ {
+ const relopt_parse_elt *elem = &stdRdOptionsTab[i];
+ relopt_gen *gen;
+ char *toast_val;
+ char *main_val;
+
+ /* Skip anything that cannot be set on a TOAST table. */
+ gen = find_reloption(elem->optname, RELOPT_KIND_TOAST);
+ if (gen == NULL)
+ continue;
+
+ toast_val = (char *) toast_opts + elem->offset;
+ main_val = (char *) main_opts + elem->offset;
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+ if (*(pg_ternary *) toast_val == PG_TERNARY_UNSET)
+ *(pg_ternary *) toast_val = *(pg_ternary *) main_val;
+ break;
+
+ case RELOPT_TYPE_INT:
+ if (*(int *) toast_val == ((relopt_int *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ case RELOPT_TYPE_REAL:
+ if (*(double *) toast_val == ((relopt_real *) gen)->default_val)
+ *(double *) toast_val = *(double *) main_val;
+ break;
+
+ case RELOPT_TYPE_ENUM:
+ if (*(int *) toast_val == ((relopt_enum *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ default:
+ elog(ERROR, "reloption \"%s\" has a type a TOAST table cannot inherit",
+ elem->optname);
+ }
+ }
+
+ return toast_opts;
+}
+
/*
* build_reloptions
*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 52116c02b59..3af66006586 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -187,6 +187,9 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_index_cleanup = VACOPTVALUE_UNSPECIFIED;
+ params.main_truncate = VACOPTVALUE_UNSPECIFIED;
+ params.main_max_eager_freeze_failure_rate = -1.0;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2203,7 +2206,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
- * specified in VACUUM command, or when running in an autovacuum worker
+ * specified in VACUUM command, or when running in an autovacuum worker. A
+ * TOAST table with no setting of its own inherits the main table's value.
*/
if (params.index_cleanup == VACOPTVALUE_UNSPECIFIED)
{
@@ -2227,11 +2231,16 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
params.index_cleanup = VACOPTVALUE_AUTO;
break;
case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
- params.index_cleanup = VACOPTVALUE_AUTO;
+ if (params.main_index_cleanup != VACOPTVALUE_UNSPECIFIED)
+ params.index_cleanup = params.main_index_cleanup;
+ else
+ params.index_cleanup = VACOPTVALUE_AUTO;
break;
}
}
+ toast_vacuum_params.main_index_cleanup = params.index_cleanup;
+
#ifdef USE_INJECTION_POINTS
if (params.index_cleanup == VACOPTVALUE_AUTO)
INJECTION_POINT("vacuum-index-cleanup-auto", NULL);
@@ -2243,16 +2252,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
/*
* Check if the vacuum_max_eager_freeze_failure_rate table storage
- * parameter was specified. This overrides the GUC value.
+ * parameter was specified. This overrides the GUC value. A TOAST table
+ * with no setting of its own inherits the main table's value.
*/
if (rel->rd_options != NULL &&
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
params.max_eager_freeze_failure_rate =
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ else if (params.main_max_eager_freeze_failure_rate >= 0.0)
+ params.max_eager_freeze_failure_rate =
+ params.main_max_eager_freeze_failure_rate;
+
+ toast_vacuum_params.main_max_eager_freeze_failure_rate =
+ params.max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
- * specified in VACUUM command, or when running in an autovacuum worker
+ * specified in VACUUM command, or when running in an autovacuum worker. A
+ * TOAST table with no setting of its own inherits the main table's value
+ * before falling back to the GUC.
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
@@ -2265,12 +2283,16 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
else
params.truncate = VACOPTVALUE_DISABLED;
}
+ else if (params.main_truncate != VACOPTVALUE_UNSPECIFIED)
+ params.truncate = params.main_truncate;
else if (vacuum_truncate)
params.truncate = VACOPTVALUE_ENABLED;
else
params.truncate = VACOPTVALUE_DISABLED;
}
+ toast_vacuum_params.main_truncate = params.truncate;
+
#ifdef USE_INJECTION_POINTS
if (params.truncate == VACOPTVALUE_AUTO)
INJECTION_POINT("vacuum-truncate-auto", NULL);
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a99f7108636..881e07ecf9e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2015,9 +2015,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2133,6 +2133,8 @@ do_autovacuum(void)
bool doanalyze;
bool wraparound;
AutoVacuumScores scores;
+ av_relation *hentry;
+ bool found;
/*
* We cannot safely process other backends' temp tables, so skip 'em.
@@ -2143,21 +2145,19 @@ do_autovacuum(void)
relid = classForm->oid;
/*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
+ * fetch reloptions -- merge any unset options from the main rel
+ *
+ * Note that this fills in the storage parameters only VACUUM
+ * consults, too. That does no harm here; whether we process the
+ * table depends on the autovacuum parameters alone.
*/
relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found)
+ relopts = merge_toast_reloptions(relopts, &hentry->ar_reloptions);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2782,6 +2782,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
autovac_table *tab = NULL;
bool wraparound;
StdRdOptions *relopts;
+ StdRdOptions *main_relopts = NULL;
bool free_relopts = false;
AutoVacuumScores scores;
@@ -2792,20 +2793,24 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE)
+
+ if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found)
- relopts = &hentry->ar_reloptions;
+ {
+ main_relopts = &hentry->ar_reloptions;
+ relopts = merge_toast_reloptions(relopts, main_relopts);
+ }
}
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2892,6 +2897,49 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ /*
+ * For TOAST tables, provide fallbacks for the options that
+ * vacuum_rel() resolves from the relation's own reloptions, which do
+ * not have the main table's values merged in.
+ */
+ tab->at_params.main_index_cleanup = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.main_truncate = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.main_max_eager_freeze_failure_rate = -1.0;
+
+ if (main_relopts != NULL)
+ {
+ switch (main_relopts->vacuum_index_cleanup)
+ {
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ tab->at_params.main_index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ break;
+ }
+
+ switch (main_relopts->vacuum_truncate)
+ {
+ case PG_TERNARY_TRUE:
+ tab->at_params.main_truncate = VACOPTVALUE_ENABLED;
+ break;
+ case PG_TERNARY_FALSE:
+ tab->at_params.main_truncate = VACOPTVALUE_DISABLED;
+ break;
+ case PG_TERNARY_UNSET:
+ break;
+ }
+
+ if (main_relopts->vacuum_max_eager_freeze_failure_rate >= 0.0)
+ tab->at_params.main_max_eager_freeze_failure_rate =
+ main_relopts->vacuum_max_eager_freeze_failure_rate;
+ }
+
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
if (avopts)
@@ -2948,9 +2996,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the StdRdOptions options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * relopts is a pointer to the StdRdOptions options (the relation's own for a
+ * plain table, or those merged with the main table's for a TOAST table), NULL
+ * if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3607,6 +3655,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
Relation rel;
TableScanDesc scan;
HeapTuple tup;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
InitMaterializedSRF(fcinfo, 0);
@@ -3616,13 +3666,64 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
rel = table_open(RelationRelationId, AccessShareLock);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parent tables' reloptions, we can generate the
+ * results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ bool free_relopts = false;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3639,12 +3740,25 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
continue;
relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (relopts)
+ free_relopts = true;
+ if (form->relkind == RELKIND_TOASTVALUE)
+ {
+ av_relation *hentry;
+ bool found;
+
+ hentry = hash_search(table_toast_map, &form->oid,
+ HASH_FIND, &found);
+ if (found)
+ relopts = merge_toast_reloptions(relopts, &hentry->ar_reloptions);
+ }
+
relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (relopts)
+ if (free_relopts)
pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
@@ -3662,6 +3776,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..6c599382f02 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -247,6 +247,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options,
extern bytea *default_reloptions(Datum reloptions, bool validate,
relopt_kind kind);
+extern struct StdRdOptions *merge_toast_reloptions(struct StdRdOptions *toast_opts,
+ struct StdRdOptions *main_opts);
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..fdfceb38396 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,14 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * Main table fallback values for TOAST tables to inherit when they have
+ * no setting of their own.
+ */
+ VacOptValue main_index_cleanup;
+ VacOptValue main_truncate;
+ double main_max_eager_freeze_failure_rate;
} VacuumParams;
/*
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..1f9acc73594 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,9 +79,38 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- TOAST table inherits main table's resolved values
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
+ over
+------
+ t
+(1 row)
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..085f64337ad 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,9 +33,25 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- TOAST table inherits main table's resolved values
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-11 21:45 Nathan Bossart <nathandbossart@gmail.com>
parent: Sami Imseih <samimseih@gmail.com>
0 siblings, 0 replies; 44+ messages in thread
From: Nathan Bossart @ 2026-08-11 21:45 UTC (permalink / raw)
To: Sami Imseih <samimseih@gmail.com>; +Cc: Greg Burd <greg@burd.me>; solai v <solai.cdac@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
On Tue, Aug 11, 2026 at 09:53:53AM -0500, Sami Imseih wrote:
> I looked these patches while looking at potential cases for injection point
> conditional strings [0], which could allow for better autovacuum testing,
> particularly for the case being discussed here.
Thanks for reviewing.
> 1/ Instead of setting toast_vacuum_params.* inside each branch, it reads
> cleaner to set toast_vacuum_params.* after params.* is done, and this
> way you only set toast_vacuum_params.*once. For example, in
> index_cleanup:
Done in v8 [0].
> 2/ A couple of comment tweaks in merge_autovac_opts()
I don't think these changes apply to v8, except for perhaps the "NB" one,
which I expanded a bit.
[0] https://postgr.es/m/anuXd35G8WLTUaBR%40nathan
--
nathan
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-12 17:17 Nathan Bossart <nathandbossart@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2026-08-12 17:17 UTC (permalink / raw)
To: Greg Burd <greg@burd.me>; +Cc: solai v <solai.cdac@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
On Tue, Aug 11, 2026 at 04:43:19PM -0500, Nathan Bossart wrote:
> Here is a new patch set with an attempt at the above. The new 0005
> contains the log_autovacuum_min_duration default change along with an
> assertion that all TOAST storage parameters have unsettable defaults. 0006
> is just some prerequisite refactoring for 0007. And 0007 is the fix with
> the merge done based on the main reloption list instead of new arrays.
After sleeping on this, I realized that we can simplify things a bit by
using merge_toast_reloptions() for manual VACUUM, too. I've added a new
0007 for that. The autovacuum fix now lives in 0008.
--
nathan
From fe0e7ae22ea5dcca1bdefc3431770c4ec97c25a6 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:35:34 -0500
Subject: [PATCH v9 1/8] Remove extract_autovac_opts().
extract_autovac_opts() returned a palloc'd copy of only the
AutoVacOpts portion of a relation's reloptions. Upcoming work
needs the rest of the StdRdOptions as well, so the callers must
keep the whole struct around.
Remove the helper and have the callers obtain reloptions from
extractRelOptions() directly. av_relation now caches a
StdRdOptions instead of an AutoVacOpts, and
relation_needs_vacanalyze() takes a StdRdOptions and extracts the
autovacuum portion itself. This is preparatory refactoring with no
change in behavior.
---
src/backend/postmaster/autovacuum.c | 122 ++++++++++------------------
1 file changed, 44 insertions(+), 78 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index ee202a4b47e..db1f5d0e575 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -202,8 +202,7 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -379,7 +378,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -388,8 +387,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2037,7 +2034,7 @@ do_autovacuum(void)
while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2076,7 +2073,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2118,7 +2115,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2141,7 +2138,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2160,7 +2157,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2776,39 +2773,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc_object(AutoVacOpts);
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2828,8 +2792,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2842,9 +2806,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2853,10 +2817,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- relation_needs_vacanalyze(relid, avopts, classForm,
+ relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
DEBUG3,
&dovacuum, &doanalyze, &wraparound,
@@ -2871,6 +2835,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_vacuum_min_duration;
int log_analyze_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2982,8 +2947,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2995,7 +2960,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none.
*
@@ -3072,7 +3037,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -3086,6 +3051,7 @@ relation_needs_vacanalyze(Oid relid,
bool force_vacuum;
bool av_enabled;
bool may_free = false;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3137,45 +3103,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
@@ -3668,7 +3634,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
- AutoVacOpts *avopts;
+ StdRdOptions *relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3684,14 +3650,14 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
- relation_needs_vacanalyze(form->oid, avopts, form,
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
- pfree(avopts);
+ if (relopts)
+ pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
vals[1] = Float8GetDatum(scores.max);
--
2.50.1 (Apple Git-155)
From 50a357bbbd3f50b7fa71d20f05f93057a7b58803 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:50:36 -0500
Subject: [PATCH v9 2/8] Make autovacuum_enabled a ternary reloption.
This commit reimplements autovacuum_enabled as a ternary, using the
support added in commit 4d6a66f675 and following the example of
vacuum_truncate. This changes only the internal representation: an
unset value still behaves as enabled, and the option accepts the
same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 19 +++++++++----------
src/backend/catalog/index.c | 3 ++-
src/backend/postmaster/autovacuum.c | 2 +-
src/include/utils/rel.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..79834126f2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -107,15 +107,6 @@ static relopt_bool boolRelOpts[] =
},
false
},
- {
- {
- "autovacuum_enabled",
- "Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
- ShareUpdateExclusiveLock
- },
- true
- },
{
{
"user_catalog_table",
@@ -168,6 +159,14 @@ static relopt_bool boolRelOpts[] =
static relopt_ternary ternaryRelOpts[] =
{
+ {
+ {
+ "autovacuum_enabled",
+ "Enables autovacuum in this relation",
+ RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ ShareUpdateExclusiveLock
+ }
+ },
{
{
"vacuum_truncate",
@@ -1976,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
static const relopt_parse_elt tab[] = {
{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_BOOL,
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
{"autovacuum_parallel_workers", RELOPT_TYPE_INT,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 4c5da7e5db0..ec21b83b6b8 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2900,7 +2900,8 @@ index_update_stats(Relation rel,
{
StdRdOptions *options = (StdRdOptions *) rel->rd_options;
- if (options != NULL && !options->autovacuum.enabled)
+ if (options != NULL &&
+ options->autovacuum.enabled == PG_TERNARY_FALSE)
update_stats = false;
}
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index db1f5d0e575..c8a1b66ca2e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3141,7 +3141,7 @@ relation_needs_vacanalyze(Oid relid,
? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 89c159b133f..2ee98e9c6cc 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -310,7 +310,7 @@ typedef struct ForeignKeyCacheInfo
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
{
- bool enabled;
+ pg_ternary enabled;
int autovacuum_parallel_workers;
int vacuum_threshold;
--
2.50.1 (Apple Git-155)
From 7a6d2ec040420d1e6949a789756837d03db27fe1 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 15:27:58 -0500
Subject: [PATCH v9 3/8] Add an "unset" value for vacuum_index_cleanup.
This commit adds a new value to StdRdOptIndexCleanup to distinguish
whether it is explicitly set, similar to ViewOptCheckOption's
VIEW_OPTION_CHECK_OPTION_NOT_SET. This changes only the internal
representation; an unset value still defaults to AUTO, and the
option accepts the same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 3 ++-
src/backend/commands/vacuum.c | 23 ++++++++++++++---------
src/include/utils/rel.h | 1 +
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79834126f2f..58eb72e2339 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -517,6 +517,7 @@ static relopt_real realRelOpts[] =
/* values from StdRdOptIndexCleanup */
static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
{
+ /* no value for NOT_SET */
{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
@@ -557,7 +558,7 @@ static relopt_enum enumRelOpts[] =
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
},
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 31f9824899c..52116c02b59 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2210,20 +2210,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
StdRdOptIndexCleanup vacuum_index_cleanup;
if (rel->rd_options == NULL)
- vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+ vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
vacuum_index_cleanup =
((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
- if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
- params.index_cleanup = VACOPTVALUE_AUTO;
- else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
- params.index_cleanup = VACOPTVALUE_ENABLED;
- else
+ switch (vacuum_index_cleanup)
{
- Assert(vacuum_index_cleanup ==
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
- params.index_cleanup = VACOPTVALUE_DISABLED;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ params.index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ params.index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
}
}
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 2ee98e9c6cc..01b6e1a872c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,6 +338,7 @@ typedef enum StdRdOptIndexCleanup
STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
} StdRdOptIndexCleanup;
typedef struct StdRdOptions
--
2.50.1 (Apple Git-155)
From 3db51711daee2470df05f19d82eab9eba89bcdc3 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 7 Aug 2026 10:55:34 -0500
Subject: [PATCH v9 4/8] Simplify autovacuum's TOAST-to-main-relation
reloptions map.
do_autovacuum() adds an entry to this map for every relation that
has a TOAST table, and uses a flag to mark the entries that have no
reloptions to pass down. The unconditional entry made sense back
when the payload was the main relation's OID: commit 7d4c9a5793
added the map so that the TOAST pass could find the parent, whose
pg_autovacuum row supplied the settings for a TOAST table that had
none of its own. Commit 834a6da4f7 replaced that lookup by copying
the parent's reloptions into the entry, which left the OID unread
and called for the flag, since a by-value AutoVacOpts cannot say
"not set".
An entry is now worth creating only when there is something to
inherit, so skip the relations that have no reloptions and let a
successful lookup speak for itself. Two relations cannot share a
TOAST table, and each pass sees a single catalog snapshot, so an
insertion can never find an existing entry; assert that rather than
quietly ignoring it.
While at it, remove two more leftovers of that same conversion:
ar_relid, unread ever since, and a NULL test on table_toast_map in
table_recheck_autovac(), which used to carry the relkind test for
get_pg_autovacuum_tuple_relid() and has had no NULL to catch since
that function went away.
---
src/backend/postmaster/autovacuum.c | 26 +++++++-------------------
1 file changed, 7 insertions(+), 19 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index c8a1b66ca2e..32efe967890 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -200,8 +200,6 @@ typedef struct avw_dbase
typedef struct av_relation
{
Oid ar_toastrelid; /* hash key - must be first */
- Oid ar_relid;
- bool ar_hasrelopts;
StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
@@ -2097,7 +2095,7 @@ do_autovacuum(void)
* this whether or not the table is going to be vacuumed, because we
* don't automatically vacuum toast tables along the parent table.
*/
- if (OidIsValid(classForm->reltoastrelid))
+ if (OidIsValid(classForm->reltoastrelid) && relopts)
{
av_relation *hentry;
bool found;
@@ -2105,19 +2103,10 @@ do_autovacuum(void)
hentry = hash_search(table_toast_map,
&classForm->reltoastrelid,
HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
- if (!found)
- {
- /* hash_search already filled in the key */
- hentry->ar_relid = relid;
- hentry->ar_hasrelopts = false;
- if (relopts != NULL)
- {
- hentry->ar_hasrelopts = true;
- memcpy(&hentry->ar_reloptions, relopts,
- sizeof(StdRdOptions));
- }
- }
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
}
/* Release stuff to avoid per-relation leakage */
@@ -2166,7 +2155,7 @@ do_autovacuum(void)
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
@@ -2809,14 +2798,13 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ else if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
--
2.50.1 (Apple Git-155)
From 043ecb3131c43f044ead262ecde87f139db8592f Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:55:44 -0500
Subject: [PATCH v9 5/8] Give TOAST storage parameters unsettable defaults.
This is preparatory work for a follow-up commit that will fill in a
TOAST table's unset storage parameters from its main table's. For
that, it must be possible to tell an option nobody set from one the
user set to the value that option happens to default to. The
parsed form of a relation's options has nowhere to record which
ones were specified, so an unset option is simply one still holding
its declared default.
Require, then, that any option a TOAST table accepts default to a
value the user cannot set. Ternaries already comply, having no
default at all, as does vacuum_index_cleanup, whose "not set"
member has no spelling. Among the numeric ones only
log_autovacuum_min_duration was in violation, defaulting to -1 with
a minimum of -1, so give it -2 instead, matching
autovacuum_vacuum_max_threshold and
autovacuum_vacuum_insert_threshold. Users won't notice; -1 already
behaved exactly as leaving the option unset does, and it still
does.
An assertion in initialize_reloptions() enforces both of the rules
the follow-up commit will rely on: that the default is unsettable,
and that anything settable on a TOAST table is settable on a heap,
since the inherited value is read from a main table's options at
the same offset. Note that this rules out bool and string options,
neither of which can express "unset".
---
src/backend/access/common/reloptions.c | 78 +++++++++++++++++++++++++-
src/backend/postmaster/autovacuum.c | 2 +-
2 files changed, 78 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 58eb72e2339..02bc8213def 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
},
{
{
@@ -613,6 +613,78 @@ static void parse_one_reloption(relopt_value *option, char *text_str,
((option).isset ? strlen((option).string_val) : \
((relopt_string *) (option).gen)->default_len)
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Verify that every option a TOAST table accepts defaults to a value the user
+ * cannot set. Nothing records which options were specified, so an option
+ * still holding its default is the only way to recognize one that was never
+ * set, and that is how a TOAST table tells which values it should take from
+ * its main table.
+ */
+static void
+assert_toast_defaults_unsettable(void)
+{
+ for (int i = 0; relOpts[i]; i++)
+ {
+ relopt_gen *gen = relOpts[i];
+
+ if ((gen->kinds & RELOPT_KIND_TOAST) == 0)
+ continue;
+
+ /*
+ * A TOAST table's value is filled in from its main table's at the
+ * same offset in the same struct, so the option must be settable on a
+ * heap too.
+ */
+ Assert((gen->kinds & RELOPT_KIND_HEAP) != 0);
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+
+ /*
+ * Ternaries carry no default, and parse_one_reloption() can
+ * only produce true or false, so PG_TERNARY_UNSET is already
+ * beyond a user's reach.
+ */
+ break;
+
+ case RELOPT_TYPE_INT:
+ {
+ relopt_int *optint = (relopt_int *) gen;
+
+ Assert(optint->default_val < optint->min ||
+ optint->default_val > optint->max);
+ break;
+ }
+
+ case RELOPT_TYPE_REAL:
+ {
+ relopt_real *optreal = (relopt_real *) gen;
+
+ Assert(optreal->default_val < optreal->min ||
+ optreal->default_val > optreal->max);
+ break;
+ }
+
+ case RELOPT_TYPE_ENUM:
+ {
+ relopt_enum *optenum = (relopt_enum *) gen;
+
+ for (relopt_enum_elt_def *elt = optenum->members;
+ elt->string_val; elt++)
+ Assert(elt->symbol_val != optenum->default_val);
+ break;
+ }
+
+ default:
+ /* Neither bools nor strings can express "unset". */
+ Assert(false);
+ }
+ }
+}
+#endif /* USE_ASSERT_CHECKING */
+
/*
* initialize_reloptions
* initialization routine, must be called before parsing
@@ -730,6 +802,10 @@ initialize_reloptions(void)
/* flag the work is complete */
need_initialization = false;
+
+#ifdef USE_ASSERT_CHECKING
+ assert_toast_defaults_unsettable();
+#endif
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 32efe967890..a99f7108636 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2832,7 +2832,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* defaults, autovacuum's own first and plain vacuum second.
*/
- /* -1 in autovac setting means use log_autovacuum_min_duration */
+ /* a negative autovac setting means use log_autovacuum_min_duration */
log_vacuum_min_duration = (avopts && avopts->log_vacuum_min_duration >= 0)
? avopts->log_vacuum_min_duration
: Log_autovacuum_min_duration;
--
2.50.1 (Apple Git-155)
From 35116e0043b5e455fa7a4a0c2be1a82dde5079a2 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:56:02 -0500
Subject: [PATCH v9 6/8] Move the StdRdOptions parse table to file scope.
This is preparatory work for a follow-up commit that will walk the
table from another function. Nothing changes but the indentation
of its entries.
---
src/backend/access/common/reloptions.c | 115 +++++++++++++------------
1 file changed, 60 insertions(+), 55 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 02bc8213def..4548eb02676 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2044,69 +2044,74 @@ fillRelOptions(void *rdopts, Size basesize,
}
+/*
+ * Parse table for StdRdOptions, which is shared by the RELOPT_KIND_HEAP and
+ * RELOPT_KIND_TOAST kinds.
+ */
+static const relopt_parse_elt stdRdOptionsTab[] = {
+ {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
+ {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
+ {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
+ {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
+ {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
+ {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
+ {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
+ {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
+ {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
+ {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
+ {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
+ {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
+ {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
+ {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
+ {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
+ {"toast_tuple_target", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, toast_tuple_target)},
+ {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
+ {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
+ {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
+ {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"user_catalog_table", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, user_catalog_table)},
+ {"parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, parallel_workers)},
+ {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+ offsetof(StdRdOptions, vacuum_index_cleanup)},
+ {"vacuum_truncate", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, vacuum_truncate)},
+ {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
+};
+
/*
* Option parser for anything that uses StdRdOptions.
*/
bytea *
default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
- static const relopt_parse_elt tab[] = {
- {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
- {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
- {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
- {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
- {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
- {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
- {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
- {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
- {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
- {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
- {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
- {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
- {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
- {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
- {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
- {"toast_tuple_target", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, toast_tuple_target)},
- {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
- {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
- {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
- {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
- {"user_catalog_table", RELOPT_TYPE_BOOL,
- offsetof(StdRdOptions, user_catalog_table)},
- {"parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, parallel_workers)},
- {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
- offsetof(StdRdOptions, vacuum_index_cleanup)},
- {"vacuum_truncate", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, vacuum_truncate)},
- {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
- };
-
return (bytea *) build_reloptions(reloptions, validate, kind,
sizeof(StdRdOptions),
- tab, lengthof(tab));
+ stdRdOptionsTab,
+ lengthof(stdRdOptionsTab));
}
/*
--
2.50.1 (Apple Git-155)
From 23b24fff0e854c2319031f60c4c64870c4488373 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 11:34:55 -0500
Subject: [PATCH v9 7/8] Fix VACUUM's handling of TOAST storage parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Presently, VACUUM does no such thing. It reads the TOAST table's
own reloptions, which hold only what was set through toast.*, so
vacuum_index_cleanup or vacuum_truncate set on the main table has
no effect on its TOAST table.
To fix, add merge_toast_reloptions(), which walks the parse table
for StdRdOptions and takes the main table's value for anything the
TOAST table left at its default. vacuum_rel() hands the main
table's parameters down when recursing to a TOAST table, and it
merges them into a copy of the TOAST table's parameters before the
values are used. This doesn't help VACUUM against a TOAST table
directly (e.g., "VACUUM pg_toast.pg_toast_5432"), but that's
probably okay because it's not the main supported way to vacuum a
TOAST table (see VACUUM's PROCESS_MAIN and PROCESS_TOAST options).
A follow-up commit will do the same for autovacuum. While this is
a bug fix, it's too intrusive for back-patching, but the issue
seems to have gone unnoticed for a very long time, anyway.
---
src/backend/access/common/reloptions.c | 93 +++++++++++++++++++
src/backend/commands/vacuum.c | 45 ++++++---
src/backend/postmaster/autovacuum.c | 1 +
src/include/access/reloptions.h | 2 +
src/include/commands/vacuum.h | 7 ++
.../injection_points/expected/vacuum.out | 11 +++
.../modules/injection_points/sql/vacuum.sql | 8 ++
7 files changed, 156 insertions(+), 11 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 4548eb02676..263ebf44a45 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2114,6 +2114,99 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
lengthof(stdRdOptionsTab));
}
+/*
+ * find reloption
+ * Look up a reloption of the given kind by name.
+ *
+ * Returns NULL if no such option can be set on relations of that kind. Note
+ * that names are unique only within a kind; "fillfactor", for example, is
+ * declared separately for heaps and for each index access method.
+ */
+static relopt_gen *
+find_reloption(const char *name, relopt_kind kind)
+{
+ if (need_initialization)
+ initialize_reloptions();
+
+ for (int i = 0; relOpts[i]; i++)
+ {
+ if ((relOpts[i]->kinds & kind) != 0 &&
+ strcmp(relOpts[i]->name, name) == 0)
+ return relOpts[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * merge_toast_reloptions
+ * Fill in a TOAST table's unset options from its main table's.
+ *
+ * Any option that may be set on a TOAST table but was not is taken from
+ * main_opts. Either argument may be NULL; if both are, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * An option counts as unset while it still holds the default declared for it
+ * above, which works because nothing a TOAST table accepts has a default the
+ * user could also set (see assert_toast_defaults_unsettable()).
+ *
+ * NB: This destructively modifies toast_opts, and what it returns may be
+ * either argument, so the caller must know which of the two it owns.
+ */
+StdRdOptions *
+merge_toast_reloptions(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ if (toast_opts == NULL)
+ return main_opts;
+ if (main_opts == NULL)
+ return toast_opts;
+
+ for (int i = 0; i < lengthof(stdRdOptionsTab); i++)
+ {
+ const relopt_parse_elt *elem = &stdRdOptionsTab[i];
+ relopt_gen *gen;
+ char *toast_val;
+ char *main_val;
+
+ /* Skip anything that cannot be set on a TOAST table. */
+ gen = find_reloption(elem->optname, RELOPT_KIND_TOAST);
+ if (gen == NULL)
+ continue;
+
+ toast_val = (char *) toast_opts + elem->offset;
+ main_val = (char *) main_opts + elem->offset;
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+ if (*(pg_ternary *) toast_val == PG_TERNARY_UNSET)
+ *(pg_ternary *) toast_val = *(pg_ternary *) main_val;
+ break;
+
+ case RELOPT_TYPE_INT:
+ if (*(int *) toast_val == ((relopt_int *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ case RELOPT_TYPE_REAL:
+ if (*(double *) toast_val == ((relopt_real *) gen)->default_val)
+ *(double *) toast_val = *(double *) main_val;
+ break;
+
+ case RELOPT_TYPE_ENUM:
+ if (*(int *) toast_val == ((relopt_enum *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ default:
+ elog(ERROR, "reloption \"%s\" has a type a TOAST table cannot inherit",
+ elem->optname);
+ }
+ }
+
+ return toast_opts;
+}
+
/*
* build_reloptions
*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 52116c02b59..8fe48920c9f 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -31,6 +31,7 @@
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
+#include "access/reloptions.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
@@ -187,6 +188,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_relopts = NULL;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2039,6 +2041,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
int save_sec_context;
int save_nestlevel;
VacuumParams toast_vacuum_params;
+ StdRdOptions *relopts;
+ StdRdOptions relopts_copy;
/*
* This function scribbles on the parameters, so make a copy early to
@@ -2201,6 +2205,19 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ /*
+ * A TOAST table takes any storage parameter it accepts but does not set
+ * from its main table, whose parameters the caller handed down for that
+ * purpose. Merge them into a copy of our own.
+ *
+ * We are careful to avoid scribbling on the relcache's copy of the
+ * options.
+ */
+ if (rel->rd_options)
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ relopts = merge_toast_reloptions(rel->rd_options ? &relopts_copy : NULL,
+ params.main_relopts);
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2209,11 +2226,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
StdRdOptIndexCleanup vacuum_index_cleanup;
- if (rel->rd_options == NULL)
+ if (relopts == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = relopts->vacuum_index_cleanup;
switch (vacuum_index_cleanup)
{
@@ -2245,10 +2261,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
- params.max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ if (relopts != NULL && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ params.max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
@@ -2256,11 +2270,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
- StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
-
- if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
+ if (relopts && relopts->vacuum_truncate != PG_TERNARY_UNSET)
{
- if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ if (relopts->vacuum_truncate == PG_TERNARY_TRUE)
params.truncate = VACOPTVALUE_ENABLED;
else
params.truncate = VACOPTVALUE_DISABLED;
@@ -2293,6 +2305,17 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
else
toast_relid = InvalidOid;
+ /*
+ * Hand our storage parameters down for the TOAST table to inherit. Take
+ * a copy while we still have the relation open; the relcache entry can go
+ * away once we close it.
+ */
+ if (OidIsValid(toast_relid) && rel->rd_options)
+ {
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ toast_vacuum_params.main_relopts = &relopts_copy;
+ }
+
/*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also lock down security-restricted operations and
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a99f7108636..1610c60ec4b 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2891,6 +2891,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ tab->at_params.main_relopts = NULL;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..6c599382f02 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -247,6 +247,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options,
extern bytea *default_reloptions(Datum reloptions, bool validate,
relopt_kind kind);
+extern struct StdRdOptions *merge_toast_reloptions(struct StdRdOptions *toast_opts,
+ struct StdRdOptions *main_opts);
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..2ebdd5e40ab 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,13 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * When vacuuming a TOAST table, its main table's storage parameters, for
+ * the TOAST table to inherit anything it doesn't set itself. NULL if the
+ * main table has none, or if this isn't a TOAST table.
+ */
+ struct StdRdOptions *main_relopts;
} VacuumParams;
/*
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..caf0cc232b4 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,9 +79,20 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..0a43e14c928 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,9 +33,17 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
--
2.50.1 (Apple Git-155)
From 93ebc91c56443e8f6394f6cbefd24ce741b00431 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 12:02:21 -0500
Subject: [PATCH v9 8/8] Fix autovacuum's handling of TOAST storage parameters.
The previous commit made VACUUM apply a main table's storage
parameters to its TOAST table, as CREATE TABLE has long documented.
Autovacuum still gets this wrong in two ways. It falls back to the
main table's autovacuum parameters only when the TOAST table has no
reloptions at all, so setting a single toast.* parameter silently
discards the rest. And it never consults the main table for the
parameters that only VACUUM reads, since it leaves those for
vacuum_rel() to resolve from the TOAST table's own reloptions.
To fix, combine the two sets with merge_toast_reloptions() rather
than choosing between them, and hand the main table's parameters
down to vacuum_rel() the way VACUUM now does.
pg_stat_autovacuum_scores uses the combined parameters for TOAST
tables as well; it has to collect the main relations' parameters
before it can do so, so it now makes a preliminary pass over
pg_class.
An existing shortcoming that this patch only makes worse is that
autovacuum remains oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/postmaster/autovacuum.c | 110 ++++++++++++++----
.../injection_points/expected/vacuum.out | 22 +++-
.../modules/injection_points/sql/vacuum.sql | 12 +-
3 files changed, 117 insertions(+), 27 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 1610c60ec4b..a67f2de209c 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2015,9 +2015,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2133,6 +2133,8 @@ do_autovacuum(void)
bool doanalyze;
bool wraparound;
AutoVacuumScores scores;
+ av_relation *hentry;
+ bool found;
/*
* We cannot safely process other backends' temp tables, so skip 'em.
@@ -2143,21 +2145,14 @@ do_autovacuum(void)
relid = classForm->oid;
/*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
+ * fetch reloptions -- merge any unset options from the main rel
*/
relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found)
+ relopts = merge_toast_reloptions(relopts, &hentry->ar_reloptions);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2782,6 +2777,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
autovac_table *tab = NULL;
bool wraparound;
StdRdOptions *relopts;
+ StdRdOptions *main_relopts = NULL;
bool free_relopts = false;
AutoVacuumScores scores;
@@ -2792,20 +2788,23 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE)
+ if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found)
- relopts = &hentry->ar_reloptions;
+ {
+ main_relopts = &hentry->ar_reloptions;
+ relopts = merge_toast_reloptions(relopts, main_relopts);
+ }
}
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2891,7 +2890,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
- tab->at_params.main_relopts = NULL;
+ tab->at_params.main_relopts = main_relopts;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
@@ -2950,8 +2949,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* being forced because of Xid or multixact wraparound.
*
* relopts is a pointer to the StdRdOptions options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * case of a plain table, or merged with the main table's for a TOAST table),
+ * NULL if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3609,6 +3608,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
TableScanDesc scan;
HeapTuple tup;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
InitMaterializedSRF(fcinfo, 0);
@@ -3617,13 +3618,62 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
rel = table_open(RelationRelationId, AccessShareLock);
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parents' reloptions, we can generate the results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ bool free_relopts = false;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3640,12 +3690,25 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
continue;
relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (relopts)
+ free_relopts = true;
+ if (form->relkind == RELKIND_TOASTVALUE)
+ {
+ av_relation *hentry;
+ bool found;
+
+ hentry = hash_search(table_toast_map, &form->oid,
+ HASH_FIND, &found);
+ if (found)
+ relopts = merge_toast_reloptions(relopts, &hentry->ar_reloptions);
+ }
+
relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (relopts)
+ if (free_relopts)
pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
@@ -3663,6 +3726,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index caf0cc232b4..196be88c6b8 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -80,15 +80,33 @@ NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
-- A TOAST table inherits what it does not set from its main table.
-CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
(autovacuum_enabled=false,
vacuum_index_cleanup=false,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_truncate=false, toast.vacuum_truncate=true,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0);
VACUUM vac_tab_toast_inherit;
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-disabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-enabled
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
+ over
+------
+ t
+(1 row)
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 0a43e14c928..dc5f25dbb58 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -34,11 +34,19 @@ VACUUM vac_tab_auto;
RESET vacuum_truncate;
-- A TOAST table inherits what it does not set from its main table.
-CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
(autovacuum_enabled=false,
vacuum_index_cleanup=false,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_truncate=false, toast.vacuum_truncate=true,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0);
VACUUM vac_tab_toast_inherit;
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
--
2.50.1 (Apple Git-155)
Attachments:
[text/plain] v9-0001-Remove-extract_autovac_opts.patch (11.5K, ../../anyqokNVrCsxt9Vp@nathan/2-v9-0001-Remove-extract_autovac_opts.patch)
download | inline diff:
From fe0e7ae22ea5dcca1bdefc3431770c4ec97c25a6 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:35:34 -0500
Subject: [PATCH v9 1/8] Remove extract_autovac_opts().
extract_autovac_opts() returned a palloc'd copy of only the
AutoVacOpts portion of a relation's reloptions. Upcoming work
needs the rest of the StdRdOptions as well, so the callers must
keep the whole struct around.
Remove the helper and have the callers obtain reloptions from
extractRelOptions() directly. av_relation now caches a
StdRdOptions instead of an AutoVacOpts, and
relation_needs_vacanalyze() takes a StdRdOptions and extracts the
autovacuum portion itself. This is preparatory refactoring with no
change in behavior.
---
src/backend/postmaster/autovacuum.c | 122 ++++++++++------------------
1 file changed, 44 insertions(+), 78 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index ee202a4b47e..db1f5d0e575 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -202,8 +202,7 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -379,7 +378,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -388,8 +387,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2037,7 +2034,7 @@ do_autovacuum(void)
while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2076,7 +2073,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2118,7 +2115,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2141,7 +2138,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2160,7 +2157,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2776,39 +2773,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc_object(AutoVacOpts);
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2828,8 +2792,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2842,9 +2806,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2853,10 +2817,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- relation_needs_vacanalyze(relid, avopts, classForm,
+ relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
DEBUG3,
&dovacuum, &doanalyze, &wraparound,
@@ -2871,6 +2835,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_vacuum_min_duration;
int log_analyze_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2982,8 +2947,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2995,7 +2960,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none.
*
@@ -3072,7 +3037,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -3086,6 +3051,7 @@ relation_needs_vacanalyze(Oid relid,
bool force_vacuum;
bool av_enabled;
bool may_free = false;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3137,45 +3103,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
@@ -3668,7 +3634,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
- AutoVacOpts *avopts;
+ StdRdOptions *relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3684,14 +3650,14 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
- relation_needs_vacanalyze(form->oid, avopts, form,
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
- pfree(avopts);
+ if (relopts)
+ pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
vals[1] = Float8GetDatum(scores.max);
--
2.50.1 (Apple Git-155)
[text/plain] v9-0002-Make-autovacuum_enabled-a-ternary-reloption.patch (3.6K, ../../anyqokNVrCsxt9Vp@nathan/3-v9-0002-Make-autovacuum_enabled-a-ternary-reloption.patch)
download | inline diff:
From 50a357bbbd3f50b7fa71d20f05f93057a7b58803 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:50:36 -0500
Subject: [PATCH v9 2/8] Make autovacuum_enabled a ternary reloption.
This commit reimplements autovacuum_enabled as a ternary, using the
support added in commit 4d6a66f675 and following the example of
vacuum_truncate. This changes only the internal representation: an
unset value still behaves as enabled, and the option accepts the
same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 19 +++++++++----------
src/backend/catalog/index.c | 3 ++-
src/backend/postmaster/autovacuum.c | 2 +-
src/include/utils/rel.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..79834126f2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -107,15 +107,6 @@ static relopt_bool boolRelOpts[] =
},
false
},
- {
- {
- "autovacuum_enabled",
- "Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
- ShareUpdateExclusiveLock
- },
- true
- },
{
{
"user_catalog_table",
@@ -168,6 +159,14 @@ static relopt_bool boolRelOpts[] =
static relopt_ternary ternaryRelOpts[] =
{
+ {
+ {
+ "autovacuum_enabled",
+ "Enables autovacuum in this relation",
+ RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ ShareUpdateExclusiveLock
+ }
+ },
{
{
"vacuum_truncate",
@@ -1976,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
static const relopt_parse_elt tab[] = {
{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_BOOL,
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
{"autovacuum_parallel_workers", RELOPT_TYPE_INT,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 4c5da7e5db0..ec21b83b6b8 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2900,7 +2900,8 @@ index_update_stats(Relation rel,
{
StdRdOptions *options = (StdRdOptions *) rel->rd_options;
- if (options != NULL && !options->autovacuum.enabled)
+ if (options != NULL &&
+ options->autovacuum.enabled == PG_TERNARY_FALSE)
update_stats = false;
}
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index db1f5d0e575..c8a1b66ca2e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3141,7 +3141,7 @@ relation_needs_vacanalyze(Oid relid,
? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 89c159b133f..2ee98e9c6cc 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -310,7 +310,7 @@ typedef struct ForeignKeyCacheInfo
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
{
- bool enabled;
+ pg_ternary enabled;
int autovacuum_parallel_workers;
int vacuum_threshold;
--
2.50.1 (Apple Git-155)
[text/plain] v9-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch (3.5K, ../../anyqokNVrCsxt9Vp@nathan/4-v9-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch)
download | inline diff:
From 7a6d2ec040420d1e6949a789756837d03db27fe1 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 15:27:58 -0500
Subject: [PATCH v9 3/8] Add an "unset" value for vacuum_index_cleanup.
This commit adds a new value to StdRdOptIndexCleanup to distinguish
whether it is explicitly set, similar to ViewOptCheckOption's
VIEW_OPTION_CHECK_OPTION_NOT_SET. This changes only the internal
representation; an unset value still defaults to AUTO, and the
option accepts the same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 3 ++-
src/backend/commands/vacuum.c | 23 ++++++++++++++---------
src/include/utils/rel.h | 1 +
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79834126f2f..58eb72e2339 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -517,6 +517,7 @@ static relopt_real realRelOpts[] =
/* values from StdRdOptIndexCleanup */
static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
{
+ /* no value for NOT_SET */
{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
@@ -557,7 +558,7 @@ static relopt_enum enumRelOpts[] =
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
},
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 31f9824899c..52116c02b59 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2210,20 +2210,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
StdRdOptIndexCleanup vacuum_index_cleanup;
if (rel->rd_options == NULL)
- vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+ vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
vacuum_index_cleanup =
((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
- if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
- params.index_cleanup = VACOPTVALUE_AUTO;
- else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
- params.index_cleanup = VACOPTVALUE_ENABLED;
- else
+ switch (vacuum_index_cleanup)
{
- Assert(vacuum_index_cleanup ==
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
- params.index_cleanup = VACOPTVALUE_DISABLED;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ params.index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ params.index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
}
}
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 2ee98e9c6cc..01b6e1a872c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,6 +338,7 @@ typedef enum StdRdOptIndexCleanup
STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
} StdRdOptIndexCleanup;
typedef struct StdRdOptions
--
2.50.1 (Apple Git-155)
[text/plain] v9-0004-Simplify-autovacuum-s-TOAST-to-main-relation-relo.patch (3.8K, ../../anyqokNVrCsxt9Vp@nathan/5-v9-0004-Simplify-autovacuum-s-TOAST-to-main-relation-relo.patch)
download | inline diff:
From 3db51711daee2470df05f19d82eab9eba89bcdc3 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 7 Aug 2026 10:55:34 -0500
Subject: [PATCH v9 4/8] Simplify autovacuum's TOAST-to-main-relation
reloptions map.
do_autovacuum() adds an entry to this map for every relation that
has a TOAST table, and uses a flag to mark the entries that have no
reloptions to pass down. The unconditional entry made sense back
when the payload was the main relation's OID: commit 7d4c9a5793
added the map so that the TOAST pass could find the parent, whose
pg_autovacuum row supplied the settings for a TOAST table that had
none of its own. Commit 834a6da4f7 replaced that lookup by copying
the parent's reloptions into the entry, which left the OID unread
and called for the flag, since a by-value AutoVacOpts cannot say
"not set".
An entry is now worth creating only when there is something to
inherit, so skip the relations that have no reloptions and let a
successful lookup speak for itself. Two relations cannot share a
TOAST table, and each pass sees a single catalog snapshot, so an
insertion can never find an existing entry; assert that rather than
quietly ignoring it.
While at it, remove two more leftovers of that same conversion:
ar_relid, unread ever since, and a NULL test on table_toast_map in
table_recheck_autovac(), which used to carry the relkind test for
get_pg_autovacuum_tuple_relid() and has had no NULL to catch since
that function went away.
---
src/backend/postmaster/autovacuum.c | 26 +++++++-------------------
1 file changed, 7 insertions(+), 19 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index c8a1b66ca2e..32efe967890 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -200,8 +200,6 @@ typedef struct avw_dbase
typedef struct av_relation
{
Oid ar_toastrelid; /* hash key - must be first */
- Oid ar_relid;
- bool ar_hasrelopts;
StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
@@ -2097,7 +2095,7 @@ do_autovacuum(void)
* this whether or not the table is going to be vacuumed, because we
* don't automatically vacuum toast tables along the parent table.
*/
- if (OidIsValid(classForm->reltoastrelid))
+ if (OidIsValid(classForm->reltoastrelid) && relopts)
{
av_relation *hentry;
bool found;
@@ -2105,19 +2103,10 @@ do_autovacuum(void)
hentry = hash_search(table_toast_map,
&classForm->reltoastrelid,
HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
- if (!found)
- {
- /* hash_search already filled in the key */
- hentry->ar_relid = relid;
- hentry->ar_hasrelopts = false;
- if (relopts != NULL)
- {
- hentry->ar_hasrelopts = true;
- memcpy(&hentry->ar_reloptions, relopts,
- sizeof(StdRdOptions));
- }
- }
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
}
/* Release stuff to avoid per-relation leakage */
@@ -2166,7 +2155,7 @@ do_autovacuum(void)
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
@@ -2809,14 +2798,13 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ else if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
--
2.50.1 (Apple Git-155)
[text/plain] v9-0005-Give-TOAST-storage-parameters-unsettable-defaults.patch (5.0K, ../../anyqokNVrCsxt9Vp@nathan/6-v9-0005-Give-TOAST-storage-parameters-unsettable-defaults.patch)
download | inline diff:
From 043ecb3131c43f044ead262ecde87f139db8592f Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:55:44 -0500
Subject: [PATCH v9 5/8] Give TOAST storage parameters unsettable defaults.
This is preparatory work for a follow-up commit that will fill in a
TOAST table's unset storage parameters from its main table's. For
that, it must be possible to tell an option nobody set from one the
user set to the value that option happens to default to. The
parsed form of a relation's options has nowhere to record which
ones were specified, so an unset option is simply one still holding
its declared default.
Require, then, that any option a TOAST table accepts default to a
value the user cannot set. Ternaries already comply, having no
default at all, as does vacuum_index_cleanup, whose "not set"
member has no spelling. Among the numeric ones only
log_autovacuum_min_duration was in violation, defaulting to -1 with
a minimum of -1, so give it -2 instead, matching
autovacuum_vacuum_max_threshold and
autovacuum_vacuum_insert_threshold. Users won't notice; -1 already
behaved exactly as leaving the option unset does, and it still
does.
An assertion in initialize_reloptions() enforces both of the rules
the follow-up commit will rely on: that the default is unsettable,
and that anything settable on a TOAST table is settable on a heap,
since the inherited value is read from a main table's options at
the same offset. Note that this rules out bool and string options,
neither of which can express "unset".
---
src/backend/access/common/reloptions.c | 78 +++++++++++++++++++++++++-
src/backend/postmaster/autovacuum.c | 2 +-
2 files changed, 78 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 58eb72e2339..02bc8213def 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
},
{
{
@@ -613,6 +613,78 @@ static void parse_one_reloption(relopt_value *option, char *text_str,
((option).isset ? strlen((option).string_val) : \
((relopt_string *) (option).gen)->default_len)
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Verify that every option a TOAST table accepts defaults to a value the user
+ * cannot set. Nothing records which options were specified, so an option
+ * still holding its default is the only way to recognize one that was never
+ * set, and that is how a TOAST table tells which values it should take from
+ * its main table.
+ */
+static void
+assert_toast_defaults_unsettable(void)
+{
+ for (int i = 0; relOpts[i]; i++)
+ {
+ relopt_gen *gen = relOpts[i];
+
+ if ((gen->kinds & RELOPT_KIND_TOAST) == 0)
+ continue;
+
+ /*
+ * A TOAST table's value is filled in from its main table's at the
+ * same offset in the same struct, so the option must be settable on a
+ * heap too.
+ */
+ Assert((gen->kinds & RELOPT_KIND_HEAP) != 0);
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+
+ /*
+ * Ternaries carry no default, and parse_one_reloption() can
+ * only produce true or false, so PG_TERNARY_UNSET is already
+ * beyond a user's reach.
+ */
+ break;
+
+ case RELOPT_TYPE_INT:
+ {
+ relopt_int *optint = (relopt_int *) gen;
+
+ Assert(optint->default_val < optint->min ||
+ optint->default_val > optint->max);
+ break;
+ }
+
+ case RELOPT_TYPE_REAL:
+ {
+ relopt_real *optreal = (relopt_real *) gen;
+
+ Assert(optreal->default_val < optreal->min ||
+ optreal->default_val > optreal->max);
+ break;
+ }
+
+ case RELOPT_TYPE_ENUM:
+ {
+ relopt_enum *optenum = (relopt_enum *) gen;
+
+ for (relopt_enum_elt_def *elt = optenum->members;
+ elt->string_val; elt++)
+ Assert(elt->symbol_val != optenum->default_val);
+ break;
+ }
+
+ default:
+ /* Neither bools nor strings can express "unset". */
+ Assert(false);
+ }
+ }
+}
+#endif /* USE_ASSERT_CHECKING */
+
/*
* initialize_reloptions
* initialization routine, must be called before parsing
@@ -730,6 +802,10 @@ initialize_reloptions(void)
/* flag the work is complete */
need_initialization = false;
+
+#ifdef USE_ASSERT_CHECKING
+ assert_toast_defaults_unsettable();
+#endif
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 32efe967890..a99f7108636 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2832,7 +2832,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* defaults, autovacuum's own first and plain vacuum second.
*/
- /* -1 in autovac setting means use log_autovacuum_min_duration */
+ /* a negative autovac setting means use log_autovacuum_min_duration */
log_vacuum_min_duration = (avopts && avopts->log_vacuum_min_duration >= 0)
? avopts->log_vacuum_min_duration
: Log_autovacuum_min_duration;
--
2.50.1 (Apple Git-155)
[text/plain] v9-0006-Move-the-StdRdOptions-parse-table-to-file-scope.patch (7.8K, ../../anyqokNVrCsxt9Vp@nathan/7-v9-0006-Move-the-StdRdOptions-parse-table-to-file-scope.patch)
download | inline diff:
From 35116e0043b5e455fa7a4a0c2be1a82dde5079a2 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:56:02 -0500
Subject: [PATCH v9 6/8] Move the StdRdOptions parse table to file scope.
This is preparatory work for a follow-up commit that will walk the
table from another function. Nothing changes but the indentation
of its entries.
---
src/backend/access/common/reloptions.c | 115 +++++++++++++------------
1 file changed, 60 insertions(+), 55 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 02bc8213def..4548eb02676 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2044,69 +2044,74 @@ fillRelOptions(void *rdopts, Size basesize,
}
+/*
+ * Parse table for StdRdOptions, which is shared by the RELOPT_KIND_HEAP and
+ * RELOPT_KIND_TOAST kinds.
+ */
+static const relopt_parse_elt stdRdOptionsTab[] = {
+ {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
+ {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
+ {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
+ {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
+ {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
+ {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
+ {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
+ {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
+ {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
+ {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
+ {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
+ {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
+ {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
+ {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
+ {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
+ {"toast_tuple_target", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, toast_tuple_target)},
+ {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
+ {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
+ {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
+ {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"user_catalog_table", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, user_catalog_table)},
+ {"parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, parallel_workers)},
+ {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+ offsetof(StdRdOptions, vacuum_index_cleanup)},
+ {"vacuum_truncate", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, vacuum_truncate)},
+ {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
+};
+
/*
* Option parser for anything that uses StdRdOptions.
*/
bytea *
default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
- static const relopt_parse_elt tab[] = {
- {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
- {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
- {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
- {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
- {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
- {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
- {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
- {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
- {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
- {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
- {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
- {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
- {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
- {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
- {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
- {"toast_tuple_target", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, toast_tuple_target)},
- {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
- {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
- {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
- {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
- {"user_catalog_table", RELOPT_TYPE_BOOL,
- offsetof(StdRdOptions, user_catalog_table)},
- {"parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, parallel_workers)},
- {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
- offsetof(StdRdOptions, vacuum_index_cleanup)},
- {"vacuum_truncate", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, vacuum_truncate)},
- {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
- };
-
return (bytea *) build_reloptions(reloptions, validate, kind,
sizeof(StdRdOptions),
- tab, lengthof(tab));
+ stdRdOptionsTab,
+ lengthof(stdRdOptionsTab));
}
/*
--
2.50.1 (Apple Git-155)
[text/plain] v9-0007-Fix-VACUUM-s-handling-of-TOAST-storage-parameters.patch (13.0K, ../../anyqokNVrCsxt9Vp@nathan/8-v9-0007-Fix-VACUUM-s-handling-of-TOAST-storage-parameters.patch)
download | inline diff:
From 23b24fff0e854c2319031f60c4c64870c4488373 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 11:34:55 -0500
Subject: [PATCH v9 7/8] Fix VACUUM's handling of TOAST storage parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Presently, VACUUM does no such thing. It reads the TOAST table's
own reloptions, which hold only what was set through toast.*, so
vacuum_index_cleanup or vacuum_truncate set on the main table has
no effect on its TOAST table.
To fix, add merge_toast_reloptions(), which walks the parse table
for StdRdOptions and takes the main table's value for anything the
TOAST table left at its default. vacuum_rel() hands the main
table's parameters down when recursing to a TOAST table, and it
merges them into a copy of the TOAST table's parameters before the
values are used. This doesn't help VACUUM against a TOAST table
directly (e.g., "VACUUM pg_toast.pg_toast_5432"), but that's
probably okay because it's not the main supported way to vacuum a
TOAST table (see VACUUM's PROCESS_MAIN and PROCESS_TOAST options).
A follow-up commit will do the same for autovacuum. While this is
a bug fix, it's too intrusive for back-patching, but the issue
seems to have gone unnoticed for a very long time, anyway.
---
src/backend/access/common/reloptions.c | 93 +++++++++++++++++++
src/backend/commands/vacuum.c | 45 ++++++---
src/backend/postmaster/autovacuum.c | 1 +
src/include/access/reloptions.h | 2 +
src/include/commands/vacuum.h | 7 ++
.../injection_points/expected/vacuum.out | 11 +++
.../modules/injection_points/sql/vacuum.sql | 8 ++
7 files changed, 156 insertions(+), 11 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 4548eb02676..263ebf44a45 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2114,6 +2114,99 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
lengthof(stdRdOptionsTab));
}
+/*
+ * find reloption
+ * Look up a reloption of the given kind by name.
+ *
+ * Returns NULL if no such option can be set on relations of that kind. Note
+ * that names are unique only within a kind; "fillfactor", for example, is
+ * declared separately for heaps and for each index access method.
+ */
+static relopt_gen *
+find_reloption(const char *name, relopt_kind kind)
+{
+ if (need_initialization)
+ initialize_reloptions();
+
+ for (int i = 0; relOpts[i]; i++)
+ {
+ if ((relOpts[i]->kinds & kind) != 0 &&
+ strcmp(relOpts[i]->name, name) == 0)
+ return relOpts[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * merge_toast_reloptions
+ * Fill in a TOAST table's unset options from its main table's.
+ *
+ * Any option that may be set on a TOAST table but was not is taken from
+ * main_opts. Either argument may be NULL; if both are, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * An option counts as unset while it still holds the default declared for it
+ * above, which works because nothing a TOAST table accepts has a default the
+ * user could also set (see assert_toast_defaults_unsettable()).
+ *
+ * NB: This destructively modifies toast_opts, and what it returns may be
+ * either argument, so the caller must know which of the two it owns.
+ */
+StdRdOptions *
+merge_toast_reloptions(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ if (toast_opts == NULL)
+ return main_opts;
+ if (main_opts == NULL)
+ return toast_opts;
+
+ for (int i = 0; i < lengthof(stdRdOptionsTab); i++)
+ {
+ const relopt_parse_elt *elem = &stdRdOptionsTab[i];
+ relopt_gen *gen;
+ char *toast_val;
+ char *main_val;
+
+ /* Skip anything that cannot be set on a TOAST table. */
+ gen = find_reloption(elem->optname, RELOPT_KIND_TOAST);
+ if (gen == NULL)
+ continue;
+
+ toast_val = (char *) toast_opts + elem->offset;
+ main_val = (char *) main_opts + elem->offset;
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+ if (*(pg_ternary *) toast_val == PG_TERNARY_UNSET)
+ *(pg_ternary *) toast_val = *(pg_ternary *) main_val;
+ break;
+
+ case RELOPT_TYPE_INT:
+ if (*(int *) toast_val == ((relopt_int *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ case RELOPT_TYPE_REAL:
+ if (*(double *) toast_val == ((relopt_real *) gen)->default_val)
+ *(double *) toast_val = *(double *) main_val;
+ break;
+
+ case RELOPT_TYPE_ENUM:
+ if (*(int *) toast_val == ((relopt_enum *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ default:
+ elog(ERROR, "reloption \"%s\" has a type a TOAST table cannot inherit",
+ elem->optname);
+ }
+ }
+
+ return toast_opts;
+}
+
/*
* build_reloptions
*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 52116c02b59..8fe48920c9f 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -31,6 +31,7 @@
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
+#include "access/reloptions.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
@@ -187,6 +188,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_relopts = NULL;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2039,6 +2041,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
int save_sec_context;
int save_nestlevel;
VacuumParams toast_vacuum_params;
+ StdRdOptions *relopts;
+ StdRdOptions relopts_copy;
/*
* This function scribbles on the parameters, so make a copy early to
@@ -2201,6 +2205,19 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ /*
+ * A TOAST table takes any storage parameter it accepts but does not set
+ * from its main table, whose parameters the caller handed down for that
+ * purpose. Merge them into a copy of our own.
+ *
+ * We are careful to avoid scribbling on the relcache's copy of the
+ * options.
+ */
+ if (rel->rd_options)
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ relopts = merge_toast_reloptions(rel->rd_options ? &relopts_copy : NULL,
+ params.main_relopts);
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2209,11 +2226,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
StdRdOptIndexCleanup vacuum_index_cleanup;
- if (rel->rd_options == NULL)
+ if (relopts == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = relopts->vacuum_index_cleanup;
switch (vacuum_index_cleanup)
{
@@ -2245,10 +2261,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
- params.max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ if (relopts != NULL && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ params.max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
@@ -2256,11 +2270,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
- StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
-
- if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
+ if (relopts && relopts->vacuum_truncate != PG_TERNARY_UNSET)
{
- if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ if (relopts->vacuum_truncate == PG_TERNARY_TRUE)
params.truncate = VACOPTVALUE_ENABLED;
else
params.truncate = VACOPTVALUE_DISABLED;
@@ -2293,6 +2305,17 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
else
toast_relid = InvalidOid;
+ /*
+ * Hand our storage parameters down for the TOAST table to inherit. Take
+ * a copy while we still have the relation open; the relcache entry can go
+ * away once we close it.
+ */
+ if (OidIsValid(toast_relid) && rel->rd_options)
+ {
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ toast_vacuum_params.main_relopts = &relopts_copy;
+ }
+
/*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also lock down security-restricted operations and
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a99f7108636..1610c60ec4b 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2891,6 +2891,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ tab->at_params.main_relopts = NULL;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..6c599382f02 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -247,6 +247,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options,
extern bytea *default_reloptions(Datum reloptions, bool validate,
relopt_kind kind);
+extern struct StdRdOptions *merge_toast_reloptions(struct StdRdOptions *toast_opts,
+ struct StdRdOptions *main_opts);
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..2ebdd5e40ab 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,13 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * When vacuuming a TOAST table, its main table's storage parameters, for
+ * the TOAST table to inherit anything it doesn't set itself. NULL if the
+ * main table has none, or if this isn't a TOAST table.
+ */
+ struct StdRdOptions *main_relopts;
} VacuumParams;
/*
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..caf0cc232b4 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,9 +79,20 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..0a43e14c928 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,9 +33,17 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
--
2.50.1 (Apple Git-155)
[text/plain] v9-0008-Fix-autovacuum-s-handling-of-TOAST-storage-parame.patch (12.1K, ../../anyqokNVrCsxt9Vp@nathan/9-v9-0008-Fix-autovacuum-s-handling-of-TOAST-storage-parame.patch)
download | inline diff:
From 93ebc91c56443e8f6394f6cbefd24ce741b00431 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 12:02:21 -0500
Subject: [PATCH v9 8/8] Fix autovacuum's handling of TOAST storage parameters.
The previous commit made VACUUM apply a main table's storage
parameters to its TOAST table, as CREATE TABLE has long documented.
Autovacuum still gets this wrong in two ways. It falls back to the
main table's autovacuum parameters only when the TOAST table has no
reloptions at all, so setting a single toast.* parameter silently
discards the rest. And it never consults the main table for the
parameters that only VACUUM reads, since it leaves those for
vacuum_rel() to resolve from the TOAST table's own reloptions.
To fix, combine the two sets with merge_toast_reloptions() rather
than choosing between them, and hand the main table's parameters
down to vacuum_rel() the way VACUUM now does.
pg_stat_autovacuum_scores uses the combined parameters for TOAST
tables as well; it has to collect the main relations' parameters
before it can do so, so it now makes a preliminary pass over
pg_class.
An existing shortcoming that this patch only makes worse is that
autovacuum remains oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/postmaster/autovacuum.c | 110 ++++++++++++++----
.../injection_points/expected/vacuum.out | 22 +++-
.../modules/injection_points/sql/vacuum.sql | 12 +-
3 files changed, 117 insertions(+), 27 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 1610c60ec4b..a67f2de209c 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2015,9 +2015,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2133,6 +2133,8 @@ do_autovacuum(void)
bool doanalyze;
bool wraparound;
AutoVacuumScores scores;
+ av_relation *hentry;
+ bool found;
/*
* We cannot safely process other backends' temp tables, so skip 'em.
@@ -2143,21 +2145,14 @@ do_autovacuum(void)
relid = classForm->oid;
/*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
+ * fetch reloptions -- merge any unset options from the main rel
*/
relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found)
+ relopts = merge_toast_reloptions(relopts, &hentry->ar_reloptions);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2782,6 +2777,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
autovac_table *tab = NULL;
bool wraparound;
StdRdOptions *relopts;
+ StdRdOptions *main_relopts = NULL;
bool free_relopts = false;
AutoVacuumScores scores;
@@ -2792,20 +2788,23 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE)
+ if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found)
- relopts = &hentry->ar_reloptions;
+ {
+ main_relopts = &hentry->ar_reloptions;
+ relopts = merge_toast_reloptions(relopts, main_relopts);
+ }
}
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2891,7 +2890,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
- tab->at_params.main_relopts = NULL;
+ tab->at_params.main_relopts = main_relopts;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
@@ -2950,8 +2949,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* being forced because of Xid or multixact wraparound.
*
* relopts is a pointer to the StdRdOptions options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * case of a plain table, or merged with the main table's for a TOAST table),
+ * NULL if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3609,6 +3608,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
TableScanDesc scan;
HeapTuple tup;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
InitMaterializedSRF(fcinfo, 0);
@@ -3617,13 +3618,62 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
rel = table_open(RelationRelationId, AccessShareLock);
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parents' reloptions, we can generate the results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ bool free_relopts = false;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3640,12 +3690,25 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
continue;
relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (relopts)
+ free_relopts = true;
+ if (form->relkind == RELKIND_TOASTVALUE)
+ {
+ av_relation *hentry;
+ bool found;
+
+ hentry = hash_search(table_toast_map, &form->oid,
+ HASH_FIND, &found);
+ if (found)
+ relopts = merge_toast_reloptions(relopts, &hentry->ar_reloptions);
+ }
+
relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (relopts)
+ if (free_relopts)
pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
@@ -3663,6 +3726,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index caf0cc232b4..196be88c6b8 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -80,15 +80,33 @@ NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
-- A TOAST table inherits what it does not set from its main table.
-CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
(autovacuum_enabled=false,
vacuum_index_cleanup=false,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_truncate=false, toast.vacuum_truncate=true,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0);
VACUUM vac_tab_toast_inherit;
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-disabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-enabled
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
+ over
+------
+ t
+(1 row)
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 0a43e14c928..dc5f25dbb58 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -34,11 +34,19 @@ VACUUM vac_tab_auto;
RESET vacuum_truncate;
-- A TOAST table inherits what it does not set from its main table.
-CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
(autovacuum_enabled=false,
vacuum_index_cleanup=false,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_truncate=false, toast.vacuum_truncate=true,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0);
VACUUM vac_tab_toast_inherit;
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-12 18:33 Sami Imseih <samimseih@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Sami Imseih @ 2026-08-12 18:33 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: Greg Burd <greg@burd.me>; solai v <solai.cdac@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
> After sleeping on this, I realized that we can simplify things a bit by
> using merge_toast_reloptions() for manual VACUUM, too. I've added a new
> 0007 for that. The autovacuum fix now lives in 0008.
Makes sense to me. A few comments:
1/ 0007 adds merge_toast_reloptions(), and for each option it calls
find_reloption() to get the type and default. find_reloption() scans the entire
relOpts[]. The options don't change, so it's just repeating the same work
every time, which also includes strcmp to match the options by name.
find_reloption() already runs initialize_reloptions(), so could
initialize_reloptions()
also build a small table of the options a toast table inherits from
its main table.
merge_toast_reloptions() then just iterates that table and no longer needs to
call find_reloption() in the scan.
This would help most when pg_stat_autovacuum_scores calls
merge_toast_reloptions()
for every toast table in its list, which occurs in 0008.
2/ There are three spots that repeat the work for a toast table,
extractRelOptions() followed by a table_toast_map lookup
and a merge_toast_reloptions().
For example, when do_autovacuum() performs the pass on the toast
tables:
```
/*
- * fetch reloptions -- if this toast table does not
have them, try the
- * main rel
+ * fetch reloptions -- merge any unset options from the main rel
*/
relopts = (StdRdOptions *) extractRelOptions(tuple,
pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid,
HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ hentry = hash_search(table_toast_map, &relid,
HASH_FIND, &found);
+ if (found)
+ relopts = merge_toast_reloptions(relopts,
&hentry->ar_reloptions);
```
as well as table_recheck_autovac() and pg_stat_get_autovacuum_scores().
The job here is to find the "effective" toast options, and it might be
worthwhile
to turn this into a single helper for clarity.
--
Sami Imseih
Amazon Web Services (AWS)
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-12 21:54 Nathan Bossart <nathandbossart@gmail.com>
parent: Sami Imseih <samimseih@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2026-08-12 21:54 UTC (permalink / raw)
To: Sami Imseih <samimseih@gmail.com>; +Cc: Greg Burd <greg@burd.me>; solai v <solai.cdac@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
On Wed, Aug 12, 2026 at 01:33:20PM -0500, Sami Imseih wrote:
> 1/ 0007 adds merge_toast_reloptions(), and for each option it calls
> find_reloption() to get the type and default. find_reloption() scans the
> entire relOpts[]. The options don't change, so it's just repeating the
> same work every time, which also includes strcmp to match the options by
> name.
>
> find_reloption() already runs initialize_reloptions(), so could
> initialize_reloptions() also build a small table of the options a toast
> table inherits from its main table. merge_toast_reloptions() then just
> iterates that table and no longer needs to call find_reloption() in the
> scan.
>
> This would help most when pg_stat_autovacuum_scores calls
> merge_toast_reloptions() for every toast table in its list, which occurs
> in 0008.
I considered this, but AFAICT that extra work actually pales in comparison
to the sequential scan of pg_class.
> 2/ There are three spots that repeat the work for a toast table,
> extractRelOptions() followed by a table_toast_map lookup and a
> merge_toast_reloptions(). For example, when do_autovacuum() performs the
> pass on the toast tables:
>
> [...]
>
> as well as table_recheck_autovac() and pg_stat_get_autovacuum_scores().
>
> The job here is to find the "effective" toast options, and it might be
> worthwhile to turn this into a single helper for clarity.
Done in v10.
--
nathan
From 6dc41b70451dd36bf54029fff9937b6581fad16d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:35:34 -0500
Subject: [PATCH v10 1/8] Remove extract_autovac_opts().
extract_autovac_opts() returned a palloc'd copy of only the
AutoVacOpts portion of a relation's reloptions. Upcoming work
needs the rest of the StdRdOptions as well, so the callers must
keep the whole struct around.
Remove the helper and have the callers obtain reloptions from
extractRelOptions() directly. av_relation now caches a
StdRdOptions instead of an AutoVacOpts, and
relation_needs_vacanalyze() takes a StdRdOptions and extracts the
autovacuum portion itself. This is preparatory refactoring with no
change in behavior.
---
src/backend/postmaster/autovacuum.c | 122 ++++++++++------------------
1 file changed, 44 insertions(+), 78 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index ee202a4b47e..db1f5d0e575 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -202,8 +202,7 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -379,7 +378,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -388,8 +387,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2037,7 +2034,7 @@ do_autovacuum(void)
while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2076,7 +2073,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2118,7 +2115,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2141,7 +2138,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2160,7 +2157,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2776,39 +2773,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc_object(AutoVacOpts);
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2828,8 +2792,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2842,9 +2806,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2853,10 +2817,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- relation_needs_vacanalyze(relid, avopts, classForm,
+ relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
DEBUG3,
&dovacuum, &doanalyze, &wraparound,
@@ -2871,6 +2835,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_vacuum_min_duration;
int log_analyze_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2982,8 +2947,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2995,7 +2960,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none.
*
@@ -3072,7 +3037,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -3086,6 +3051,7 @@ relation_needs_vacanalyze(Oid relid,
bool force_vacuum;
bool av_enabled;
bool may_free = false;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3137,45 +3103,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
@@ -3668,7 +3634,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
- AutoVacOpts *avopts;
+ StdRdOptions *relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3684,14 +3650,14 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
- relation_needs_vacanalyze(form->oid, avopts, form,
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
- pfree(avopts);
+ if (relopts)
+ pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
vals[1] = Float8GetDatum(scores.max);
--
2.50.1 (Apple Git-155)
From 50d310fabfc91e80e1d04c63a82e5677ab72477d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:50:36 -0500
Subject: [PATCH v10 2/8] Make autovacuum_enabled a ternary reloption.
This commit reimplements autovacuum_enabled as a ternary, using the
support added in commit 4d6a66f675 and following the example of
vacuum_truncate. This changes only the internal representation: an
unset value still behaves as enabled, and the option accepts the
same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 19 +++++++++----------
src/backend/catalog/index.c | 3 ++-
src/backend/postmaster/autovacuum.c | 2 +-
src/include/utils/rel.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..79834126f2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -107,15 +107,6 @@ static relopt_bool boolRelOpts[] =
},
false
},
- {
- {
- "autovacuum_enabled",
- "Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
- ShareUpdateExclusiveLock
- },
- true
- },
{
{
"user_catalog_table",
@@ -168,6 +159,14 @@ static relopt_bool boolRelOpts[] =
static relopt_ternary ternaryRelOpts[] =
{
+ {
+ {
+ "autovacuum_enabled",
+ "Enables autovacuum in this relation",
+ RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ ShareUpdateExclusiveLock
+ }
+ },
{
{
"vacuum_truncate",
@@ -1976,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
static const relopt_parse_elt tab[] = {
{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_BOOL,
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
{"autovacuum_parallel_workers", RELOPT_TYPE_INT,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 4c5da7e5db0..ec21b83b6b8 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2900,7 +2900,8 @@ index_update_stats(Relation rel,
{
StdRdOptions *options = (StdRdOptions *) rel->rd_options;
- if (options != NULL && !options->autovacuum.enabled)
+ if (options != NULL &&
+ options->autovacuum.enabled == PG_TERNARY_FALSE)
update_stats = false;
}
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index db1f5d0e575..c8a1b66ca2e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3141,7 +3141,7 @@ relation_needs_vacanalyze(Oid relid,
? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 89c159b133f..2ee98e9c6cc 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -310,7 +310,7 @@ typedef struct ForeignKeyCacheInfo
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
{
- bool enabled;
+ pg_ternary enabled;
int autovacuum_parallel_workers;
int vacuum_threshold;
--
2.50.1 (Apple Git-155)
From c8212728cefc3053f4b64c19002966a31b16d81d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 15:27:58 -0500
Subject: [PATCH v10 3/8] Add an "unset" value for vacuum_index_cleanup.
This commit adds a new value to StdRdOptIndexCleanup to distinguish
whether it is explicitly set, similar to ViewOptCheckOption's
VIEW_OPTION_CHECK_OPTION_NOT_SET. This changes only the internal
representation; an unset value still defaults to AUTO, and the
option accepts the same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 3 ++-
src/backend/commands/vacuum.c | 23 ++++++++++++++---------
src/include/utils/rel.h | 1 +
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79834126f2f..58eb72e2339 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -517,6 +517,7 @@ static relopt_real realRelOpts[] =
/* values from StdRdOptIndexCleanup */
static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
{
+ /* no value for NOT_SET */
{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
@@ -557,7 +558,7 @@ static relopt_enum enumRelOpts[] =
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
},
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 31f9824899c..52116c02b59 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2210,20 +2210,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
StdRdOptIndexCleanup vacuum_index_cleanup;
if (rel->rd_options == NULL)
- vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+ vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
vacuum_index_cleanup =
((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
- if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
- params.index_cleanup = VACOPTVALUE_AUTO;
- else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
- params.index_cleanup = VACOPTVALUE_ENABLED;
- else
+ switch (vacuum_index_cleanup)
{
- Assert(vacuum_index_cleanup ==
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
- params.index_cleanup = VACOPTVALUE_DISABLED;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ params.index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ params.index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
}
}
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 2ee98e9c6cc..01b6e1a872c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,6 +338,7 @@ typedef enum StdRdOptIndexCleanup
STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
} StdRdOptIndexCleanup;
typedef struct StdRdOptions
--
2.50.1 (Apple Git-155)
From f1b3a6544b8562120764ebf0ffff4cb8a87570e5 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 7 Aug 2026 10:55:34 -0500
Subject: [PATCH v10 4/8] Simplify autovacuum's TOAST-to-main-relation
reloptions map.
do_autovacuum() adds an entry to this map for every relation that
has a TOAST table, and uses a flag to mark the entries that have no
reloptions to pass down. The unconditional entry made sense back
when the payload was the main relation's OID: commit 7d4c9a5793
added the map so that the TOAST pass could find the parent, whose
pg_autovacuum row supplied the settings for a TOAST table that had
none of its own. Commit 834a6da4f7 replaced that lookup by copying
the parent's reloptions into the entry, which left the OID unread
and called for the flag, since a by-value AutoVacOpts cannot say
"not set".
An entry is now worth creating only when there is something to
inherit, so skip the relations that have no reloptions and let a
successful lookup speak for itself. Two relations cannot share a
TOAST table, and each pass sees a single catalog snapshot, so an
insertion can never find an existing entry; assert that rather than
quietly ignoring it.
While at it, remove two more leftovers of that same conversion:
ar_relid, unread ever since, and a NULL test on table_toast_map in
table_recheck_autovac(), which used to carry the relkind test for
get_pg_autovacuum_tuple_relid() and has had no NULL to catch since
that function went away.
---
src/backend/postmaster/autovacuum.c | 26 +++++++-------------------
1 file changed, 7 insertions(+), 19 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index c8a1b66ca2e..32efe967890 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -200,8 +200,6 @@ typedef struct avw_dbase
typedef struct av_relation
{
Oid ar_toastrelid; /* hash key - must be first */
- Oid ar_relid;
- bool ar_hasrelopts;
StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
@@ -2097,7 +2095,7 @@ do_autovacuum(void)
* this whether or not the table is going to be vacuumed, because we
* don't automatically vacuum toast tables along the parent table.
*/
- if (OidIsValid(classForm->reltoastrelid))
+ if (OidIsValid(classForm->reltoastrelid) && relopts)
{
av_relation *hentry;
bool found;
@@ -2105,19 +2103,10 @@ do_autovacuum(void)
hentry = hash_search(table_toast_map,
&classForm->reltoastrelid,
HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
- if (!found)
- {
- /* hash_search already filled in the key */
- hentry->ar_relid = relid;
- hentry->ar_hasrelopts = false;
- if (relopts != NULL)
- {
- hentry->ar_hasrelopts = true;
- memcpy(&hentry->ar_reloptions, relopts,
- sizeof(StdRdOptions));
- }
- }
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
}
/* Release stuff to avoid per-relation leakage */
@@ -2166,7 +2155,7 @@ do_autovacuum(void)
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
@@ -2809,14 +2798,13 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ else if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
--
2.50.1 (Apple Git-155)
From 935e0d3023fd089f8b9ccbefe60ea369c4865c18 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:55:44 -0500
Subject: [PATCH v10 5/8] Give TOAST storage parameters unsettable defaults.
This is preparatory work for a follow-up commit that will fill in a
TOAST table's unset storage parameters from its main table's. For
that, it must be possible to tell an option nobody set from one the
user set to the value that option happens to default to. The
parsed form of a relation's options has nowhere to record which
ones were specified, so an unset option is simply one still holding
its declared default.
Require, then, that any option a TOAST table accepts default to a
value the user cannot set. Ternaries already comply, having no
default at all, as does vacuum_index_cleanup, whose "not set"
member has no spelling. Among the numeric ones only
log_autovacuum_min_duration was in violation, defaulting to -1 with
a minimum of -1, so give it -2 instead, matching
autovacuum_vacuum_max_threshold and
autovacuum_vacuum_insert_threshold. Users won't notice; -1 already
behaved exactly as leaving the option unset does, and it still
does.
An assertion in initialize_reloptions() enforces both of the rules
the follow-up commit will rely on: that the default is unsettable,
and that anything settable on a TOAST table is settable on a heap,
since the inherited value is read from a main table's options at
the same offset. Note that this rules out bool and string options,
neither of which can express "unset".
---
src/backend/access/common/reloptions.c | 78 +++++++++++++++++++++++++-
src/backend/postmaster/autovacuum.c | 2 +-
2 files changed, 78 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 58eb72e2339..02bc8213def 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
},
{
{
@@ -613,6 +613,78 @@ static void parse_one_reloption(relopt_value *option, char *text_str,
((option).isset ? strlen((option).string_val) : \
((relopt_string *) (option).gen)->default_len)
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Verify that every option a TOAST table accepts defaults to a value the user
+ * cannot set. Nothing records which options were specified, so an option
+ * still holding its default is the only way to recognize one that was never
+ * set, and that is how a TOAST table tells which values it should take from
+ * its main table.
+ */
+static void
+assert_toast_defaults_unsettable(void)
+{
+ for (int i = 0; relOpts[i]; i++)
+ {
+ relopt_gen *gen = relOpts[i];
+
+ if ((gen->kinds & RELOPT_KIND_TOAST) == 0)
+ continue;
+
+ /*
+ * A TOAST table's value is filled in from its main table's at the
+ * same offset in the same struct, so the option must be settable on a
+ * heap too.
+ */
+ Assert((gen->kinds & RELOPT_KIND_HEAP) != 0);
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+
+ /*
+ * Ternaries carry no default, and parse_one_reloption() can
+ * only produce true or false, so PG_TERNARY_UNSET is already
+ * beyond a user's reach.
+ */
+ break;
+
+ case RELOPT_TYPE_INT:
+ {
+ relopt_int *optint = (relopt_int *) gen;
+
+ Assert(optint->default_val < optint->min ||
+ optint->default_val > optint->max);
+ break;
+ }
+
+ case RELOPT_TYPE_REAL:
+ {
+ relopt_real *optreal = (relopt_real *) gen;
+
+ Assert(optreal->default_val < optreal->min ||
+ optreal->default_val > optreal->max);
+ break;
+ }
+
+ case RELOPT_TYPE_ENUM:
+ {
+ relopt_enum *optenum = (relopt_enum *) gen;
+
+ for (relopt_enum_elt_def *elt = optenum->members;
+ elt->string_val; elt++)
+ Assert(elt->symbol_val != optenum->default_val);
+ break;
+ }
+
+ default:
+ /* Neither bools nor strings can express "unset". */
+ Assert(false);
+ }
+ }
+}
+#endif /* USE_ASSERT_CHECKING */
+
/*
* initialize_reloptions
* initialization routine, must be called before parsing
@@ -730,6 +802,10 @@ initialize_reloptions(void)
/* flag the work is complete */
need_initialization = false;
+
+#ifdef USE_ASSERT_CHECKING
+ assert_toast_defaults_unsettable();
+#endif
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 32efe967890..a99f7108636 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2832,7 +2832,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* defaults, autovacuum's own first and plain vacuum second.
*/
- /* -1 in autovac setting means use log_autovacuum_min_duration */
+ /* a negative autovac setting means use log_autovacuum_min_duration */
log_vacuum_min_duration = (avopts && avopts->log_vacuum_min_duration >= 0)
? avopts->log_vacuum_min_duration
: Log_autovacuum_min_duration;
--
2.50.1 (Apple Git-155)
From da1e24c8125ff931c4161a26c30fc6d2ae09c85d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:56:02 -0500
Subject: [PATCH v10 6/8] Move the StdRdOptions parse table to file scope.
This is preparatory work for a follow-up commit that will walk the
table from another function. Nothing changes but the indentation
of its entries.
---
src/backend/access/common/reloptions.c | 115 +++++++++++++------------
1 file changed, 60 insertions(+), 55 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 02bc8213def..4548eb02676 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2044,69 +2044,74 @@ fillRelOptions(void *rdopts, Size basesize,
}
+/*
+ * Parse table for StdRdOptions, which is shared by the RELOPT_KIND_HEAP and
+ * RELOPT_KIND_TOAST kinds.
+ */
+static const relopt_parse_elt stdRdOptionsTab[] = {
+ {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
+ {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
+ {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
+ {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
+ {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
+ {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
+ {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
+ {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
+ {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
+ {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
+ {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
+ {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
+ {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
+ {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
+ {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
+ {"toast_tuple_target", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, toast_tuple_target)},
+ {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
+ {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
+ {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
+ {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"user_catalog_table", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, user_catalog_table)},
+ {"parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, parallel_workers)},
+ {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+ offsetof(StdRdOptions, vacuum_index_cleanup)},
+ {"vacuum_truncate", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, vacuum_truncate)},
+ {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
+};
+
/*
* Option parser for anything that uses StdRdOptions.
*/
bytea *
default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
- static const relopt_parse_elt tab[] = {
- {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
- {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
- {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
- {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
- {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
- {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
- {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
- {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
- {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
- {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
- {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
- {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
- {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
- {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
- {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
- {"toast_tuple_target", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, toast_tuple_target)},
- {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
- {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
- {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
- {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
- {"user_catalog_table", RELOPT_TYPE_BOOL,
- offsetof(StdRdOptions, user_catalog_table)},
- {"parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, parallel_workers)},
- {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
- offsetof(StdRdOptions, vacuum_index_cleanup)},
- {"vacuum_truncate", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, vacuum_truncate)},
- {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
- };
-
return (bytea *) build_reloptions(reloptions, validate, kind,
sizeof(StdRdOptions),
- tab, lengthof(tab));
+ stdRdOptionsTab,
+ lengthof(stdRdOptionsTab));
}
/*
--
2.50.1 (Apple Git-155)
From 3410dc14e074aae2f7dc3d21545a5699fcddbbd6 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 11:34:55 -0500
Subject: [PATCH v10 7/8] Fix VACUUM's handling of TOAST storage parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Presently, VACUUM does no such thing. It reads the TOAST table's
own reloptions, which hold only what was set through toast.*, so
vacuum_index_cleanup or vacuum_truncate set on the main table has
no effect on its TOAST table.
To fix, add merge_toast_reloptions(), which walks the parse table
for StdRdOptions and takes the main table's value for anything the
TOAST table left at its default. vacuum_rel() hands the main
table's parameters down when recursing to a TOAST table, and it
merges them into a copy of the TOAST table's parameters before the
values are used. This doesn't help VACUUM against a TOAST table
directly (e.g., "VACUUM pg_toast.pg_toast_5432"), but that's
probably okay because it's not the main supported way to vacuum a
TOAST table (see VACUUM's PROCESS_MAIN and PROCESS_TOAST options).
A follow-up commit will do the same for autovacuum. While this is
a bug fix, it's too intrusive for back-patching, but the issue
seems to have gone unnoticed for a very long time, anyway.
---
src/backend/access/common/reloptions.c | 93 +++++++++++++++++++
src/backend/commands/vacuum.c | 45 ++++++---
src/backend/postmaster/autovacuum.c | 1 +
src/include/access/reloptions.h | 2 +
src/include/commands/vacuum.h | 7 ++
.../injection_points/expected/vacuum.out | 11 +++
.../modules/injection_points/sql/vacuum.sql | 8 ++
7 files changed, 156 insertions(+), 11 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 4548eb02676..263ebf44a45 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2114,6 +2114,99 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
lengthof(stdRdOptionsTab));
}
+/*
+ * find reloption
+ * Look up a reloption of the given kind by name.
+ *
+ * Returns NULL if no such option can be set on relations of that kind. Note
+ * that names are unique only within a kind; "fillfactor", for example, is
+ * declared separately for heaps and for each index access method.
+ */
+static relopt_gen *
+find_reloption(const char *name, relopt_kind kind)
+{
+ if (need_initialization)
+ initialize_reloptions();
+
+ for (int i = 0; relOpts[i]; i++)
+ {
+ if ((relOpts[i]->kinds & kind) != 0 &&
+ strcmp(relOpts[i]->name, name) == 0)
+ return relOpts[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * merge_toast_reloptions
+ * Fill in a TOAST table's unset options from its main table's.
+ *
+ * Any option that may be set on a TOAST table but was not is taken from
+ * main_opts. Either argument may be NULL; if both are, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * An option counts as unset while it still holds the default declared for it
+ * above, which works because nothing a TOAST table accepts has a default the
+ * user could also set (see assert_toast_defaults_unsettable()).
+ *
+ * NB: This destructively modifies toast_opts, and what it returns may be
+ * either argument, so the caller must know which of the two it owns.
+ */
+StdRdOptions *
+merge_toast_reloptions(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ if (toast_opts == NULL)
+ return main_opts;
+ if (main_opts == NULL)
+ return toast_opts;
+
+ for (int i = 0; i < lengthof(stdRdOptionsTab); i++)
+ {
+ const relopt_parse_elt *elem = &stdRdOptionsTab[i];
+ relopt_gen *gen;
+ char *toast_val;
+ char *main_val;
+
+ /* Skip anything that cannot be set on a TOAST table. */
+ gen = find_reloption(elem->optname, RELOPT_KIND_TOAST);
+ if (gen == NULL)
+ continue;
+
+ toast_val = (char *) toast_opts + elem->offset;
+ main_val = (char *) main_opts + elem->offset;
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+ if (*(pg_ternary *) toast_val == PG_TERNARY_UNSET)
+ *(pg_ternary *) toast_val = *(pg_ternary *) main_val;
+ break;
+
+ case RELOPT_TYPE_INT:
+ if (*(int *) toast_val == ((relopt_int *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ case RELOPT_TYPE_REAL:
+ if (*(double *) toast_val == ((relopt_real *) gen)->default_val)
+ *(double *) toast_val = *(double *) main_val;
+ break;
+
+ case RELOPT_TYPE_ENUM:
+ if (*(int *) toast_val == ((relopt_enum *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ default:
+ elog(ERROR, "reloption \"%s\" has a type a TOAST table cannot inherit",
+ elem->optname);
+ }
+ }
+
+ return toast_opts;
+}
+
/*
* build_reloptions
*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 52116c02b59..8fe48920c9f 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -31,6 +31,7 @@
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
+#include "access/reloptions.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
@@ -187,6 +188,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_relopts = NULL;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2039,6 +2041,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
int save_sec_context;
int save_nestlevel;
VacuumParams toast_vacuum_params;
+ StdRdOptions *relopts;
+ StdRdOptions relopts_copy;
/*
* This function scribbles on the parameters, so make a copy early to
@@ -2201,6 +2205,19 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ /*
+ * A TOAST table takes any storage parameter it accepts but does not set
+ * from its main table, whose parameters the caller handed down for that
+ * purpose. Merge them into a copy of our own.
+ *
+ * We are careful to avoid scribbling on the relcache's copy of the
+ * options.
+ */
+ if (rel->rd_options)
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ relopts = merge_toast_reloptions(rel->rd_options ? &relopts_copy : NULL,
+ params.main_relopts);
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2209,11 +2226,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
StdRdOptIndexCleanup vacuum_index_cleanup;
- if (rel->rd_options == NULL)
+ if (relopts == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = relopts->vacuum_index_cleanup;
switch (vacuum_index_cleanup)
{
@@ -2245,10 +2261,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
- params.max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ if (relopts != NULL && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ params.max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
@@ -2256,11 +2270,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
- StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
-
- if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
+ if (relopts && relopts->vacuum_truncate != PG_TERNARY_UNSET)
{
- if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ if (relopts->vacuum_truncate == PG_TERNARY_TRUE)
params.truncate = VACOPTVALUE_ENABLED;
else
params.truncate = VACOPTVALUE_DISABLED;
@@ -2293,6 +2305,17 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
else
toast_relid = InvalidOid;
+ /*
+ * Hand our storage parameters down for the TOAST table to inherit. Take
+ * a copy while we still have the relation open; the relcache entry can go
+ * away once we close it.
+ */
+ if (OidIsValid(toast_relid) && rel->rd_options)
+ {
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ toast_vacuum_params.main_relopts = &relopts_copy;
+ }
+
/*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also lock down security-restricted operations and
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a99f7108636..1610c60ec4b 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2891,6 +2891,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ tab->at_params.main_relopts = NULL;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..6c599382f02 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -247,6 +247,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options,
extern bytea *default_reloptions(Datum reloptions, bool validate,
relopt_kind kind);
+extern struct StdRdOptions *merge_toast_reloptions(struct StdRdOptions *toast_opts,
+ struct StdRdOptions *main_opts);
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..2ebdd5e40ab 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,13 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * When vacuuming a TOAST table, its main table's storage parameters, for
+ * the TOAST table to inherit anything it doesn't set itself. NULL if the
+ * main table has none, or if this isn't a TOAST table.
+ */
+ struct StdRdOptions *main_relopts;
} VacuumParams;
/*
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..caf0cc232b4 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,9 +79,20 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..0a43e14c928 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,9 +33,17 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
--
2.50.1 (Apple Git-155)
From 7bebf3e0720875ea970a17ae8e360f29fb83e9ac Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 12:02:21 -0500
Subject: [PATCH v10 8/8] Fix autovacuum's handling of TOAST storage
parameters.
The previous commit made VACUUM apply a main table's storage
parameters to its TOAST table, as CREATE TABLE has long documented.
Autovacuum still gets this wrong in two ways. It falls back to the
main table's autovacuum parameters only when the TOAST table has no
reloptions at all, so setting a single toast.* parameter silently
discards the rest. And it never consults the main table for the
parameters that only VACUUM reads, since it leaves those for
vacuum_rel() to resolve from the TOAST table's own reloptions.
To fix, combine the two sets with merge_toast_reloptions() rather
than choosing between them, and hand the main table's parameters
down to vacuum_rel() the way VACUUM now does.
pg_stat_autovacuum_scores uses the combined parameters for TOAST
tables as well; it has to collect the main relations' parameters
before it can do so, so it now makes a preliminary pass over
pg_class.
An existing shortcoming that this patch only makes worse is that
autovacuum remains oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/postmaster/autovacuum.c | 147 +++++++++++++-----
.../injection_points/expected/vacuum.out | 22 ++-
.../modules/injection_points/sql/vacuum.sql | 12 +-
3 files changed, 136 insertions(+), 45 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 1610c60ec4b..a7e97b880f5 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1913,6 +1913,36 @@ TableToProcessComparator(const ListCell *a, const ListCell *b)
return (t2->score < t1->score) ? -1 : (t2->score > t1->score) ? 1 : 0;
}
+/*
+ * get_effective_relopts
+ * Fetch the storage parameters that apply to a relation.
+ *
+ * This looks up and, for TOAST tables, merges reloptions. Returns NULL if the
+ * relation neither sets nor inherits anything. main_opts returns main table's
+ * reloptions, if any. free_relopts returns whether return value is palloc'd.
+ */
+static StdRdOptions *
+get_effective_relopts(HeapTuple tup, TupleDesc desc, HTAB *toast_map,
+ StdRdOptions **main_opts, bool *free_relopts)
+{
+ Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found = false;
+
+ /* look up our relopts */
+ relopts = (StdRdOptions *) extractRelOptions(tup, desc, NULL);
+ *free_relopts = (relopts != NULL);
+
+ /* if we're a TOAST table, look up our parent's relopts, too */
+ if (classForm->relkind == RELKIND_TOASTVALUE)
+ hentry = hash_search(toast_map, &classForm->oid, HASH_FIND, &found);
+ *main_opts = found ? &hentry->ar_reloptions : NULL;
+
+ /* return the merged reloptions */
+ return merge_toast_reloptions(relopts, *main_opts);
+}
+
/*
* Process a database table-by-table
*
@@ -2015,9 +2045,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2128,7 +2158,8 @@ do_autovacuum(void)
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
+ bool free_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2142,22 +2173,9 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ /* fetch reloptions -- merge any unset options from the main rel */
+ relopts = get_effective_relopts(tuple, pg_class_desc, table_toast_map,
+ &main_relopts, &free_relopts);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2782,7 +2800,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
autovac_table *tab = NULL;
bool wraparound;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
+ bool free_relopts;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2792,21 +2811,11 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
- relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE)
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ relopts = get_effective_relopts(classTup, pg_class_desc, table_toast_map,
+ &main_relopts, &free_relopts);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2891,7 +2900,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
- tab->at_params.main_relopts = NULL;
+ tab->at_params.main_relopts = main_relopts;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
@@ -2950,8 +2959,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* being forced because of Xid or multixact wraparound.
*
* relopts is a pointer to the StdRdOptions options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * case of a plain table, or merged with the main table's for a TOAST table),
+ * NULL if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3609,6 +3618,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
TableScanDesc scan;
HeapTuple tup;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
InitMaterializedSRF(fcinfo, 0);
@@ -3617,13 +3628,63 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
rel = table_open(RelationRelationId, AccessShareLock);
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parents' reloptions, we can generate the results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ StdRdOptions *main_relopts;
+ bool free_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3639,13 +3700,16 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relopts = get_effective_relopts(tup, RelationGetDescr(rel),
+ table_toast_map, &main_relopts,
+ &free_relopts);
+
relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (relopts)
+ if (free_relopts)
pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
@@ -3663,6 +3727,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index caf0cc232b4..196be88c6b8 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -80,15 +80,33 @@ NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
-- A TOAST table inherits what it does not set from its main table.
-CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
(autovacuum_enabled=false,
vacuum_index_cleanup=false,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_truncate=false, toast.vacuum_truncate=true,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0);
VACUUM vac_tab_toast_inherit;
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-disabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-enabled
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
+ over
+------
+ t
+(1 row)
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 0a43e14c928..dc5f25dbb58 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -34,11 +34,19 @@ VACUUM vac_tab_auto;
RESET vacuum_truncate;
-- A TOAST table inherits what it does not set from its main table.
-CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
(autovacuum_enabled=false,
vacuum_index_cleanup=false,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_truncate=false, toast.vacuum_truncate=true,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0);
VACUUM vac_tab_toast_inherit;
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
--
2.50.1 (Apple Git-155)
Attachments:
[text/plain] v10-0001-Remove-extract_autovac_opts.patch (11.5K, ../../anzrfLbr_j4Ep52a@nathan/2-v10-0001-Remove-extract_autovac_opts.patch)
download | inline diff:
From 6dc41b70451dd36bf54029fff9937b6581fad16d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:35:34 -0500
Subject: [PATCH v10 1/8] Remove extract_autovac_opts().
extract_autovac_opts() returned a palloc'd copy of only the
AutoVacOpts portion of a relation's reloptions. Upcoming work
needs the rest of the StdRdOptions as well, so the callers must
keep the whole struct around.
Remove the helper and have the callers obtain reloptions from
extractRelOptions() directly. av_relation now caches a
StdRdOptions instead of an AutoVacOpts, and
relation_needs_vacanalyze() takes a StdRdOptions and extracts the
autovacuum portion itself. This is preparatory refactoring with no
change in behavior.
---
src/backend/postmaster/autovacuum.c | 122 ++++++++++------------------
1 file changed, 44 insertions(+), 78 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index ee202a4b47e..db1f5d0e575 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -202,8 +202,7 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -379,7 +378,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -388,8 +387,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2037,7 +2034,7 @@ do_autovacuum(void)
while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2076,7 +2073,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2118,7 +2115,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2141,7 +2138,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2160,7 +2157,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2776,39 +2773,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc_object(AutoVacOpts);
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2828,8 +2792,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2842,9 +2806,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2853,10 +2817,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- relation_needs_vacanalyze(relid, avopts, classForm,
+ relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
DEBUG3,
&dovacuum, &doanalyze, &wraparound,
@@ -2871,6 +2835,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_vacuum_min_duration;
int log_analyze_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2982,8 +2947,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2995,7 +2960,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none.
*
@@ -3072,7 +3037,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -3086,6 +3051,7 @@ relation_needs_vacanalyze(Oid relid,
bool force_vacuum;
bool av_enabled;
bool may_free = false;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3137,45 +3103,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
@@ -3668,7 +3634,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
- AutoVacOpts *avopts;
+ StdRdOptions *relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3684,14 +3650,14 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
- relation_needs_vacanalyze(form->oid, avopts, form,
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
- pfree(avopts);
+ if (relopts)
+ pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
vals[1] = Float8GetDatum(scores.max);
--
2.50.1 (Apple Git-155)
[text/plain] v10-0002-Make-autovacuum_enabled-a-ternary-reloption.patch (3.6K, ../../anzrfLbr_j4Ep52a@nathan/3-v10-0002-Make-autovacuum_enabled-a-ternary-reloption.patch)
download | inline diff:
From 50d310fabfc91e80e1d04c63a82e5677ab72477d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:50:36 -0500
Subject: [PATCH v10 2/8] Make autovacuum_enabled a ternary reloption.
This commit reimplements autovacuum_enabled as a ternary, using the
support added in commit 4d6a66f675 and following the example of
vacuum_truncate. This changes only the internal representation: an
unset value still behaves as enabled, and the option accepts the
same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 19 +++++++++----------
src/backend/catalog/index.c | 3 ++-
src/backend/postmaster/autovacuum.c | 2 +-
src/include/utils/rel.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..79834126f2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -107,15 +107,6 @@ static relopt_bool boolRelOpts[] =
},
false
},
- {
- {
- "autovacuum_enabled",
- "Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
- ShareUpdateExclusiveLock
- },
- true
- },
{
{
"user_catalog_table",
@@ -168,6 +159,14 @@ static relopt_bool boolRelOpts[] =
static relopt_ternary ternaryRelOpts[] =
{
+ {
+ {
+ "autovacuum_enabled",
+ "Enables autovacuum in this relation",
+ RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ ShareUpdateExclusiveLock
+ }
+ },
{
{
"vacuum_truncate",
@@ -1976,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
static const relopt_parse_elt tab[] = {
{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_BOOL,
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
{"autovacuum_parallel_workers", RELOPT_TYPE_INT,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 4c5da7e5db0..ec21b83b6b8 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2900,7 +2900,8 @@ index_update_stats(Relation rel,
{
StdRdOptions *options = (StdRdOptions *) rel->rd_options;
- if (options != NULL && !options->autovacuum.enabled)
+ if (options != NULL &&
+ options->autovacuum.enabled == PG_TERNARY_FALSE)
update_stats = false;
}
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index db1f5d0e575..c8a1b66ca2e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3141,7 +3141,7 @@ relation_needs_vacanalyze(Oid relid,
? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 89c159b133f..2ee98e9c6cc 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -310,7 +310,7 @@ typedef struct ForeignKeyCacheInfo
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
{
- bool enabled;
+ pg_ternary enabled;
int autovacuum_parallel_workers;
int vacuum_threshold;
--
2.50.1 (Apple Git-155)
[text/plain] v10-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch (3.5K, ../../anzrfLbr_j4Ep52a@nathan/4-v10-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch)
download | inline diff:
From c8212728cefc3053f4b64c19002966a31b16d81d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 15:27:58 -0500
Subject: [PATCH v10 3/8] Add an "unset" value for vacuum_index_cleanup.
This commit adds a new value to StdRdOptIndexCleanup to distinguish
whether it is explicitly set, similar to ViewOptCheckOption's
VIEW_OPTION_CHECK_OPTION_NOT_SET. This changes only the internal
representation; an unset value still defaults to AUTO, and the
option accepts the same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 3 ++-
src/backend/commands/vacuum.c | 23 ++++++++++++++---------
src/include/utils/rel.h | 1 +
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79834126f2f..58eb72e2339 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -517,6 +517,7 @@ static relopt_real realRelOpts[] =
/* values from StdRdOptIndexCleanup */
static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
{
+ /* no value for NOT_SET */
{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
@@ -557,7 +558,7 @@ static relopt_enum enumRelOpts[] =
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
},
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 31f9824899c..52116c02b59 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2210,20 +2210,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
StdRdOptIndexCleanup vacuum_index_cleanup;
if (rel->rd_options == NULL)
- vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+ vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
vacuum_index_cleanup =
((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
- if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
- params.index_cleanup = VACOPTVALUE_AUTO;
- else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
- params.index_cleanup = VACOPTVALUE_ENABLED;
- else
+ switch (vacuum_index_cleanup)
{
- Assert(vacuum_index_cleanup ==
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
- params.index_cleanup = VACOPTVALUE_DISABLED;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ params.index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ params.index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
}
}
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 2ee98e9c6cc..01b6e1a872c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,6 +338,7 @@ typedef enum StdRdOptIndexCleanup
STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
} StdRdOptIndexCleanup;
typedef struct StdRdOptions
--
2.50.1 (Apple Git-155)
[text/plain] v10-0004-Simplify-autovacuum-s-TOAST-to-main-relation-rel.patch (3.8K, ../../anzrfLbr_j4Ep52a@nathan/5-v10-0004-Simplify-autovacuum-s-TOAST-to-main-relation-rel.patch)
download | inline diff:
From f1b3a6544b8562120764ebf0ffff4cb8a87570e5 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 7 Aug 2026 10:55:34 -0500
Subject: [PATCH v10 4/8] Simplify autovacuum's TOAST-to-main-relation
reloptions map.
do_autovacuum() adds an entry to this map for every relation that
has a TOAST table, and uses a flag to mark the entries that have no
reloptions to pass down. The unconditional entry made sense back
when the payload was the main relation's OID: commit 7d4c9a5793
added the map so that the TOAST pass could find the parent, whose
pg_autovacuum row supplied the settings for a TOAST table that had
none of its own. Commit 834a6da4f7 replaced that lookup by copying
the parent's reloptions into the entry, which left the OID unread
and called for the flag, since a by-value AutoVacOpts cannot say
"not set".
An entry is now worth creating only when there is something to
inherit, so skip the relations that have no reloptions and let a
successful lookup speak for itself. Two relations cannot share a
TOAST table, and each pass sees a single catalog snapshot, so an
insertion can never find an existing entry; assert that rather than
quietly ignoring it.
While at it, remove two more leftovers of that same conversion:
ar_relid, unread ever since, and a NULL test on table_toast_map in
table_recheck_autovac(), which used to carry the relkind test for
get_pg_autovacuum_tuple_relid() and has had no NULL to catch since
that function went away.
---
src/backend/postmaster/autovacuum.c | 26 +++++++-------------------
1 file changed, 7 insertions(+), 19 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index c8a1b66ca2e..32efe967890 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -200,8 +200,6 @@ typedef struct avw_dbase
typedef struct av_relation
{
Oid ar_toastrelid; /* hash key - must be first */
- Oid ar_relid;
- bool ar_hasrelopts;
StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
@@ -2097,7 +2095,7 @@ do_autovacuum(void)
* this whether or not the table is going to be vacuumed, because we
* don't automatically vacuum toast tables along the parent table.
*/
- if (OidIsValid(classForm->reltoastrelid))
+ if (OidIsValid(classForm->reltoastrelid) && relopts)
{
av_relation *hentry;
bool found;
@@ -2105,19 +2103,10 @@ do_autovacuum(void)
hentry = hash_search(table_toast_map,
&classForm->reltoastrelid,
HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
- if (!found)
- {
- /* hash_search already filled in the key */
- hentry->ar_relid = relid;
- hentry->ar_hasrelopts = false;
- if (relopts != NULL)
- {
- hentry->ar_hasrelopts = true;
- memcpy(&hentry->ar_reloptions, relopts,
- sizeof(StdRdOptions));
- }
- }
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
}
/* Release stuff to avoid per-relation leakage */
@@ -2166,7 +2155,7 @@ do_autovacuum(void)
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
@@ -2809,14 +2798,13 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ else if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
--
2.50.1 (Apple Git-155)
[text/plain] v10-0005-Give-TOAST-storage-parameters-unsettable-default.patch (5.0K, ../../anzrfLbr_j4Ep52a@nathan/6-v10-0005-Give-TOAST-storage-parameters-unsettable-default.patch)
download | inline diff:
From 935e0d3023fd089f8b9ccbefe60ea369c4865c18 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:55:44 -0500
Subject: [PATCH v10 5/8] Give TOAST storage parameters unsettable defaults.
This is preparatory work for a follow-up commit that will fill in a
TOAST table's unset storage parameters from its main table's. For
that, it must be possible to tell an option nobody set from one the
user set to the value that option happens to default to. The
parsed form of a relation's options has nowhere to record which
ones were specified, so an unset option is simply one still holding
its declared default.
Require, then, that any option a TOAST table accepts default to a
value the user cannot set. Ternaries already comply, having no
default at all, as does vacuum_index_cleanup, whose "not set"
member has no spelling. Among the numeric ones only
log_autovacuum_min_duration was in violation, defaulting to -1 with
a minimum of -1, so give it -2 instead, matching
autovacuum_vacuum_max_threshold and
autovacuum_vacuum_insert_threshold. Users won't notice; -1 already
behaved exactly as leaving the option unset does, and it still
does.
An assertion in initialize_reloptions() enforces both of the rules
the follow-up commit will rely on: that the default is unsettable,
and that anything settable on a TOAST table is settable on a heap,
since the inherited value is read from a main table's options at
the same offset. Note that this rules out bool and string options,
neither of which can express "unset".
---
src/backend/access/common/reloptions.c | 78 +++++++++++++++++++++++++-
src/backend/postmaster/autovacuum.c | 2 +-
2 files changed, 78 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 58eb72e2339..02bc8213def 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
},
{
{
@@ -613,6 +613,78 @@ static void parse_one_reloption(relopt_value *option, char *text_str,
((option).isset ? strlen((option).string_val) : \
((relopt_string *) (option).gen)->default_len)
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Verify that every option a TOAST table accepts defaults to a value the user
+ * cannot set. Nothing records which options were specified, so an option
+ * still holding its default is the only way to recognize one that was never
+ * set, and that is how a TOAST table tells which values it should take from
+ * its main table.
+ */
+static void
+assert_toast_defaults_unsettable(void)
+{
+ for (int i = 0; relOpts[i]; i++)
+ {
+ relopt_gen *gen = relOpts[i];
+
+ if ((gen->kinds & RELOPT_KIND_TOAST) == 0)
+ continue;
+
+ /*
+ * A TOAST table's value is filled in from its main table's at the
+ * same offset in the same struct, so the option must be settable on a
+ * heap too.
+ */
+ Assert((gen->kinds & RELOPT_KIND_HEAP) != 0);
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+
+ /*
+ * Ternaries carry no default, and parse_one_reloption() can
+ * only produce true or false, so PG_TERNARY_UNSET is already
+ * beyond a user's reach.
+ */
+ break;
+
+ case RELOPT_TYPE_INT:
+ {
+ relopt_int *optint = (relopt_int *) gen;
+
+ Assert(optint->default_val < optint->min ||
+ optint->default_val > optint->max);
+ break;
+ }
+
+ case RELOPT_TYPE_REAL:
+ {
+ relopt_real *optreal = (relopt_real *) gen;
+
+ Assert(optreal->default_val < optreal->min ||
+ optreal->default_val > optreal->max);
+ break;
+ }
+
+ case RELOPT_TYPE_ENUM:
+ {
+ relopt_enum *optenum = (relopt_enum *) gen;
+
+ for (relopt_enum_elt_def *elt = optenum->members;
+ elt->string_val; elt++)
+ Assert(elt->symbol_val != optenum->default_val);
+ break;
+ }
+
+ default:
+ /* Neither bools nor strings can express "unset". */
+ Assert(false);
+ }
+ }
+}
+#endif /* USE_ASSERT_CHECKING */
+
/*
* initialize_reloptions
* initialization routine, must be called before parsing
@@ -730,6 +802,10 @@ initialize_reloptions(void)
/* flag the work is complete */
need_initialization = false;
+
+#ifdef USE_ASSERT_CHECKING
+ assert_toast_defaults_unsettable();
+#endif
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 32efe967890..a99f7108636 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2832,7 +2832,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* defaults, autovacuum's own first and plain vacuum second.
*/
- /* -1 in autovac setting means use log_autovacuum_min_duration */
+ /* a negative autovac setting means use log_autovacuum_min_duration */
log_vacuum_min_duration = (avopts && avopts->log_vacuum_min_duration >= 0)
? avopts->log_vacuum_min_duration
: Log_autovacuum_min_duration;
--
2.50.1 (Apple Git-155)
[text/plain] v10-0006-Move-the-StdRdOptions-parse-table-to-file-scope.patch (7.8K, ../../anzrfLbr_j4Ep52a@nathan/7-v10-0006-Move-the-StdRdOptions-parse-table-to-file-scope.patch)
download | inline diff:
From da1e24c8125ff931c4161a26c30fc6d2ae09c85d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:56:02 -0500
Subject: [PATCH v10 6/8] Move the StdRdOptions parse table to file scope.
This is preparatory work for a follow-up commit that will walk the
table from another function. Nothing changes but the indentation
of its entries.
---
src/backend/access/common/reloptions.c | 115 +++++++++++++------------
1 file changed, 60 insertions(+), 55 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 02bc8213def..4548eb02676 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2044,69 +2044,74 @@ fillRelOptions(void *rdopts, Size basesize,
}
+/*
+ * Parse table for StdRdOptions, which is shared by the RELOPT_KIND_HEAP and
+ * RELOPT_KIND_TOAST kinds.
+ */
+static const relopt_parse_elt stdRdOptionsTab[] = {
+ {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
+ {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
+ {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
+ {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
+ {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
+ {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
+ {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
+ {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
+ {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
+ {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
+ {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
+ {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
+ {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
+ {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
+ {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
+ {"toast_tuple_target", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, toast_tuple_target)},
+ {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
+ {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
+ {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
+ {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"user_catalog_table", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, user_catalog_table)},
+ {"parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, parallel_workers)},
+ {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+ offsetof(StdRdOptions, vacuum_index_cleanup)},
+ {"vacuum_truncate", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, vacuum_truncate)},
+ {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
+};
+
/*
* Option parser for anything that uses StdRdOptions.
*/
bytea *
default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
- static const relopt_parse_elt tab[] = {
- {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
- {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
- {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
- {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
- {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
- {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
- {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
- {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
- {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
- {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
- {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
- {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
- {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
- {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
- {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
- {"toast_tuple_target", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, toast_tuple_target)},
- {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
- {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
- {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
- {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
- {"user_catalog_table", RELOPT_TYPE_BOOL,
- offsetof(StdRdOptions, user_catalog_table)},
- {"parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, parallel_workers)},
- {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
- offsetof(StdRdOptions, vacuum_index_cleanup)},
- {"vacuum_truncate", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, vacuum_truncate)},
- {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
- };
-
return (bytea *) build_reloptions(reloptions, validate, kind,
sizeof(StdRdOptions),
- tab, lengthof(tab));
+ stdRdOptionsTab,
+ lengthof(stdRdOptionsTab));
}
/*
--
2.50.1 (Apple Git-155)
[text/plain] v10-0007-Fix-VACUUM-s-handling-of-TOAST-storage-parameter.patch (13.0K, ../../anzrfLbr_j4Ep52a@nathan/8-v10-0007-Fix-VACUUM-s-handling-of-TOAST-storage-parameter.patch)
download | inline diff:
From 3410dc14e074aae2f7dc3d21545a5699fcddbbd6 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 11:34:55 -0500
Subject: [PATCH v10 7/8] Fix VACUUM's handling of TOAST storage parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Presently, VACUUM does no such thing. It reads the TOAST table's
own reloptions, which hold only what was set through toast.*, so
vacuum_index_cleanup or vacuum_truncate set on the main table has
no effect on its TOAST table.
To fix, add merge_toast_reloptions(), which walks the parse table
for StdRdOptions and takes the main table's value for anything the
TOAST table left at its default. vacuum_rel() hands the main
table's parameters down when recursing to a TOAST table, and it
merges them into a copy of the TOAST table's parameters before the
values are used. This doesn't help VACUUM against a TOAST table
directly (e.g., "VACUUM pg_toast.pg_toast_5432"), but that's
probably okay because it's not the main supported way to vacuum a
TOAST table (see VACUUM's PROCESS_MAIN and PROCESS_TOAST options).
A follow-up commit will do the same for autovacuum. While this is
a bug fix, it's too intrusive for back-patching, but the issue
seems to have gone unnoticed for a very long time, anyway.
---
src/backend/access/common/reloptions.c | 93 +++++++++++++++++++
src/backend/commands/vacuum.c | 45 ++++++---
src/backend/postmaster/autovacuum.c | 1 +
src/include/access/reloptions.h | 2 +
src/include/commands/vacuum.h | 7 ++
.../injection_points/expected/vacuum.out | 11 +++
.../modules/injection_points/sql/vacuum.sql | 8 ++
7 files changed, 156 insertions(+), 11 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 4548eb02676..263ebf44a45 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2114,6 +2114,99 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
lengthof(stdRdOptionsTab));
}
+/*
+ * find reloption
+ * Look up a reloption of the given kind by name.
+ *
+ * Returns NULL if no such option can be set on relations of that kind. Note
+ * that names are unique only within a kind; "fillfactor", for example, is
+ * declared separately for heaps and for each index access method.
+ */
+static relopt_gen *
+find_reloption(const char *name, relopt_kind kind)
+{
+ if (need_initialization)
+ initialize_reloptions();
+
+ for (int i = 0; relOpts[i]; i++)
+ {
+ if ((relOpts[i]->kinds & kind) != 0 &&
+ strcmp(relOpts[i]->name, name) == 0)
+ return relOpts[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * merge_toast_reloptions
+ * Fill in a TOAST table's unset options from its main table's.
+ *
+ * Any option that may be set on a TOAST table but was not is taken from
+ * main_opts. Either argument may be NULL; if both are, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * An option counts as unset while it still holds the default declared for it
+ * above, which works because nothing a TOAST table accepts has a default the
+ * user could also set (see assert_toast_defaults_unsettable()).
+ *
+ * NB: This destructively modifies toast_opts, and what it returns may be
+ * either argument, so the caller must know which of the two it owns.
+ */
+StdRdOptions *
+merge_toast_reloptions(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ if (toast_opts == NULL)
+ return main_opts;
+ if (main_opts == NULL)
+ return toast_opts;
+
+ for (int i = 0; i < lengthof(stdRdOptionsTab); i++)
+ {
+ const relopt_parse_elt *elem = &stdRdOptionsTab[i];
+ relopt_gen *gen;
+ char *toast_val;
+ char *main_val;
+
+ /* Skip anything that cannot be set on a TOAST table. */
+ gen = find_reloption(elem->optname, RELOPT_KIND_TOAST);
+ if (gen == NULL)
+ continue;
+
+ toast_val = (char *) toast_opts + elem->offset;
+ main_val = (char *) main_opts + elem->offset;
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+ if (*(pg_ternary *) toast_val == PG_TERNARY_UNSET)
+ *(pg_ternary *) toast_val = *(pg_ternary *) main_val;
+ break;
+
+ case RELOPT_TYPE_INT:
+ if (*(int *) toast_val == ((relopt_int *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ case RELOPT_TYPE_REAL:
+ if (*(double *) toast_val == ((relopt_real *) gen)->default_val)
+ *(double *) toast_val = *(double *) main_val;
+ break;
+
+ case RELOPT_TYPE_ENUM:
+ if (*(int *) toast_val == ((relopt_enum *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ default:
+ elog(ERROR, "reloption \"%s\" has a type a TOAST table cannot inherit",
+ elem->optname);
+ }
+ }
+
+ return toast_opts;
+}
+
/*
* build_reloptions
*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 52116c02b59..8fe48920c9f 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -31,6 +31,7 @@
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
+#include "access/reloptions.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
@@ -187,6 +188,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_relopts = NULL;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2039,6 +2041,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
int save_sec_context;
int save_nestlevel;
VacuumParams toast_vacuum_params;
+ StdRdOptions *relopts;
+ StdRdOptions relopts_copy;
/*
* This function scribbles on the parameters, so make a copy early to
@@ -2201,6 +2205,19 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ /*
+ * A TOAST table takes any storage parameter it accepts but does not set
+ * from its main table, whose parameters the caller handed down for that
+ * purpose. Merge them into a copy of our own.
+ *
+ * We are careful to avoid scribbling on the relcache's copy of the
+ * options.
+ */
+ if (rel->rd_options)
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ relopts = merge_toast_reloptions(rel->rd_options ? &relopts_copy : NULL,
+ params.main_relopts);
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2209,11 +2226,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
StdRdOptIndexCleanup vacuum_index_cleanup;
- if (rel->rd_options == NULL)
+ if (relopts == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = relopts->vacuum_index_cleanup;
switch (vacuum_index_cleanup)
{
@@ -2245,10 +2261,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
- params.max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ if (relopts != NULL && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ params.max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
@@ -2256,11 +2270,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
- StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
-
- if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
+ if (relopts && relopts->vacuum_truncate != PG_TERNARY_UNSET)
{
- if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ if (relopts->vacuum_truncate == PG_TERNARY_TRUE)
params.truncate = VACOPTVALUE_ENABLED;
else
params.truncate = VACOPTVALUE_DISABLED;
@@ -2293,6 +2305,17 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
else
toast_relid = InvalidOid;
+ /*
+ * Hand our storage parameters down for the TOAST table to inherit. Take
+ * a copy while we still have the relation open; the relcache entry can go
+ * away once we close it.
+ */
+ if (OidIsValid(toast_relid) && rel->rd_options)
+ {
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ toast_vacuum_params.main_relopts = &relopts_copy;
+ }
+
/*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also lock down security-restricted operations and
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a99f7108636..1610c60ec4b 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2891,6 +2891,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ tab->at_params.main_relopts = NULL;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..6c599382f02 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -247,6 +247,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options,
extern bytea *default_reloptions(Datum reloptions, bool validate,
relopt_kind kind);
+extern struct StdRdOptions *merge_toast_reloptions(struct StdRdOptions *toast_opts,
+ struct StdRdOptions *main_opts);
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..2ebdd5e40ab 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,13 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * When vacuuming a TOAST table, its main table's storage parameters, for
+ * the TOAST table to inherit anything it doesn't set itself. NULL if the
+ * main table has none, or if this isn't a TOAST table.
+ */
+ struct StdRdOptions *main_relopts;
} VacuumParams;
/*
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..caf0cc232b4 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,9 +79,20 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..0a43e14c928 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,9 +33,17 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
--
2.50.1 (Apple Git-155)
[text/plain] v10-0008-Fix-autovacuum-s-handling-of-TOAST-storage-param.patch (13.4K, ../../anzrfLbr_j4Ep52a@nathan/9-v10-0008-Fix-autovacuum-s-handling-of-TOAST-storage-param.patch)
download | inline diff:
From 7bebf3e0720875ea970a17ae8e360f29fb83e9ac Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 12:02:21 -0500
Subject: [PATCH v10 8/8] Fix autovacuum's handling of TOAST storage
parameters.
The previous commit made VACUUM apply a main table's storage
parameters to its TOAST table, as CREATE TABLE has long documented.
Autovacuum still gets this wrong in two ways. It falls back to the
main table's autovacuum parameters only when the TOAST table has no
reloptions at all, so setting a single toast.* parameter silently
discards the rest. And it never consults the main table for the
parameters that only VACUUM reads, since it leaves those for
vacuum_rel() to resolve from the TOAST table's own reloptions.
To fix, combine the two sets with merge_toast_reloptions() rather
than choosing between them, and hand the main table's parameters
down to vacuum_rel() the way VACUUM now does.
pg_stat_autovacuum_scores uses the combined parameters for TOAST
tables as well; it has to collect the main relations' parameters
before it can do so, so it now makes a preliminary pass over
pg_class.
An existing shortcoming that this patch only makes worse is that
autovacuum remains oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/postmaster/autovacuum.c | 147 +++++++++++++-----
.../injection_points/expected/vacuum.out | 22 ++-
.../modules/injection_points/sql/vacuum.sql | 12 +-
3 files changed, 136 insertions(+), 45 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 1610c60ec4b..a7e97b880f5 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1913,6 +1913,36 @@ TableToProcessComparator(const ListCell *a, const ListCell *b)
return (t2->score < t1->score) ? -1 : (t2->score > t1->score) ? 1 : 0;
}
+/*
+ * get_effective_relopts
+ * Fetch the storage parameters that apply to a relation.
+ *
+ * This looks up and, for TOAST tables, merges reloptions. Returns NULL if the
+ * relation neither sets nor inherits anything. main_opts returns main table's
+ * reloptions, if any. free_relopts returns whether return value is palloc'd.
+ */
+static StdRdOptions *
+get_effective_relopts(HeapTuple tup, TupleDesc desc, HTAB *toast_map,
+ StdRdOptions **main_opts, bool *free_relopts)
+{
+ Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found = false;
+
+ /* look up our relopts */
+ relopts = (StdRdOptions *) extractRelOptions(tup, desc, NULL);
+ *free_relopts = (relopts != NULL);
+
+ /* if we're a TOAST table, look up our parent's relopts, too */
+ if (classForm->relkind == RELKIND_TOASTVALUE)
+ hentry = hash_search(toast_map, &classForm->oid, HASH_FIND, &found);
+ *main_opts = found ? &hentry->ar_reloptions : NULL;
+
+ /* return the merged reloptions */
+ return merge_toast_reloptions(relopts, *main_opts);
+}
+
/*
* Process a database table-by-table
*
@@ -2015,9 +2045,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2128,7 +2158,8 @@ do_autovacuum(void)
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
+ bool free_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2142,22 +2173,9 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ /* fetch reloptions -- merge any unset options from the main rel */
+ relopts = get_effective_relopts(tuple, pg_class_desc, table_toast_map,
+ &main_relopts, &free_relopts);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2782,7 +2800,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
autovac_table *tab = NULL;
bool wraparound;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
+ bool free_relopts;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2792,21 +2811,11 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
- relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE)
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ relopts = get_effective_relopts(classTup, pg_class_desc, table_toast_map,
+ &main_relopts, &free_relopts);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2891,7 +2900,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
- tab->at_params.main_relopts = NULL;
+ tab->at_params.main_relopts = main_relopts;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
@@ -2950,8 +2959,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* being forced because of Xid or multixact wraparound.
*
* relopts is a pointer to the StdRdOptions options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * case of a plain table, or merged with the main table's for a TOAST table),
+ * NULL if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3609,6 +3618,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
TableScanDesc scan;
HeapTuple tup;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
InitMaterializedSRF(fcinfo, 0);
@@ -3617,13 +3628,63 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
rel = table_open(RelationRelationId, AccessShareLock);
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parents' reloptions, we can generate the results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ StdRdOptions *main_relopts;
+ bool free_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3639,13 +3700,16 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relopts = get_effective_relopts(tup, RelationGetDescr(rel),
+ table_toast_map, &main_relopts,
+ &free_relopts);
+
relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (relopts)
+ if (free_relopts)
pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
@@ -3663,6 +3727,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index caf0cc232b4..196be88c6b8 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -80,15 +80,33 @@ NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
-- A TOAST table inherits what it does not set from its main table.
-CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
(autovacuum_enabled=false,
vacuum_index_cleanup=false,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_truncate=false, toast.vacuum_truncate=true,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0);
VACUUM vac_tab_toast_inherit;
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-disabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-enabled
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
+ over
+------
+ t
+(1 row)
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 0a43e14c928..dc5f25dbb58 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -34,11 +34,19 @@ VACUUM vac_tab_auto;
RESET vacuum_truncate;
-- A TOAST table inherits what it does not set from its main table.
-CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
(autovacuum_enabled=false,
vacuum_index_cleanup=false,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_truncate=false, toast.vacuum_truncate=true,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0);
VACUUM vac_tab_toast_inherit;
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-13 00:41 Nathan Bossart <nathandbossart@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2026-08-13 00:41 UTC (permalink / raw)
To: Sami Imseih <samimseih@gmail.com>; +Cc: Greg Burd <greg@burd.me>; solai v <solai.cdac@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; Michael Paquier <michael@paquier.xyz>; pgsql-hackers
Here's a v11 in which I've tried to fix the compiler warning that cfbot is
complaining about. No other changes.
--
nathan
From 6dc41b70451dd36bf54029fff9937b6581fad16d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:35:34 -0500
Subject: [PATCH v11 1/8] Remove extract_autovac_opts().
extract_autovac_opts() returned a palloc'd copy of only the
AutoVacOpts portion of a relation's reloptions. Upcoming work
needs the rest of the StdRdOptions as well, so the callers must
keep the whole struct around.
Remove the helper and have the callers obtain reloptions from
extractRelOptions() directly. av_relation now caches a
StdRdOptions instead of an AutoVacOpts, and
relation_needs_vacanalyze() takes a StdRdOptions and extracts the
autovacuum portion itself. This is preparatory refactoring with no
change in behavior.
---
src/backend/postmaster/autovacuum.c | 122 ++++++++++------------------
1 file changed, 44 insertions(+), 78 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index ee202a4b47e..db1f5d0e575 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -202,8 +202,7 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -379,7 +378,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -388,8 +387,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2037,7 +2034,7 @@ do_autovacuum(void)
while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2076,7 +2073,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2118,7 +2115,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2141,7 +2138,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2160,7 +2157,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2776,39 +2773,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc_object(AutoVacOpts);
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2828,8 +2792,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2842,9 +2806,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2853,10 +2817,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- relation_needs_vacanalyze(relid, avopts, classForm,
+ relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
DEBUG3,
&dovacuum, &doanalyze, &wraparound,
@@ -2871,6 +2835,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_vacuum_min_duration;
int log_analyze_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2982,8 +2947,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2995,7 +2960,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none.
*
@@ -3072,7 +3037,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -3086,6 +3051,7 @@ relation_needs_vacanalyze(Oid relid,
bool force_vacuum;
bool av_enabled;
bool may_free = false;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3137,45 +3103,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
@@ -3668,7 +3634,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
- AutoVacOpts *avopts;
+ StdRdOptions *relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3684,14 +3650,14 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
- relation_needs_vacanalyze(form->oid, avopts, form,
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
- pfree(avopts);
+ if (relopts)
+ pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
vals[1] = Float8GetDatum(scores.max);
--
2.50.1 (Apple Git-155)
From 50d310fabfc91e80e1d04c63a82e5677ab72477d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:50:36 -0500
Subject: [PATCH v11 2/8] Make autovacuum_enabled a ternary reloption.
This commit reimplements autovacuum_enabled as a ternary, using the
support added in commit 4d6a66f675 and following the example of
vacuum_truncate. This changes only the internal representation: an
unset value still behaves as enabled, and the option accepts the
same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 19 +++++++++----------
src/backend/catalog/index.c | 3 ++-
src/backend/postmaster/autovacuum.c | 2 +-
src/include/utils/rel.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..79834126f2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -107,15 +107,6 @@ static relopt_bool boolRelOpts[] =
},
false
},
- {
- {
- "autovacuum_enabled",
- "Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
- ShareUpdateExclusiveLock
- },
- true
- },
{
{
"user_catalog_table",
@@ -168,6 +159,14 @@ static relopt_bool boolRelOpts[] =
static relopt_ternary ternaryRelOpts[] =
{
+ {
+ {
+ "autovacuum_enabled",
+ "Enables autovacuum in this relation",
+ RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ ShareUpdateExclusiveLock
+ }
+ },
{
{
"vacuum_truncate",
@@ -1976,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
static const relopt_parse_elt tab[] = {
{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_BOOL,
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
{"autovacuum_parallel_workers", RELOPT_TYPE_INT,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 4c5da7e5db0..ec21b83b6b8 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2900,7 +2900,8 @@ index_update_stats(Relation rel,
{
StdRdOptions *options = (StdRdOptions *) rel->rd_options;
- if (options != NULL && !options->autovacuum.enabled)
+ if (options != NULL &&
+ options->autovacuum.enabled == PG_TERNARY_FALSE)
update_stats = false;
}
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index db1f5d0e575..c8a1b66ca2e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3141,7 +3141,7 @@ relation_needs_vacanalyze(Oid relid,
? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 89c159b133f..2ee98e9c6cc 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -310,7 +310,7 @@ typedef struct ForeignKeyCacheInfo
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
{
- bool enabled;
+ pg_ternary enabled;
int autovacuum_parallel_workers;
int vacuum_threshold;
--
2.50.1 (Apple Git-155)
From c8212728cefc3053f4b64c19002966a31b16d81d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 15:27:58 -0500
Subject: [PATCH v11 3/8] Add an "unset" value for vacuum_index_cleanup.
This commit adds a new value to StdRdOptIndexCleanup to distinguish
whether it is explicitly set, similar to ViewOptCheckOption's
VIEW_OPTION_CHECK_OPTION_NOT_SET. This changes only the internal
representation; an unset value still defaults to AUTO, and the
option accepts the same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 3 ++-
src/backend/commands/vacuum.c | 23 ++++++++++++++---------
src/include/utils/rel.h | 1 +
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79834126f2f..58eb72e2339 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -517,6 +517,7 @@ static relopt_real realRelOpts[] =
/* values from StdRdOptIndexCleanup */
static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
{
+ /* no value for NOT_SET */
{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
@@ -557,7 +558,7 @@ static relopt_enum enumRelOpts[] =
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
},
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 31f9824899c..52116c02b59 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2210,20 +2210,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
StdRdOptIndexCleanup vacuum_index_cleanup;
if (rel->rd_options == NULL)
- vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+ vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
vacuum_index_cleanup =
((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
- if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
- params.index_cleanup = VACOPTVALUE_AUTO;
- else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
- params.index_cleanup = VACOPTVALUE_ENABLED;
- else
+ switch (vacuum_index_cleanup)
{
- Assert(vacuum_index_cleanup ==
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
- params.index_cleanup = VACOPTVALUE_DISABLED;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ params.index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ params.index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
}
}
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 2ee98e9c6cc..01b6e1a872c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,6 +338,7 @@ typedef enum StdRdOptIndexCleanup
STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
} StdRdOptIndexCleanup;
typedef struct StdRdOptions
--
2.50.1 (Apple Git-155)
From f1b3a6544b8562120764ebf0ffff4cb8a87570e5 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 7 Aug 2026 10:55:34 -0500
Subject: [PATCH v11 4/8] Simplify autovacuum's TOAST-to-main-relation
reloptions map.
do_autovacuum() adds an entry to this map for every relation that
has a TOAST table, and uses a flag to mark the entries that have no
reloptions to pass down. The unconditional entry made sense back
when the payload was the main relation's OID: commit 7d4c9a5793
added the map so that the TOAST pass could find the parent, whose
pg_autovacuum row supplied the settings for a TOAST table that had
none of its own. Commit 834a6da4f7 replaced that lookup by copying
the parent's reloptions into the entry, which left the OID unread
and called for the flag, since a by-value AutoVacOpts cannot say
"not set".
An entry is now worth creating only when there is something to
inherit, so skip the relations that have no reloptions and let a
successful lookup speak for itself. Two relations cannot share a
TOAST table, and each pass sees a single catalog snapshot, so an
insertion can never find an existing entry; assert that rather than
quietly ignoring it.
While at it, remove two more leftovers of that same conversion:
ar_relid, unread ever since, and a NULL test on table_toast_map in
table_recheck_autovac(), which used to carry the relkind test for
get_pg_autovacuum_tuple_relid() and has had no NULL to catch since
that function went away.
---
src/backend/postmaster/autovacuum.c | 26 +++++++-------------------
1 file changed, 7 insertions(+), 19 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index c8a1b66ca2e..32efe967890 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -200,8 +200,6 @@ typedef struct avw_dbase
typedef struct av_relation
{
Oid ar_toastrelid; /* hash key - must be first */
- Oid ar_relid;
- bool ar_hasrelopts;
StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
@@ -2097,7 +2095,7 @@ do_autovacuum(void)
* this whether or not the table is going to be vacuumed, because we
* don't automatically vacuum toast tables along the parent table.
*/
- if (OidIsValid(classForm->reltoastrelid))
+ if (OidIsValid(classForm->reltoastrelid) && relopts)
{
av_relation *hentry;
bool found;
@@ -2105,19 +2103,10 @@ do_autovacuum(void)
hentry = hash_search(table_toast_map,
&classForm->reltoastrelid,
HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
- if (!found)
- {
- /* hash_search already filled in the key */
- hentry->ar_relid = relid;
- hentry->ar_hasrelopts = false;
- if (relopts != NULL)
- {
- hentry->ar_hasrelopts = true;
- memcpy(&hentry->ar_reloptions, relopts,
- sizeof(StdRdOptions));
- }
- }
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
}
/* Release stuff to avoid per-relation leakage */
@@ -2166,7 +2155,7 @@ do_autovacuum(void)
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
@@ -2809,14 +2798,13 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ else if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
--
2.50.1 (Apple Git-155)
From 935e0d3023fd089f8b9ccbefe60ea369c4865c18 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:55:44 -0500
Subject: [PATCH v11 5/8] Give TOAST storage parameters unsettable defaults.
This is preparatory work for a follow-up commit that will fill in a
TOAST table's unset storage parameters from its main table's. For
that, it must be possible to tell an option nobody set from one the
user set to the value that option happens to default to. The
parsed form of a relation's options has nowhere to record which
ones were specified, so an unset option is simply one still holding
its declared default.
Require, then, that any option a TOAST table accepts default to a
value the user cannot set. Ternaries already comply, having no
default at all, as does vacuum_index_cleanup, whose "not set"
member has no spelling. Among the numeric ones only
log_autovacuum_min_duration was in violation, defaulting to -1 with
a minimum of -1, so give it -2 instead, matching
autovacuum_vacuum_max_threshold and
autovacuum_vacuum_insert_threshold. Users won't notice; -1 already
behaved exactly as leaving the option unset does, and it still
does.
An assertion in initialize_reloptions() enforces both of the rules
the follow-up commit will rely on: that the default is unsettable,
and that anything settable on a TOAST table is settable on a heap,
since the inherited value is read from a main table's options at
the same offset. Note that this rules out bool and string options,
neither of which can express "unset".
---
src/backend/access/common/reloptions.c | 78 +++++++++++++++++++++++++-
src/backend/postmaster/autovacuum.c | 2 +-
2 files changed, 78 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 58eb72e2339..02bc8213def 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
},
{
{
@@ -613,6 +613,78 @@ static void parse_one_reloption(relopt_value *option, char *text_str,
((option).isset ? strlen((option).string_val) : \
((relopt_string *) (option).gen)->default_len)
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Verify that every option a TOAST table accepts defaults to a value the user
+ * cannot set. Nothing records which options were specified, so an option
+ * still holding its default is the only way to recognize one that was never
+ * set, and that is how a TOAST table tells which values it should take from
+ * its main table.
+ */
+static void
+assert_toast_defaults_unsettable(void)
+{
+ for (int i = 0; relOpts[i]; i++)
+ {
+ relopt_gen *gen = relOpts[i];
+
+ if ((gen->kinds & RELOPT_KIND_TOAST) == 0)
+ continue;
+
+ /*
+ * A TOAST table's value is filled in from its main table's at the
+ * same offset in the same struct, so the option must be settable on a
+ * heap too.
+ */
+ Assert((gen->kinds & RELOPT_KIND_HEAP) != 0);
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+
+ /*
+ * Ternaries carry no default, and parse_one_reloption() can
+ * only produce true or false, so PG_TERNARY_UNSET is already
+ * beyond a user's reach.
+ */
+ break;
+
+ case RELOPT_TYPE_INT:
+ {
+ relopt_int *optint = (relopt_int *) gen;
+
+ Assert(optint->default_val < optint->min ||
+ optint->default_val > optint->max);
+ break;
+ }
+
+ case RELOPT_TYPE_REAL:
+ {
+ relopt_real *optreal = (relopt_real *) gen;
+
+ Assert(optreal->default_val < optreal->min ||
+ optreal->default_val > optreal->max);
+ break;
+ }
+
+ case RELOPT_TYPE_ENUM:
+ {
+ relopt_enum *optenum = (relopt_enum *) gen;
+
+ for (relopt_enum_elt_def *elt = optenum->members;
+ elt->string_val; elt++)
+ Assert(elt->symbol_val != optenum->default_val);
+ break;
+ }
+
+ default:
+ /* Neither bools nor strings can express "unset". */
+ Assert(false);
+ }
+ }
+}
+#endif /* USE_ASSERT_CHECKING */
+
/*
* initialize_reloptions
* initialization routine, must be called before parsing
@@ -730,6 +802,10 @@ initialize_reloptions(void)
/* flag the work is complete */
need_initialization = false;
+
+#ifdef USE_ASSERT_CHECKING
+ assert_toast_defaults_unsettable();
+#endif
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 32efe967890..a99f7108636 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2832,7 +2832,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* defaults, autovacuum's own first and plain vacuum second.
*/
- /* -1 in autovac setting means use log_autovacuum_min_duration */
+ /* a negative autovac setting means use log_autovacuum_min_duration */
log_vacuum_min_duration = (avopts && avopts->log_vacuum_min_duration >= 0)
? avopts->log_vacuum_min_duration
: Log_autovacuum_min_duration;
--
2.50.1 (Apple Git-155)
From da1e24c8125ff931c4161a26c30fc6d2ae09c85d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:56:02 -0500
Subject: [PATCH v11 6/8] Move the StdRdOptions parse table to file scope.
This is preparatory work for a follow-up commit that will walk the
table from another function. Nothing changes but the indentation
of its entries.
---
src/backend/access/common/reloptions.c | 115 +++++++++++++------------
1 file changed, 60 insertions(+), 55 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 02bc8213def..4548eb02676 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2044,69 +2044,74 @@ fillRelOptions(void *rdopts, Size basesize,
}
+/*
+ * Parse table for StdRdOptions, which is shared by the RELOPT_KIND_HEAP and
+ * RELOPT_KIND_TOAST kinds.
+ */
+static const relopt_parse_elt stdRdOptionsTab[] = {
+ {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
+ {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
+ {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
+ {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
+ {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
+ {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
+ {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
+ {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
+ {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
+ {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
+ {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
+ {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
+ {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
+ {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
+ {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
+ {"toast_tuple_target", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, toast_tuple_target)},
+ {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
+ {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
+ {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
+ {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"user_catalog_table", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, user_catalog_table)},
+ {"parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, parallel_workers)},
+ {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+ offsetof(StdRdOptions, vacuum_index_cleanup)},
+ {"vacuum_truncate", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, vacuum_truncate)},
+ {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
+};
+
/*
* Option parser for anything that uses StdRdOptions.
*/
bytea *
default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
- static const relopt_parse_elt tab[] = {
- {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
- {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
- {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
- {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
- {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
- {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
- {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
- {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
- {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
- {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
- {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
- {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
- {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
- {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
- {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
- {"toast_tuple_target", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, toast_tuple_target)},
- {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
- {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
- {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
- {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
- {"user_catalog_table", RELOPT_TYPE_BOOL,
- offsetof(StdRdOptions, user_catalog_table)},
- {"parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, parallel_workers)},
- {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
- offsetof(StdRdOptions, vacuum_index_cleanup)},
- {"vacuum_truncate", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, vacuum_truncate)},
- {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
- };
-
return (bytea *) build_reloptions(reloptions, validate, kind,
sizeof(StdRdOptions),
- tab, lengthof(tab));
+ stdRdOptionsTab,
+ lengthof(stdRdOptionsTab));
}
/*
--
2.50.1 (Apple Git-155)
From 3410dc14e074aae2f7dc3d21545a5699fcddbbd6 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 11:34:55 -0500
Subject: [PATCH v11 7/8] Fix VACUUM's handling of TOAST storage parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Presently, VACUUM does no such thing. It reads the TOAST table's
own reloptions, which hold only what was set through toast.*, so
vacuum_index_cleanup or vacuum_truncate set on the main table has
no effect on its TOAST table.
To fix, add merge_toast_reloptions(), which walks the parse table
for StdRdOptions and takes the main table's value for anything the
TOAST table left at its default. vacuum_rel() hands the main
table's parameters down when recursing to a TOAST table, and it
merges them into a copy of the TOAST table's parameters before the
values are used. This doesn't help VACUUM against a TOAST table
directly (e.g., "VACUUM pg_toast.pg_toast_5432"), but that's
probably okay because it's not the main supported way to vacuum a
TOAST table (see VACUUM's PROCESS_MAIN and PROCESS_TOAST options).
A follow-up commit will do the same for autovacuum. While this is
a bug fix, it's too intrusive for back-patching, but the issue
seems to have gone unnoticed for a very long time, anyway.
---
src/backend/access/common/reloptions.c | 93 +++++++++++++++++++
src/backend/commands/vacuum.c | 45 ++++++---
src/backend/postmaster/autovacuum.c | 1 +
src/include/access/reloptions.h | 2 +
src/include/commands/vacuum.h | 7 ++
.../injection_points/expected/vacuum.out | 11 +++
.../modules/injection_points/sql/vacuum.sql | 8 ++
7 files changed, 156 insertions(+), 11 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 4548eb02676..263ebf44a45 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2114,6 +2114,99 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
lengthof(stdRdOptionsTab));
}
+/*
+ * find reloption
+ * Look up a reloption of the given kind by name.
+ *
+ * Returns NULL if no such option can be set on relations of that kind. Note
+ * that names are unique only within a kind; "fillfactor", for example, is
+ * declared separately for heaps and for each index access method.
+ */
+static relopt_gen *
+find_reloption(const char *name, relopt_kind kind)
+{
+ if (need_initialization)
+ initialize_reloptions();
+
+ for (int i = 0; relOpts[i]; i++)
+ {
+ if ((relOpts[i]->kinds & kind) != 0 &&
+ strcmp(relOpts[i]->name, name) == 0)
+ return relOpts[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * merge_toast_reloptions
+ * Fill in a TOAST table's unset options from its main table's.
+ *
+ * Any option that may be set on a TOAST table but was not is taken from
+ * main_opts. Either argument may be NULL; if both are, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * An option counts as unset while it still holds the default declared for it
+ * above, which works because nothing a TOAST table accepts has a default the
+ * user could also set (see assert_toast_defaults_unsettable()).
+ *
+ * NB: This destructively modifies toast_opts, and what it returns may be
+ * either argument, so the caller must know which of the two it owns.
+ */
+StdRdOptions *
+merge_toast_reloptions(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ if (toast_opts == NULL)
+ return main_opts;
+ if (main_opts == NULL)
+ return toast_opts;
+
+ for (int i = 0; i < lengthof(stdRdOptionsTab); i++)
+ {
+ const relopt_parse_elt *elem = &stdRdOptionsTab[i];
+ relopt_gen *gen;
+ char *toast_val;
+ char *main_val;
+
+ /* Skip anything that cannot be set on a TOAST table. */
+ gen = find_reloption(elem->optname, RELOPT_KIND_TOAST);
+ if (gen == NULL)
+ continue;
+
+ toast_val = (char *) toast_opts + elem->offset;
+ main_val = (char *) main_opts + elem->offset;
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+ if (*(pg_ternary *) toast_val == PG_TERNARY_UNSET)
+ *(pg_ternary *) toast_val = *(pg_ternary *) main_val;
+ break;
+
+ case RELOPT_TYPE_INT:
+ if (*(int *) toast_val == ((relopt_int *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ case RELOPT_TYPE_REAL:
+ if (*(double *) toast_val == ((relopt_real *) gen)->default_val)
+ *(double *) toast_val = *(double *) main_val;
+ break;
+
+ case RELOPT_TYPE_ENUM:
+ if (*(int *) toast_val == ((relopt_enum *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ default:
+ elog(ERROR, "reloption \"%s\" has a type a TOAST table cannot inherit",
+ elem->optname);
+ }
+ }
+
+ return toast_opts;
+}
+
/*
* build_reloptions
*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 52116c02b59..8fe48920c9f 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -31,6 +31,7 @@
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
+#include "access/reloptions.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
@@ -187,6 +188,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_relopts = NULL;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2039,6 +2041,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
int save_sec_context;
int save_nestlevel;
VacuumParams toast_vacuum_params;
+ StdRdOptions *relopts;
+ StdRdOptions relopts_copy;
/*
* This function scribbles on the parameters, so make a copy early to
@@ -2201,6 +2205,19 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ /*
+ * A TOAST table takes any storage parameter it accepts but does not set
+ * from its main table, whose parameters the caller handed down for that
+ * purpose. Merge them into a copy of our own.
+ *
+ * We are careful to avoid scribbling on the relcache's copy of the
+ * options.
+ */
+ if (rel->rd_options)
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ relopts = merge_toast_reloptions(rel->rd_options ? &relopts_copy : NULL,
+ params.main_relopts);
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2209,11 +2226,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
StdRdOptIndexCleanup vacuum_index_cleanup;
- if (rel->rd_options == NULL)
+ if (relopts == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = relopts->vacuum_index_cleanup;
switch (vacuum_index_cleanup)
{
@@ -2245,10 +2261,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
- params.max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ if (relopts != NULL && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ params.max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
@@ -2256,11 +2270,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
- StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
-
- if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
+ if (relopts && relopts->vacuum_truncate != PG_TERNARY_UNSET)
{
- if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ if (relopts->vacuum_truncate == PG_TERNARY_TRUE)
params.truncate = VACOPTVALUE_ENABLED;
else
params.truncate = VACOPTVALUE_DISABLED;
@@ -2293,6 +2305,17 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
else
toast_relid = InvalidOid;
+ /*
+ * Hand our storage parameters down for the TOAST table to inherit. Take
+ * a copy while we still have the relation open; the relcache entry can go
+ * away once we close it.
+ */
+ if (OidIsValid(toast_relid) && rel->rd_options)
+ {
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ toast_vacuum_params.main_relopts = &relopts_copy;
+ }
+
/*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also lock down security-restricted operations and
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a99f7108636..1610c60ec4b 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2891,6 +2891,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ tab->at_params.main_relopts = NULL;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..6c599382f02 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -247,6 +247,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options,
extern bytea *default_reloptions(Datum reloptions, bool validate,
relopt_kind kind);
+extern struct StdRdOptions *merge_toast_reloptions(struct StdRdOptions *toast_opts,
+ struct StdRdOptions *main_opts);
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..2ebdd5e40ab 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,13 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * When vacuuming a TOAST table, its main table's storage parameters, for
+ * the TOAST table to inherit anything it doesn't set itself. NULL if the
+ * main table has none, or if this isn't a TOAST table.
+ */
+ struct StdRdOptions *main_relopts;
} VacuumParams;
/*
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..caf0cc232b4 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,9 +79,20 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..0a43e14c928 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,9 +33,17 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
--
2.50.1 (Apple Git-155)
From ccb289cf086d91fb3539354f90ce14a018bcd75a Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 12:02:21 -0500
Subject: [PATCH v11 8/8] Fix autovacuum's handling of TOAST storage
parameters.
The previous commit made VACUUM apply a main table's storage
parameters to its TOAST table, as CREATE TABLE has long documented.
Autovacuum still gets this wrong in two ways. It falls back to the
main table's autovacuum parameters only when the TOAST table has no
reloptions at all, so setting a single toast.* parameter silently
discards the rest. And it never consults the main table for the
parameters that only VACUUM reads, since it leaves those for
vacuum_rel() to resolve from the TOAST table's own reloptions.
To fix, combine the two sets with merge_toast_reloptions() rather
than choosing between them, and hand the main table's parameters
down to vacuum_rel() the way VACUUM now does.
pg_stat_autovacuum_scores uses the combined parameters for TOAST
tables as well; it has to collect the main relations' parameters
before it can do so, so it now makes a preliminary pass over
pg_class.
An existing shortcoming that this patch only makes worse is that
autovacuum remains oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/postmaster/autovacuum.c | 146 +++++++++++++-----
.../injection_points/expected/vacuum.out | 22 ++-
.../modules/injection_points/sql/vacuum.sql | 12 +-
3 files changed, 135 insertions(+), 45 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 1610c60ec4b..9a050aca669 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1913,6 +1913,35 @@ TableToProcessComparator(const ListCell *a, const ListCell *b)
return (t2->score < t1->score) ? -1 : (t2->score > t1->score) ? 1 : 0;
}
+/*
+ * get_effective_relopts
+ * Fetch the storage parameters that apply to a relation.
+ *
+ * This looks up and, for TOAST tables, merges reloptions. Returns NULL if the
+ * relation neither sets nor inherits anything. main_opts returns main table's
+ * reloptions, if any. free_relopts returns whether return value is palloc'd.
+ */
+static StdRdOptions *
+get_effective_relopts(HeapTuple tup, TupleDesc desc, HTAB *toast_map,
+ StdRdOptions **main_opts, bool *free_relopts)
+{
+ Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ av_relation *hentry = NULL;
+
+ /* look up our relopts */
+ relopts = (StdRdOptions *) extractRelOptions(tup, desc, NULL);
+ *free_relopts = (relopts != NULL);
+
+ /* if we're a TOAST table, look up our parent's relopts, too */
+ if (classForm->relkind == RELKIND_TOASTVALUE)
+ hentry = hash_search(toast_map, &classForm->oid, HASH_FIND, NULL);
+ *main_opts = hentry ? &hentry->ar_reloptions : NULL;
+
+ /* return the merged reloptions */
+ return merge_toast_reloptions(relopts, *main_opts);
+}
+
/*
* Process a database table-by-table
*
@@ -2015,9 +2044,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2128,7 +2157,8 @@ do_autovacuum(void)
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
+ bool free_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2142,22 +2172,9 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ /* fetch reloptions -- merge any unset options from the main rel */
+ relopts = get_effective_relopts(tuple, pg_class_desc, table_toast_map,
+ &main_relopts, &free_relopts);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2782,7 +2799,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
autovac_table *tab = NULL;
bool wraparound;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
+ bool free_relopts;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2792,21 +2810,11 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
- relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE)
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ relopts = get_effective_relopts(classTup, pg_class_desc, table_toast_map,
+ &main_relopts, &free_relopts);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2891,7 +2899,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
- tab->at_params.main_relopts = NULL;
+ tab->at_params.main_relopts = main_relopts;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
@@ -2950,8 +2958,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* being forced because of Xid or multixact wraparound.
*
* relopts is a pointer to the StdRdOptions options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * case of a plain table, or merged with the main table's for a TOAST table),
+ * NULL if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3609,6 +3617,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
TableScanDesc scan;
HeapTuple tup;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
InitMaterializedSRF(fcinfo, 0);
@@ -3617,13 +3627,63 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
rel = table_open(RelationRelationId, AccessShareLock);
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parents' reloptions, we can generate the results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ StdRdOptions *main_relopts;
+ bool free_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3639,13 +3699,16 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relopts = get_effective_relopts(tup, RelationGetDescr(rel),
+ table_toast_map, &main_relopts,
+ &free_relopts);
+
relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (relopts)
+ if (free_relopts)
pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
@@ -3663,6 +3726,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index caf0cc232b4..196be88c6b8 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -80,15 +80,33 @@ NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
-- A TOAST table inherits what it does not set from its main table.
-CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
(autovacuum_enabled=false,
vacuum_index_cleanup=false,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_truncate=false, toast.vacuum_truncate=true,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0);
VACUUM vac_tab_toast_inherit;
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-disabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-enabled
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
+ over
+------
+ t
+(1 row)
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 0a43e14c928..dc5f25dbb58 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -34,11 +34,19 @@ VACUUM vac_tab_auto;
RESET vacuum_truncate;
-- A TOAST table inherits what it does not set from its main table.
-CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
(autovacuum_enabled=false,
vacuum_index_cleanup=false,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_truncate=false, toast.vacuum_truncate=true,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0);
VACUUM vac_tab_toast_inherit;
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
--
2.50.1 (Apple Git-155)
Attachments:
[text/plain] v11-0001-Remove-extract_autovac_opts.patch (11.5K, ../../an0Sz84q2aQQnqX5@nathan/2-v11-0001-Remove-extract_autovac_opts.patch)
download | inline diff:
From 6dc41b70451dd36bf54029fff9937b6581fad16d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:35:34 -0500
Subject: [PATCH v11 1/8] Remove extract_autovac_opts().
extract_autovac_opts() returned a palloc'd copy of only the
AutoVacOpts portion of a relation's reloptions. Upcoming work
needs the rest of the StdRdOptions as well, so the callers must
keep the whole struct around.
Remove the helper and have the callers obtain reloptions from
extractRelOptions() directly. av_relation now caches a
StdRdOptions instead of an AutoVacOpts, and
relation_needs_vacanalyze() takes a StdRdOptions and extracts the
autovacuum portion itself. This is preparatory refactoring with no
change in behavior.
---
src/backend/postmaster/autovacuum.c | 122 ++++++++++------------------
1 file changed, 44 insertions(+), 78 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index ee202a4b47e..db1f5d0e575 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -202,8 +202,7 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -379,7 +378,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -388,8 +387,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2037,7 +2034,7 @@ do_autovacuum(void)
while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2076,7 +2073,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2118,7 +2115,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2141,7 +2138,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2160,7 +2157,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2776,39 +2773,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc_object(AutoVacOpts);
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2828,8 +2792,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2842,9 +2806,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2853,10 +2817,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- relation_needs_vacanalyze(relid, avopts, classForm,
+ relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
DEBUG3,
&dovacuum, &doanalyze, &wraparound,
@@ -2871,6 +2835,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_vacuum_min_duration;
int log_analyze_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2982,8 +2947,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2995,7 +2960,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none.
*
@@ -3072,7 +3037,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -3086,6 +3051,7 @@ relation_needs_vacanalyze(Oid relid,
bool force_vacuum;
bool av_enabled;
bool may_free = false;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3137,45 +3103,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
@@ -3668,7 +3634,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
- AutoVacOpts *avopts;
+ StdRdOptions *relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3684,14 +3650,14 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
- relation_needs_vacanalyze(form->oid, avopts, form,
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
- pfree(avopts);
+ if (relopts)
+ pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
vals[1] = Float8GetDatum(scores.max);
--
2.50.1 (Apple Git-155)
[text/plain] v11-0002-Make-autovacuum_enabled-a-ternary-reloption.patch (3.6K, ../../an0Sz84q2aQQnqX5@nathan/3-v11-0002-Make-autovacuum_enabled-a-ternary-reloption.patch)
download | inline diff:
From 50d310fabfc91e80e1d04c63a82e5677ab72477d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:50:36 -0500
Subject: [PATCH v11 2/8] Make autovacuum_enabled a ternary reloption.
This commit reimplements autovacuum_enabled as a ternary, using the
support added in commit 4d6a66f675 and following the example of
vacuum_truncate. This changes only the internal representation: an
unset value still behaves as enabled, and the option accepts the
same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 19 +++++++++----------
src/backend/catalog/index.c | 3 ++-
src/backend/postmaster/autovacuum.c | 2 +-
src/include/utils/rel.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..79834126f2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -107,15 +107,6 @@ static relopt_bool boolRelOpts[] =
},
false
},
- {
- {
- "autovacuum_enabled",
- "Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
- ShareUpdateExclusiveLock
- },
- true
- },
{
{
"user_catalog_table",
@@ -168,6 +159,14 @@ static relopt_bool boolRelOpts[] =
static relopt_ternary ternaryRelOpts[] =
{
+ {
+ {
+ "autovacuum_enabled",
+ "Enables autovacuum in this relation",
+ RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ ShareUpdateExclusiveLock
+ }
+ },
{
{
"vacuum_truncate",
@@ -1976,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
static const relopt_parse_elt tab[] = {
{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_BOOL,
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
{"autovacuum_parallel_workers", RELOPT_TYPE_INT,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 4c5da7e5db0..ec21b83b6b8 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2900,7 +2900,8 @@ index_update_stats(Relation rel,
{
StdRdOptions *options = (StdRdOptions *) rel->rd_options;
- if (options != NULL && !options->autovacuum.enabled)
+ if (options != NULL &&
+ options->autovacuum.enabled == PG_TERNARY_FALSE)
update_stats = false;
}
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index db1f5d0e575..c8a1b66ca2e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3141,7 +3141,7 @@ relation_needs_vacanalyze(Oid relid,
? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 89c159b133f..2ee98e9c6cc 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -310,7 +310,7 @@ typedef struct ForeignKeyCacheInfo
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
{
- bool enabled;
+ pg_ternary enabled;
int autovacuum_parallel_workers;
int vacuum_threshold;
--
2.50.1 (Apple Git-155)
[text/plain] v11-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch (3.5K, ../../an0Sz84q2aQQnqX5@nathan/4-v11-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch)
download | inline diff:
From c8212728cefc3053f4b64c19002966a31b16d81d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 15:27:58 -0500
Subject: [PATCH v11 3/8] Add an "unset" value for vacuum_index_cleanup.
This commit adds a new value to StdRdOptIndexCleanup to distinguish
whether it is explicitly set, similar to ViewOptCheckOption's
VIEW_OPTION_CHECK_OPTION_NOT_SET. This changes only the internal
representation; an unset value still defaults to AUTO, and the
option accepts the same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 3 ++-
src/backend/commands/vacuum.c | 23 ++++++++++++++---------
src/include/utils/rel.h | 1 +
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79834126f2f..58eb72e2339 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -517,6 +517,7 @@ static relopt_real realRelOpts[] =
/* values from StdRdOptIndexCleanup */
static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
{
+ /* no value for NOT_SET */
{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
@@ -557,7 +558,7 @@ static relopt_enum enumRelOpts[] =
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
},
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 31f9824899c..52116c02b59 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2210,20 +2210,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
StdRdOptIndexCleanup vacuum_index_cleanup;
if (rel->rd_options == NULL)
- vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+ vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
vacuum_index_cleanup =
((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
- if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
- params.index_cleanup = VACOPTVALUE_AUTO;
- else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
- params.index_cleanup = VACOPTVALUE_ENABLED;
- else
+ switch (vacuum_index_cleanup)
{
- Assert(vacuum_index_cleanup ==
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
- params.index_cleanup = VACOPTVALUE_DISABLED;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ params.index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ params.index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
}
}
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 2ee98e9c6cc..01b6e1a872c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,6 +338,7 @@ typedef enum StdRdOptIndexCleanup
STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
} StdRdOptIndexCleanup;
typedef struct StdRdOptions
--
2.50.1 (Apple Git-155)
[text/plain] v11-0004-Simplify-autovacuum-s-TOAST-to-main-relation-rel.patch (3.8K, ../../an0Sz84q2aQQnqX5@nathan/5-v11-0004-Simplify-autovacuum-s-TOAST-to-main-relation-rel.patch)
download | inline diff:
From f1b3a6544b8562120764ebf0ffff4cb8a87570e5 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 7 Aug 2026 10:55:34 -0500
Subject: [PATCH v11 4/8] Simplify autovacuum's TOAST-to-main-relation
reloptions map.
do_autovacuum() adds an entry to this map for every relation that
has a TOAST table, and uses a flag to mark the entries that have no
reloptions to pass down. The unconditional entry made sense back
when the payload was the main relation's OID: commit 7d4c9a5793
added the map so that the TOAST pass could find the parent, whose
pg_autovacuum row supplied the settings for a TOAST table that had
none of its own. Commit 834a6da4f7 replaced that lookup by copying
the parent's reloptions into the entry, which left the OID unread
and called for the flag, since a by-value AutoVacOpts cannot say
"not set".
An entry is now worth creating only when there is something to
inherit, so skip the relations that have no reloptions and let a
successful lookup speak for itself. Two relations cannot share a
TOAST table, and each pass sees a single catalog snapshot, so an
insertion can never find an existing entry; assert that rather than
quietly ignoring it.
While at it, remove two more leftovers of that same conversion:
ar_relid, unread ever since, and a NULL test on table_toast_map in
table_recheck_autovac(), which used to carry the relkind test for
get_pg_autovacuum_tuple_relid() and has had no NULL to catch since
that function went away.
---
src/backend/postmaster/autovacuum.c | 26 +++++++-------------------
1 file changed, 7 insertions(+), 19 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index c8a1b66ca2e..32efe967890 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -200,8 +200,6 @@ typedef struct avw_dbase
typedef struct av_relation
{
Oid ar_toastrelid; /* hash key - must be first */
- Oid ar_relid;
- bool ar_hasrelopts;
StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
@@ -2097,7 +2095,7 @@ do_autovacuum(void)
* this whether or not the table is going to be vacuumed, because we
* don't automatically vacuum toast tables along the parent table.
*/
- if (OidIsValid(classForm->reltoastrelid))
+ if (OidIsValid(classForm->reltoastrelid) && relopts)
{
av_relation *hentry;
bool found;
@@ -2105,19 +2103,10 @@ do_autovacuum(void)
hentry = hash_search(table_toast_map,
&classForm->reltoastrelid,
HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
- if (!found)
- {
- /* hash_search already filled in the key */
- hentry->ar_relid = relid;
- hentry->ar_hasrelopts = false;
- if (relopts != NULL)
- {
- hentry->ar_hasrelopts = true;
- memcpy(&hentry->ar_reloptions, relopts,
- sizeof(StdRdOptions));
- }
- }
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
}
/* Release stuff to avoid per-relation leakage */
@@ -2166,7 +2155,7 @@ do_autovacuum(void)
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
@@ -2809,14 +2798,13 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ else if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
--
2.50.1 (Apple Git-155)
[text/plain] v11-0005-Give-TOAST-storage-parameters-unsettable-default.patch (5.0K, ../../an0Sz84q2aQQnqX5@nathan/6-v11-0005-Give-TOAST-storage-parameters-unsettable-default.patch)
download | inline diff:
From 935e0d3023fd089f8b9ccbefe60ea369c4865c18 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:55:44 -0500
Subject: [PATCH v11 5/8] Give TOAST storage parameters unsettable defaults.
This is preparatory work for a follow-up commit that will fill in a
TOAST table's unset storage parameters from its main table's. For
that, it must be possible to tell an option nobody set from one the
user set to the value that option happens to default to. The
parsed form of a relation's options has nowhere to record which
ones were specified, so an unset option is simply one still holding
its declared default.
Require, then, that any option a TOAST table accepts default to a
value the user cannot set. Ternaries already comply, having no
default at all, as does vacuum_index_cleanup, whose "not set"
member has no spelling. Among the numeric ones only
log_autovacuum_min_duration was in violation, defaulting to -1 with
a minimum of -1, so give it -2 instead, matching
autovacuum_vacuum_max_threshold and
autovacuum_vacuum_insert_threshold. Users won't notice; -1 already
behaved exactly as leaving the option unset does, and it still
does.
An assertion in initialize_reloptions() enforces both of the rules
the follow-up commit will rely on: that the default is unsettable,
and that anything settable on a TOAST table is settable on a heap,
since the inherited value is read from a main table's options at
the same offset. Note that this rules out bool and string options,
neither of which can express "unset".
---
src/backend/access/common/reloptions.c | 78 +++++++++++++++++++++++++-
src/backend/postmaster/autovacuum.c | 2 +-
2 files changed, 78 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 58eb72e2339..02bc8213def 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
},
{
{
@@ -613,6 +613,78 @@ static void parse_one_reloption(relopt_value *option, char *text_str,
((option).isset ? strlen((option).string_val) : \
((relopt_string *) (option).gen)->default_len)
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Verify that every option a TOAST table accepts defaults to a value the user
+ * cannot set. Nothing records which options were specified, so an option
+ * still holding its default is the only way to recognize one that was never
+ * set, and that is how a TOAST table tells which values it should take from
+ * its main table.
+ */
+static void
+assert_toast_defaults_unsettable(void)
+{
+ for (int i = 0; relOpts[i]; i++)
+ {
+ relopt_gen *gen = relOpts[i];
+
+ if ((gen->kinds & RELOPT_KIND_TOAST) == 0)
+ continue;
+
+ /*
+ * A TOAST table's value is filled in from its main table's at the
+ * same offset in the same struct, so the option must be settable on a
+ * heap too.
+ */
+ Assert((gen->kinds & RELOPT_KIND_HEAP) != 0);
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+
+ /*
+ * Ternaries carry no default, and parse_one_reloption() can
+ * only produce true or false, so PG_TERNARY_UNSET is already
+ * beyond a user's reach.
+ */
+ break;
+
+ case RELOPT_TYPE_INT:
+ {
+ relopt_int *optint = (relopt_int *) gen;
+
+ Assert(optint->default_val < optint->min ||
+ optint->default_val > optint->max);
+ break;
+ }
+
+ case RELOPT_TYPE_REAL:
+ {
+ relopt_real *optreal = (relopt_real *) gen;
+
+ Assert(optreal->default_val < optreal->min ||
+ optreal->default_val > optreal->max);
+ break;
+ }
+
+ case RELOPT_TYPE_ENUM:
+ {
+ relopt_enum *optenum = (relopt_enum *) gen;
+
+ for (relopt_enum_elt_def *elt = optenum->members;
+ elt->string_val; elt++)
+ Assert(elt->symbol_val != optenum->default_val);
+ break;
+ }
+
+ default:
+ /* Neither bools nor strings can express "unset". */
+ Assert(false);
+ }
+ }
+}
+#endif /* USE_ASSERT_CHECKING */
+
/*
* initialize_reloptions
* initialization routine, must be called before parsing
@@ -730,6 +802,10 @@ initialize_reloptions(void)
/* flag the work is complete */
need_initialization = false;
+
+#ifdef USE_ASSERT_CHECKING
+ assert_toast_defaults_unsettable();
+#endif
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 32efe967890..a99f7108636 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2832,7 +2832,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* defaults, autovacuum's own first and plain vacuum second.
*/
- /* -1 in autovac setting means use log_autovacuum_min_duration */
+ /* a negative autovac setting means use log_autovacuum_min_duration */
log_vacuum_min_duration = (avopts && avopts->log_vacuum_min_duration >= 0)
? avopts->log_vacuum_min_duration
: Log_autovacuum_min_duration;
--
2.50.1 (Apple Git-155)
[text/plain] v11-0006-Move-the-StdRdOptions-parse-table-to-file-scope.patch (7.8K, ../../an0Sz84q2aQQnqX5@nathan/7-v11-0006-Move-the-StdRdOptions-parse-table-to-file-scope.patch)
download | inline diff:
From da1e24c8125ff931c4161a26c30fc6d2ae09c85d Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:56:02 -0500
Subject: [PATCH v11 6/8] Move the StdRdOptions parse table to file scope.
This is preparatory work for a follow-up commit that will walk the
table from another function. Nothing changes but the indentation
of its entries.
---
src/backend/access/common/reloptions.c | 115 +++++++++++++------------
1 file changed, 60 insertions(+), 55 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 02bc8213def..4548eb02676 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2044,69 +2044,74 @@ fillRelOptions(void *rdopts, Size basesize,
}
+/*
+ * Parse table for StdRdOptions, which is shared by the RELOPT_KIND_HEAP and
+ * RELOPT_KIND_TOAST kinds.
+ */
+static const relopt_parse_elt stdRdOptionsTab[] = {
+ {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
+ {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
+ {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
+ {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
+ {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
+ {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
+ {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
+ {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
+ {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
+ {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
+ {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
+ {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
+ {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
+ {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
+ {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
+ {"toast_tuple_target", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, toast_tuple_target)},
+ {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
+ {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
+ {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
+ {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"user_catalog_table", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, user_catalog_table)},
+ {"parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, parallel_workers)},
+ {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+ offsetof(StdRdOptions, vacuum_index_cleanup)},
+ {"vacuum_truncate", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, vacuum_truncate)},
+ {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
+};
+
/*
* Option parser for anything that uses StdRdOptions.
*/
bytea *
default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
- static const relopt_parse_elt tab[] = {
- {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
- {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
- {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
- {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
- {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
- {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
- {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
- {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
- {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
- {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
- {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
- {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
- {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
- {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
- {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
- {"toast_tuple_target", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, toast_tuple_target)},
- {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
- {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
- {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
- {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
- {"user_catalog_table", RELOPT_TYPE_BOOL,
- offsetof(StdRdOptions, user_catalog_table)},
- {"parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, parallel_workers)},
- {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
- offsetof(StdRdOptions, vacuum_index_cleanup)},
- {"vacuum_truncate", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, vacuum_truncate)},
- {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
- };
-
return (bytea *) build_reloptions(reloptions, validate, kind,
sizeof(StdRdOptions),
- tab, lengthof(tab));
+ stdRdOptionsTab,
+ lengthof(stdRdOptionsTab));
}
/*
--
2.50.1 (Apple Git-155)
[text/plain] v11-0007-Fix-VACUUM-s-handling-of-TOAST-storage-parameter.patch (13.0K, ../../an0Sz84q2aQQnqX5@nathan/8-v11-0007-Fix-VACUUM-s-handling-of-TOAST-storage-parameter.patch)
download | inline diff:
From 3410dc14e074aae2f7dc3d21545a5699fcddbbd6 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 11:34:55 -0500
Subject: [PATCH v11 7/8] Fix VACUUM's handling of TOAST storage parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Presently, VACUUM does no such thing. It reads the TOAST table's
own reloptions, which hold only what was set through toast.*, so
vacuum_index_cleanup or vacuum_truncate set on the main table has
no effect on its TOAST table.
To fix, add merge_toast_reloptions(), which walks the parse table
for StdRdOptions and takes the main table's value for anything the
TOAST table left at its default. vacuum_rel() hands the main
table's parameters down when recursing to a TOAST table, and it
merges them into a copy of the TOAST table's parameters before the
values are used. This doesn't help VACUUM against a TOAST table
directly (e.g., "VACUUM pg_toast.pg_toast_5432"), but that's
probably okay because it's not the main supported way to vacuum a
TOAST table (see VACUUM's PROCESS_MAIN and PROCESS_TOAST options).
A follow-up commit will do the same for autovacuum. While this is
a bug fix, it's too intrusive for back-patching, but the issue
seems to have gone unnoticed for a very long time, anyway.
---
src/backend/access/common/reloptions.c | 93 +++++++++++++++++++
src/backend/commands/vacuum.c | 45 ++++++---
src/backend/postmaster/autovacuum.c | 1 +
src/include/access/reloptions.h | 2 +
src/include/commands/vacuum.h | 7 ++
.../injection_points/expected/vacuum.out | 11 +++
.../modules/injection_points/sql/vacuum.sql | 8 ++
7 files changed, 156 insertions(+), 11 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 4548eb02676..263ebf44a45 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2114,6 +2114,99 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
lengthof(stdRdOptionsTab));
}
+/*
+ * find reloption
+ * Look up a reloption of the given kind by name.
+ *
+ * Returns NULL if no such option can be set on relations of that kind. Note
+ * that names are unique only within a kind; "fillfactor", for example, is
+ * declared separately for heaps and for each index access method.
+ */
+static relopt_gen *
+find_reloption(const char *name, relopt_kind kind)
+{
+ if (need_initialization)
+ initialize_reloptions();
+
+ for (int i = 0; relOpts[i]; i++)
+ {
+ if ((relOpts[i]->kinds & kind) != 0 &&
+ strcmp(relOpts[i]->name, name) == 0)
+ return relOpts[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * merge_toast_reloptions
+ * Fill in a TOAST table's unset options from its main table's.
+ *
+ * Any option that may be set on a TOAST table but was not is taken from
+ * main_opts. Either argument may be NULL; if both are, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * An option counts as unset while it still holds the default declared for it
+ * above, which works because nothing a TOAST table accepts has a default the
+ * user could also set (see assert_toast_defaults_unsettable()).
+ *
+ * NB: This destructively modifies toast_opts, and what it returns may be
+ * either argument, so the caller must know which of the two it owns.
+ */
+StdRdOptions *
+merge_toast_reloptions(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ if (toast_opts == NULL)
+ return main_opts;
+ if (main_opts == NULL)
+ return toast_opts;
+
+ for (int i = 0; i < lengthof(stdRdOptionsTab); i++)
+ {
+ const relopt_parse_elt *elem = &stdRdOptionsTab[i];
+ relopt_gen *gen;
+ char *toast_val;
+ char *main_val;
+
+ /* Skip anything that cannot be set on a TOAST table. */
+ gen = find_reloption(elem->optname, RELOPT_KIND_TOAST);
+ if (gen == NULL)
+ continue;
+
+ toast_val = (char *) toast_opts + elem->offset;
+ main_val = (char *) main_opts + elem->offset;
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+ if (*(pg_ternary *) toast_val == PG_TERNARY_UNSET)
+ *(pg_ternary *) toast_val = *(pg_ternary *) main_val;
+ break;
+
+ case RELOPT_TYPE_INT:
+ if (*(int *) toast_val == ((relopt_int *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ case RELOPT_TYPE_REAL:
+ if (*(double *) toast_val == ((relopt_real *) gen)->default_val)
+ *(double *) toast_val = *(double *) main_val;
+ break;
+
+ case RELOPT_TYPE_ENUM:
+ if (*(int *) toast_val == ((relopt_enum *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ default:
+ elog(ERROR, "reloption \"%s\" has a type a TOAST table cannot inherit",
+ elem->optname);
+ }
+ }
+
+ return toast_opts;
+}
+
/*
* build_reloptions
*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 52116c02b59..8fe48920c9f 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -31,6 +31,7 @@
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
+#include "access/reloptions.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
@@ -187,6 +188,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_relopts = NULL;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2039,6 +2041,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
int save_sec_context;
int save_nestlevel;
VacuumParams toast_vacuum_params;
+ StdRdOptions *relopts;
+ StdRdOptions relopts_copy;
/*
* This function scribbles on the parameters, so make a copy early to
@@ -2201,6 +2205,19 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ /*
+ * A TOAST table takes any storage parameter it accepts but does not set
+ * from its main table, whose parameters the caller handed down for that
+ * purpose. Merge them into a copy of our own.
+ *
+ * We are careful to avoid scribbling on the relcache's copy of the
+ * options.
+ */
+ if (rel->rd_options)
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ relopts = merge_toast_reloptions(rel->rd_options ? &relopts_copy : NULL,
+ params.main_relopts);
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2209,11 +2226,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
StdRdOptIndexCleanup vacuum_index_cleanup;
- if (rel->rd_options == NULL)
+ if (relopts == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = relopts->vacuum_index_cleanup;
switch (vacuum_index_cleanup)
{
@@ -2245,10 +2261,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
- params.max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ if (relopts != NULL && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ params.max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
@@ -2256,11 +2270,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
- StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
-
- if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
+ if (relopts && relopts->vacuum_truncate != PG_TERNARY_UNSET)
{
- if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ if (relopts->vacuum_truncate == PG_TERNARY_TRUE)
params.truncate = VACOPTVALUE_ENABLED;
else
params.truncate = VACOPTVALUE_DISABLED;
@@ -2293,6 +2305,17 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
else
toast_relid = InvalidOid;
+ /*
+ * Hand our storage parameters down for the TOAST table to inherit. Take
+ * a copy while we still have the relation open; the relcache entry can go
+ * away once we close it.
+ */
+ if (OidIsValid(toast_relid) && rel->rd_options)
+ {
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ toast_vacuum_params.main_relopts = &relopts_copy;
+ }
+
/*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also lock down security-restricted operations and
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a99f7108636..1610c60ec4b 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2891,6 +2891,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ tab->at_params.main_relopts = NULL;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..6c599382f02 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -247,6 +247,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options,
extern bytea *default_reloptions(Datum reloptions, bool validate,
relopt_kind kind);
+extern struct StdRdOptions *merge_toast_reloptions(struct StdRdOptions *toast_opts,
+ struct StdRdOptions *main_opts);
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..2ebdd5e40ab 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,13 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * When vacuuming a TOAST table, its main table's storage parameters, for
+ * the TOAST table to inherit anything it doesn't set itself. NULL if the
+ * main table has none, or if this isn't a TOAST table.
+ */
+ struct StdRdOptions *main_relopts;
} VacuumParams;
/*
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..caf0cc232b4 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,9 +79,20 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..0a43e14c928 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,9 +33,17 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
--
2.50.1 (Apple Git-155)
[text/plain] v11-0008-Fix-autovacuum-s-handling-of-TOAST-storage-param.patch (13.4K, ../../an0Sz84q2aQQnqX5@nathan/9-v11-0008-Fix-autovacuum-s-handling-of-TOAST-storage-param.patch)
download | inline diff:
From ccb289cf086d91fb3539354f90ce14a018bcd75a Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 12:02:21 -0500
Subject: [PATCH v11 8/8] Fix autovacuum's handling of TOAST storage
parameters.
The previous commit made VACUUM apply a main table's storage
parameters to its TOAST table, as CREATE TABLE has long documented.
Autovacuum still gets this wrong in two ways. It falls back to the
main table's autovacuum parameters only when the TOAST table has no
reloptions at all, so setting a single toast.* parameter silently
discards the rest. And it never consults the main table for the
parameters that only VACUUM reads, since it leaves those for
vacuum_rel() to resolve from the TOAST table's own reloptions.
To fix, combine the two sets with merge_toast_reloptions() rather
than choosing between them, and hand the main table's parameters
down to vacuum_rel() the way VACUUM now does.
pg_stat_autovacuum_scores uses the combined parameters for TOAST
tables as well; it has to collect the main relations' parameters
before it can do so, so it now makes a preliminary pass over
pg_class.
An existing shortcoming that this patch only makes worse is that
autovacuum remains oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/postmaster/autovacuum.c | 146 +++++++++++++-----
.../injection_points/expected/vacuum.out | 22 ++-
.../modules/injection_points/sql/vacuum.sql | 12 +-
3 files changed, 135 insertions(+), 45 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 1610c60ec4b..9a050aca669 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1913,6 +1913,35 @@ TableToProcessComparator(const ListCell *a, const ListCell *b)
return (t2->score < t1->score) ? -1 : (t2->score > t1->score) ? 1 : 0;
}
+/*
+ * get_effective_relopts
+ * Fetch the storage parameters that apply to a relation.
+ *
+ * This looks up and, for TOAST tables, merges reloptions. Returns NULL if the
+ * relation neither sets nor inherits anything. main_opts returns main table's
+ * reloptions, if any. free_relopts returns whether return value is palloc'd.
+ */
+static StdRdOptions *
+get_effective_relopts(HeapTuple tup, TupleDesc desc, HTAB *toast_map,
+ StdRdOptions **main_opts, bool *free_relopts)
+{
+ Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ av_relation *hentry = NULL;
+
+ /* look up our relopts */
+ relopts = (StdRdOptions *) extractRelOptions(tup, desc, NULL);
+ *free_relopts = (relopts != NULL);
+
+ /* if we're a TOAST table, look up our parent's relopts, too */
+ if (classForm->relkind == RELKIND_TOASTVALUE)
+ hentry = hash_search(toast_map, &classForm->oid, HASH_FIND, NULL);
+ *main_opts = hentry ? &hentry->ar_reloptions : NULL;
+
+ /* return the merged reloptions */
+ return merge_toast_reloptions(relopts, *main_opts);
+}
+
/*
* Process a database table-by-table
*
@@ -2015,9 +2044,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2128,7 +2157,8 @@ do_autovacuum(void)
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
+ bool free_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2142,22 +2172,9 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ /* fetch reloptions -- merge any unset options from the main rel */
+ relopts = get_effective_relopts(tuple, pg_class_desc, table_toast_map,
+ &main_relopts, &free_relopts);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2782,7 +2799,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
autovac_table *tab = NULL;
bool wraparound;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
+ bool free_relopts;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2792,21 +2810,11 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
- relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE)
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ relopts = get_effective_relopts(classTup, pg_class_desc, table_toast_map,
+ &main_relopts, &free_relopts);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2891,7 +2899,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
- tab->at_params.main_relopts = NULL;
+ tab->at_params.main_relopts = main_relopts;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
@@ -2950,8 +2958,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* being forced because of Xid or multixact wraparound.
*
* relopts is a pointer to the StdRdOptions options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * case of a plain table, or merged with the main table's for a TOAST table),
+ * NULL if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3609,6 +3617,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
TableScanDesc scan;
HeapTuple tup;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
InitMaterializedSRF(fcinfo, 0);
@@ -3617,13 +3627,63 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
rel = table_open(RelationRelationId, AccessShareLock);
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parents' reloptions, we can generate the results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ StdRdOptions *main_relopts;
+ bool free_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3639,13 +3699,16 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relopts = get_effective_relopts(tup, RelationGetDescr(rel),
+ table_toast_map, &main_relopts,
+ &free_relopts);
+
relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (relopts)
+ if (free_relopts)
pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
@@ -3663,6 +3726,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index caf0cc232b4..196be88c6b8 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -80,15 +80,33 @@ NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
-- A TOAST table inherits what it does not set from its main table.
-CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
(autovacuum_enabled=false,
vacuum_index_cleanup=false,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_truncate=false, toast.vacuum_truncate=true,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0);
VACUUM vac_tab_toast_inherit;
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-disabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
NOTICE: notice triggered for injection point vacuum-truncate-enabled
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
+ over
+------
+ t
+(1 row)
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 0a43e14c928..dc5f25dbb58 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -34,11 +34,19 @@ VACUUM vac_tab_auto;
RESET vacuum_truncate;
-- A TOAST table inherits what it does not set from its main table.
-CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+CREATE TABLE vac_tab_toast_inherit(i int, j text STORAGE EXTERNAL) WITH
(autovacuum_enabled=false,
vacuum_index_cleanup=false,
- vacuum_truncate=false, toast.vacuum_truncate=true);
+ vacuum_truncate=false, toast.vacuum_truncate=true,
+ autovacuum_vacuum_insert_threshold=1,
+ autovacuum_vacuum_insert_scale_factor=0);
VACUUM vac_tab_toast_inherit;
+INSERT INTO vac_tab_toast_inherit
+ VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+SELECT pg_stat_force_next_flush();
+SELECT s.vacuum_insert_score > 1 AS over
+ FROM pg_class c, pg_stat_autovacuum_scores s
+ WHERE s.relid = c.reltoastrelid AND c.relname = 'vac_tab_toast_inherit';
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
--
2.50.1 (Apple Git-155)
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-13 06:28 Michael Paquier <michael@paquier.xyz>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 44+ messages in thread
From: Michael Paquier @ 2026-08-13 06:28 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: Sami Imseih <samimseih@gmail.com>; Greg Burd <greg@burd.me>; solai v <solai.cdac@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; pgsql-hackers
On Wed, Aug 12, 2026 at 07:41:51PM -0500, Nathan Bossart wrote:
> Here's a v11 in which I've tried to fix the compiler warning that cfbot is
> complaining about. No other changes.
I have put my eyes on the v11 series.
Not much to offer about 0001. extract_autovac_opts() dates back from
2009, where the per-table autovacuum reloptions have been added by
Alvaro in 834a6da4f72d. I thought that this was newer than that.
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
This bit was in 0002. Making the unset ternary state the same as
enabled should work.
Not much to say about 0003 and CLEANUP_NOT_SET matching to a
VACOPTVALUE_AUTO.
- if (!found)
- {
- /* hash_search already filled in the key */
- hentry->ar_relid = relid;
- hentry->ar_hasrelopts = false;
- if (relopts != NULL)
- {
In 0004, I was wondering if this makes the code weaker on some
aspects, because we are switching from a logic where we always had
an entry in the mapping hashtable for a main relation with a TOAST
table to a logic where a NULL entry could mean either:
- Main relation has no TOAST table.
- Main relation has a TOAST table but no reloptions to inherit from.
Before that the difference was made with ar_hasrelopts being set or
not. I cannot think of anything on top of my mind, but I'm also
wondering if it could be better to always have an entry if a main
relation has a TOAST table, just keep the ar_reloptions to NULL and
rely on that to decide if there are options to inherit, acting as a
replacement of ar_hasrelopts.
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
In 0005, this one is log_autovacuum_min_duration. I'd wish for a
cleaner way to mark that than what looks like to me a default_val, but
well.. That's not new.
Nothing to say about 0006, I saw the link with 0007.
+ * When vacuuming a TOAST table, its main table's storage parameters, for
+ * the TOAST table to inherit anything it doesn't set itself. NULL if the
+ * main table has none, or if this isn't a TOAST table.
In 0007, that may be just me but I am having a hard time parsing that,
especially the " to inherit anything it doesn't set itself". Okay,
this means that this is only set when dealing with a TOAST table, to
track the reloptions of its parent relation.
+ if (rel->rd_options)
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
[...]
+ * NB: This destructively modifies toast_opts, and what it returns may be
+ * either argument, so the caller must know which of the two it owns.
Hmm. I am not really cool with this as an API contract. That can
bite. That's not re-entrant, to begin with, and on top of that this
function returns the merged result. It would be saner to create a
copy, and return the copy as a result, copy that we do anyway before
the sole caller of the function with a memcpy(). :)
+ /* if we're a TOAST table, look up our parent's relopts, too */
+ if (classForm->relkind == RELKIND_TOASTVALUE)
+ hentry = hash_search(toast_map, &classForm->oid, HASH_FIND, NULL);
+ *main_opts = hentry ? &hentry->ar_reloptions : NULL;
+
+ /* return the merged reloptions */
+ return merge_toast_reloptions(relopts, *main_opts);
Hmm. We have three callers of get_effective_relopts(), and some paths
can call it for a main relation, meaning that the
merge_toast_reloptions() makes little sense because there is nothing
to merge. Should this enforce a check so as we try to merge
reloptions only when dealing with a toast relation, or should the
callers for that by themselves based on the classForm->relkind?
In 0008, some tests would be nice for the autovacuum case, at least.
That would mean a TAP test to check a bit what do_autovacuum() does,
and now the SQL test in injection_points only looks after
pg_stat_get_autovacuum_scores(). I am honestly puzzled by the reason
why this is added inside injection_points at all. There is no
dependency to a point, and no new information with the NOTICE
messages. A better location would fit better the purpose of the score
test.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../an1jsPckxubEKa2s@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-13 16:41 Nathan Bossart <nathandbossart@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 2 replies; 44+ messages in thread
From: Nathan Bossart @ 2026-08-13 16:41 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Sami Imseih <samimseih@gmail.com>; Greg Burd <greg@burd.me>; solai v <solai.cdac@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; pgsql-hackers
On Thu, Aug 13, 2026 at 03:28:23PM +0900, Michael Paquier wrote:
> I have put my eyes on the v11 series.
Appreciate the review.
> Not much to offer about 0001. extract_autovac_opts() dates back from
> 2009, where the per-table autovacuum reloptions have been added by
> Alvaro in 834a6da4f72d. I thought that this was newer than that.
>
> + av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
>
> This bit was in 0002. Making the unset ternary state the same as
> enabled should work.
>
> Not much to say about 0003 and CLEANUP_NOT_SET matching to a
> VACOPTVALUE_AUTO.
I'll plan on committing these soon to get them out of the way.
> - if (!found)
> - {
> - /* hash_search already filled in the key */
> - hentry->ar_relid = relid;
> - hentry->ar_hasrelopts = false;
> - if (relopts != NULL)
> - {
>
> In 0004, I was wondering if this makes the code weaker on some
> aspects, because we are switching from a logic where we always had
> an entry in the mapping hashtable for a main relation with a TOAST
> table to a logic where a NULL entry could mean either:
> - Main relation has no TOAST table.
> - Main relation has a TOAST table but no reloptions to inherit from.
> Before that the difference was made with ar_hasrelopts being set or
> not. I cannot think of anything on top of my mind, but I'm also
> wondering if it could be better to always have an entry if a main
> relation has a TOAST table, just keep the ar_reloptions to NULL and
> rely on that to decide if there are options to inherit, acting as a
> replacement of ar_hasrelopts.
I personally don't see much point in tracking additional information we
don't need. We can already tell if the table in question is a TOAST table,
so a missing entry in the hash table means that we didn't find any main
table relopts for it. *shrug*
>
> + * When vacuuming a TOAST table, its main table's storage parameters, for
> + * the TOAST table to inherit anything it doesn't set itself. NULL if the
> + * main table has none, or if this isn't a TOAST table.
>
> In 0007, that may be just me but I am having a hard time parsing that,
> especially the " to inherit anything it doesn't set itself". Okay,
> this means that this is only set when dealing with a TOAST table, to
> track the reloptions of its parent relation.
Reworded to the following in v12:
When vacuuming a TOAST table, this holds the main table's storage
parameters (or NULL if it doesn't have any). If a reloption is unset
on the TOAST table but _is_ set on the main table, we use the main
table's setting.
> + if (rel->rd_options)
> + memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
> [...]
> + * NB: This destructively modifies toast_opts, and what it returns may be
> + * either argument, so the caller must know which of the two it owns.
>
> Hmm. I am not really cool with this as an API contract. That can
> bite. That's not re-entrant, to begin with, and on top of that this
> function returns the merged result. It would be saner to create a
> copy, and return the copy as a result, copy that we do anyway before
> the sole caller of the function with a memcpy(). :)
Done in v12.
> + /* if we're a TOAST table, look up our parent's relopts, too */
> + if (classForm->relkind == RELKIND_TOASTVALUE)
> + hentry = hash_search(toast_map, &classForm->oid, HASH_FIND, NULL);
> + *main_opts = hentry ? &hentry->ar_reloptions : NULL;
> +
> + /* return the merged reloptions */
> + return merge_toast_reloptions(relopts, *main_opts);
>
> Hmm. We have three callers of get_effective_relopts(), and some paths
> can call it for a main relation, meaning that the
> merge_toast_reloptions() makes little sense because there is nothing
> to merge. Should this enforce a check so as we try to merge
> reloptions only when dealing with a toast relation, or should the
> callers for that by themselves based on the classForm->relkind?
It enforces that already. The relkind check in the function ensures that
main_opts is always NULL for non-TOAST relations, and
merge_toast_reloptions() always returns the first argument when the second
is NULL. I do think this could be called out a bit better, which I've
tried to do in v12.
> In 0008, some tests would be nice for the autovacuum case, at least.
> That would mean a TAP test to check a bit what do_autovacuum() does,
> and now the SQL test in injection_points only looks after
> pg_stat_get_autovacuum_scores(). I am honestly puzzled by the reason
> why this is added inside injection_points at all. There is no
> dependency to a point, and no new information with the NOTICE
> messages. A better location would fit better the purpose of the score
> test.
I only put it there because 0007 added a similar test, and 0007 and 0008
used to be one patch. In v12, I've tried my hand at a TAP test.
--
nathan
From 49ce2cb2cbb43c6dfd42b750fe3086b91c3c23a8 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:35:34 -0500
Subject: [PATCH v12 1/8] Remove extract_autovac_opts().
extract_autovac_opts() returned a palloc'd copy of only the
AutoVacOpts portion of a relation's reloptions. Upcoming work
needs the rest of the StdRdOptions as well, so the callers must
keep the whole struct around.
Remove the helper and have the callers obtain reloptions from
extractRelOptions() directly. av_relation now caches a
StdRdOptions instead of an AutoVacOpts, and
relation_needs_vacanalyze() takes a StdRdOptions and extracts the
autovacuum portion itself. This is preparatory refactoring with no
change in behavior.
---
src/backend/postmaster/autovacuum.c | 122 ++++++++++------------------
1 file changed, 44 insertions(+), 78 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index ee202a4b47e..db1f5d0e575 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -202,8 +202,7 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -379,7 +378,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -388,8 +387,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2037,7 +2034,7 @@ do_autovacuum(void)
while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2076,7 +2073,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2118,7 +2115,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2141,7 +2138,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2160,7 +2157,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2776,39 +2773,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc_object(AutoVacOpts);
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2828,8 +2792,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2842,9 +2806,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2853,10 +2817,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- relation_needs_vacanalyze(relid, avopts, classForm,
+ relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
DEBUG3,
&dovacuum, &doanalyze, &wraparound,
@@ -2871,6 +2835,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_vacuum_min_duration;
int log_analyze_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2982,8 +2947,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2995,7 +2960,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none.
*
@@ -3072,7 +3037,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -3086,6 +3051,7 @@ relation_needs_vacanalyze(Oid relid,
bool force_vacuum;
bool av_enabled;
bool may_free = false;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3137,45 +3103,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
@@ -3668,7 +3634,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
- AutoVacOpts *avopts;
+ StdRdOptions *relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3684,14 +3650,14 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
- relation_needs_vacanalyze(form->oid, avopts, form,
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
- pfree(avopts);
+ if (relopts)
+ pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
vals[1] = Float8GetDatum(scores.max);
--
2.55.0
From 3a8eaa617042b09e72b96cc9919b7af7cbdb1980 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:50:36 -0500
Subject: [PATCH v12 2/8] Make autovacuum_enabled a ternary reloption.
This commit reimplements autovacuum_enabled as a ternary, using the
support added in commit 4d6a66f675 and following the example of
vacuum_truncate. This changes only the internal representation: an
unset value still behaves as enabled, and the option accepts the
same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 19 +++++++++----------
src/backend/catalog/index.c | 3 ++-
src/backend/postmaster/autovacuum.c | 2 +-
src/include/utils/rel.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..79834126f2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -107,15 +107,6 @@ static relopt_bool boolRelOpts[] =
},
false
},
- {
- {
- "autovacuum_enabled",
- "Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
- ShareUpdateExclusiveLock
- },
- true
- },
{
{
"user_catalog_table",
@@ -168,6 +159,14 @@ static relopt_bool boolRelOpts[] =
static relopt_ternary ternaryRelOpts[] =
{
+ {
+ {
+ "autovacuum_enabled",
+ "Enables autovacuum in this relation",
+ RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ ShareUpdateExclusiveLock
+ }
+ },
{
{
"vacuum_truncate",
@@ -1976,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
static const relopt_parse_elt tab[] = {
{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_BOOL,
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
{"autovacuum_parallel_workers", RELOPT_TYPE_INT,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 4c5da7e5db0..ec21b83b6b8 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2900,7 +2900,8 @@ index_update_stats(Relation rel,
{
StdRdOptions *options = (StdRdOptions *) rel->rd_options;
- if (options != NULL && !options->autovacuum.enabled)
+ if (options != NULL &&
+ options->autovacuum.enabled == PG_TERNARY_FALSE)
update_stats = false;
}
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index db1f5d0e575..c8a1b66ca2e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3141,7 +3141,7 @@ relation_needs_vacanalyze(Oid relid,
? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 89c159b133f..2ee98e9c6cc 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -310,7 +310,7 @@ typedef struct ForeignKeyCacheInfo
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
{
- bool enabled;
+ pg_ternary enabled;
int autovacuum_parallel_workers;
int vacuum_threshold;
--
2.55.0
From 9bdd9bb9a7b2e5119ba518a2cd130024f2bef147 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 15:27:58 -0500
Subject: [PATCH v12 3/8] Add an "unset" value for vacuum_index_cleanup.
This commit adds a new value to StdRdOptIndexCleanup to distinguish
whether it is explicitly set, similar to ViewOptCheckOption's
VIEW_OPTION_CHECK_OPTION_NOT_SET. This changes only the internal
representation; an unset value still defaults to AUTO, and the
option accepts the same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 3 ++-
src/backend/commands/vacuum.c | 23 ++++++++++++++---------
src/include/utils/rel.h | 1 +
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79834126f2f..58eb72e2339 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -517,6 +517,7 @@ static relopt_real realRelOpts[] =
/* values from StdRdOptIndexCleanup */
static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
{
+ /* no value for NOT_SET */
{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
@@ -557,7 +558,7 @@ static relopt_enum enumRelOpts[] =
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
},
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 31f9824899c..52116c02b59 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2210,20 +2210,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
StdRdOptIndexCleanup vacuum_index_cleanup;
if (rel->rd_options == NULL)
- vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+ vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
vacuum_index_cleanup =
((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
- if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
- params.index_cleanup = VACOPTVALUE_AUTO;
- else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
- params.index_cleanup = VACOPTVALUE_ENABLED;
- else
+ switch (vacuum_index_cleanup)
{
- Assert(vacuum_index_cleanup ==
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
- params.index_cleanup = VACOPTVALUE_DISABLED;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ params.index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ params.index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
}
}
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 2ee98e9c6cc..01b6e1a872c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,6 +338,7 @@ typedef enum StdRdOptIndexCleanup
STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
} StdRdOptIndexCleanup;
typedef struct StdRdOptions
--
2.55.0
From a8cd5f5418ad2a7469b6a4c078175ec533c26b61 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 7 Aug 2026 10:55:34 -0500
Subject: [PATCH v12 4/8] Simplify autovacuum's TOAST-to-main-relation
reloptions map.
do_autovacuum() adds an entry to this map for every relation that
has a TOAST table, and uses a flag to mark the entries that have no
reloptions to pass down. The unconditional entry made sense back
when the payload was the main relation's OID: commit 7d4c9a5793
added the map so that the TOAST pass could find the parent, whose
pg_autovacuum row supplied the settings for a TOAST table that had
none of its own. Commit 834a6da4f7 replaced that lookup by copying
the parent's reloptions into the entry, which left the OID unread
and called for the flag, since a by-value AutoVacOpts cannot say
"not set".
An entry is now worth creating only when there is something to
inherit, so skip the relations that have no reloptions and let a
successful lookup speak for itself. Two relations cannot share a
TOAST table, and each pass sees a single catalog snapshot, so an
insertion can never find an existing entry; assert that rather than
quietly ignoring it.
While at it, remove two more leftovers of that same conversion:
ar_relid, unread ever since, and a NULL test on table_toast_map in
table_recheck_autovac(), which used to carry the relkind test for
get_pg_autovacuum_tuple_relid() and has had no NULL to catch since
that function went away.
---
src/backend/postmaster/autovacuum.c | 26 +++++++-------------------
1 file changed, 7 insertions(+), 19 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index c8a1b66ca2e..32efe967890 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -200,8 +200,6 @@ typedef struct avw_dbase
typedef struct av_relation
{
Oid ar_toastrelid; /* hash key - must be first */
- Oid ar_relid;
- bool ar_hasrelopts;
StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
@@ -2097,7 +2095,7 @@ do_autovacuum(void)
* this whether or not the table is going to be vacuumed, because we
* don't automatically vacuum toast tables along the parent table.
*/
- if (OidIsValid(classForm->reltoastrelid))
+ if (OidIsValid(classForm->reltoastrelid) && relopts)
{
av_relation *hentry;
bool found;
@@ -2105,19 +2103,10 @@ do_autovacuum(void)
hentry = hash_search(table_toast_map,
&classForm->reltoastrelid,
HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
- if (!found)
- {
- /* hash_search already filled in the key */
- hentry->ar_relid = relid;
- hentry->ar_hasrelopts = false;
- if (relopts != NULL)
- {
- hentry->ar_hasrelopts = true;
- memcpy(&hentry->ar_reloptions, relopts,
- sizeof(StdRdOptions));
- }
- }
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
}
/* Release stuff to avoid per-relation leakage */
@@ -2166,7 +2155,7 @@ do_autovacuum(void)
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
@@ -2809,14 +2798,13 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ else if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
--
2.55.0
From bc4ce38a771eb6f1e101d0fd353501808be825e2 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:55:44 -0500
Subject: [PATCH v12 5/8] Give TOAST storage parameters unsettable defaults.
This is preparatory work for a follow-up commit that will fill in a
TOAST table's unset storage parameters from its main table's. For
that, it must be possible to tell an option nobody set from one the
user set to the value that option happens to default to. The
parsed form of a relation's options has nowhere to record which
ones were specified, so an unset option is simply one still holding
its declared default.
Require, then, that any option a TOAST table accepts default to a
value the user cannot set. Ternaries already comply, having no
default at all, as does vacuum_index_cleanup, whose "not set"
member has no spelling. Among the numeric ones only
log_autovacuum_min_duration was in violation, defaulting to -1 with
a minimum of -1, so give it -2 instead, matching
autovacuum_vacuum_max_threshold and
autovacuum_vacuum_insert_threshold. Users won't notice; -1 already
behaved exactly as leaving the option unset does, and it still
does.
An assertion in initialize_reloptions() enforces both of the rules
the follow-up commit will rely on: that the default is unsettable,
and that anything settable on a TOAST table is settable on a heap,
since the inherited value is read from a main table's options at
the same offset. Note that this rules out bool and string options,
neither of which can express "unset".
---
src/backend/access/common/reloptions.c | 78 +++++++++++++++++++++++++-
src/backend/postmaster/autovacuum.c | 2 +-
2 files changed, 78 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 58eb72e2339..02bc8213def 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
},
{
{
@@ -613,6 +613,78 @@ static void parse_one_reloption(relopt_value *option, char *text_str,
((option).isset ? strlen((option).string_val) : \
((relopt_string *) (option).gen)->default_len)
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Verify that every option a TOAST table accepts defaults to a value the user
+ * cannot set. Nothing records which options were specified, so an option
+ * still holding its default is the only way to recognize one that was never
+ * set, and that is how a TOAST table tells which values it should take from
+ * its main table.
+ */
+static void
+assert_toast_defaults_unsettable(void)
+{
+ for (int i = 0; relOpts[i]; i++)
+ {
+ relopt_gen *gen = relOpts[i];
+
+ if ((gen->kinds & RELOPT_KIND_TOAST) == 0)
+ continue;
+
+ /*
+ * A TOAST table's value is filled in from its main table's at the
+ * same offset in the same struct, so the option must be settable on a
+ * heap too.
+ */
+ Assert((gen->kinds & RELOPT_KIND_HEAP) != 0);
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+
+ /*
+ * Ternaries carry no default, and parse_one_reloption() can
+ * only produce true or false, so PG_TERNARY_UNSET is already
+ * beyond a user's reach.
+ */
+ break;
+
+ case RELOPT_TYPE_INT:
+ {
+ relopt_int *optint = (relopt_int *) gen;
+
+ Assert(optint->default_val < optint->min ||
+ optint->default_val > optint->max);
+ break;
+ }
+
+ case RELOPT_TYPE_REAL:
+ {
+ relopt_real *optreal = (relopt_real *) gen;
+
+ Assert(optreal->default_val < optreal->min ||
+ optreal->default_val > optreal->max);
+ break;
+ }
+
+ case RELOPT_TYPE_ENUM:
+ {
+ relopt_enum *optenum = (relopt_enum *) gen;
+
+ for (relopt_enum_elt_def *elt = optenum->members;
+ elt->string_val; elt++)
+ Assert(elt->symbol_val != optenum->default_val);
+ break;
+ }
+
+ default:
+ /* Neither bools nor strings can express "unset". */
+ Assert(false);
+ }
+ }
+}
+#endif /* USE_ASSERT_CHECKING */
+
/*
* initialize_reloptions
* initialization routine, must be called before parsing
@@ -730,6 +802,10 @@ initialize_reloptions(void)
/* flag the work is complete */
need_initialization = false;
+
+#ifdef USE_ASSERT_CHECKING
+ assert_toast_defaults_unsettable();
+#endif
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 32efe967890..a99f7108636 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2832,7 +2832,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* defaults, autovacuum's own first and plain vacuum second.
*/
- /* -1 in autovac setting means use log_autovacuum_min_duration */
+ /* a negative autovac setting means use log_autovacuum_min_duration */
log_vacuum_min_duration = (avopts && avopts->log_vacuum_min_duration >= 0)
? avopts->log_vacuum_min_duration
: Log_autovacuum_min_duration;
--
2.55.0
From 038fc5d6b9a9aded9d06d957e1a49d56083d9840 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:56:02 -0500
Subject: [PATCH v12 6/8] Move the StdRdOptions parse table to file scope.
This is preparatory work for a follow-up commit that will walk the
table from another function. Nothing changes but the indentation
of its entries.
---
src/backend/access/common/reloptions.c | 115 +++++++++++++------------
1 file changed, 60 insertions(+), 55 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 02bc8213def..4548eb02676 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2044,69 +2044,74 @@ fillRelOptions(void *rdopts, Size basesize,
}
+/*
+ * Parse table for StdRdOptions, which is shared by the RELOPT_KIND_HEAP and
+ * RELOPT_KIND_TOAST kinds.
+ */
+static const relopt_parse_elt stdRdOptionsTab[] = {
+ {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
+ {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
+ {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
+ {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
+ {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
+ {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
+ {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
+ {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
+ {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
+ {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
+ {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
+ {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
+ {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
+ {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
+ {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
+ {"toast_tuple_target", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, toast_tuple_target)},
+ {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
+ {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
+ {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
+ {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"user_catalog_table", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, user_catalog_table)},
+ {"parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, parallel_workers)},
+ {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+ offsetof(StdRdOptions, vacuum_index_cleanup)},
+ {"vacuum_truncate", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, vacuum_truncate)},
+ {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
+};
+
/*
* Option parser for anything that uses StdRdOptions.
*/
bytea *
default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
- static const relopt_parse_elt tab[] = {
- {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
- {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
- {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
- {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
- {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
- {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
- {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
- {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
- {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
- {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
- {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
- {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
- {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
- {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
- {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
- {"toast_tuple_target", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, toast_tuple_target)},
- {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
- {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
- {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
- {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
- {"user_catalog_table", RELOPT_TYPE_BOOL,
- offsetof(StdRdOptions, user_catalog_table)},
- {"parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, parallel_workers)},
- {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
- offsetof(StdRdOptions, vacuum_index_cleanup)},
- {"vacuum_truncate", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, vacuum_truncate)},
- {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
- };
-
return (bytea *) build_reloptions(reloptions, validate, kind,
sizeof(StdRdOptions),
- tab, lengthof(tab));
+ stdRdOptionsTab,
+ lengthof(stdRdOptionsTab));
}
/*
--
2.55.0
From d9d70b1a5674db7be3be5f29403bcd492be35f38 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 11:34:55 -0500
Subject: [PATCH v12 7/8] Fix VACUUM's handling of TOAST storage parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Presently, VACUUM does no such thing. It reads the TOAST table's
own reloptions, which hold only what was set through toast.*, so
vacuum_index_cleanup or vacuum_truncate set on the main table has
no effect on its TOAST table.
To fix, add merge_toast_reloptions(), which walks the parse table
for StdRdOptions and takes the main table's value for anything the
TOAST table left at its default. vacuum_rel() hands the main
table's parameters down when recursing to a TOAST table, and it
merges them into a copy of the TOAST table's parameters before the
values are used. This doesn't help VACUUM against a TOAST table
directly (e.g., "VACUUM pg_toast.pg_toast_5432"), but that's
probably okay because it's not the main supported way to vacuum a
TOAST table (see VACUUM's PROCESS_MAIN and PROCESS_TOAST options).
A follow-up commit will do the same for autovacuum. While this is
a bug fix, it's too intrusive for back-patching, but the issue
seems to have gone unnoticed for a very long time, anyway.
---
src/backend/access/common/reloptions.c | 103 ++++++++++++++++++
src/backend/commands/vacuum.c | 40 +++++--
src/backend/postmaster/autovacuum.c | 1 +
src/include/access/reloptions.h | 2 +
src/include/commands/vacuum.h | 8 ++
.../injection_points/expected/vacuum.out | 11 ++
.../modules/injection_points/sql/vacuum.sql | 8 ++
7 files changed, 162 insertions(+), 11 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 4548eb02676..5ea167ad644 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2114,6 +2114,109 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
lengthof(stdRdOptionsTab));
}
+/*
+ * find reloption
+ * Look up a reloption of the given kind by name.
+ *
+ * Returns NULL if no such option can be set on relations of that kind. Note
+ * that names are unique only within a kind; "fillfactor", for example, is
+ * declared separately for heaps and for each index access method.
+ */
+static relopt_gen *
+find_reloption(const char *name, relopt_kind kind)
+{
+ if (need_initialization)
+ initialize_reloptions();
+
+ for (int i = 0; relOpts[i]; i++)
+ {
+ if ((relOpts[i]->kinds & kind) != 0 &&
+ strcmp(relOpts[i]->name, name) == 0)
+ return relOpts[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * merge_toast_reloptions
+ * Fill in a TOAST table's unset options from its main table's.
+ *
+ * Any option that may be set on a TOAST table but was not is taken from
+ * main_opts. Either argument may be NULL; if both are, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * An option counts as unset while it still holds the default declared for it
+ * above, which works because nothing a TOAST table accepts has a default the
+ * user could also set (see assert_toast_defaults_unsettable()).
+ *
+ * If the return value is not NULL, it is palloc'd.
+ */
+StdRdOptions *
+merge_toast_reloptions(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ StdRdOptions *ret;
+
+ /* if both arguments are NULL, return NULL */
+ if (toast_opts == NULL && main_opts == NULL)
+ return NULL;
+
+ /* if one argument is NULL, return the non-NULL one */
+ ret = palloc_object(StdRdOptions);
+ if (toast_opts == NULL || main_opts == NULL)
+ {
+ memcpy(ret, main_opts ? main_opts : toast_opts, sizeof(StdRdOptions));
+ return ret;
+ }
+
+ /* replace unset TOAST relopts with the main table's */
+ memcpy(ret, toast_opts, sizeof(StdRdOptions));
+ for (int i = 0; i < lengthof(stdRdOptionsTab); i++)
+ {
+ const relopt_parse_elt *elem = &stdRdOptionsTab[i];
+ relopt_gen *gen;
+ char *toast_val;
+ char *main_val;
+
+ /* skip anything that cannot be set on a TOAST table */
+ gen = find_reloption(elem->optname, RELOPT_KIND_TOAST);
+ if (gen == NULL)
+ continue;
+
+ toast_val = (char *) ret + elem->offset;
+ main_val = (char *) main_opts + elem->offset;
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+ if (*(pg_ternary *) toast_val == PG_TERNARY_UNSET)
+ *(pg_ternary *) toast_val = *(pg_ternary *) main_val;
+ break;
+
+ case RELOPT_TYPE_INT:
+ if (*(int *) toast_val == ((relopt_int *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ case RELOPT_TYPE_REAL:
+ if (*(double *) toast_val == ((relopt_real *) gen)->default_val)
+ *(double *) toast_val = *(double *) main_val;
+ break;
+
+ case RELOPT_TYPE_ENUM:
+ if (*(int *) toast_val == ((relopt_enum *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ default:
+ elog(ERROR, "reloption \"%s\" has a type a TOAST table cannot inherit",
+ elem->optname);
+ }
+ }
+
+ return ret;
+}
+
/*
* build_reloptions
*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 52116c02b59..c119983889c 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -31,6 +31,7 @@
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
+#include "access/reloptions.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
@@ -187,6 +188,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_relopts = NULL;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2039,6 +2041,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
int save_sec_context;
int save_nestlevel;
VacuumParams toast_vacuum_params;
+ StdRdOptions *relopts;
+ StdRdOptions relopts_copy;
/*
* This function scribbles on the parameters, so make a copy early to
@@ -2201,6 +2205,14 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ /*
+ * A TOAST table takes any storage parameter it accepts but does not set
+ * from its main table, whose parameters the caller handed down for that
+ * purpose. Merge them into a copy of our own.
+ */
+ relopts = merge_toast_reloptions((StdRdOptions *) rel->rd_options,
+ params.main_relopts);
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2209,11 +2221,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
StdRdOptIndexCleanup vacuum_index_cleanup;
- if (rel->rd_options == NULL)
+ if (relopts == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = relopts->vacuum_index_cleanup;
switch (vacuum_index_cleanup)
{
@@ -2245,10 +2256,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
- params.max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ if (relopts != NULL && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ params.max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
@@ -2256,11 +2265,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
- StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
-
- if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
+ if (relopts && relopts->vacuum_truncate != PG_TERNARY_UNSET)
{
- if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ if (relopts->vacuum_truncate == PG_TERNARY_TRUE)
params.truncate = VACOPTVALUE_ENABLED;
else
params.truncate = VACOPTVALUE_DISABLED;
@@ -2293,6 +2300,17 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
else
toast_relid = InvalidOid;
+ /*
+ * Hand our storage parameters down for the TOAST table to inherit. Take
+ * a copy while we still have the relation open; the relcache entry can go
+ * away once we close it.
+ */
+ if (OidIsValid(toast_relid) && rel->rd_options)
+ {
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ toast_vacuum_params.main_relopts = &relopts_copy;
+ }
+
/*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also lock down security-restricted operations and
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a99f7108636..1610c60ec4b 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2891,6 +2891,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ tab->at_params.main_relopts = NULL;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..6c599382f02 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -247,6 +247,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options,
extern bytea *default_reloptions(Datum reloptions, bool validate,
relopt_kind kind);
+extern struct StdRdOptions *merge_toast_reloptions(struct StdRdOptions *toast_opts,
+ struct StdRdOptions *main_opts);
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..55ce22f4c5b 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,14 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * When vacuuming a TOAST table, this holds the main table's storage
+ * parameters (or NULL if it doesn't have any). If a storage parameter is
+ * unset on the TOAST table but _is_ set on the main table, we use the
+ * main table's setting.
+ */
+ struct StdRdOptions *main_relopts;
} VacuumParams;
/*
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..caf0cc232b4 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,9 +79,20 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..0a43e14c928 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,9 +33,17 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
--
2.55.0
From 3d16b08296d1cfc623e65c9ba8b61ba47ce3aa6e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 12:02:21 -0500
Subject: [PATCH v12 8/8] Fix autovacuum's handling of TOAST storage
parameters.
The previous commit made VACUUM apply a main table's storage
parameters to its TOAST table, as CREATE TABLE has long documented.
Autovacuum still gets this wrong in two ways. It falls back to the
main table's autovacuum parameters only when the TOAST table has no
reloptions at all, so setting a single toast.* parameter silently
discards the rest. And it never consults the main table for the
parameters that only VACUUM reads, since it leaves those for
vacuum_rel() to resolve from the TOAST table's own reloptions.
To fix, combine the two sets with merge_toast_reloptions() rather
than choosing between them, and hand the main table's parameters
down to vacuum_rel() the way VACUUM now does.
pg_stat_autovacuum_scores uses the combined parameters for TOAST
tables as well; it has to collect the main relations' parameters
before it can do so, so it now makes a preliminary pass over
pg_class.
An existing shortcoming that this patch only makes worse is that
autovacuum remains oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/postmaster/autovacuum.c | 149 +++++++++++++-----
src/test/modules/test_autovacuum/meson.build | 1 +
.../test_autovacuum/t/002_toast_relopts.pl | 69 ++++++++
3 files changed, 177 insertions(+), 42 deletions(-)
create mode 100644 src/test/modules/test_autovacuum/t/002_toast_relopts.pl
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 1610c60ec4b..437080a0a41 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1913,6 +1913,40 @@ TableToProcessComparator(const ListCell *a, const ListCell *b)
return (t2->score < t1->score) ? -1 : (t2->score > t1->score) ? 1 : 0;
}
+/*
+ * get_effective_relopts
+ * Fetch the storage parameters that apply to a relation.
+ *
+ * This looks up the reloptions for the pg_class relation in "tup". If it is a
+ * TOAST table, we also merge in any unset reloptions with the main table's
+ * stored in "toast_map". If the relation neither sets nor inherits any
+ * reloptions, this function returns NULL. Else, a palloc'd copy of the
+ * applicable reloptions are returned.
+ *
+ * If "tup" refers to a TOAST table and the toast_map has reloptions stored for
+ * its main relation, we return a pointer to the main table's reloptions via
+ * *main_opts. Else, main_opts is set to NULL.
+ */
+static StdRdOptions *
+get_effective_relopts(HeapTuple tup, TupleDesc desc, HTAB *toast_map,
+ StdRdOptions **main_opts)
+{
+ Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ av_relation *hentry = NULL;
+
+ /* look up our relopts */
+ relopts = (StdRdOptions *) extractRelOptions(tup, desc, NULL);
+
+ /* if we're a TOAST table, look up our parent's relopts, too */
+ if (classForm->relkind == RELKIND_TOASTVALUE)
+ hentry = hash_search(toast_map, &classForm->oid, HASH_FIND, NULL);
+ *main_opts = hentry ? &hentry->ar_reloptions : NULL;
+
+ /* return the merged reloptions */
+ return merge_toast_reloptions(relopts, *main_opts);
+}
+
/*
* Process a database table-by-table
*
@@ -2015,9 +2049,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2128,7 +2162,7 @@ do_autovacuum(void)
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2142,22 +2176,9 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ /* fetch reloptions -- merge any unset options from the main rel */
+ relopts = get_effective_relopts(tuple, pg_class_desc, table_toast_map,
+ &main_relopts);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2176,7 +2197,7 @@ do_autovacuum(void)
}
/* Release stuff to avoid leakage */
- if (free_relopts)
+ if (relopts)
pfree(relopts);
}
@@ -2782,7 +2803,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
autovac_table *tab = NULL;
bool wraparound;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2792,21 +2813,11 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
- relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE)
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ relopts = get_effective_relopts(classTup, pg_class_desc, table_toast_map,
+ &main_relopts);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2891,7 +2902,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
- tab->at_params.main_relopts = NULL;
+ tab->at_params.main_relopts = main_relopts;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
@@ -2936,7 +2947,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_relopts)
+ if (relopts)
pfree(relopts);
heap_freetuple(classTup);
return tab;
@@ -2950,8 +2961,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* being forced because of Xid or multixact wraparound.
*
* relopts is a pointer to the StdRdOptions options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * case of a plain table, or merged with the main table's for a TOAST table),
+ * NULL if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3609,6 +3620,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
TableScanDesc scan;
HeapTuple tup;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
InitMaterializedSRF(fcinfo, 0);
@@ -3617,13 +3630,62 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
rel = table_open(RelationRelationId, AccessShareLock);
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parents' reloptions, we can generate the results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ StdRdOptions *main_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3639,7 +3701,9 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relopts = get_effective_relopts(tup, RelationGetDescr(rel),
+ table_toast_map, &main_relopts);
+
relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
@@ -3663,6 +3727,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
diff --git a/src/test/modules/test_autovacuum/meson.build b/src/test/modules/test_autovacuum/meson.build
index 86e392bc0de..970b9aaae4b 100644
--- a/src/test/modules/test_autovacuum/meson.build
+++ b/src/test/modules/test_autovacuum/meson.build
@@ -10,6 +10,7 @@ tests += {
},
'tests': [
't/001_parallel_autovacuum.pl',
+ 't/002_toast_relopts.pl',
],
},
}
diff --git a/src/test/modules/test_autovacuum/t/002_toast_relopts.pl b/src/test/modules/test_autovacuum/t/002_toast_relopts.pl
new file mode 100644
index 00000000000..ba7596cc82b
--- /dev/null
+++ b/src/test/modules/test_autovacuum/t/002_toast_relopts.pl
@@ -0,0 +1,69 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test autovacuum's handling of TOAST storage parameters
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Create a test node with autovacuum disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+autovacuum = off
+autovacuum_naptime = '1s'
+});
+$node->start;
+
+# Create TOAST table that is eligible for autovacuum due to inherited relopts.
+$node->safe_psql(
+ 'postgres', qq{
+ CREATE TABLE toast_relopts (i int, j text STORAGE EXTERNAL) WITH
+ (autovacuum_enabled = false, toast.autovacuum_enabled = true,
+ autovacuum_vacuum_threshold = 1,
+ autovacuum_vacuum_scale_factor = 0,
+ autovacuum_vacuum_insert_threshold = 1,
+ autovacuum_vacuum_insert_scale_factor = 0,
+ vacuum_truncate = false);
+ INSERT INTO toast_relopts VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+ SELECT pg_stat_force_next_flush();
+});
+
+# Get TOAST table's OID for following commands.
+my $toast = $node->safe_psql('postgres',
+ "SELECT reltoastrelid::regclass FROM pg_class WHERE oid = 'toast_relopts'::regclass"
+);
+
+# Verify scores view used inherited insert threshold.
+is( $node->safe_psql(
+ 'postgres', qq{
+ SELECT vacuum_insert_score > 1 FROM pg_stat_autovacuum_scores
+ WHERE relid = '$toast'::regclass
+}),
+ 't',
+ 'inherited insert threshold in pg_stat_autovacuum_scores');
+
+# Delete all rows so that we can verify inherited vacuum_truncate takes effect.
+$node->safe_psql('postgres', 'DELETE FROM toast_relopts');
+
+# Enable autovacuum.
+$node->append_conf('postgresql.conf', 'autovacuum = on');
+$node->reload;
+
+# Wait until autovacuum processes the table.
+ok( $node->poll_query_until(
+ 'postgres', qq{
+ SELECT last_autovacuum IS NOT NULL FROM pg_stat_all_tables
+ WHERE relid = '$toast'::regclass
+}),
+ 'autovacuum of a TOAST table with inherited thresholds');
+
+# Verify autovacuum didn't truncate the table.
+is($node->safe_psql('postgres', "SELECT pg_relation_size('$toast') > 0"),
+ 't', 'inherited vacuum_truncate');
+
+$node->stop;
+done_testing();
--
2.55.0
Attachments:
[text/plain] v12-0001-Remove-extract_autovac_opts.patch (11.5K, ../../an3z11i-Y0buWpol@nathan/2-v12-0001-Remove-extract_autovac_opts.patch)
download | inline diff:
From 49ce2cb2cbb43c6dfd42b750fe3086b91c3c23a8 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:35:34 -0500
Subject: [PATCH v12 1/8] Remove extract_autovac_opts().
extract_autovac_opts() returned a palloc'd copy of only the
AutoVacOpts portion of a relation's reloptions. Upcoming work
needs the rest of the StdRdOptions as well, so the callers must
keep the whole struct around.
Remove the helper and have the callers obtain reloptions from
extractRelOptions() directly. av_relation now caches a
StdRdOptions instead of an AutoVacOpts, and
relation_needs_vacanalyze() takes a StdRdOptions and extracts the
autovacuum portion itself. This is preparatory refactoring with no
change in behavior.
---
src/backend/postmaster/autovacuum.c | 122 ++++++++++------------------
1 file changed, 44 insertions(+), 78 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index ee202a4b47e..db1f5d0e575 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -202,8 +202,7 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -379,7 +378,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -388,8 +387,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2037,7 +2034,7 @@ do_autovacuum(void)
while ((tuple = heap_getnext(relScan, ForwardScanDirection)) != NULL)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2076,7 +2073,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
/* Check if it needs vacuum or analyze */
relation_needs_vacanalyze(relid, relopts, classForm,
@@ -2118,7 +2115,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2141,7 +2138,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2160,7 +2157,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2776,39 +2773,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc_object(AutoVacOpts);
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2828,8 +2792,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2842,9 +2806,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2853,10 +2817,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- relation_needs_vacanalyze(relid, avopts, classForm,
+ relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
DEBUG3,
&dovacuum, &doanalyze, &wraparound,
@@ -2871,6 +2835,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_vacuum_min_duration;
int log_analyze_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2982,8 +2947,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2995,7 +2960,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none.
*
@@ -3072,7 +3037,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
int elevel,
@@ -3086,6 +3051,7 @@ relation_needs_vacanalyze(Oid relid,
bool force_vacuum;
bool av_enabled;
bool may_free = false;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3137,45 +3103,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
@@ -3668,7 +3634,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
- AutoVacOpts *avopts;
+ StdRdOptions *relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3684,14 +3650,14 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
- relation_needs_vacanalyze(form->oid, avopts, form,
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
- pfree(avopts);
+ if (relopts)
+ pfree(relopts);
vals[0] = ObjectIdGetDatum(form->oid);
vals[1] = Float8GetDatum(scores.max);
--
2.55.0
[text/plain] v12-0002-Make-autovacuum_enabled-a-ternary-reloption.patch (3.6K, ../../an3z11i-Y0buWpol@nathan/3-v12-0002-Make-autovacuum_enabled-a-ternary-reloption.patch)
download | inline diff:
From 3a8eaa617042b09e72b96cc9919b7af7cbdb1980 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 14:50:36 -0500
Subject: [PATCH v12 2/8] Make autovacuum_enabled a ternary reloption.
This commit reimplements autovacuum_enabled as a ternary, using the
support added in commit 4d6a66f675 and following the example of
vacuum_truncate. This changes only the internal representation: an
unset value still behaves as enabled, and the option accepts the
same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 19 +++++++++----------
src/backend/catalog/index.c | 3 ++-
src/backend/postmaster/autovacuum.c | 2 +-
src/include/utils/rel.h | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 3e832c3797e..79834126f2f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -107,15 +107,6 @@ static relopt_bool boolRelOpts[] =
},
false
},
- {
- {
- "autovacuum_enabled",
- "Enables autovacuum in this relation",
- RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
- ShareUpdateExclusiveLock
- },
- true
- },
{
{
"user_catalog_table",
@@ -168,6 +159,14 @@ static relopt_bool boolRelOpts[] =
static relopt_ternary ternaryRelOpts[] =
{
+ {
+ {
+ "autovacuum_enabled",
+ "Enables autovacuum in this relation",
+ RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+ ShareUpdateExclusiveLock
+ }
+ },
{
{
"vacuum_truncate",
@@ -1976,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
static const relopt_parse_elt tab[] = {
{"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_BOOL,
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
{"autovacuum_parallel_workers", RELOPT_TYPE_INT,
offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 4c5da7e5db0..ec21b83b6b8 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2900,7 +2900,8 @@ index_update_stats(Relation rel,
{
StdRdOptions *options = (StdRdOptions *) rel->rd_options;
- if (options != NULL && !options->autovacuum.enabled)
+ if (options != NULL &&
+ options->autovacuum.enabled == PG_TERNARY_FALSE)
update_stats = false;
}
else
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index db1f5d0e575..c8a1b66ca2e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3141,7 +3141,7 @@ relation_needs_vacanalyze(Oid relid,
? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (avopts ? avopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled != PG_TERNARY_FALSE : true);
av_enabled &= AutoVacuumingActive();
relfrozenxid = classForm->relfrozenxid;
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 89c159b133f..2ee98e9c6cc 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -310,7 +310,7 @@ typedef struct ForeignKeyCacheInfo
/* autovacuum-related reloptions. */
typedef struct AutoVacOpts
{
- bool enabled;
+ pg_ternary enabled;
int autovacuum_parallel_workers;
int vacuum_threshold;
--
2.55.0
[text/plain] v12-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch (3.5K, ../../an3z11i-Y0buWpol@nathan/4-v12-0003-Add-an-unset-value-for-vacuum_index_cleanup.patch)
download | inline diff:
From 9bdd9bb9a7b2e5119ba518a2cd130024f2bef147 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 8 Jun 2026 15:27:58 -0500
Subject: [PATCH v12 3/8] Add an "unset" value for vacuum_index_cleanup.
This commit adds a new value to StdRdOptIndexCleanup to distinguish
whether it is explicitly set, similar to ViewOptCheckOption's
VIEW_OPTION_CHECK_OPTION_NOT_SET. This changes only the internal
representation; an unset value still defaults to AUTO, and the
option accepts the same input as before.
This is preparatory work for a follow-up commit that will make use
of the new "unset" state.
---
src/backend/access/common/reloptions.c | 3 ++-
src/backend/commands/vacuum.c | 23 ++++++++++++++---------
src/include/utils/rel.h | 1 +
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 79834126f2f..58eb72e2339 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -517,6 +517,7 @@ static relopt_real realRelOpts[] =
/* values from StdRdOptIndexCleanup */
static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
{
+ /* no value for NOT_SET */
{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
@@ -557,7 +558,7 @@ static relopt_enum enumRelOpts[] =
ShareUpdateExclusiveLock
},
StdRdOptIndexCleanupValues,
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
},
{
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 31f9824899c..52116c02b59 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2210,20 +2210,25 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
StdRdOptIndexCleanup vacuum_index_cleanup;
if (rel->rd_options == NULL)
- vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+ vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
vacuum_index_cleanup =
((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
- if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
- params.index_cleanup = VACOPTVALUE_AUTO;
- else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
- params.index_cleanup = VACOPTVALUE_ENABLED;
- else
+ switch (vacuum_index_cleanup)
{
- Assert(vacuum_index_cleanup ==
- STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
- params.index_cleanup = VACOPTVALUE_DISABLED;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ params.index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ params.index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET:
+ params.index_cleanup = VACOPTVALUE_AUTO;
+ break;
}
}
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 2ee98e9c6cc..01b6e1a872c 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -338,6 +338,7 @@ typedef enum StdRdOptIndexCleanup
STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
+ STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET,
} StdRdOptIndexCleanup;
typedef struct StdRdOptions
--
2.55.0
[text/plain] v12-0004-Simplify-autovacuum-s-TOAST-to-main-relation-rel.patch (3.8K, ../../an3z11i-Y0buWpol@nathan/5-v12-0004-Simplify-autovacuum-s-TOAST-to-main-relation-rel.patch)
download | inline diff:
From a8cd5f5418ad2a7469b6a4c078175ec533c26b61 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 7 Aug 2026 10:55:34 -0500
Subject: [PATCH v12 4/8] Simplify autovacuum's TOAST-to-main-relation
reloptions map.
do_autovacuum() adds an entry to this map for every relation that
has a TOAST table, and uses a flag to mark the entries that have no
reloptions to pass down. The unconditional entry made sense back
when the payload was the main relation's OID: commit 7d4c9a5793
added the map so that the TOAST pass could find the parent, whose
pg_autovacuum row supplied the settings for a TOAST table that had
none of its own. Commit 834a6da4f7 replaced that lookup by copying
the parent's reloptions into the entry, which left the OID unread
and called for the flag, since a by-value AutoVacOpts cannot say
"not set".
An entry is now worth creating only when there is something to
inherit, so skip the relations that have no reloptions and let a
successful lookup speak for itself. Two relations cannot share a
TOAST table, and each pass sees a single catalog snapshot, so an
insertion can never find an existing entry; assert that rather than
quietly ignoring it.
While at it, remove two more leftovers of that same conversion:
ar_relid, unread ever since, and a NULL test on table_toast_map in
table_recheck_autovac(), which used to carry the relkind test for
get_pg_autovacuum_tuple_relid() and has had no NULL to catch since
that function went away.
---
src/backend/postmaster/autovacuum.c | 26 +++++++-------------------
1 file changed, 7 insertions(+), 19 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index c8a1b66ca2e..32efe967890 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -200,8 +200,6 @@ typedef struct avw_dbase
typedef struct av_relation
{
Oid ar_toastrelid; /* hash key - must be first */
- Oid ar_relid;
- bool ar_hasrelopts;
StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
@@ -2097,7 +2095,7 @@ do_autovacuum(void)
* this whether or not the table is going to be vacuumed, because we
* don't automatically vacuum toast tables along the parent table.
*/
- if (OidIsValid(classForm->reltoastrelid))
+ if (OidIsValid(classForm->reltoastrelid) && relopts)
{
av_relation *hentry;
bool found;
@@ -2105,19 +2103,10 @@ do_autovacuum(void)
hentry = hash_search(table_toast_map,
&classForm->reltoastrelid,
HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
- if (!found)
- {
- /* hash_search already filled in the key */
- hentry->ar_relid = relid;
- hentry->ar_hasrelopts = false;
- if (relopts != NULL)
- {
- hentry->ar_hasrelopts = true;
- memcpy(&hentry->ar_reloptions, relopts,
- sizeof(StdRdOptions));
- }
- }
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
}
/* Release stuff to avoid per-relation leakage */
@@ -2166,7 +2155,7 @@ do_autovacuum(void)
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
@@ -2809,14 +2798,13 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ else if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
--
2.55.0
[text/plain] v12-0005-Give-TOAST-storage-parameters-unsettable-default.patch (5.0K, ../../an3z11i-Y0buWpol@nathan/6-v12-0005-Give-TOAST-storage-parameters-unsettable-default.patch)
download | inline diff:
From bc4ce38a771eb6f1e101d0fd353501808be825e2 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:55:44 -0500
Subject: [PATCH v12 5/8] Give TOAST storage parameters unsettable defaults.
This is preparatory work for a follow-up commit that will fill in a
TOAST table's unset storage parameters from its main table's. For
that, it must be possible to tell an option nobody set from one the
user set to the value that option happens to default to. The
parsed form of a relation's options has nowhere to record which
ones were specified, so an unset option is simply one still holding
its declared default.
Require, then, that any option a TOAST table accepts default to a
value the user cannot set. Ternaries already comply, having no
default at all, as does vacuum_index_cleanup, whose "not set"
member has no spelling. Among the numeric ones only
log_autovacuum_min_duration was in violation, defaulting to -1 with
a minimum of -1, so give it -2 instead, matching
autovacuum_vacuum_max_threshold and
autovacuum_vacuum_insert_threshold. Users won't notice; -1 already
behaved exactly as leaving the option unset does, and it still
does.
An assertion in initialize_reloptions() enforces both of the rules
the follow-up commit will rely on: that the default is unsettable,
and that anything settable on a TOAST table is settable on a heap,
since the inherited value is read from a main table's options at
the same offset. Note that this rules out bool and string options,
neither of which can express "unset".
---
src/backend/access/common/reloptions.c | 78 +++++++++++++++++++++++++-
src/backend/postmaster/autovacuum.c | 2 +-
2 files changed, 78 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 58eb72e2339..02bc8213def 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
},
{
{
@@ -613,6 +613,78 @@ static void parse_one_reloption(relopt_value *option, char *text_str,
((option).isset ? strlen((option).string_val) : \
((relopt_string *) (option).gen)->default_len)
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Verify that every option a TOAST table accepts defaults to a value the user
+ * cannot set. Nothing records which options were specified, so an option
+ * still holding its default is the only way to recognize one that was never
+ * set, and that is how a TOAST table tells which values it should take from
+ * its main table.
+ */
+static void
+assert_toast_defaults_unsettable(void)
+{
+ for (int i = 0; relOpts[i]; i++)
+ {
+ relopt_gen *gen = relOpts[i];
+
+ if ((gen->kinds & RELOPT_KIND_TOAST) == 0)
+ continue;
+
+ /*
+ * A TOAST table's value is filled in from its main table's at the
+ * same offset in the same struct, so the option must be settable on a
+ * heap too.
+ */
+ Assert((gen->kinds & RELOPT_KIND_HEAP) != 0);
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+
+ /*
+ * Ternaries carry no default, and parse_one_reloption() can
+ * only produce true or false, so PG_TERNARY_UNSET is already
+ * beyond a user's reach.
+ */
+ break;
+
+ case RELOPT_TYPE_INT:
+ {
+ relopt_int *optint = (relopt_int *) gen;
+
+ Assert(optint->default_val < optint->min ||
+ optint->default_val > optint->max);
+ break;
+ }
+
+ case RELOPT_TYPE_REAL:
+ {
+ relopt_real *optreal = (relopt_real *) gen;
+
+ Assert(optreal->default_val < optreal->min ||
+ optreal->default_val > optreal->max);
+ break;
+ }
+
+ case RELOPT_TYPE_ENUM:
+ {
+ relopt_enum *optenum = (relopt_enum *) gen;
+
+ for (relopt_enum_elt_def *elt = optenum->members;
+ elt->string_val; elt++)
+ Assert(elt->symbol_val != optenum->default_val);
+ break;
+ }
+
+ default:
+ /* Neither bools nor strings can express "unset". */
+ Assert(false);
+ }
+ }
+}
+#endif /* USE_ASSERT_CHECKING */
+
/*
* initialize_reloptions
* initialization routine, must be called before parsing
@@ -730,6 +802,10 @@ initialize_reloptions(void)
/* flag the work is complete */
need_initialization = false;
+
+#ifdef USE_ASSERT_CHECKING
+ assert_toast_defaults_unsettable();
+#endif
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 32efe967890..a99f7108636 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2832,7 +2832,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* defaults, autovacuum's own first and plain vacuum second.
*/
- /* -1 in autovac setting means use log_autovacuum_min_duration */
+ /* a negative autovac setting means use log_autovacuum_min_duration */
log_vacuum_min_duration = (avopts && avopts->log_vacuum_min_duration >= 0)
? avopts->log_vacuum_min_duration
: Log_autovacuum_min_duration;
--
2.55.0
[text/plain] v12-0006-Move-the-StdRdOptions-parse-table-to-file-scope.patch (7.8K, ../../an3z11i-Y0buWpol@nathan/7-v12-0006-Move-the-StdRdOptions-parse-table-to-file-scope.patch)
download | inline diff:
From 038fc5d6b9a9aded9d06d957e1a49d56083d9840 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:56:02 -0500
Subject: [PATCH v12 6/8] Move the StdRdOptions parse table to file scope.
This is preparatory work for a follow-up commit that will walk the
table from another function. Nothing changes but the indentation
of its entries.
---
src/backend/access/common/reloptions.c | 115 +++++++++++++------------
1 file changed, 60 insertions(+), 55 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 02bc8213def..4548eb02676 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2044,69 +2044,74 @@ fillRelOptions(void *rdopts, Size basesize,
}
+/*
+ * Parse table for StdRdOptions, which is shared by the RELOPT_KIND_HEAP and
+ * RELOPT_KIND_TOAST kinds.
+ */
+static const relopt_parse_elt stdRdOptionsTab[] = {
+ {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
+ {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
+ {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
+ {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
+ {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
+ {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
+ {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
+ {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
+ {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
+ {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
+ {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
+ {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
+ {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
+ {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
+ {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
+ {"toast_tuple_target", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, toast_tuple_target)},
+ {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
+ {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
+ {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
+ {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"user_catalog_table", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, user_catalog_table)},
+ {"parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, parallel_workers)},
+ {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+ offsetof(StdRdOptions, vacuum_index_cleanup)},
+ {"vacuum_truncate", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, vacuum_truncate)},
+ {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
+};
+
/*
* Option parser for anything that uses StdRdOptions.
*/
bytea *
default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
- static const relopt_parse_elt tab[] = {
- {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
- {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
- {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
- {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
- {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
- {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
- {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
- {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
- {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
- {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
- {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
- {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
- {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
- {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
- {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
- {"toast_tuple_target", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, toast_tuple_target)},
- {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
- {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
- {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
- {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
- {"user_catalog_table", RELOPT_TYPE_BOOL,
- offsetof(StdRdOptions, user_catalog_table)},
- {"parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, parallel_workers)},
- {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
- offsetof(StdRdOptions, vacuum_index_cleanup)},
- {"vacuum_truncate", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, vacuum_truncate)},
- {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
- };
-
return (bytea *) build_reloptions(reloptions, validate, kind,
sizeof(StdRdOptions),
- tab, lengthof(tab));
+ stdRdOptionsTab,
+ lengthof(stdRdOptionsTab));
}
/*
--
2.55.0
[text/plain] v12-0007-Fix-VACUUM-s-handling-of-TOAST-storage-parameter.patch (13.1K, ../../an3z11i-Y0buWpol@nathan/8-v12-0007-Fix-VACUUM-s-handling-of-TOAST-storage-parameter.patch)
download | inline diff:
From d9d70b1a5674db7be3be5f29403bcd492be35f38 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 11:34:55 -0500
Subject: [PATCH v12 7/8] Fix VACUUM's handling of TOAST storage parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Presently, VACUUM does no such thing. It reads the TOAST table's
own reloptions, which hold only what was set through toast.*, so
vacuum_index_cleanup or vacuum_truncate set on the main table has
no effect on its TOAST table.
To fix, add merge_toast_reloptions(), which walks the parse table
for StdRdOptions and takes the main table's value for anything the
TOAST table left at its default. vacuum_rel() hands the main
table's parameters down when recursing to a TOAST table, and it
merges them into a copy of the TOAST table's parameters before the
values are used. This doesn't help VACUUM against a TOAST table
directly (e.g., "VACUUM pg_toast.pg_toast_5432"), but that's
probably okay because it's not the main supported way to vacuum a
TOAST table (see VACUUM's PROCESS_MAIN and PROCESS_TOAST options).
A follow-up commit will do the same for autovacuum. While this is
a bug fix, it's too intrusive for back-patching, but the issue
seems to have gone unnoticed for a very long time, anyway.
---
src/backend/access/common/reloptions.c | 103 ++++++++++++++++++
src/backend/commands/vacuum.c | 40 +++++--
src/backend/postmaster/autovacuum.c | 1 +
src/include/access/reloptions.h | 2 +
src/include/commands/vacuum.h | 8 ++
.../injection_points/expected/vacuum.out | 11 ++
.../modules/injection_points/sql/vacuum.sql | 8 ++
7 files changed, 162 insertions(+), 11 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 4548eb02676..5ea167ad644 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2114,6 +2114,109 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
lengthof(stdRdOptionsTab));
}
+/*
+ * find reloption
+ * Look up a reloption of the given kind by name.
+ *
+ * Returns NULL if no such option can be set on relations of that kind. Note
+ * that names are unique only within a kind; "fillfactor", for example, is
+ * declared separately for heaps and for each index access method.
+ */
+static relopt_gen *
+find_reloption(const char *name, relopt_kind kind)
+{
+ if (need_initialization)
+ initialize_reloptions();
+
+ for (int i = 0; relOpts[i]; i++)
+ {
+ if ((relOpts[i]->kinds & kind) != 0 &&
+ strcmp(relOpts[i]->name, name) == 0)
+ return relOpts[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * merge_toast_reloptions
+ * Fill in a TOAST table's unset options from its main table's.
+ *
+ * Any option that may be set on a TOAST table but was not is taken from
+ * main_opts. Either argument may be NULL; if both are, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * An option counts as unset while it still holds the default declared for it
+ * above, which works because nothing a TOAST table accepts has a default the
+ * user could also set (see assert_toast_defaults_unsettable()).
+ *
+ * If the return value is not NULL, it is palloc'd.
+ */
+StdRdOptions *
+merge_toast_reloptions(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ StdRdOptions *ret;
+
+ /* if both arguments are NULL, return NULL */
+ if (toast_opts == NULL && main_opts == NULL)
+ return NULL;
+
+ /* if one argument is NULL, return the non-NULL one */
+ ret = palloc_object(StdRdOptions);
+ if (toast_opts == NULL || main_opts == NULL)
+ {
+ memcpy(ret, main_opts ? main_opts : toast_opts, sizeof(StdRdOptions));
+ return ret;
+ }
+
+ /* replace unset TOAST relopts with the main table's */
+ memcpy(ret, toast_opts, sizeof(StdRdOptions));
+ for (int i = 0; i < lengthof(stdRdOptionsTab); i++)
+ {
+ const relopt_parse_elt *elem = &stdRdOptionsTab[i];
+ relopt_gen *gen;
+ char *toast_val;
+ char *main_val;
+
+ /* skip anything that cannot be set on a TOAST table */
+ gen = find_reloption(elem->optname, RELOPT_KIND_TOAST);
+ if (gen == NULL)
+ continue;
+
+ toast_val = (char *) ret + elem->offset;
+ main_val = (char *) main_opts + elem->offset;
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+ if (*(pg_ternary *) toast_val == PG_TERNARY_UNSET)
+ *(pg_ternary *) toast_val = *(pg_ternary *) main_val;
+ break;
+
+ case RELOPT_TYPE_INT:
+ if (*(int *) toast_val == ((relopt_int *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ case RELOPT_TYPE_REAL:
+ if (*(double *) toast_val == ((relopt_real *) gen)->default_val)
+ *(double *) toast_val = *(double *) main_val;
+ break;
+
+ case RELOPT_TYPE_ENUM:
+ if (*(int *) toast_val == ((relopt_enum *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ default:
+ elog(ERROR, "reloption \"%s\" has a type a TOAST table cannot inherit",
+ elem->optname);
+ }
+ }
+
+ return ret;
+}
+
/*
* build_reloptions
*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 52116c02b59..c119983889c 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -31,6 +31,7 @@
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
+#include "access/reloptions.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
@@ -187,6 +188,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_relopts = NULL;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2039,6 +2041,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
int save_sec_context;
int save_nestlevel;
VacuumParams toast_vacuum_params;
+ StdRdOptions *relopts;
+ StdRdOptions relopts_copy;
/*
* This function scribbles on the parameters, so make a copy early to
@@ -2201,6 +2205,14 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ /*
+ * A TOAST table takes any storage parameter it accepts but does not set
+ * from its main table, whose parameters the caller handed down for that
+ * purpose. Merge them into a copy of our own.
+ */
+ relopts = merge_toast_reloptions((StdRdOptions *) rel->rd_options,
+ params.main_relopts);
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2209,11 +2221,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
StdRdOptIndexCleanup vacuum_index_cleanup;
- if (rel->rd_options == NULL)
+ if (relopts == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = relopts->vacuum_index_cleanup;
switch (vacuum_index_cleanup)
{
@@ -2245,10 +2256,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
- params.max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ if (relopts != NULL && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ params.max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
@@ -2256,11 +2265,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
- StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
-
- if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
+ if (relopts && relopts->vacuum_truncate != PG_TERNARY_UNSET)
{
- if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ if (relopts->vacuum_truncate == PG_TERNARY_TRUE)
params.truncate = VACOPTVALUE_ENABLED;
else
params.truncate = VACOPTVALUE_DISABLED;
@@ -2293,6 +2300,17 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
else
toast_relid = InvalidOid;
+ /*
+ * Hand our storage parameters down for the TOAST table to inherit. Take
+ * a copy while we still have the relation open; the relcache entry can go
+ * away once we close it.
+ */
+ if (OidIsValid(toast_relid) && rel->rd_options)
+ {
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ toast_vacuum_params.main_relopts = &relopts_copy;
+ }
+
/*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also lock down security-restricted operations and
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index a99f7108636..1610c60ec4b 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2891,6 +2891,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ tab->at_params.main_relopts = NULL;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..6c599382f02 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -247,6 +247,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options,
extern bytea *default_reloptions(Datum reloptions, bool validate,
relopt_kind kind);
+extern struct StdRdOptions *merge_toast_reloptions(struct StdRdOptions *toast_opts,
+ struct StdRdOptions *main_opts);
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..55ce22f4c5b 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,14 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * When vacuuming a TOAST table, this holds the main table's storage
+ * parameters (or NULL if it doesn't have any). If a storage parameter is
+ * unset on the TOAST table but _is_ set on the main table, we use the
+ * main table's setting.
+ */
+ struct StdRdOptions *main_relopts;
} VacuumParams;
/*
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..caf0cc232b4 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,9 +79,20 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..0a43e14c928 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,9 +33,17 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
--
2.55.0
[text/plain] v12-0008-Fix-autovacuum-s-handling-of-TOAST-storage-param.patch (13.7K, ../../an3z11i-Y0buWpol@nathan/9-v12-0008-Fix-autovacuum-s-handling-of-TOAST-storage-param.patch)
download | inline diff:
From 3d16b08296d1cfc623e65c9ba8b61ba47ce3aa6e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 12:02:21 -0500
Subject: [PATCH v12 8/8] Fix autovacuum's handling of TOAST storage
parameters.
The previous commit made VACUUM apply a main table's storage
parameters to its TOAST table, as CREATE TABLE has long documented.
Autovacuum still gets this wrong in two ways. It falls back to the
main table's autovacuum parameters only when the TOAST table has no
reloptions at all, so setting a single toast.* parameter silently
discards the rest. And it never consults the main table for the
parameters that only VACUUM reads, since it leaves those for
vacuum_rel() to resolve from the TOAST table's own reloptions.
To fix, combine the two sets with merge_toast_reloptions() rather
than choosing between them, and hand the main table's parameters
down to vacuum_rel() the way VACUUM now does.
pg_stat_autovacuum_scores uses the combined parameters for TOAST
tables as well; it has to collect the main relations' parameters
before it can do so, so it now makes a preliminary pass over
pg_class.
An existing shortcoming that this patch only makes worse is that
autovacuum remains oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/postmaster/autovacuum.c | 149 +++++++++++++-----
src/test/modules/test_autovacuum/meson.build | 1 +
.../test_autovacuum/t/002_toast_relopts.pl | 69 ++++++++
3 files changed, 177 insertions(+), 42 deletions(-)
create mode 100644 src/test/modules/test_autovacuum/t/002_toast_relopts.pl
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 1610c60ec4b..437080a0a41 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1913,6 +1913,40 @@ TableToProcessComparator(const ListCell *a, const ListCell *b)
return (t2->score < t1->score) ? -1 : (t2->score > t1->score) ? 1 : 0;
}
+/*
+ * get_effective_relopts
+ * Fetch the storage parameters that apply to a relation.
+ *
+ * This looks up the reloptions for the pg_class relation in "tup". If it is a
+ * TOAST table, we also merge in any unset reloptions with the main table's
+ * stored in "toast_map". If the relation neither sets nor inherits any
+ * reloptions, this function returns NULL. Else, a palloc'd copy of the
+ * applicable reloptions are returned.
+ *
+ * If "tup" refers to a TOAST table and the toast_map has reloptions stored for
+ * its main relation, we return a pointer to the main table's reloptions via
+ * *main_opts. Else, main_opts is set to NULL.
+ */
+static StdRdOptions *
+get_effective_relopts(HeapTuple tup, TupleDesc desc, HTAB *toast_map,
+ StdRdOptions **main_opts)
+{
+ Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ av_relation *hentry = NULL;
+
+ /* look up our relopts */
+ relopts = (StdRdOptions *) extractRelOptions(tup, desc, NULL);
+
+ /* if we're a TOAST table, look up our parent's relopts, too */
+ if (classForm->relkind == RELKIND_TOASTVALUE)
+ hentry = hash_search(toast_map, &classForm->oid, HASH_FIND, NULL);
+ *main_opts = hentry ? &hentry->ar_reloptions : NULL;
+
+ /* return the merged reloptions */
+ return merge_toast_reloptions(relopts, *main_opts);
+}
+
/*
* Process a database table-by-table
*
@@ -2015,9 +2049,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2128,7 +2162,7 @@ do_autovacuum(void)
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2142,22 +2176,9 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ /* fetch reloptions -- merge any unset options from the main rel */
+ relopts = get_effective_relopts(tuple, pg_class_desc, table_toast_map,
+ &main_relopts);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2176,7 +2197,7 @@ do_autovacuum(void)
}
/* Release stuff to avoid leakage */
- if (free_relopts)
+ if (relopts)
pfree(relopts);
}
@@ -2782,7 +2803,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
autovac_table *tab = NULL;
bool wraparound;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2792,21 +2813,11 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
- relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE)
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ relopts = get_effective_relopts(classTup, pg_class_desc, table_toast_map,
+ &main_relopts);
relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
@@ -2891,7 +2902,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
- tab->at_params.main_relopts = NULL;
+ tab->at_params.main_relopts = main_relopts;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
@@ -2936,7 +2947,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_relopts)
+ if (relopts)
pfree(relopts);
heap_freetuple(classTup);
return tab;
@@ -2950,8 +2961,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* being forced because of Xid or multixact wraparound.
*
* relopts is a pointer to the StdRdOptions options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * case of a plain table, or merged with the main table's for a TOAST table),
+ * NULL if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3609,6 +3620,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
TableScanDesc scan;
HeapTuple tup;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
InitMaterializedSRF(fcinfo, 0);
@@ -3617,13 +3630,62 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
rel = table_open(RelationRelationId, AccessShareLock);
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parents' reloptions, we can generate the results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ StdRdOptions *main_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3639,7 +3701,9 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relopts = get_effective_relopts(tup, RelationGetDescr(rel),
+ table_toast_map, &main_relopts);
+
relation_needs_vacanalyze(form->oid, relopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
@@ -3663,6 +3727,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
diff --git a/src/test/modules/test_autovacuum/meson.build b/src/test/modules/test_autovacuum/meson.build
index 86e392bc0de..970b9aaae4b 100644
--- a/src/test/modules/test_autovacuum/meson.build
+++ b/src/test/modules/test_autovacuum/meson.build
@@ -10,6 +10,7 @@ tests += {
},
'tests': [
't/001_parallel_autovacuum.pl',
+ 't/002_toast_relopts.pl',
],
},
}
diff --git a/src/test/modules/test_autovacuum/t/002_toast_relopts.pl b/src/test/modules/test_autovacuum/t/002_toast_relopts.pl
new file mode 100644
index 00000000000..ba7596cc82b
--- /dev/null
+++ b/src/test/modules/test_autovacuum/t/002_toast_relopts.pl
@@ -0,0 +1,69 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test autovacuum's handling of TOAST storage parameters
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Create a test node with autovacuum disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+autovacuum = off
+autovacuum_naptime = '1s'
+});
+$node->start;
+
+# Create TOAST table that is eligible for autovacuum due to inherited relopts.
+$node->safe_psql(
+ 'postgres', qq{
+ CREATE TABLE toast_relopts (i int, j text STORAGE EXTERNAL) WITH
+ (autovacuum_enabled = false, toast.autovacuum_enabled = true,
+ autovacuum_vacuum_threshold = 1,
+ autovacuum_vacuum_scale_factor = 0,
+ autovacuum_vacuum_insert_threshold = 1,
+ autovacuum_vacuum_insert_scale_factor = 0,
+ vacuum_truncate = false);
+ INSERT INTO toast_relopts VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+ SELECT pg_stat_force_next_flush();
+});
+
+# Get TOAST table's OID for following commands.
+my $toast = $node->safe_psql('postgres',
+ "SELECT reltoastrelid::regclass FROM pg_class WHERE oid = 'toast_relopts'::regclass"
+);
+
+# Verify scores view used inherited insert threshold.
+is( $node->safe_psql(
+ 'postgres', qq{
+ SELECT vacuum_insert_score > 1 FROM pg_stat_autovacuum_scores
+ WHERE relid = '$toast'::regclass
+}),
+ 't',
+ 'inherited insert threshold in pg_stat_autovacuum_scores');
+
+# Delete all rows so that we can verify inherited vacuum_truncate takes effect.
+$node->safe_psql('postgres', 'DELETE FROM toast_relopts');
+
+# Enable autovacuum.
+$node->append_conf('postgresql.conf', 'autovacuum = on');
+$node->reload;
+
+# Wait until autovacuum processes the table.
+ok( $node->poll_query_until(
+ 'postgres', qq{
+ SELECT last_autovacuum IS NOT NULL FROM pg_stat_all_tables
+ WHERE relid = '$toast'::regclass
+}),
+ 'autovacuum of a TOAST table with inherited thresholds');
+
+# Verify autovacuum didn't truncate the table.
+is($node->safe_psql('postgres', "SELECT pg_relation_size('$toast') > 0"),
+ 't', 'inherited vacuum_truncate');
+
+$node->stop;
+done_testing();
--
2.55.0
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-17 20:08 Nathan Bossart <nathandbossart@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
1 sibling, 0 replies; 44+ messages in thread
From: Nathan Bossart @ 2026-08-17 20:08 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Sami Imseih <samimseih@gmail.com>; Greg Burd <greg@burd.me>; solai v <solai.cdac@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; pgsql-hackers
On Thu, Aug 13, 2026 at 11:41:59AM -0500, Nathan Bossart wrote:
> I'll plan on committing these soon to get them out of the way.
I've committed 0001-0003. Barring more feedback, I'm hoping to commit the
rest soon. Here is a rebased patch set.
--
nathan
From 24bc61a37c9d6a027a90df5b982756c987b1f272 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 7 Aug 2026 10:55:34 -0500
Subject: [PATCH v13 1/5] Simplify autovacuum's TOAST-to-main-relation
reloptions map.
do_autovacuum() adds an entry to this map for every relation that
has a TOAST table, and uses a flag to mark the entries that have no
reloptions to pass down. The unconditional entry made sense back
when the payload was the main relation's OID: commit 7d4c9a5793
added the map so that the TOAST pass could find the parent, whose
pg_autovacuum row supplied the settings for a TOAST table that had
none of its own. Commit 834a6da4f7 replaced that lookup by copying
the parent's reloptions into the entry, which left the OID unread
and called for the flag, since a by-value AutoVacOpts cannot say
"not set".
An entry is now worth creating only when there is something to
inherit, so skip the relations that have no reloptions and let a
successful lookup speak for itself. Two relations cannot share a
TOAST table, and each pass sees a single catalog snapshot, so an
insertion can never find an existing entry; assert that rather than
quietly ignoring it.
While at it, remove two more leftovers of that same conversion:
ar_relid, unread ever since, and a NULL test on table_toast_map in
table_recheck_autovac(), which used to carry the relkind test for
get_pg_autovacuum_tuple_relid() and has had no NULL to catch since
that function went away.
---
src/backend/postmaster/autovacuum.c | 26 +++++++-------------------
1 file changed, 7 insertions(+), 19 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 874454891d3..e0fc551d12a 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -200,8 +200,6 @@ typedef struct avw_dbase
typedef struct av_relation
{
Oid ar_toastrelid; /* hash key - must be first */
- Oid ar_relid;
- bool ar_hasrelopts;
StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
@@ -2099,7 +2097,7 @@ do_autovacuum(void)
* this whether or not the table is going to be vacuumed, because we
* don't automatically vacuum toast tables along the parent table.
*/
- if (OidIsValid(classForm->reltoastrelid))
+ if (OidIsValid(classForm->reltoastrelid) && relopts)
{
av_relation *hentry;
bool found;
@@ -2107,19 +2105,10 @@ do_autovacuum(void)
hentry = hash_search(table_toast_map,
&classForm->reltoastrelid,
HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
- if (!found)
- {
- /* hash_search already filled in the key */
- hentry->ar_relid = relid;
- hentry->ar_hasrelopts = false;
- if (relopts != NULL)
- {
- hentry->ar_hasrelopts = true;
- memcpy(&hentry->ar_reloptions, relopts,
- sizeof(StdRdOptions));
- }
- }
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
}
/* Release stuff to avoid per-relation leakage */
@@ -2168,7 +2157,7 @@ do_autovacuum(void)
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
@@ -2814,14 +2803,13 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ else if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
--
2.55.0
From 0118a82edbec79bd91af526e1902334a320d4103 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:55:44 -0500
Subject: [PATCH v13 2/5] Give TOAST storage parameters unsettable defaults.
This is preparatory work for a follow-up commit that will fill in a
TOAST table's unset storage parameters from its main table's. For
that, it must be possible to tell an option nobody set from one the
user set to the value that option happens to default to. The
parsed form of a relation's options has nowhere to record which
ones were specified, so an unset option is simply one still holding
its declared default.
Require, then, that any option a TOAST table accepts default to a
value the user cannot set. Ternaries already comply, having no
default at all, as does vacuum_index_cleanup, whose "not set"
member has no spelling. Among the numeric ones only
log_autovacuum_min_duration was in violation, defaulting to -1 with
a minimum of -1, so give it -2 instead, matching
autovacuum_vacuum_max_threshold and
autovacuum_vacuum_insert_threshold. Users won't notice; -1 already
behaved exactly as leaving the option unset does, and it still
does.
An assertion in initialize_reloptions() enforces both of the rules
the follow-up commit will rely on: that the default is unsettable,
and that anything settable on a TOAST table is settable on a heap,
since the inherited value is read from a main table's options at
the same offset. Note that this rules out bool and string options,
neither of which can express "unset".
---
src/backend/access/common/reloptions.c | 78 +++++++++++++++++++++++++-
src/backend/postmaster/autovacuum.c | 2 +-
2 files changed, 78 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index fab8294e732..a0716ff7819 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
},
{
{
@@ -613,6 +613,78 @@ static void parse_one_reloption(relopt_value *option, char *text_str,
((option).isset ? strlen((option).string_val) : \
((relopt_string *) (option).gen)->default_len)
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Verify that every option a TOAST table accepts defaults to a value the user
+ * cannot set. Nothing records which options were specified, so an option
+ * still holding its default is the only way to recognize one that was never
+ * set, and that is how a TOAST table tells which values it should take from
+ * its main table.
+ */
+static void
+assert_toast_defaults_unsettable(void)
+{
+ for (int i = 0; relOpts[i]; i++)
+ {
+ relopt_gen *gen = relOpts[i];
+
+ if ((gen->kinds & RELOPT_KIND_TOAST) == 0)
+ continue;
+
+ /*
+ * A TOAST table's value is filled in from its main table's at the
+ * same offset in the same struct, so the option must be settable on a
+ * heap too.
+ */
+ Assert((gen->kinds & RELOPT_KIND_HEAP) != 0);
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+
+ /*
+ * Ternaries carry no default, and parse_one_reloption() can
+ * only produce true or false, so PG_TERNARY_UNSET is already
+ * beyond a user's reach.
+ */
+ break;
+
+ case RELOPT_TYPE_INT:
+ {
+ relopt_int *optint = (relopt_int *) gen;
+
+ Assert(optint->default_val < optint->min ||
+ optint->default_val > optint->max);
+ break;
+ }
+
+ case RELOPT_TYPE_REAL:
+ {
+ relopt_real *optreal = (relopt_real *) gen;
+
+ Assert(optreal->default_val < optreal->min ||
+ optreal->default_val > optreal->max);
+ break;
+ }
+
+ case RELOPT_TYPE_ENUM:
+ {
+ relopt_enum *optenum = (relopt_enum *) gen;
+
+ for (relopt_enum_elt_def *elt = optenum->members;
+ elt->string_val; elt++)
+ Assert(elt->symbol_val != optenum->default_val);
+ break;
+ }
+
+ default:
+ /* Neither bools nor strings can express "unset". */
+ Assert(false);
+ }
+ }
+}
+#endif /* USE_ASSERT_CHECKING */
+
/*
* initialize_reloptions
* initialization routine, must be called before parsing
@@ -730,6 +802,10 @@ initialize_reloptions(void)
/* flag the work is complete */
need_initialization = false;
+
+#ifdef USE_ASSERT_CHECKING
+ assert_toast_defaults_unsettable();
+#endif
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index e0fc551d12a..77557fa4bbf 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2838,7 +2838,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* defaults, autovacuum's own first and plain vacuum second.
*/
- /* -1 in autovac setting means use log_autovacuum_min_duration */
+ /* a negative autovac setting means use log_autovacuum_min_duration */
log_vacuum_min_duration = (avopts && avopts->log_vacuum_min_duration >= 0)
? avopts->log_vacuum_min_duration
: Log_autovacuum_min_duration;
--
2.55.0
From aa47cbbd04d3d5f1342d72ccfca4d0b64f37708f Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:56:02 -0500
Subject: [PATCH v13 3/5] Move the StdRdOptions parse table to file scope.
This is preparatory work for a follow-up commit that will walk the
table from another function. Nothing changes but the indentation
of its entries.
---
src/backend/access/common/reloptions.c | 115 +++++++++++++------------
1 file changed, 60 insertions(+), 55 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index a0716ff7819..cffe9350d4f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2043,69 +2043,74 @@ fillRelOptions(void *rdopts, Size basesize,
}
+/*
+ * Parse table for StdRdOptions, which is shared by the RELOPT_KIND_HEAP and
+ * RELOPT_KIND_TOAST kinds.
+ */
+static const relopt_parse_elt stdRdOptionsTab[] = {
+ {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
+ {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
+ {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
+ {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
+ {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
+ {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
+ {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
+ {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
+ {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
+ {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
+ {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
+ {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
+ {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
+ {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
+ {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
+ {"toast_tuple_target", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, toast_tuple_target)},
+ {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
+ {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
+ {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
+ {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"user_catalog_table", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, user_catalog_table)},
+ {"parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, parallel_workers)},
+ {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+ offsetof(StdRdOptions, vacuum_index_cleanup)},
+ {"vacuum_truncate", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, vacuum_truncate)},
+ {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
+};
+
/*
* Option parser for anything that uses StdRdOptions.
*/
bytea *
default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
- static const relopt_parse_elt tab[] = {
- {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
- {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
- {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
- {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
- {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
- {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
- {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
- {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
- {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
- {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
- {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
- {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
- {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
- {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
- {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
- {"toast_tuple_target", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, toast_tuple_target)},
- {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
- {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
- {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
- {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
- {"user_catalog_table", RELOPT_TYPE_BOOL,
- offsetof(StdRdOptions, user_catalog_table)},
- {"parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, parallel_workers)},
- {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
- offsetof(StdRdOptions, vacuum_index_cleanup)},
- {"vacuum_truncate", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, vacuum_truncate)},
- {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
- };
-
return (bytea *) build_reloptions(reloptions, validate, kind,
sizeof(StdRdOptions),
- tab, lengthof(tab));
+ stdRdOptionsTab,
+ lengthof(stdRdOptionsTab));
}
/*
--
2.55.0
From c232a2c7220844697da250d56ed63988cb159a8c Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 11:34:55 -0500
Subject: [PATCH v13 4/5] Fix VACUUM's handling of TOAST storage parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Presently, VACUUM does no such thing. It reads the TOAST table's
own reloptions, which hold only what was set through toast.*, so
vacuum_index_cleanup or vacuum_truncate set on the main table has
no effect on its TOAST table.
To fix, add merge_toast_reloptions(), which walks the parse table
for StdRdOptions and takes the main table's value for anything the
TOAST table left at its default. vacuum_rel() hands the main
table's parameters down when recursing to a TOAST table, and it
merges them into a copy of the TOAST table's parameters before the
values are used. This doesn't help VACUUM against a TOAST table
directly (e.g., "VACUUM pg_toast.pg_toast_5432"), but that's
probably okay because it's not the main supported way to vacuum a
TOAST table (see VACUUM's PROCESS_MAIN and PROCESS_TOAST options).
A follow-up commit will do the same for autovacuum. While this is
a bug fix, it's too intrusive for back-patching, but the issue
seems to have gone unnoticed for a very long time, anyway.
---
src/backend/access/common/reloptions.c | 103 ++++++++++++++++++
src/backend/commands/vacuum.c | 40 +++++--
src/backend/postmaster/autovacuum.c | 1 +
src/include/access/reloptions.h | 2 +
src/include/commands/vacuum.h | 8 ++
.../injection_points/expected/vacuum.out | 11 ++
.../modules/injection_points/sql/vacuum.sql | 8 ++
7 files changed, 162 insertions(+), 11 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index cffe9350d4f..bbd40070a06 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2113,6 +2113,109 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
lengthof(stdRdOptionsTab));
}
+/*
+ * find reloption
+ * Look up a reloption of the given kind by name.
+ *
+ * Returns NULL if no such option can be set on relations of that kind. Note
+ * that names are unique only within a kind; "fillfactor", for example, is
+ * declared separately for heaps and for each index access method.
+ */
+static relopt_gen *
+find_reloption(const char *name, relopt_kind kind)
+{
+ if (need_initialization)
+ initialize_reloptions();
+
+ for (int i = 0; relOpts[i]; i++)
+ {
+ if ((relOpts[i]->kinds & kind) != 0 &&
+ strcmp(relOpts[i]->name, name) == 0)
+ return relOpts[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * merge_toast_reloptions
+ * Fill in a TOAST table's unset options from its main table's.
+ *
+ * Any option that may be set on a TOAST table but was not is taken from
+ * main_opts. Either argument may be NULL; if both are, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * An option counts as unset while it still holds the default declared for it
+ * above, which works because nothing a TOAST table accepts has a default the
+ * user could also set (see assert_toast_defaults_unsettable()).
+ *
+ * If the return value is not NULL, it is palloc'd.
+ */
+StdRdOptions *
+merge_toast_reloptions(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ StdRdOptions *ret;
+
+ /* if both arguments are NULL, return NULL */
+ if (toast_opts == NULL && main_opts == NULL)
+ return NULL;
+
+ /* if one argument is NULL, return the non-NULL one */
+ ret = palloc_object(StdRdOptions);
+ if (toast_opts == NULL || main_opts == NULL)
+ {
+ memcpy(ret, main_opts ? main_opts : toast_opts, sizeof(StdRdOptions));
+ return ret;
+ }
+
+ /* replace unset TOAST relopts with the main table's */
+ memcpy(ret, toast_opts, sizeof(StdRdOptions));
+ for (int i = 0; i < lengthof(stdRdOptionsTab); i++)
+ {
+ const relopt_parse_elt *elem = &stdRdOptionsTab[i];
+ relopt_gen *gen;
+ char *toast_val;
+ char *main_val;
+
+ /* skip anything that cannot be set on a TOAST table */
+ gen = find_reloption(elem->optname, RELOPT_KIND_TOAST);
+ if (gen == NULL)
+ continue;
+
+ toast_val = (char *) ret + elem->offset;
+ main_val = (char *) main_opts + elem->offset;
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+ if (*(pg_ternary *) toast_val == PG_TERNARY_UNSET)
+ *(pg_ternary *) toast_val = *(pg_ternary *) main_val;
+ break;
+
+ case RELOPT_TYPE_INT:
+ if (*(int *) toast_val == ((relopt_int *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ case RELOPT_TYPE_REAL:
+ if (*(double *) toast_val == ((relopt_real *) gen)->default_val)
+ *(double *) toast_val = *(double *) main_val;
+ break;
+
+ case RELOPT_TYPE_ENUM:
+ if (*(int *) toast_val == ((relopt_enum *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ default:
+ elog(ERROR, "reloption \"%s\" has a type a TOAST table cannot inherit",
+ elem->optname);
+ }
+ }
+
+ return ret;
+}
+
/*
* build_reloptions
*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 64eed16a160..458cf956c10 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -31,6 +31,7 @@
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
+#include "access/reloptions.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
@@ -187,6 +188,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_relopts = NULL;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2039,6 +2041,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
int save_sec_context;
int save_nestlevel;
VacuumParams toast_vacuum_params;
+ StdRdOptions *relopts;
+ StdRdOptions relopts_copy;
/*
* This function scribbles on the parameters, so make a copy early to
@@ -2201,6 +2205,14 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ /*
+ * A TOAST table takes any storage parameter it accepts but does not set
+ * from its main table, whose parameters the caller handed down for that
+ * purpose. Merge them into a copy of our own.
+ */
+ relopts = merge_toast_reloptions((StdRdOptions *) rel->rd_options,
+ params.main_relopts);
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2209,11 +2221,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
StdRdOptIndexCleanup vacuum_index_cleanup;
- if (rel->rd_options == NULL)
+ if (relopts == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = relopts->vacuum_index_cleanup;
switch (vacuum_index_cleanup)
{
@@ -2245,10 +2256,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
- params.max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ if (relopts != NULL && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ params.max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
@@ -2256,11 +2265,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
- StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
-
- if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
+ if (relopts && relopts->vacuum_truncate != PG_TERNARY_UNSET)
{
- if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ if (relopts->vacuum_truncate == PG_TERNARY_TRUE)
params.truncate = VACOPTVALUE_ENABLED;
else
params.truncate = VACOPTVALUE_DISABLED;
@@ -2293,6 +2300,17 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
else
toast_relid = InvalidOid;
+ /*
+ * Hand our storage parameters down for the TOAST table to inherit. Take
+ * a copy while we still have the relation open; the relcache entry can go
+ * away once we close it.
+ */
+ if (OidIsValid(toast_relid) && rel->rd_options)
+ {
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ toast_vacuum_params.main_relopts = &relopts_copy;
+ }
+
/*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also lock down security-restricted operations and
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 77557fa4bbf..0b1fcec7b29 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2897,6 +2897,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ tab->at_params.main_relopts = NULL;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..6c599382f02 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -247,6 +247,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options,
extern bytea *default_reloptions(Datum reloptions, bool validate,
relopt_kind kind);
+extern struct StdRdOptions *merge_toast_reloptions(struct StdRdOptions *toast_opts,
+ struct StdRdOptions *main_opts);
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..55ce22f4c5b 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,14 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * When vacuuming a TOAST table, this holds the main table's storage
+ * parameters (or NULL if it doesn't have any). If a storage parameter is
+ * unset on the TOAST table but _is_ set on the main table, we use the
+ * main table's setting.
+ */
+ struct StdRdOptions *main_relopts;
} VacuumParams;
/*
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..caf0cc232b4 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,9 +79,20 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..0a43e14c928 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,9 +33,17 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
--
2.55.0
From ada6537d99e5bed2aa1a5d1d47c6daa4d7f7ac16 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 12:02:21 -0500
Subject: [PATCH v13 5/5] Fix autovacuum's handling of TOAST storage
parameters.
The previous commit made VACUUM apply a main table's storage
parameters to its TOAST table, as CREATE TABLE has long documented.
Autovacuum still gets this wrong in two ways. It falls back to the
main table's autovacuum parameters only when the TOAST table has no
reloptions at all, so setting a single toast.* parameter silently
discards the rest. And it never consults the main table for the
parameters that only VACUUM reads, since it leaves those for
vacuum_rel() to resolve from the TOAST table's own reloptions.
To fix, combine the two sets with merge_toast_reloptions() rather
than choosing between them, and hand the main table's parameters
down to vacuum_rel() the way VACUUM now does.
pg_stat_autovacuum_scores uses the combined parameters for TOAST
tables as well; it has to collect the main relations' parameters
before it can do so, so it now makes a preliminary pass over
pg_class.
An existing shortcoming that this patch only makes worse is that
autovacuum remains oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/postmaster/autovacuum.c | 148 +++++++++++++-----
src/test/modules/test_autovacuum/meson.build | 1 +
.../test_autovacuum/t/002_toast_relopts.pl | 69 ++++++++
3 files changed, 176 insertions(+), 42 deletions(-)
create mode 100644 src/test/modules/test_autovacuum/t/002_toast_relopts.pl
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 0b1fcec7b29..be239132572 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1913,6 +1913,40 @@ TableToProcessComparator(const ListCell *a, const ListCell *b)
return (t2->score < t1->score) ? -1 : (t2->score > t1->score) ? 1 : 0;
}
+/*
+ * get_effective_relopts
+ * Fetch the storage parameters that apply to a relation.
+ *
+ * This looks up the reloptions for the pg_class relation in "tup". If it is a
+ * TOAST table, we also merge in any unset reloptions with the main table's
+ * stored in "toast_map". If the relation neither sets nor inherits any
+ * reloptions, this function returns NULL. Else, a palloc'd copy of the
+ * applicable reloptions are returned.
+ *
+ * If "tup" refers to a TOAST table and the toast_map has reloptions stored for
+ * its main relation, we return a pointer to the main table's reloptions via
+ * *main_opts. Else, main_opts is set to NULL.
+ */
+static StdRdOptions *
+get_effective_relopts(HeapTuple tup, TupleDesc desc, HTAB *toast_map,
+ StdRdOptions **main_opts)
+{
+ Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ av_relation *hentry = NULL;
+
+ /* look up our relopts */
+ relopts = (StdRdOptions *) extractRelOptions(tup, desc, NULL);
+
+ /* if we're a TOAST table, look up our parent's relopts, too */
+ if (classForm->relkind == RELKIND_TOASTVALUE)
+ hentry = hash_search(toast_map, &classForm->oid, HASH_FIND, NULL);
+ *main_opts = hentry ? &hentry->ar_reloptions : NULL;
+
+ /* return the merged reloptions */
+ return merge_toast_reloptions(relopts, *main_opts);
+}
+
/*
* Process a database table-by-table
*
@@ -2015,9 +2049,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2130,7 +2164,7 @@ do_autovacuum(void)
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2144,22 +2178,9 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ /* fetch reloptions -- merge any unset options from the main rel */
+ relopts = get_effective_relopts(tuple, pg_class_desc, table_toast_map,
+ &main_relopts);
relation_needs_vacanalyze(relid,
relopts ? &relopts->autovacuum : NULL,
@@ -2180,7 +2201,7 @@ do_autovacuum(void)
}
/* Release stuff to avoid leakage */
- if (free_relopts)
+ if (relopts)
pfree(relopts);
}
@@ -2787,7 +2808,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool wraparound;
AutoVacOpts *avopts;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2797,21 +2818,11 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
- relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE)
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ relopts = get_effective_relopts(classTup, pg_class_desc, table_toast_map,
+ &main_relopts);
avopts = relopts ? &relopts->autovacuum : NULL;
@@ -2897,7 +2908,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
- tab->at_params.main_relopts = NULL;
+ tab->at_params.main_relopts = main_relopts;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
@@ -2942,7 +2953,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_relopts)
+ if (relopts)
pfree(relopts);
heap_freetuple(classTup);
return tab;
@@ -2956,8 +2967,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* being forced because of Xid or multixact wraparound.
*
* relopts is a pointer to the AutoVacOpts options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * case of a plain table, or merged with the main table's for a TOAST table),
+ * NULL if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3614,6 +3625,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
TableScanDesc scan;
HeapTuple tup;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
InitMaterializedSRF(fcinfo, 0);
@@ -3622,13 +3635,62 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
rel = table_open(RelationRelationId, AccessShareLock);
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parents' reloptions, we can generate the results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ StdRdOptions *main_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3644,7 +3706,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relopts = get_effective_relopts(tup, RelationGetDescr(rel),
+ table_toast_map, &main_relopts);
relation_needs_vacanalyze(form->oid,
relopts ? &relopts->autovacuum : NULL,
form,
@@ -3670,6 +3733,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
diff --git a/src/test/modules/test_autovacuum/meson.build b/src/test/modules/test_autovacuum/meson.build
index 86e392bc0de..970b9aaae4b 100644
--- a/src/test/modules/test_autovacuum/meson.build
+++ b/src/test/modules/test_autovacuum/meson.build
@@ -10,6 +10,7 @@ tests += {
},
'tests': [
't/001_parallel_autovacuum.pl',
+ 't/002_toast_relopts.pl',
],
},
}
diff --git a/src/test/modules/test_autovacuum/t/002_toast_relopts.pl b/src/test/modules/test_autovacuum/t/002_toast_relopts.pl
new file mode 100644
index 00000000000..ba7596cc82b
--- /dev/null
+++ b/src/test/modules/test_autovacuum/t/002_toast_relopts.pl
@@ -0,0 +1,69 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test autovacuum's handling of TOAST storage parameters
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Create a test node with autovacuum disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+autovacuum = off
+autovacuum_naptime = '1s'
+});
+$node->start;
+
+# Create TOAST table that is eligible for autovacuum due to inherited relopts.
+$node->safe_psql(
+ 'postgres', qq{
+ CREATE TABLE toast_relopts (i int, j text STORAGE EXTERNAL) WITH
+ (autovacuum_enabled = false, toast.autovacuum_enabled = true,
+ autovacuum_vacuum_threshold = 1,
+ autovacuum_vacuum_scale_factor = 0,
+ autovacuum_vacuum_insert_threshold = 1,
+ autovacuum_vacuum_insert_scale_factor = 0,
+ vacuum_truncate = false);
+ INSERT INTO toast_relopts VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+ SELECT pg_stat_force_next_flush();
+});
+
+# Get TOAST table's OID for following commands.
+my $toast = $node->safe_psql('postgres',
+ "SELECT reltoastrelid::regclass FROM pg_class WHERE oid = 'toast_relopts'::regclass"
+);
+
+# Verify scores view used inherited insert threshold.
+is( $node->safe_psql(
+ 'postgres', qq{
+ SELECT vacuum_insert_score > 1 FROM pg_stat_autovacuum_scores
+ WHERE relid = '$toast'::regclass
+}),
+ 't',
+ 'inherited insert threshold in pg_stat_autovacuum_scores');
+
+# Delete all rows so that we can verify inherited vacuum_truncate takes effect.
+$node->safe_psql('postgres', 'DELETE FROM toast_relopts');
+
+# Enable autovacuum.
+$node->append_conf('postgresql.conf', 'autovacuum = on');
+$node->reload;
+
+# Wait until autovacuum processes the table.
+ok( $node->poll_query_until(
+ 'postgres', qq{
+ SELECT last_autovacuum IS NOT NULL FROM pg_stat_all_tables
+ WHERE relid = '$toast'::regclass
+}),
+ 'autovacuum of a TOAST table with inherited thresholds');
+
+# Verify autovacuum didn't truncate the table.
+is($node->safe_psql('postgres', "SELECT pg_relation_size('$toast') > 0"),
+ 't', 'inherited vacuum_truncate');
+
+$node->stop;
+done_testing();
--
2.55.0
Attachments:
[text/plain] v13-0001-Simplify-autovacuum-s-TOAST-to-main-relation-rel.patch (3.8K, ../../aoNqVz4Nf50FGfld@nathan/2-v13-0001-Simplify-autovacuum-s-TOAST-to-main-relation-rel.patch)
download | inline diff:
From 24bc61a37c9d6a027a90df5b982756c987b1f272 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 7 Aug 2026 10:55:34 -0500
Subject: [PATCH v13 1/5] Simplify autovacuum's TOAST-to-main-relation
reloptions map.
do_autovacuum() adds an entry to this map for every relation that
has a TOAST table, and uses a flag to mark the entries that have no
reloptions to pass down. The unconditional entry made sense back
when the payload was the main relation's OID: commit 7d4c9a5793
added the map so that the TOAST pass could find the parent, whose
pg_autovacuum row supplied the settings for a TOAST table that had
none of its own. Commit 834a6da4f7 replaced that lookup by copying
the parent's reloptions into the entry, which left the OID unread
and called for the flag, since a by-value AutoVacOpts cannot say
"not set".
An entry is now worth creating only when there is something to
inherit, so skip the relations that have no reloptions and let a
successful lookup speak for itself. Two relations cannot share a
TOAST table, and each pass sees a single catalog snapshot, so an
insertion can never find an existing entry; assert that rather than
quietly ignoring it.
While at it, remove two more leftovers of that same conversion:
ar_relid, unread ever since, and a NULL test on table_toast_map in
table_recheck_autovac(), which used to carry the relkind test for
get_pg_autovacuum_tuple_relid() and has had no NULL to catch since
that function went away.
---
src/backend/postmaster/autovacuum.c | 26 +++++++-------------------
1 file changed, 7 insertions(+), 19 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 874454891d3..e0fc551d12a 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -200,8 +200,6 @@ typedef struct avw_dbase
typedef struct av_relation
{
Oid ar_toastrelid; /* hash key - must be first */
- Oid ar_relid;
- bool ar_hasrelopts;
StdRdOptions ar_reloptions; /* copy of main table's reloptions */
} av_relation;
@@ -2099,7 +2097,7 @@ do_autovacuum(void)
* this whether or not the table is going to be vacuumed, because we
* don't automatically vacuum toast tables along the parent table.
*/
- if (OidIsValid(classForm->reltoastrelid))
+ if (OidIsValid(classForm->reltoastrelid) && relopts)
{
av_relation *hentry;
bool found;
@@ -2107,19 +2105,10 @@ do_autovacuum(void)
hentry = hash_search(table_toast_map,
&classForm->reltoastrelid,
HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
- if (!found)
- {
- /* hash_search already filled in the key */
- hentry->ar_relid = relid;
- hentry->ar_hasrelopts = false;
- if (relopts != NULL)
- {
- hentry->ar_hasrelopts = true;
- memcpy(&hentry->ar_reloptions, relopts,
- sizeof(StdRdOptions));
- }
- }
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
}
/* Release stuff to avoid per-relation leakage */
@@ -2168,7 +2157,7 @@ do_autovacuum(void)
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
@@ -2814,14 +2803,13 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE &&
- table_toast_map != NULL)
+ else if (classForm->relkind == RELKIND_TOASTVALUE)
{
av_relation *hentry;
bool found;
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found && hentry->ar_hasrelopts)
+ if (found)
relopts = &hentry->ar_reloptions;
}
--
2.55.0
[text/plain] v13-0002-Give-TOAST-storage-parameters-unsettable-default.patch (5.0K, ../../aoNqVz4Nf50FGfld@nathan/3-v13-0002-Give-TOAST-storage-parameters-unsettable-default.patch)
download | inline diff:
From 0118a82edbec79bd91af526e1902334a320d4103 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:55:44 -0500
Subject: [PATCH v13 2/5] Give TOAST storage parameters unsettable defaults.
This is preparatory work for a follow-up commit that will fill in a
TOAST table's unset storage parameters from its main table's. For
that, it must be possible to tell an option nobody set from one the
user set to the value that option happens to default to. The
parsed form of a relation's options has nowhere to record which
ones were specified, so an unset option is simply one still holding
its declared default.
Require, then, that any option a TOAST table accepts default to a
value the user cannot set. Ternaries already comply, having no
default at all, as does vacuum_index_cleanup, whose "not set"
member has no spelling. Among the numeric ones only
log_autovacuum_min_duration was in violation, defaulting to -1 with
a minimum of -1, so give it -2 instead, matching
autovacuum_vacuum_max_threshold and
autovacuum_vacuum_insert_threshold. Users won't notice; -1 already
behaved exactly as leaving the option unset does, and it still
does.
An assertion in initialize_reloptions() enforces both of the rules
the follow-up commit will rely on: that the default is unsettable,
and that anything settable on a TOAST table is settable on a heap,
since the inherited value is read from a main table's options at
the same offset. Note that this rules out bool and string options,
neither of which can express "unset".
---
src/backend/access/common/reloptions.c | 78 +++++++++++++++++++++++++-
src/backend/postmaster/autovacuum.c | 2 +-
2 files changed, 78 insertions(+), 2 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index fab8294e732..a0716ff7819 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
ShareUpdateExclusiveLock
},
- -1, -1, INT_MAX
+ -2, -1, INT_MAX
},
{
{
@@ -613,6 +613,78 @@ static void parse_one_reloption(relopt_value *option, char *text_str,
((option).isset ? strlen((option).string_val) : \
((relopt_string *) (option).gen)->default_len)
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Verify that every option a TOAST table accepts defaults to a value the user
+ * cannot set. Nothing records which options were specified, so an option
+ * still holding its default is the only way to recognize one that was never
+ * set, and that is how a TOAST table tells which values it should take from
+ * its main table.
+ */
+static void
+assert_toast_defaults_unsettable(void)
+{
+ for (int i = 0; relOpts[i]; i++)
+ {
+ relopt_gen *gen = relOpts[i];
+
+ if ((gen->kinds & RELOPT_KIND_TOAST) == 0)
+ continue;
+
+ /*
+ * A TOAST table's value is filled in from its main table's at the
+ * same offset in the same struct, so the option must be settable on a
+ * heap too.
+ */
+ Assert((gen->kinds & RELOPT_KIND_HEAP) != 0);
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+
+ /*
+ * Ternaries carry no default, and parse_one_reloption() can
+ * only produce true or false, so PG_TERNARY_UNSET is already
+ * beyond a user's reach.
+ */
+ break;
+
+ case RELOPT_TYPE_INT:
+ {
+ relopt_int *optint = (relopt_int *) gen;
+
+ Assert(optint->default_val < optint->min ||
+ optint->default_val > optint->max);
+ break;
+ }
+
+ case RELOPT_TYPE_REAL:
+ {
+ relopt_real *optreal = (relopt_real *) gen;
+
+ Assert(optreal->default_val < optreal->min ||
+ optreal->default_val > optreal->max);
+ break;
+ }
+
+ case RELOPT_TYPE_ENUM:
+ {
+ relopt_enum *optenum = (relopt_enum *) gen;
+
+ for (relopt_enum_elt_def *elt = optenum->members;
+ elt->string_val; elt++)
+ Assert(elt->symbol_val != optenum->default_val);
+ break;
+ }
+
+ default:
+ /* Neither bools nor strings can express "unset". */
+ Assert(false);
+ }
+ }
+}
+#endif /* USE_ASSERT_CHECKING */
+
/*
* initialize_reloptions
* initialization routine, must be called before parsing
@@ -730,6 +802,10 @@ initialize_reloptions(void)
/* flag the work is complete */
need_initialization = false;
+
+#ifdef USE_ASSERT_CHECKING
+ assert_toast_defaults_unsettable();
+#endif
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index e0fc551d12a..77557fa4bbf 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2838,7 +2838,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* defaults, autovacuum's own first and plain vacuum second.
*/
- /* -1 in autovac setting means use log_autovacuum_min_duration */
+ /* a negative autovac setting means use log_autovacuum_min_duration */
log_vacuum_min_duration = (avopts && avopts->log_vacuum_min_duration >= 0)
? avopts->log_vacuum_min_duration
: Log_autovacuum_min_duration;
--
2.55.0
[text/plain] v13-0003-Move-the-StdRdOptions-parse-table-to-file-scope.patch (7.8K, ../../aoNqVz4Nf50FGfld@nathan/4-v13-0003-Move-the-StdRdOptions-parse-table-to-file-scope.patch)
download | inline diff:
From aa47cbbd04d3d5f1342d72ccfca4d0b64f37708f Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:56:02 -0500
Subject: [PATCH v13 3/5] Move the StdRdOptions parse table to file scope.
This is preparatory work for a follow-up commit that will walk the
table from another function. Nothing changes but the indentation
of its entries.
---
src/backend/access/common/reloptions.c | 115 +++++++++++++------------
1 file changed, 60 insertions(+), 55 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index a0716ff7819..cffe9350d4f 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2043,69 +2043,74 @@ fillRelOptions(void *rdopts, Size basesize,
}
+/*
+ * Parse table for StdRdOptions, which is shared by the RELOPT_KIND_HEAP and
+ * RELOPT_KIND_TOAST kinds.
+ */
+static const relopt_parse_elt stdRdOptionsTab[] = {
+ {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
+ {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
+ {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
+ {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
+ {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
+ {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
+ {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
+ {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
+ {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
+ {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
+ {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
+ {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
+ {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
+ {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
+ {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
+ {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
+ {"toast_tuple_target", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, toast_tuple_target)},
+ {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
+ {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
+ {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
+ {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"user_catalog_table", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, user_catalog_table)},
+ {"parallel_workers", RELOPT_TYPE_INT,
+ offsetof(StdRdOptions, parallel_workers)},
+ {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+ offsetof(StdRdOptions, vacuum_index_cleanup)},
+ {"vacuum_truncate", RELOPT_TYPE_TERNARY,
+ offsetof(StdRdOptions, vacuum_truncate)},
+ {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
+ offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
+};
+
/*
* Option parser for anything that uses StdRdOptions.
*/
bytea *
default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{
- static const relopt_parse_elt tab[] = {
- {"fillfactor", RELOPT_TYPE_INT, offsetof(StdRdOptions, fillfactor)},
- {"autovacuum_enabled", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, enabled)},
- {"autovacuum_parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, autovacuum_parallel_workers)},
- {"autovacuum_vacuum_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_threshold)},
- {"autovacuum_vacuum_max_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_max_threshold)},
- {"autovacuum_vacuum_insert_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_threshold)},
- {"autovacuum_analyze_threshold", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
- {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
- {"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
- {"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_max_age)},
- {"autovacuum_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_table_age)},
- {"autovacuum_multixact_freeze_min_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_min_age)},
- {"autovacuum_multixact_freeze_max_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_max_age)},
- {"autovacuum_multixact_freeze_table_age", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, multixact_freeze_table_age)},
- {"log_autovacuum_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_vacuum_min_duration)},
- {"log_autoanalyze_min_duration", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, log_analyze_min_duration)},
- {"toast_tuple_target", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, toast_tuple_target)},
- {"autovacuum_vacuum_cost_delay", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_delay)},
- {"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_scale_factor)},
- {"autovacuum_vacuum_insert_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_ins_scale_factor)},
- {"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_scale_factor)},
- {"user_catalog_table", RELOPT_TYPE_BOOL,
- offsetof(StdRdOptions, user_catalog_table)},
- {"parallel_workers", RELOPT_TYPE_INT,
- offsetof(StdRdOptions, parallel_workers)},
- {"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
- offsetof(StdRdOptions, vacuum_index_cleanup)},
- {"vacuum_truncate", RELOPT_TYPE_TERNARY,
- offsetof(StdRdOptions, vacuum_truncate)},
- {"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
- };
-
return (bytea *) build_reloptions(reloptions, validate, kind,
sizeof(StdRdOptions),
- tab, lengthof(tab));
+ stdRdOptionsTab,
+ lengthof(stdRdOptionsTab));
}
/*
--
2.55.0
[text/plain] v13-0004-Fix-VACUUM-s-handling-of-TOAST-storage-parameter.patch (13.1K, ../../aoNqVz4Nf50FGfld@nathan/5-v13-0004-Fix-VACUUM-s-handling-of-TOAST-storage-parameter.patch)
download | inline diff:
From c232a2c7220844697da250d56ed63988cb159a8c Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 11:34:55 -0500
Subject: [PATCH v13 4/5] Fix VACUUM's handling of TOAST storage parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Presently, VACUUM does no such thing. It reads the TOAST table's
own reloptions, which hold only what was set through toast.*, so
vacuum_index_cleanup or vacuum_truncate set on the main table has
no effect on its TOAST table.
To fix, add merge_toast_reloptions(), which walks the parse table
for StdRdOptions and takes the main table's value for anything the
TOAST table left at its default. vacuum_rel() hands the main
table's parameters down when recursing to a TOAST table, and it
merges them into a copy of the TOAST table's parameters before the
values are used. This doesn't help VACUUM against a TOAST table
directly (e.g., "VACUUM pg_toast.pg_toast_5432"), but that's
probably okay because it's not the main supported way to vacuum a
TOAST table (see VACUUM's PROCESS_MAIN and PROCESS_TOAST options).
A follow-up commit will do the same for autovacuum. While this is
a bug fix, it's too intrusive for back-patching, but the issue
seems to have gone unnoticed for a very long time, anyway.
---
src/backend/access/common/reloptions.c | 103 ++++++++++++++++++
src/backend/commands/vacuum.c | 40 +++++--
src/backend/postmaster/autovacuum.c | 1 +
src/include/access/reloptions.h | 2 +
src/include/commands/vacuum.h | 8 ++
.../injection_points/expected/vacuum.out | 11 ++
.../modules/injection_points/sql/vacuum.sql | 8 ++
7 files changed, 162 insertions(+), 11 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index cffe9350d4f..bbd40070a06 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2113,6 +2113,109 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
lengthof(stdRdOptionsTab));
}
+/*
+ * find reloption
+ * Look up a reloption of the given kind by name.
+ *
+ * Returns NULL if no such option can be set on relations of that kind. Note
+ * that names are unique only within a kind; "fillfactor", for example, is
+ * declared separately for heaps and for each index access method.
+ */
+static relopt_gen *
+find_reloption(const char *name, relopt_kind kind)
+{
+ if (need_initialization)
+ initialize_reloptions();
+
+ for (int i = 0; relOpts[i]; i++)
+ {
+ if ((relOpts[i]->kinds & kind) != 0 &&
+ strcmp(relOpts[i]->name, name) == 0)
+ return relOpts[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * merge_toast_reloptions
+ * Fill in a TOAST table's unset options from its main table's.
+ *
+ * Any option that may be set on a TOAST table but was not is taken from
+ * main_opts. Either argument may be NULL; if both are, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * An option counts as unset while it still holds the default declared for it
+ * above, which works because nothing a TOAST table accepts has a default the
+ * user could also set (see assert_toast_defaults_unsettable()).
+ *
+ * If the return value is not NULL, it is palloc'd.
+ */
+StdRdOptions *
+merge_toast_reloptions(StdRdOptions *toast_opts, StdRdOptions *main_opts)
+{
+ StdRdOptions *ret;
+
+ /* if both arguments are NULL, return NULL */
+ if (toast_opts == NULL && main_opts == NULL)
+ return NULL;
+
+ /* if one argument is NULL, return the non-NULL one */
+ ret = palloc_object(StdRdOptions);
+ if (toast_opts == NULL || main_opts == NULL)
+ {
+ memcpy(ret, main_opts ? main_opts : toast_opts, sizeof(StdRdOptions));
+ return ret;
+ }
+
+ /* replace unset TOAST relopts with the main table's */
+ memcpy(ret, toast_opts, sizeof(StdRdOptions));
+ for (int i = 0; i < lengthof(stdRdOptionsTab); i++)
+ {
+ const relopt_parse_elt *elem = &stdRdOptionsTab[i];
+ relopt_gen *gen;
+ char *toast_val;
+ char *main_val;
+
+ /* skip anything that cannot be set on a TOAST table */
+ gen = find_reloption(elem->optname, RELOPT_KIND_TOAST);
+ if (gen == NULL)
+ continue;
+
+ toast_val = (char *) ret + elem->offset;
+ main_val = (char *) main_opts + elem->offset;
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+ if (*(pg_ternary *) toast_val == PG_TERNARY_UNSET)
+ *(pg_ternary *) toast_val = *(pg_ternary *) main_val;
+ break;
+
+ case RELOPT_TYPE_INT:
+ if (*(int *) toast_val == ((relopt_int *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ case RELOPT_TYPE_REAL:
+ if (*(double *) toast_val == ((relopt_real *) gen)->default_val)
+ *(double *) toast_val = *(double *) main_val;
+ break;
+
+ case RELOPT_TYPE_ENUM:
+ if (*(int *) toast_val == ((relopt_enum *) gen)->default_val)
+ *(int *) toast_val = *(int *) main_val;
+ break;
+
+ default:
+ elog(ERROR, "reloption \"%s\" has a type a TOAST table cannot inherit",
+ elem->optname);
+ }
+ }
+
+ return ret;
+}
+
/*
* build_reloptions
*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 64eed16a160..458cf956c10 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -31,6 +31,7 @@
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
+#include "access/reloptions.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
@@ -187,6 +188,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_relopts = NULL;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2039,6 +2041,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
int save_sec_context;
int save_nestlevel;
VacuumParams toast_vacuum_params;
+ StdRdOptions *relopts;
+ StdRdOptions relopts_copy;
/*
* This function scribbles on the parameters, so make a copy early to
@@ -2201,6 +2205,14 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ /*
+ * A TOAST table takes any storage parameter it accepts but does not set
+ * from its main table, whose parameters the caller handed down for that
+ * purpose. Merge them into a copy of our own.
+ */
+ relopts = merge_toast_reloptions((StdRdOptions *) rel->rd_options,
+ params.main_relopts);
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2209,11 +2221,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
StdRdOptIndexCleanup vacuum_index_cleanup;
- if (rel->rd_options == NULL)
+ if (relopts == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = relopts->vacuum_index_cleanup;
switch (vacuum_index_cleanup)
{
@@ -2245,10 +2256,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
- params.max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ if (relopts != NULL && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ params.max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
@@ -2256,11 +2265,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
- StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
-
- if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
+ if (relopts && relopts->vacuum_truncate != PG_TERNARY_UNSET)
{
- if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ if (relopts->vacuum_truncate == PG_TERNARY_TRUE)
params.truncate = VACOPTVALUE_ENABLED;
else
params.truncate = VACOPTVALUE_DISABLED;
@@ -2293,6 +2300,17 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
else
toast_relid = InvalidOid;
+ /*
+ * Hand our storage parameters down for the TOAST table to inherit. Take
+ * a copy while we still have the relation open; the relcache entry can go
+ * away once we close it.
+ */
+ if (OidIsValid(toast_relid) && rel->rd_options)
+ {
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ toast_vacuum_params.main_relopts = &relopts_copy;
+ }
+
/*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also lock down security-restricted operations and
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 77557fa4bbf..0b1fcec7b29 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2897,6 +2897,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ tab->at_params.main_relopts = NULL;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..6c599382f02 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -247,6 +247,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options,
extern bytea *default_reloptions(Datum reloptions, bool validate,
relopt_kind kind);
+extern struct StdRdOptions *merge_toast_reloptions(struct StdRdOptions *toast_opts,
+ struct StdRdOptions *main_opts);
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..55ce22f4c5b 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,14 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * When vacuuming a TOAST table, this holds the main table's storage
+ * parameters (or NULL if it doesn't have any). If a storage parameter is
+ * unset on the TOAST table but _is_ set on the main table, we use the
+ * main table's setting.
+ */
+ struct StdRdOptions *main_relopts;
} VacuumParams;
/*
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..caf0cc232b4 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,9 +79,20 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..0a43e14c928 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,9 +33,17 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
--
2.55.0
[text/plain] v13-0005-Fix-autovacuum-s-handling-of-TOAST-storage-param.patch (13.6K, ../../aoNqVz4Nf50FGfld@nathan/6-v13-0005-Fix-autovacuum-s-handling-of-TOAST-storage-param.patch)
download | inline diff:
From ada6537d99e5bed2aa1a5d1d47c6daa4d7f7ac16 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Wed, 12 Aug 2026 12:02:21 -0500
Subject: [PATCH v13 5/5] Fix autovacuum's handling of TOAST storage
parameters.
The previous commit made VACUUM apply a main table's storage
parameters to its TOAST table, as CREATE TABLE has long documented.
Autovacuum still gets this wrong in two ways. It falls back to the
main table's autovacuum parameters only when the TOAST table has no
reloptions at all, so setting a single toast.* parameter silently
discards the rest. And it never consults the main table for the
parameters that only VACUUM reads, since it leaves those for
vacuum_rel() to resolve from the TOAST table's own reloptions.
To fix, combine the two sets with merge_toast_reloptions() rather
than choosing between them, and hand the main table's parameters
down to vacuum_rel() the way VACUUM now does.
pg_stat_autovacuum_scores uses the combined parameters for TOAST
tables as well; it has to collect the main relations' parameters
before it can do so, so it now makes a preliminary pass over
pg_class.
An existing shortcoming that this patch only makes worse is that
autovacuum remains oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
---
src/backend/postmaster/autovacuum.c | 148 +++++++++++++-----
src/test/modules/test_autovacuum/meson.build | 1 +
.../test_autovacuum/t/002_toast_relopts.pl | 69 ++++++++
3 files changed, 176 insertions(+), 42 deletions(-)
create mode 100644 src/test/modules/test_autovacuum/t/002_toast_relopts.pl
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 0b1fcec7b29..be239132572 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1913,6 +1913,40 @@ TableToProcessComparator(const ListCell *a, const ListCell *b)
return (t2->score < t1->score) ? -1 : (t2->score > t1->score) ? 1 : 0;
}
+/*
+ * get_effective_relopts
+ * Fetch the storage parameters that apply to a relation.
+ *
+ * This looks up the reloptions for the pg_class relation in "tup". If it is a
+ * TOAST table, we also merge in any unset reloptions with the main table's
+ * stored in "toast_map". If the relation neither sets nor inherits any
+ * reloptions, this function returns NULL. Else, a palloc'd copy of the
+ * applicable reloptions are returned.
+ *
+ * If "tup" refers to a TOAST table and the toast_map has reloptions stored for
+ * its main relation, we return a pointer to the main table's reloptions via
+ * *main_opts. Else, main_opts is set to NULL.
+ */
+static StdRdOptions *
+get_effective_relopts(HeapTuple tup, TupleDesc desc, HTAB *toast_map,
+ StdRdOptions **main_opts)
+{
+ Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ av_relation *hentry = NULL;
+
+ /* look up our relopts */
+ relopts = (StdRdOptions *) extractRelOptions(tup, desc, NULL);
+
+ /* if we're a TOAST table, look up our parent's relopts, too */
+ if (classForm->relkind == RELKIND_TOASTVALUE)
+ hentry = hash_search(toast_map, &classForm->oid, HASH_FIND, NULL);
+ *main_opts = hentry ? &hentry->ar_reloptions : NULL;
+
+ /* return the merged reloptions */
+ return merge_toast_reloptions(relopts, *main_opts);
+}
+
/*
* Process a database table-by-table
*
@@ -2015,9 +2049,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2130,7 +2164,7 @@ do_autovacuum(void)
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2144,22 +2178,9 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ /* fetch reloptions -- merge any unset options from the main rel */
+ relopts = get_effective_relopts(tuple, pg_class_desc, table_toast_map,
+ &main_relopts);
relation_needs_vacanalyze(relid,
relopts ? &relopts->autovacuum : NULL,
@@ -2180,7 +2201,7 @@ do_autovacuum(void)
}
/* Release stuff to avoid leakage */
- if (free_relopts)
+ if (relopts)
pfree(relopts);
}
@@ -2787,7 +2808,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool wraparound;
AutoVacOpts *avopts;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2797,21 +2818,11 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
- relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE)
- {
- av_relation *hentry;
- bool found;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
- if (found)
- relopts = &hentry->ar_reloptions;
- }
+ relopts = get_effective_relopts(classTup, pg_class_desc, table_toast_map,
+ &main_relopts);
avopts = relopts ? &relopts->autovacuum : NULL;
@@ -2897,7 +2908,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
- tab->at_params.main_relopts = NULL;
+ tab->at_params.main_relopts = main_relopts;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
@@ -2942,7 +2953,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_relopts)
+ if (relopts)
pfree(relopts);
heap_freetuple(classTup);
return tab;
@@ -2956,8 +2967,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* being forced because of Xid or multixact wraparound.
*
* relopts is a pointer to the AutoVacOpts options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * case of a plain table, or merged with the main table's for a TOAST table),
+ * NULL if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3614,6 +3625,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
TableScanDesc scan;
HeapTuple tup;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
InitMaterializedSRF(fcinfo, 0);
@@ -3622,13 +3635,62 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
rel = table_open(RelationRelationId, AccessShareLock);
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parents' reloptions, we can generate the results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ StdRdOptions *main_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3644,7 +3706,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relopts = get_effective_relopts(tup, RelationGetDescr(rel),
+ table_toast_map, &main_relopts);
relation_needs_vacanalyze(form->oid,
relopts ? &relopts->autovacuum : NULL,
form,
@@ -3670,6 +3733,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
diff --git a/src/test/modules/test_autovacuum/meson.build b/src/test/modules/test_autovacuum/meson.build
index 86e392bc0de..970b9aaae4b 100644
--- a/src/test/modules/test_autovacuum/meson.build
+++ b/src/test/modules/test_autovacuum/meson.build
@@ -10,6 +10,7 @@ tests += {
},
'tests': [
't/001_parallel_autovacuum.pl',
+ 't/002_toast_relopts.pl',
],
},
}
diff --git a/src/test/modules/test_autovacuum/t/002_toast_relopts.pl b/src/test/modules/test_autovacuum/t/002_toast_relopts.pl
new file mode 100644
index 00000000000..ba7596cc82b
--- /dev/null
+++ b/src/test/modules/test_autovacuum/t/002_toast_relopts.pl
@@ -0,0 +1,69 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test autovacuum's handling of TOAST storage parameters
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Create a test node with autovacuum disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+autovacuum = off
+autovacuum_naptime = '1s'
+});
+$node->start;
+
+# Create TOAST table that is eligible for autovacuum due to inherited relopts.
+$node->safe_psql(
+ 'postgres', qq{
+ CREATE TABLE toast_relopts (i int, j text STORAGE EXTERNAL) WITH
+ (autovacuum_enabled = false, toast.autovacuum_enabled = true,
+ autovacuum_vacuum_threshold = 1,
+ autovacuum_vacuum_scale_factor = 0,
+ autovacuum_vacuum_insert_threshold = 1,
+ autovacuum_vacuum_insert_scale_factor = 0,
+ vacuum_truncate = false);
+ INSERT INTO toast_relopts VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+ SELECT pg_stat_force_next_flush();
+});
+
+# Get TOAST table's OID for following commands.
+my $toast = $node->safe_psql('postgres',
+ "SELECT reltoastrelid::regclass FROM pg_class WHERE oid = 'toast_relopts'::regclass"
+);
+
+# Verify scores view used inherited insert threshold.
+is( $node->safe_psql(
+ 'postgres', qq{
+ SELECT vacuum_insert_score > 1 FROM pg_stat_autovacuum_scores
+ WHERE relid = '$toast'::regclass
+}),
+ 't',
+ 'inherited insert threshold in pg_stat_autovacuum_scores');
+
+# Delete all rows so that we can verify inherited vacuum_truncate takes effect.
+$node->safe_psql('postgres', 'DELETE FROM toast_relopts');
+
+# Enable autovacuum.
+$node->append_conf('postgresql.conf', 'autovacuum = on');
+$node->reload;
+
+# Wait until autovacuum processes the table.
+ok( $node->poll_query_until(
+ 'postgres', qq{
+ SELECT last_autovacuum IS NOT NULL FROM pg_stat_all_tables
+ WHERE relid = '$toast'::regclass
+}),
+ 'autovacuum of a TOAST table with inherited thresholds');
+
+# Verify autovacuum didn't truncate the table.
+is($node->safe_psql('postgres', "SELECT pg_relation_size('$toast') > 0"),
+ 't', 'inherited vacuum_truncate');
+
+$node->stop;
+done_testing();
--
2.55.0
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-19 07:16 Michael Paquier <michael@paquier.xyz>
parent: Nathan Bossart <nathandbossart@gmail.com>
1 sibling, 1 reply; 44+ messages in thread
From: Michael Paquier @ 2026-08-19 07:16 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: Sami Imseih <samimseih@gmail.com>; Greg Burd <greg@burd.me>; solai v <solai.cdac@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; pgsql-hackers
On Thu, Aug 13, 2026 at 11:41:59AM -0500, Nathan Bossart wrote:
> On Thu, Aug 13, 2026 at 03:28:23PM +0900, Michael Paquier wrote:
>> In 0004, I was wondering if this makes the code weaker on some
>> aspects, because we are switching from a logic where we always had
>> an entry in the mapping hashtable for a main relation with a TOAST
>> table to a logic where a NULL entry could mean either:
Looking at the five remaining patches in v13, replying to the message
where v12 was posted.
> I personally don't see much point in tracking additional information we
> don't need. We can already tell if the table in question is a TOAST table,
> so a missing entry in the hash table means that we didn't find any main
> table relopts for it. *shrug*
Hmm. Okay. Fine by me at the end.
>> + if (rel->rd_options)
>> + memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
>> [...]
>> + * NB: This destructively modifies toast_opts, and what it returns may be
>> + * either argument, so the caller must know which of the two it owns.
>>
>> Hmm. I am not really cool with this as an API contract. That can
>> bite. That's not re-entrant, to begin with, and on top of that this
>> function returns the merged result. It would be saner to create a
>> copy, and return the copy as a result, copy that we do anyway before
>> the sole caller of the function with a memcpy(). :)
>
> Done in v12.
The API contract in v13-0004 looks much better to me now. No more
overwrites of the inputs. It's almost like you could add some const
markers.
>> Hmm. We have three callers of get_effective_relopts(), and some paths
>> can call it for a main relation, meaning that the
>> merge_toast_reloptions() makes little sense because there is nothing
>> to merge. Should this enforce a check so as we try to merge
>> reloptions only when dealing with a toast relation, or should the
>> callers for that by themselves based on the classForm->relkind?
>
> It enforces that already. The relkind check in the function ensures that
> main_opts is always NULL for non-TOAST relations, and
> merge_toast_reloptions() always returns the first argument when the second
> is NULL. I do think this could be called out a bit better, which I've
> tried to do in v12.
At the end of the day, get_effective_relopts() acts as a thin wrapper
of extractRelOptions(), merging two existing code patterns and
re-using the same pattern for the scoring. Perhaps "effective" is the
term that troubles me here, while having merge_toast_reloptions().
You need the merge_*() for the vacuum part, but I'm also wondering if
this could not be reworked with less routines overall. I don't have a
clean idea on top of my mind now, and that does not count as an
objection. This gives an impression of being slightly
overcomplicated.
>> In 0008, some tests would be nice for the autovacuum case, at least.
>> That would mean a TAP test to check a bit what do_autovacuum() does,
>> and now the SQL test in injection_points only looks after
>> pg_stat_get_autovacuum_scores(). I am honestly puzzled by the reason
>> why this is added inside injection_points at all. There is no
>> dependency to a point, and no new information with the NOTICE
>> messages. A better location would fit better the purpose of the score
>> test.
>
> I only put it there because 0007 added a similar test, and 0007 and 0008
> used to be one patch. In v12, I've tried my hand at a TAP test.
The test looks pretty nice here.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../aoVYWGzEUwpJxi5u@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-21 22:26 Nathan Bossart <nathandbossart@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 1 reply; 44+ messages in thread
From: Nathan Bossart @ 2026-08-21 22:26 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Sami Imseih <samimseih@gmail.com>; Greg Burd <greg@burd.me>; solai v <solai.cdac@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; pgsql-hackers
I've committed everything but the last two patches. I've attached a
rebased patch set.
On Wed, Aug 19, 2026 at 04:16:40PM +0900, Michael Paquier wrote:
> The API contract in v13-0004 looks much better to me now. No more
> overwrites of the inputs. It's almost like you could add some const
> markers.
Done in v14.
> At the end of the day, get_effective_relopts() acts as a thin wrapper
> of extractRelOptions(), merging two existing code patterns and
> re-using the same pattern for the scoring. Perhaps "effective" is the
> term that troubles me here, while having merge_toast_reloptions().
> You need the merge_*() for the vacuum part, but I'm also wondering if
> this could not be reworked with less routines overall. I don't have a
> clean idea on top of my mind now, and that does not count as an
> objection. This gives an impression of being slightly
> overcomplicated.
I haven't thought of anything better.
> The test looks pretty nice here.
Thanks for reviewing!
--
nathan
From d02ad463b6d269f89ae1b202014667f1814cac00 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 21 Aug 2026 16:47:17 -0500
Subject: [PATCH v14 1/2] Fix VACUUM's handling of TOAST storage parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Presently, VACUUM does no such thing. It reads the TOAST table's
own reloptions, which hold only what was set through toast.*, so
vacuum_index_cleanup or vacuum_truncate set on the main table has
no effect on its TOAST table.
To fix, add merge_toast_reloptions(), which walks the parse table
for StdRdOptions and takes the main table's value for anything the
TOAST table left at its default. vacuum_rel() hands the main
table's parameters down when recursing to a TOAST table, and it
merges them into a copy of the TOAST table's parameters before the
values are used. This doesn't help VACUUM against a TOAST table
directly (e.g., "VACUUM pg_toast.pg_toast_5432"), but that's
probably okay because it's not the main supported way to vacuum a
TOAST table (see VACUUM's PROCESS_MAIN and PROCESS_TOAST options).
A follow-up commit will do the same for autovacuum. While this is
a bug fix, it's too intrusive for back-patching, but the issue
seems to have gone unnoticed for a very long time, anyway.
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Sami Imseih <samimseih@gmail.com>
Reviewed-by: Greg Burd <greg@burd.me>
Tested-by: solai v <solai.cdac@gmail.com>
Discussion: https://postgr.es/m/aFRxC1W_kZU9OjJ9%40nathan
---
src/backend/access/common/reloptions.c | 104 ++++++++++++++++++
src/backend/commands/vacuum.c | 40 +++++--
src/backend/postmaster/autovacuum.c | 1 +
src/include/access/reloptions.h | 2 +
src/include/commands/vacuum.h | 8 ++
.../injection_points/expected/vacuum.out | 11 ++
.../modules/injection_points/sql/vacuum.sql | 8 ++
7 files changed, 163 insertions(+), 11 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index d7e63c10d58..ca5a0726dda 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2110,6 +2110,110 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
lengthof(stdRdOptionsTab));
}
+/*
+ * find_reloption
+ * Look up a reloption of the given kind by name.
+ *
+ * Returns NULL if no such option can be set on relations of that kind. Note
+ * that names are unique only within a kind; "fillfactor", for example, is
+ * declared separately for heaps and for each index access method.
+ */
+static relopt_gen *
+find_reloption(const char *name, relopt_kind kind)
+{
+ if (need_initialization)
+ initialize_reloptions();
+
+ for (int i = 0; relOpts[i]; i++)
+ {
+ if ((relOpts[i]->kinds & kind) != 0 &&
+ strcmp(relOpts[i]->name, name) == 0)
+ return relOpts[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * merge_toast_reloptions
+ * Fill in a TOAST table's unset options from its main table's.
+ *
+ * Any option that may be set on a TOAST table but was not is taken from
+ * main_opts. Either argument may be NULL; if both are, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * An option counts as unset while it still holds the default declared for it
+ * above, which works because nothing a TOAST table accepts has a default the
+ * user could also set (see assert_toast_defaults_unsettable()).
+ *
+ * If the return value is not NULL, it is palloc'd.
+ */
+StdRdOptions *
+merge_toast_reloptions(const StdRdOptions *toast_opts,
+ const StdRdOptions *main_opts)
+{
+ StdRdOptions *ret;
+
+ /* if both arguments are NULL, return NULL */
+ if (toast_opts == NULL && main_opts == NULL)
+ return NULL;
+
+ /* if one argument is NULL, return the non-NULL one */
+ ret = palloc_object(StdRdOptions);
+ if (toast_opts == NULL || main_opts == NULL)
+ {
+ memcpy(ret, main_opts ? main_opts : toast_opts, sizeof(StdRdOptions));
+ return ret;
+ }
+
+ /* replace unset TOAST relopts with the main table's */
+ memcpy(ret, toast_opts, sizeof(StdRdOptions));
+ for (int i = 0; i < lengthof(stdRdOptionsTab); i++)
+ {
+ const relopt_parse_elt *elem = &stdRdOptionsTab[i];
+ relopt_gen *gen;
+ char *toast_val;
+ const char *main_val;
+
+ /* skip anything that cannot be set on a TOAST table */
+ gen = find_reloption(elem->optname, RELOPT_KIND_TOAST);
+ if (gen == NULL)
+ continue;
+
+ toast_val = (char *) ret + elem->offset;
+ main_val = (const char *) main_opts + elem->offset;
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+ if (*(pg_ternary *) toast_val == PG_TERNARY_UNSET)
+ *(pg_ternary *) toast_val = *(const pg_ternary *) main_val;
+ break;
+
+ case RELOPT_TYPE_INT:
+ if (*(int *) toast_val == ((relopt_int *) gen)->default_val)
+ *(int *) toast_val = *(const int *) main_val;
+ break;
+
+ case RELOPT_TYPE_REAL:
+ if (*(double *) toast_val == ((relopt_real *) gen)->default_val)
+ *(double *) toast_val = *(const double *) main_val;
+ break;
+
+ case RELOPT_TYPE_ENUM:
+ if (*(int *) toast_val == ((relopt_enum *) gen)->default_val)
+ *(int *) toast_val = *(const int *) main_val;
+ break;
+
+ default:
+ elog(ERROR, "reloption \"%s\" has a type a TOAST table cannot inherit",
+ elem->optname);
+ }
+ }
+
+ return ret;
+}
+
/*
* build_reloptions
*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 64eed16a160..458cf956c10 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -31,6 +31,7 @@
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
+#include "access/reloptions.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
@@ -187,6 +188,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_relopts = NULL;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2039,6 +2041,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
int save_sec_context;
int save_nestlevel;
VacuumParams toast_vacuum_params;
+ StdRdOptions *relopts;
+ StdRdOptions relopts_copy;
/*
* This function scribbles on the parameters, so make a copy early to
@@ -2201,6 +2205,14 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ /*
+ * A TOAST table takes any storage parameter it accepts but does not set
+ * from its main table, whose parameters the caller handed down for that
+ * purpose. Merge them into a copy of our own.
+ */
+ relopts = merge_toast_reloptions((StdRdOptions *) rel->rd_options,
+ params.main_relopts);
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2209,11 +2221,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
StdRdOptIndexCleanup vacuum_index_cleanup;
- if (rel->rd_options == NULL)
+ if (relopts == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = relopts->vacuum_index_cleanup;
switch (vacuum_index_cleanup)
{
@@ -2245,10 +2256,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
- params.max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ if (relopts != NULL && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ params.max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
@@ -2256,11 +2265,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
- StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
-
- if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
+ if (relopts && relopts->vacuum_truncate != PG_TERNARY_UNSET)
{
- if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ if (relopts->vacuum_truncate == PG_TERNARY_TRUE)
params.truncate = VACOPTVALUE_ENABLED;
else
params.truncate = VACOPTVALUE_DISABLED;
@@ -2293,6 +2300,17 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
else
toast_relid = InvalidOid;
+ /*
+ * Hand our storage parameters down for the TOAST table to inherit. Take
+ * a copy while we still have the relation open; the relcache entry can go
+ * away once we close it.
+ */
+ if (OidIsValid(toast_relid) && rel->rd_options)
+ {
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ toast_vacuum_params.main_relopts = &relopts_copy;
+ }
+
/*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also lock down security-restricted operations and
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 9c73c10fb11..30d6a5feb17 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2895,6 +2895,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ tab->at_params.main_relopts = NULL;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..ccff4717b62 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -247,6 +247,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options,
extern bytea *default_reloptions(Datum reloptions, bool validate,
relopt_kind kind);
+extern struct StdRdOptions *merge_toast_reloptions(const struct StdRdOptions *toast_opts,
+ const struct StdRdOptions *main_opts);
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..6e3c912bf5c 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,14 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * When vacuuming a TOAST table, this holds the main table's storage
+ * parameters (or NULL if it doesn't have any). If a storage parameter is
+ * unset on the TOAST table but _is_ set on the main table, we use the
+ * main table's setting.
+ */
+ const struct StdRdOptions *main_relopts;
} VacuumParams;
/*
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..caf0cc232b4 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,9 +79,20 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..0a43e14c928 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,9 +33,17 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
--
2.55.0
From bbad7a91083b6e2cfed1d69132e989fc7f18b316 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 21 Aug 2026 17:01:41 -0500
Subject: [PATCH v14 2/2] Fix autovacuum's handling of TOAST storage
parameters.
The previous commit made VACUUM apply a main table's storage
parameters to its TOAST table, as CREATE TABLE has long documented.
Autovacuum still gets this wrong in two ways. It falls back to the
main table's autovacuum parameters only when the TOAST table has no
reloptions at all, so setting a single toast.* parameter silently
discards the rest. And it never consults the main table for the
parameters that only VACUUM reads, since it leaves those for
vacuum_rel() to resolve from the TOAST table's own reloptions.
To fix, combine the two sets with merge_toast_reloptions() rather
than choosing between them, and hand the main table's parameters
down to vacuum_rel() the way VACUUM now does.
pg_stat_autovacuum_scores uses the combined parameters for TOAST
tables as well; it has to collect the main relations' parameters
before it can do so, so it now makes a preliminary pass over
pg_class.
An existing shortcoming that this patch only makes worse is that
autovacuum remains oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Sami Imseih <samimseih@gmail.com>
Reviewed-by: Greg Burd <greg@burd.me>
Tested-by: solai v <solai.cdac@gmail.com>
Discussion: https://postgr.es/m/aFRxC1W_kZU9OjJ9%40nathan
---
src/backend/postmaster/autovacuum.c | 151 +++++++++++++-----
src/test/modules/test_autovacuum/meson.build | 1 +
.../test_autovacuum/t/002_toast_relopts.pl | 69 ++++++++
3 files changed, 181 insertions(+), 40 deletions(-)
create mode 100644 src/test/modules/test_autovacuum/t/002_toast_relopts.pl
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 30d6a5feb17..e44884f3ef8 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1913,6 +1913,45 @@ TableToProcessComparator(const ListCell *a, const ListCell *b)
return (t2->score < t1->score) ? -1 : (t2->score > t1->score) ? 1 : 0;
}
+/*
+ * get_effective_relopts
+ * Fetch the storage parameters that apply to a relation.
+ *
+ * This looks up the reloptions for the pg_class relation in "tup". If it is a
+ * TOAST table, we also merge in any unset reloptions with the main table's
+ * stored in "toast_map". If the relation neither sets nor inherits any
+ * reloptions, this function returns NULL. Else, a palloc'd copy of the
+ * applicable reloptions is returned.
+ *
+ * If "tup" refers to a TOAST table and "toast_map" has reloptions stored for
+ * its main relation, we return a pointer to the main table's reloptions via
+ * *main_opts. Else, *main_opts is set to NULL.
+ */
+static StdRdOptions *
+get_effective_relopts(HeapTuple tup, TupleDesc desc, HTAB *toast_map,
+ StdRdOptions **main_opts)
+{
+ Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ StdRdOptions *ret;
+ av_relation *hentry = NULL;
+
+ /* look up our relopts */
+ relopts = (StdRdOptions *) extractRelOptions(tup, desc, NULL);
+
+ /* if we're a TOAST table, look up our parent's relopts, too */
+ if (classForm->relkind == RELKIND_TOASTVALUE)
+ hentry = hash_search(toast_map, &classForm->oid, HASH_FIND, NULL);
+ *main_opts = hentry ? &hentry->ar_reloptions : NULL;
+
+ /* return the merged reloptions */
+ ret = merge_toast_reloptions(relopts, *main_opts);
+
+ if (relopts)
+ pfree(relopts);
+ return ret;
+}
+
/*
* Process a database table-by-table
*
@@ -2015,9 +2054,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2130,7 +2169,7 @@ do_autovacuum(void)
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2144,21 +2183,9 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, NULL);
- if (hentry)
- relopts = &hentry->ar_reloptions;
- }
+ /* fetch reloptions -- merge any unset options from the main rel */
+ relopts = get_effective_relopts(tuple, pg_class_desc, table_toast_map,
+ &main_relopts);
relation_needs_vacanalyze(relid,
relopts ? &relopts->autovacuum : NULL,
@@ -2179,7 +2206,7 @@ do_autovacuum(void)
}
/* Release stuff to avoid leakage */
- if (free_relopts)
+ if (relopts)
pfree(relopts);
}
@@ -2786,7 +2813,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool wraparound;
AutoVacOpts *avopts;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2796,20 +2823,11 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
- relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE)
- {
- av_relation *hentry;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, NULL);
- if (hentry)
- relopts = &hentry->ar_reloptions;
- }
+ relopts = get_effective_relopts(classTup, pg_class_desc, table_toast_map,
+ &main_relopts);
avopts = relopts ? &relopts->autovacuum : NULL;
@@ -2895,7 +2913,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
- tab->at_params.main_relopts = NULL;
+ tab->at_params.main_relopts = main_relopts;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
@@ -2940,7 +2958,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_relopts)
+ if (relopts)
pfree(relopts);
heap_freetuple(classTup);
return tab;
@@ -2954,8 +2972,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* being forced because of Xid or multixact wraparound.
*
* relopts is a pointer to the AutoVacOpts options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * case of a plain table, or merged with the main table's for a TOAST table),
+ * NULL if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3612,6 +3630,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
TableScanDesc scan;
HeapTuple tup;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
InitMaterializedSRF(fcinfo, 0);
@@ -3620,13 +3640,62 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
rel = table_open(RelationRelationId, AccessShareLock);
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parents' reloptions, we can generate the results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ StdRdOptions *main_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3642,7 +3711,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relopts = get_effective_relopts(tup, RelationGetDescr(rel),
+ table_toast_map, &main_relopts);
relation_needs_vacanalyze(form->oid,
relopts ? &relopts->autovacuum : NULL,
form,
@@ -3668,6 +3738,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
diff --git a/src/test/modules/test_autovacuum/meson.build b/src/test/modules/test_autovacuum/meson.build
index 86e392bc0de..970b9aaae4b 100644
--- a/src/test/modules/test_autovacuum/meson.build
+++ b/src/test/modules/test_autovacuum/meson.build
@@ -10,6 +10,7 @@ tests += {
},
'tests': [
't/001_parallel_autovacuum.pl',
+ 't/002_toast_relopts.pl',
],
},
}
diff --git a/src/test/modules/test_autovacuum/t/002_toast_relopts.pl b/src/test/modules/test_autovacuum/t/002_toast_relopts.pl
new file mode 100644
index 00000000000..ba7596cc82b
--- /dev/null
+++ b/src/test/modules/test_autovacuum/t/002_toast_relopts.pl
@@ -0,0 +1,69 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test autovacuum's handling of TOAST storage parameters
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Create a test node with autovacuum disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+autovacuum = off
+autovacuum_naptime = '1s'
+});
+$node->start;
+
+# Create TOAST table that is eligible for autovacuum due to inherited relopts.
+$node->safe_psql(
+ 'postgres', qq{
+ CREATE TABLE toast_relopts (i int, j text STORAGE EXTERNAL) WITH
+ (autovacuum_enabled = false, toast.autovacuum_enabled = true,
+ autovacuum_vacuum_threshold = 1,
+ autovacuum_vacuum_scale_factor = 0,
+ autovacuum_vacuum_insert_threshold = 1,
+ autovacuum_vacuum_insert_scale_factor = 0,
+ vacuum_truncate = false);
+ INSERT INTO toast_relopts VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+ SELECT pg_stat_force_next_flush();
+});
+
+# Get TOAST table's OID for following commands.
+my $toast = $node->safe_psql('postgres',
+ "SELECT reltoastrelid::regclass FROM pg_class WHERE oid = 'toast_relopts'::regclass"
+);
+
+# Verify scores view used inherited insert threshold.
+is( $node->safe_psql(
+ 'postgres', qq{
+ SELECT vacuum_insert_score > 1 FROM pg_stat_autovacuum_scores
+ WHERE relid = '$toast'::regclass
+}),
+ 't',
+ 'inherited insert threshold in pg_stat_autovacuum_scores');
+
+# Delete all rows so that we can verify inherited vacuum_truncate takes effect.
+$node->safe_psql('postgres', 'DELETE FROM toast_relopts');
+
+# Enable autovacuum.
+$node->append_conf('postgresql.conf', 'autovacuum = on');
+$node->reload;
+
+# Wait until autovacuum processes the table.
+ok( $node->poll_query_until(
+ 'postgres', qq{
+ SELECT last_autovacuum IS NOT NULL FROM pg_stat_all_tables
+ WHERE relid = '$toast'::regclass
+}),
+ 'autovacuum of a TOAST table with inherited thresholds');
+
+# Verify autovacuum didn't truncate the table.
+is($node->safe_psql('postgres', "SELECT pg_relation_size('$toast') > 0"),
+ 't', 'inherited vacuum_truncate');
+
+$node->stop;
+done_testing();
--
2.55.0
Attachments:
[text/plain] v14-0001-Fix-VACUUM-s-handling-of-TOAST-storage-parameter.patch (13.4K, ../../aojQhYmdgAK_tBbk@nathan/2-v14-0001-Fix-VACUUM-s-handling-of-TOAST-storage-parameter.patch)
download | inline diff:
From d02ad463b6d269f89ae1b202014667f1814cac00 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 21 Aug 2026 16:47:17 -0500
Subject: [PATCH v14 1/2] Fix VACUUM's handling of TOAST storage parameters.
Per the documentation for CREATE TABLE:
If a table parameter value is set and the equivalent toast.
parameter is not, the TOAST table will use the table's
parameter value.
Presently, VACUUM does no such thing. It reads the TOAST table's
own reloptions, which hold only what was set through toast.*, so
vacuum_index_cleanup or vacuum_truncate set on the main table has
no effect on its TOAST table.
To fix, add merge_toast_reloptions(), which walks the parse table
for StdRdOptions and takes the main table's value for anything the
TOAST table left at its default. vacuum_rel() hands the main
table's parameters down when recursing to a TOAST table, and it
merges them into a copy of the TOAST table's parameters before the
values are used. This doesn't help VACUUM against a TOAST table
directly (e.g., "VACUUM pg_toast.pg_toast_5432"), but that's
probably okay because it's not the main supported way to vacuum a
TOAST table (see VACUUM's PROCESS_MAIN and PROCESS_TOAST options).
A follow-up commit will do the same for autovacuum. While this is
a bug fix, it's too intrusive for back-patching, but the issue
seems to have gone unnoticed for a very long time, anyway.
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Sami Imseih <samimseih@gmail.com>
Reviewed-by: Greg Burd <greg@burd.me>
Tested-by: solai v <solai.cdac@gmail.com>
Discussion: https://postgr.es/m/aFRxC1W_kZU9OjJ9%40nathan
---
src/backend/access/common/reloptions.c | 104 ++++++++++++++++++
src/backend/commands/vacuum.c | 40 +++++--
src/backend/postmaster/autovacuum.c | 1 +
src/include/access/reloptions.h | 2 +
src/include/commands/vacuum.h | 8 ++
.../injection_points/expected/vacuum.out | 11 ++
.../modules/injection_points/sql/vacuum.sql | 8 ++
7 files changed, 163 insertions(+), 11 deletions(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index d7e63c10d58..ca5a0726dda 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -2110,6 +2110,110 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
lengthof(stdRdOptionsTab));
}
+/*
+ * find_reloption
+ * Look up a reloption of the given kind by name.
+ *
+ * Returns NULL if no such option can be set on relations of that kind. Note
+ * that names are unique only within a kind; "fillfactor", for example, is
+ * declared separately for heaps and for each index access method.
+ */
+static relopt_gen *
+find_reloption(const char *name, relopt_kind kind)
+{
+ if (need_initialization)
+ initialize_reloptions();
+
+ for (int i = 0; relOpts[i]; i++)
+ {
+ if ((relOpts[i]->kinds & kind) != 0 &&
+ strcmp(relOpts[i]->name, name) == 0)
+ return relOpts[i];
+ }
+
+ return NULL;
+}
+
+/*
+ * merge_toast_reloptions
+ * Fill in a TOAST table's unset options from its main table's.
+ *
+ * Any option that may be set on a TOAST table but was not is taken from
+ * main_opts. Either argument may be NULL; if both are, NULL is returned.
+ * Otherwise, the options to use are returned.
+ *
+ * An option counts as unset while it still holds the default declared for it
+ * above, which works because nothing a TOAST table accepts has a default the
+ * user could also set (see assert_toast_defaults_unsettable()).
+ *
+ * If the return value is not NULL, it is palloc'd.
+ */
+StdRdOptions *
+merge_toast_reloptions(const StdRdOptions *toast_opts,
+ const StdRdOptions *main_opts)
+{
+ StdRdOptions *ret;
+
+ /* if both arguments are NULL, return NULL */
+ if (toast_opts == NULL && main_opts == NULL)
+ return NULL;
+
+ /* if one argument is NULL, return the non-NULL one */
+ ret = palloc_object(StdRdOptions);
+ if (toast_opts == NULL || main_opts == NULL)
+ {
+ memcpy(ret, main_opts ? main_opts : toast_opts, sizeof(StdRdOptions));
+ return ret;
+ }
+
+ /* replace unset TOAST relopts with the main table's */
+ memcpy(ret, toast_opts, sizeof(StdRdOptions));
+ for (int i = 0; i < lengthof(stdRdOptionsTab); i++)
+ {
+ const relopt_parse_elt *elem = &stdRdOptionsTab[i];
+ relopt_gen *gen;
+ char *toast_val;
+ const char *main_val;
+
+ /* skip anything that cannot be set on a TOAST table */
+ gen = find_reloption(elem->optname, RELOPT_KIND_TOAST);
+ if (gen == NULL)
+ continue;
+
+ toast_val = (char *) ret + elem->offset;
+ main_val = (const char *) main_opts + elem->offset;
+
+ switch (gen->type)
+ {
+ case RELOPT_TYPE_TERNARY:
+ if (*(pg_ternary *) toast_val == PG_TERNARY_UNSET)
+ *(pg_ternary *) toast_val = *(const pg_ternary *) main_val;
+ break;
+
+ case RELOPT_TYPE_INT:
+ if (*(int *) toast_val == ((relopt_int *) gen)->default_val)
+ *(int *) toast_val = *(const int *) main_val;
+ break;
+
+ case RELOPT_TYPE_REAL:
+ if (*(double *) toast_val == ((relopt_real *) gen)->default_val)
+ *(double *) toast_val = *(const double *) main_val;
+ break;
+
+ case RELOPT_TYPE_ENUM:
+ if (*(int *) toast_val == ((relopt_enum *) gen)->default_val)
+ *(int *) toast_val = *(const int *) main_val;
+ break;
+
+ default:
+ elog(ERROR, "reloption \"%s\" has a type a TOAST table cannot inherit",
+ elem->optname);
+ }
+ }
+
+ return ret;
+}
+
/*
* build_reloptions
*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 64eed16a160..458cf956c10 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -31,6 +31,7 @@
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/multixact.h"
+#include "access/reloptions.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
@@ -187,6 +188,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/* Will be set later if we recurse to a TOAST table. */
params.toast_parent = InvalidOid;
+ params.main_relopts = NULL;
/*
* Set this to an invalid value so it is clear whether or not a
@@ -2039,6 +2041,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
int save_sec_context;
int save_nestlevel;
VacuumParams toast_vacuum_params;
+ StdRdOptions *relopts;
+ StdRdOptions relopts_copy;
/*
* This function scribbles on the parameters, so make a copy early to
@@ -2201,6 +2205,14 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ /*
+ * A TOAST table takes any storage parameter it accepts but does not set
+ * from its main table, whose parameters the caller handed down for that
+ * purpose. Merge them into a copy of our own.
+ */
+ relopts = merge_toast_reloptions((StdRdOptions *) rel->rd_options,
+ params.main_relopts);
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2209,11 +2221,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
{
StdRdOptIndexCleanup vacuum_index_cleanup;
- if (rel->rd_options == NULL)
+ if (relopts == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_NOT_SET;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = relopts->vacuum_index_cleanup;
switch (vacuum_index_cleanup)
{
@@ -2245,10 +2256,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
- params.max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ if (relopts != NULL && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ params.max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
@@ -2256,11 +2265,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
*/
if (params.truncate == VACOPTVALUE_UNSPECIFIED)
{
- StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
-
- if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET)
+ if (relopts && relopts->vacuum_truncate != PG_TERNARY_UNSET)
{
- if (opts->vacuum_truncate == PG_TERNARY_TRUE)
+ if (relopts->vacuum_truncate == PG_TERNARY_TRUE)
params.truncate = VACOPTVALUE_ENABLED;
else
params.truncate = VACOPTVALUE_DISABLED;
@@ -2293,6 +2300,17 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
else
toast_relid = InvalidOid;
+ /*
+ * Hand our storage parameters down for the TOAST table to inherit. Take
+ * a copy while we still have the relation open; the relcache entry can go
+ * away once we close it.
+ */
+ if (OidIsValid(toast_relid) && rel->rd_options)
+ {
+ memcpy(&relopts_copy, rel->rd_options, sizeof(StdRdOptions));
+ toast_vacuum_params.main_relopts = &relopts_copy;
+ }
+
/*
* Switch to the table owner's userid, so that any index functions are run
* as that user. Also lock down security-restricted operations and
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 9c73c10fb11..30d6a5feb17 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2895,6 +2895,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
+ tab->at_params.main_relopts = NULL;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index e8cb7f7a627..ccff4717b62 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -247,6 +247,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options,
extern bytea *default_reloptions(Datum reloptions, bool validate,
relopt_kind kind);
+extern struct StdRdOptions *merge_toast_reloptions(const struct StdRdOptions *toast_opts,
+ const struct StdRdOptions *main_opts);
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index e62f23748dc..6e3c912bf5c 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -248,6 +248,14 @@ typedef struct VacuumParams
* disabled.
*/
int nworkers;
+
+ /*
+ * When vacuuming a TOAST table, this holds the main table's storage
+ * parameters (or NULL if it doesn't have any). If a storage parameter is
+ * unset on the TOAST table but _is_ set on the main table, we use the
+ * main table's setting.
+ */
+ const struct StdRdOptions *main_relopts;
} VacuumParams;
/*
diff --git a/src/test/modules/injection_points/expected/vacuum.out b/src/test/modules/injection_points/expected/vacuum.out
index 58df59fa927..caf0cc232b4 100644
--- a/src/test/modules/injection_points/expected/vacuum.out
+++ b/src/test/modules/injection_points/expected/vacuum.out
@@ -79,9 +79,20 @@ NOTICE: notice triggered for injection point vacuum-truncate-enabled
NOTICE: notice triggered for injection point vacuum-index-cleanup-auto
NOTICE: notice triggered for injection point vacuum-truncate-enabled
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-disabled
+NOTICE: notice triggered for injection point vacuum-index-cleanup-disabled
+NOTICE: notice triggered for injection point vacuum-truncate-enabled
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
injection_points_detach
diff --git a/src/test/modules/injection_points/sql/vacuum.sql b/src/test/modules/injection_points/sql/vacuum.sql
index 23760dd0f38..0a43e14c928 100644
--- a/src/test/modules/injection_points/sql/vacuum.sql
+++ b/src/test/modules/injection_points/sql/vacuum.sql
@@ -33,9 +33,17 @@ SET vacuum_truncate = true;
VACUUM vac_tab_auto;
RESET vacuum_truncate;
+-- A TOAST table inherits what it does not set from its main table.
+CREATE TABLE vac_tab_toast_inherit(i int, j text) WITH
+ (autovacuum_enabled=false,
+ vacuum_index_cleanup=false,
+ vacuum_truncate=false, toast.vacuum_truncate=true);
+VACUUM vac_tab_toast_inherit;
+
DROP TABLE vac_tab_auto;
DROP TABLE vac_tab_on_toast_off;
DROP TABLE vac_tab_off_toast_on;
+DROP TABLE vac_tab_toast_inherit;
-- Cleanup
SELECT injection_points_detach('vacuum-index-cleanup-auto');
--
2.55.0
[text/plain] v14-0002-Fix-autovacuum-s-handling-of-TOAST-storage-param.patch (13.9K, ../../aojQhYmdgAK_tBbk@nathan/3-v14-0002-Fix-autovacuum-s-handling-of-TOAST-storage-param.patch)
download | inline diff:
From bbad7a91083b6e2cfed1d69132e989fc7f18b316 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 21 Aug 2026 17:01:41 -0500
Subject: [PATCH v14 2/2] Fix autovacuum's handling of TOAST storage
parameters.
The previous commit made VACUUM apply a main table's storage
parameters to its TOAST table, as CREATE TABLE has long documented.
Autovacuum still gets this wrong in two ways. It falls back to the
main table's autovacuum parameters only when the TOAST table has no
reloptions at all, so setting a single toast.* parameter silently
discards the rest. And it never consults the main table for the
parameters that only VACUUM reads, since it leaves those for
vacuum_rel() to resolve from the TOAST table's own reloptions.
To fix, combine the two sets with merge_toast_reloptions() rather
than choosing between them, and hand the main table's parameters
down to vacuum_rel() the way VACUUM now does.
pg_stat_autovacuum_scores uses the combined parameters for TOAST
tables as well; it has to collect the main relations' parameters
before it can do so, so it now makes a preliminary pass over
pg_class.
An existing shortcoming that this patch only makes worse is that
autovacuum remains oblivious to concurrent storage parameter
changes on the main table. That is, the main table's parameters
may be captured long before its TOAST table is processed, and a
user may very well have altered the settings in the meantime.
Fixing that would likely require additional pg_class lookups, and
it's not clear if it's worth the trouble.
While this is a bug fix, it's too intrusive for back-patching, but
the issue seems to have gone unnoticed for a very long time,
anyway.
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Sami Imseih <samimseih@gmail.com>
Reviewed-by: Greg Burd <greg@burd.me>
Tested-by: solai v <solai.cdac@gmail.com>
Discussion: https://postgr.es/m/aFRxC1W_kZU9OjJ9%40nathan
---
src/backend/postmaster/autovacuum.c | 151 +++++++++++++-----
src/test/modules/test_autovacuum/meson.build | 1 +
.../test_autovacuum/t/002_toast_relopts.pl | 69 ++++++++
3 files changed, 181 insertions(+), 40 deletions(-)
create mode 100644 src/test/modules/test_autovacuum/t/002_toast_relopts.pl
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 30d6a5feb17..e44884f3ef8 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1913,6 +1913,45 @@ TableToProcessComparator(const ListCell *a, const ListCell *b)
return (t2->score < t1->score) ? -1 : (t2->score > t1->score) ? 1 : 0;
}
+/*
+ * get_effective_relopts
+ * Fetch the storage parameters that apply to a relation.
+ *
+ * This looks up the reloptions for the pg_class relation in "tup". If it is a
+ * TOAST table, we also merge in any unset reloptions with the main table's
+ * stored in "toast_map". If the relation neither sets nor inherits any
+ * reloptions, this function returns NULL. Else, a palloc'd copy of the
+ * applicable reloptions is returned.
+ *
+ * If "tup" refers to a TOAST table and "toast_map" has reloptions stored for
+ * its main relation, we return a pointer to the main table's reloptions via
+ * *main_opts. Else, *main_opts is set to NULL.
+ */
+static StdRdOptions *
+get_effective_relopts(HeapTuple tup, TupleDesc desc, HTAB *toast_map,
+ StdRdOptions **main_opts)
+{
+ Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ StdRdOptions *ret;
+ av_relation *hentry = NULL;
+
+ /* look up our relopts */
+ relopts = (StdRdOptions *) extractRelOptions(tup, desc, NULL);
+
+ /* if we're a TOAST table, look up our parent's relopts, too */
+ if (classForm->relkind == RELKIND_TOASTVALUE)
+ hentry = hash_search(toast_map, &classForm->oid, HASH_FIND, NULL);
+ *main_opts = hentry ? &hentry->ar_reloptions : NULL;
+
+ /* return the merged reloptions */
+ ret = merge_toast_reloptions(relopts, *main_opts);
+
+ if (relopts)
+ pfree(relopts);
+ return ret;
+}
+
/*
* Process a database table-by-table
*
@@ -2015,9 +2054,9 @@ do_autovacuum(void)
* We do this in two passes: on the first one we collect the list of plain
* relations and materialized views, and on the second one we collect
* TOAST tables. The reason for doing the second pass is that during it we
- * want to use the main relation's pg_class.reloptions entry if the TOAST
- * table does not have any, and we cannot obtain it unless we know
- * beforehand what's the main table OID.
+ * want to fill in any storage parameters that the TOAST table does not
+ * set with the main relation's, and we cannot obtain those values unless
+ * we know beforehand what's the main table OID.
*
* We need to check TOAST tables separately because in cases with short,
* wide tables there might be proportionally much more activity in the
@@ -2130,7 +2169,7 @@ do_autovacuum(void)
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
Oid relid;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -2144,21 +2183,9 @@ do_autovacuum(void)
relid = classForm->oid;
- /*
- * fetch reloptions -- if this toast table does not have them, try the
- * main rel
- */
- relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else
- {
- av_relation *hentry;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, NULL);
- if (hentry)
- relopts = &hentry->ar_reloptions;
- }
+ /* fetch reloptions -- merge any unset options from the main rel */
+ relopts = get_effective_relopts(tuple, pg_class_desc, table_toast_map,
+ &main_relopts);
relation_needs_vacanalyze(relid,
relopts ? &relopts->autovacuum : NULL,
@@ -2179,7 +2206,7 @@ do_autovacuum(void)
}
/* Release stuff to avoid leakage */
- if (free_relopts)
+ if (relopts)
pfree(relopts);
}
@@ -2786,7 +2813,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool wraparound;
AutoVacOpts *avopts;
StdRdOptions *relopts;
- bool free_relopts = false;
+ StdRdOptions *main_relopts;
AutoVacuumScores scores;
/* fetch the relation's relcache entry */
@@ -2796,20 +2823,11 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
classForm = (Form_pg_class) GETSTRUCT(classTup);
/*
- * Get the applicable reloptions. If it is a TOAST table, try to get the
- * main table reloptions if the toast table itself doesn't have.
+ * Get the applicable reloptions. If it is a TOAST table, merge in the
+ * main table's reloptions where they are unset.
*/
- relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
- if (relopts)
- free_relopts = true;
- else if (classForm->relkind == RELKIND_TOASTVALUE)
- {
- av_relation *hentry;
-
- hentry = hash_search(table_toast_map, &relid, HASH_FIND, NULL);
- if (hentry)
- relopts = &hentry->ar_reloptions;
- }
+ relopts = get_effective_relopts(classTup, pg_class_desc, table_toast_map,
+ &main_relopts);
avopts = relopts ? &relopts->autovacuum : NULL;
@@ -2895,7 +2913,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.log_vacuum_min_duration = log_vacuum_min_duration;
tab->at_params.log_analyze_min_duration = log_analyze_min_duration;
tab->at_params.toast_parent = InvalidOid;
- tab->at_params.main_relopts = NULL;
+ tab->at_params.main_relopts = main_relopts;
/* Determine the number of parallel vacuum workers to use */
tab->at_params.nworkers = 0;
@@ -2940,7 +2958,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_relopts)
+ if (relopts)
pfree(relopts);
heap_freetuple(classTup);
return tab;
@@ -2954,8 +2972,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* being forced because of Xid or multixact wraparound.
*
* relopts is a pointer to the AutoVacOpts options (either for itself in the
- * case of a plain table, or for either itself or its parent table in the case
- * of a TOAST table), NULL if none.
+ * case of a plain table, or merged with the main table's for a TOAST table),
+ * NULL if none.
*
* A table needs to be vacuumed if the number of dead tuples exceeds a
* threshold. This threshold is calculated as
@@ -3612,6 +3630,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
TableScanDesc scan;
HeapTuple tup;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
InitMaterializedSRF(fcinfo, 0);
@@ -3620,13 +3640,62 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * reloptions, which we need in order to compute their TOAST tables'
+ * effective options below.
+ */
rel = table_open(RelationRelationId, AccessShareLock);
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
StdRdOptions *relopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ if (!relopts)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ memcpy(&hentry->ar_reloptions, relopts, sizeof(StdRdOptions));
+
+ pfree(relopts);
+ }
+ table_endscan(scan);
+
+ /*
+ * Now that we have all parents' reloptions, we can generate the results.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ StdRdOptions *relopts;
+ StdRdOptions *main_relopts;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3642,7 +3711,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
- relopts = (StdRdOptions *) extractRelOptions(tup, RelationGetDescr(rel), NULL);
+ relopts = get_effective_relopts(tup, RelationGetDescr(rel),
+ table_toast_map, &main_relopts);
relation_needs_vacanalyze(form->oid,
relopts ? &relopts->autovacuum : NULL,
form,
@@ -3668,6 +3738,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
diff --git a/src/test/modules/test_autovacuum/meson.build b/src/test/modules/test_autovacuum/meson.build
index 86e392bc0de..970b9aaae4b 100644
--- a/src/test/modules/test_autovacuum/meson.build
+++ b/src/test/modules/test_autovacuum/meson.build
@@ -10,6 +10,7 @@ tests += {
},
'tests': [
't/001_parallel_autovacuum.pl',
+ 't/002_toast_relopts.pl',
],
},
}
diff --git a/src/test/modules/test_autovacuum/t/002_toast_relopts.pl b/src/test/modules/test_autovacuum/t/002_toast_relopts.pl
new file mode 100644
index 00000000000..ba7596cc82b
--- /dev/null
+++ b/src/test/modules/test_autovacuum/t/002_toast_relopts.pl
@@ -0,0 +1,69 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test autovacuum's handling of TOAST storage parameters
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Create a test node with autovacuum disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq{
+autovacuum = off
+autovacuum_naptime = '1s'
+});
+$node->start;
+
+# Create TOAST table that is eligible for autovacuum due to inherited relopts.
+$node->safe_psql(
+ 'postgres', qq{
+ CREATE TABLE toast_relopts (i int, j text STORAGE EXTERNAL) WITH
+ (autovacuum_enabled = false, toast.autovacuum_enabled = true,
+ autovacuum_vacuum_threshold = 1,
+ autovacuum_vacuum_scale_factor = 0,
+ autovacuum_vacuum_insert_threshold = 1,
+ autovacuum_vacuum_insert_scale_factor = 0,
+ vacuum_truncate = false);
+ INSERT INTO toast_relopts VALUES (1, repeat('a', 10000)), (2, repeat('b', 10000));
+ SELECT pg_stat_force_next_flush();
+});
+
+# Get TOAST table's OID for following commands.
+my $toast = $node->safe_psql('postgres',
+ "SELECT reltoastrelid::regclass FROM pg_class WHERE oid = 'toast_relopts'::regclass"
+);
+
+# Verify scores view used inherited insert threshold.
+is( $node->safe_psql(
+ 'postgres', qq{
+ SELECT vacuum_insert_score > 1 FROM pg_stat_autovacuum_scores
+ WHERE relid = '$toast'::regclass
+}),
+ 't',
+ 'inherited insert threshold in pg_stat_autovacuum_scores');
+
+# Delete all rows so that we can verify inherited vacuum_truncate takes effect.
+$node->safe_psql('postgres', 'DELETE FROM toast_relopts');
+
+# Enable autovacuum.
+$node->append_conf('postgresql.conf', 'autovacuum = on');
+$node->reload;
+
+# Wait until autovacuum processes the table.
+ok( $node->poll_query_until(
+ 'postgres', qq{
+ SELECT last_autovacuum IS NOT NULL FROM pg_stat_all_tables
+ WHERE relid = '$toast'::regclass
+}),
+ 'autovacuum of a TOAST table with inherited thresholds');
+
+# Verify autovacuum didn't truncate the table.
+is($node->safe_psql('postgres', "SELECT pg_relation_size('$toast') > 0"),
+ 't', 'inherited vacuum_truncate');
+
+$node->stop;
+done_testing();
--
2.55.0
^ permalink raw reply [nested|flat] 44+ messages in thread
* Re: problems with toast.* reloptions
@ 2026-08-24 21:34 Nathan Bossart <nathandbossart@gmail.com>
parent: Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 0 replies; 44+ messages in thread
From: Nathan Bossart @ 2026-08-24 21:34 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Sami Imseih <samimseih@gmail.com>; Greg Burd <greg@burd.me>; solai v <solai.cdac@gmail.com>; Nikita Malakhov <hukutoc@gmail.com>; pgsql-hackers
Committed.
--
nathan
^ permalink raw reply [nested|flat] 44+ messages in thread
end of thread, other threads:[~2026-08-24 21:34 UTC | newest]
Thread overview: 44+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2024-10-17 17:16 [PATCH v7 10/15] heapam: Add batch mode mvcc check and use it in page mode Andres Freund <andres@anarazel.de>
2024-10-17 17:16 [PATCH v1 06/14] heapam: Add batch mode mvcc check and use it in page mode Andres Freund <andres@anarazel.de>
2024-10-17 17:16 [PATCH v2 07/15] heapam: Add batch mode mvcc check and use it in page mode Andres Freund <andres@anarazel.de>
2024-10-17 17:16 [PATCH v6 11/14] heapam: Add batch mode mvcc check and use it in page mode Andres Freund <andres@anarazel.de>
2024-10-17 17:16 [PATCH v8 05/10] heapam: Add batch mode mvcc check and use it in page mode Andres Freund <andres@anarazel.de>
2025-06-19 20:20 problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2025-06-20 02:05 ` Re: problems with toast.* reloptions Michael Paquier <michael@paquier.xyz>
2025-06-20 19:12 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2025-06-22 03:45 ` Re: problems with toast.* reloptions shihao zhong <zhong950419@gmail.com>
2025-06-23 15:59 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2025-06-24 18:21 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2025-07-02 14:44 ` Re: problems with toast.* reloptions shihao zhong <zhong950419@gmail.com>
2025-07-15 18:50 ` Re: problems with toast.* reloptions Shayon Mukherjee <shayonj@gmail.com>
2025-06-23 20:59 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2025-06-24 05:10 ` Re: problems with toast.* reloptions Michael Paquier <michael@paquier.xyz>
2025-06-24 16:38 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-05-01 21:41 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-05-04 15:41 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-05-06 21:31 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-05-26 05:55 ` Re: problems with toast.* reloptions solai v <solai.cdac@gmail.com>
2026-05-26 14:23 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-06-02 09:42 ` Re: problems with toast.* reloptions solai v <solai.cdac@gmail.com>
2026-06-02 12:31 ` Re: problems with toast.* reloptions Nikita Malakhov <hukutoc@gmail.com>
2026-06-02 14:08 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-06-03 19:27 ` Re: problems with toast.* reloptions Nikita Malakhov <hukutoc@gmail.com>
2026-06-09 18:31 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-06-10 11:41 ` Re: problems with toast.* reloptions solai v <solai.cdac@gmail.com>
2026-08-06 21:38 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-08-09 13:37 ` Re: problems with toast.* reloptions Greg Burd <greg@burd.me>
2026-08-10 06:08 ` Re: problems with toast.* reloptions solai v <solai.cdac@gmail.com>
2026-08-10 16:30 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-08-11 14:53 ` Re: problems with toast.* reloptions Sami Imseih <samimseih@gmail.com>
2026-08-11 21:45 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-08-11 21:43 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-08-12 17:17 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-08-12 18:33 ` Re: problems with toast.* reloptions Sami Imseih <samimseih@gmail.com>
2026-08-12 21:54 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-08-13 00:41 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-08-13 06:28 ` Re: problems with toast.* reloptions Michael Paquier <michael@paquier.xyz>
2026-08-13 16:41 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-08-17 20:08 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-08-19 07:16 ` Re: problems with toast.* reloptions Michael Paquier <michael@paquier.xyz>
2026-08-21 22:26 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
2026-08-24 21:34 ` Re: problems with toast.* reloptions Nathan Bossart <nathandbossart@gmail.com>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox