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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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