From 0118a82edbec79bd91af526e1902334a320d4103 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 10 Aug 2026 15:55:44 -0500
Subject: [PATCH v13 2/5] Give TOAST storage parameters unsettable defaults.

This is preparatory work for a follow-up commit that will fill in a
TOAST table's unset storage parameters from its main table's.  For
that, it must be possible to tell an option nobody set from one the
user set to the value that option happens to default to.  The
parsed form of a relation's options has nowhere to record which
ones were specified, so an unset option is simply one still holding
its declared default.

Require, then, that any option a TOAST table accepts default to a
value the user cannot set.  Ternaries already comply, having no
default at all, as does vacuum_index_cleanup, whose "not set"
member has no spelling.  Among the numeric ones only
log_autovacuum_min_duration was in violation, defaulting to -1 with
a minimum of -1, so give it -2 instead, matching
autovacuum_vacuum_max_threshold and
autovacuum_vacuum_insert_threshold.  Users won't notice; -1 already
behaved exactly as leaving the option unset does, and it still
does.

An assertion in initialize_reloptions() enforces both of the rules
the follow-up commit will rely on: that the default is unsettable,
and that anything settable on a TOAST table is settable on a heap,
since the inherited value is read from a main table's options at
the same offset.  Note that this rules out bool and string options,
neither of which can express "unset".
---
 src/backend/access/common/reloptions.c | 78 +++++++++++++++++++++++++-
 src/backend/postmaster/autovacuum.c    |  2 +-
 2 files changed, 78 insertions(+), 2 deletions(-)

diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index fab8294e732..a0716ff7819 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -348,7 +348,7 @@ static relopt_int intRelOpts[] =
 			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
 			ShareUpdateExclusiveLock
 		},
-		-1, -1, INT_MAX
+		-2, -1, INT_MAX
 	},
 	{
 		{
@@ -613,6 +613,78 @@ static void parse_one_reloption(relopt_value *option, char *text_str,
 	((option).isset ? strlen((option).string_val) : \
 	 ((relopt_string *) (option).gen)->default_len)
 
+#ifdef USE_ASSERT_CHECKING
+/*
+ * Verify that every option a TOAST table accepts defaults to a value the user
+ * cannot set.  Nothing records which options were specified, so an option
+ * still holding its default is the only way to recognize one that was never
+ * set, and that is how a TOAST table tells which values it should take from
+ * its main table.
+ */
+static void
+assert_toast_defaults_unsettable(void)
+{
+	for (int i = 0; relOpts[i]; i++)
+	{
+		relopt_gen *gen = relOpts[i];
+
+		if ((gen->kinds & RELOPT_KIND_TOAST) == 0)
+			continue;
+
+		/*
+		 * A TOAST table's value is filled in from its main table's at the
+		 * same offset in the same struct, so the option must be settable on a
+		 * heap too.
+		 */
+		Assert((gen->kinds & RELOPT_KIND_HEAP) != 0);
+
+		switch (gen->type)
+		{
+			case RELOPT_TYPE_TERNARY:
+
+				/*
+				 * Ternaries carry no default, and parse_one_reloption() can
+				 * only produce true or false, so PG_TERNARY_UNSET is already
+				 * beyond a user's reach.
+				 */
+				break;
+
+			case RELOPT_TYPE_INT:
+				{
+					relopt_int *optint = (relopt_int *) gen;
+
+					Assert(optint->default_val < optint->min ||
+						   optint->default_val > optint->max);
+					break;
+				}
+
+			case RELOPT_TYPE_REAL:
+				{
+					relopt_real *optreal = (relopt_real *) gen;
+
+					Assert(optreal->default_val < optreal->min ||
+						   optreal->default_val > optreal->max);
+					break;
+				}
+
+			case RELOPT_TYPE_ENUM:
+				{
+					relopt_enum *optenum = (relopt_enum *) gen;
+
+					for (relopt_enum_elt_def *elt = optenum->members;
+						 elt->string_val; elt++)
+						Assert(elt->symbol_val != optenum->default_val);
+					break;
+				}
+
+			default:
+				/* Neither bools nor strings can express "unset". */
+				Assert(false);
+		}
+	}
+}
+#endif							/* USE_ASSERT_CHECKING */
+
 /*
  * initialize_reloptions
  *		initialization routine, must be called before parsing
@@ -730,6 +802,10 @@ initialize_reloptions(void)
 
 	/* flag the work is complete */
 	need_initialization = false;
+
+#ifdef USE_ASSERT_CHECKING
+	assert_toast_defaults_unsettable();
+#endif
 }
 
 /*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index e0fc551d12a..77557fa4bbf 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2838,7 +2838,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 		 * defaults, autovacuum's own first and plain vacuum second.
 		 */
 
-		/* -1 in autovac setting means use log_autovacuum_min_duration */
+		/* a negative autovac setting means use log_autovacuum_min_duration */
 		log_vacuum_min_duration = (avopts && avopts->log_vacuum_min_duration >= 0)
 			? avopts->log_vacuum_min_duration
 			: Log_autovacuum_min_duration;
-- 
2.55.0

