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

Reply via email to