agora inbox for pgsql-hackers@postgresql.org  
help / color / mirror / Atom feed
Re: [PATCH] ternary reloption type
13+ messages / 6 participants
[nested] [flat]

* Re: [PATCH] ternary reloption type
@ 2025-09-04 17:07 Nikolay Shaplov <dhyan@nataraj.su>
  2025-09-12 13:46 ` Re: [PATCH] ternary reloption type Timur Magomedov <t.magomedov@postgrespro.ru>
  0 siblings, 1 reply; 13+ messages in thread

From: Nikolay Shaplov @ 2025-09-04 17:07 UTC (permalink / raw)
  To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Álvaro Herrera <alvherre@alvh.no-ip.org>; Timur Magomedov <t.magomedov@postgrespro.ru>; Chris Travers <chris.travers@gmail.com>; +Cc: Nikolay Shaplov <dhyan@nataraj.su>

My English is bad :-(

It is either trinary, or ternary, but not what I've written in previous 
message.

Thanks to Timur for pointing to this issue.

Here goes a new version of the patch with proper naming for an new option 
type. 



-- 
Nikolay Shaplov aka Nataraj
Fuzzing Engineer at Postgres Professional
Matrix IM: @dhyan:nataraj.su

Attachments:

  [text/x-patch] v2-0001-Add-ternary-reloption-type.patch (4.8K, ../../11330814.CDJkKcVGEf@thinkpad-pgpro/2-v2-0001-Add-ternary-reloption-type.patch)
  download | inline diff:
From 2cc1d02bba61c07af99f92932124d65f3f34a51e Mon Sep 17 00:00:00 2001
From: Nikolay Shaplov <dhyan@nataraj.su>
Date: Thu, 4 Sep 2025 17:34:46 +0300
Subject: [PATCH v2 1/4] Add ternary reloption type

There is a tendency for boolean reloptions in PostgreSQL code, one with "on"
and "off" values, to be replaced with options with "on", "off",
"[use_global_settings]" behaviour.

For `vacuum_index_cleanup" and gist's `buffering` reloption such behavior have
been implemented as enum-tyoe option.

For vacuum_truncate this behaviour have been umplemented by adding additional
`is_set` flag to `bytea` representation of the reloptions.

Both solutions looks like workaround hacks to implement option with three
available states.

This patch introduce "ternary" reloption type, that behave like bool option,
but also has an additional "unset" state. This state may be reachable only by
not setting or RESETting an option, like in `vacuum_truncate` option, or it
may have some text alias that allow user to explicitly set it, like "auto" in
`vacuum_index_cleanup` or gist's `buffering`

`vacuum_truncate`, `vacuum_index_cleanup` and gist's `buffering` reloptions are
reimplemented as ternary  reloptions without significant behaviour changes.

---

This patch is split into four parts, to help reviewer grasp the login behind
it. I guess it is better to commit it as a single commit

---

Part1: Add regression tests that would be tests for ternary reloptions in
future, but now it checks the behaviour of reloptions that would become
ternary. There behaviour should not change, so adding tests before changing
anything. These tests should pass before applying the patch, and after it.
---
 src/test/regress/expected/reloptions.out | 36 ++++++++++++++++++++++++
 src/test/regress/sql/reloptions.sql      | 21 ++++++++++++++
 2 files changed, 57 insertions(+)

diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index 9de19b4e3f..1c99f79ab0 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -98,6 +98,42 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
  {fillfactor=13,autovacuum_enabled=false}
 (1 row)
 
+-- Tests for future (FIXME) ternary options
+-- behave as boolean option: accept unassigned name and truncated value
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+       reloptions       
+------------------------
+ {vacuum_truncate=true}
+(1 row)
+
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate=FaLS);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+       reloptions       
+------------------------
+ {vacuum_truncate=fals}
+(1 row)
+
+-- preferred "true" alias is used when storing
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=on);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+        reloptions         
+---------------------------
+ {vacuum_index_cleanup=on}
+(1 row)
+
+-- custom "third" value is available
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=auto);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+         reloptions          
+-----------------------------
+ {vacuum_index_cleanup=auto}
+(1 row)
+
 -- Test vacuum_truncate option
 DROP TABLE reloptions_test;
 CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index 24fbe0b478..f5980dafcb 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -59,6 +59,27 @@ UPDATE pg_class
 ALTER TABLE reloptions_test RESET (illegal_option);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 
+-- Tests for future (FIXME) ternary options
+
+-- behave as boolean option: accept unassigned name and truncated value
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate=FaLS);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
+-- preferred "true" alias is used when storing
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=on);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
+-- custom "third" value is available
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=auto);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
 -- Test vacuum_truncate option
 DROP TABLE reloptions_test;
 
-- 
2.39.2

  [text/x-patch] v2-0002-Introduce-ternary-reloptions.patch (11.6K, ../../11330814.CDJkKcVGEf@thinkpad-pgpro/3-v2-0002-Introduce-ternary-reloptions.patch)
  download | inline diff:
From cab91ca4dca83f9755b6b2a40664c94194491cef Mon Sep 17 00:00:00 2001
From: Nikolay Shaplov <dhyan@nataraj.su>
Date: Thu, 4 Sep 2025 17:41:39 +0300
Subject: [PATCH v2 2/4] Introduce ternary reloptions

Introduce ternary reloption as a replacement for current `vacuum_truncate`
implementation. Remove `vacuum_truncate_set` additional flag and using
`TERNARY_UNSET` value instead.
---
 src/backend/access/common/reloptions.c | 129 ++++++++++++++++++++-----
 src/backend/commands/vacuum.c          |   4 +-
 src/include/access/reloptions.h        |  26 ++---
 src/include/c.h                        |  16 +++
 src/include/utils/rel.h                |   3 +-
 5 files changed, 135 insertions(+), 43 deletions(-)

diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 0af3fea68f..f543f11001 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -40,9 +40,9 @@
  *
  * To add an option:
  *
- * (i) decide on a type (bool, integer, real, enum, string), name, default
- * value, upper and lower bounds (if applicable); for strings, consider a
- * validation routine.
+ * (i) decide on a type (bool, ternary, integer, real, enum, string), name,
+ * default value, upper and lower bounds (if applicable); for strings,
+ * consider a validation routine.
  * (ii) add a record below (or use add_<type>_reloption).
  * (iii) add it to the appropriate options struct (perhaps StdRdOptions)
  * (iv) add it to the appropriate handling routine (perhaps
@@ -147,15 +147,6 @@ static relopt_bool boolRelOpts[] =
 		},
 		false
 	},
-	{
-		{
-			"vacuum_truncate",
-			"Enables vacuum to truncate empty pages at the end of this table",
-			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
-			ShareUpdateExclusiveLock
-		},
-		true
-	},
 	{
 		{
 			"deduplicate_items",
@@ -170,6 +161,21 @@ static relopt_bool boolRelOpts[] =
 	{{NULL}}
 };
 
+static relopt_ternary ternaryRelOpts[] =
+{
+	{
+		{
+			"vacuum_truncate",
+			"Enables vacuum to truncate empty pages at the end of this table",
+			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+			ShareUpdateExclusiveLock
+		},
+		TERNARY_UNSET
+	},
+	/* list terminator */
+	{{NULL}}
+};
+
 static relopt_int intRelOpts[] =
 {
 	{
@@ -600,6 +606,13 @@ initialize_reloptions(void)
 								   boolRelOpts[i].gen.lockmode));
 		j++;
 	}
+	for (i = 0; ternaryRelOpts[i].gen.name; i++)
+	{
+		Assert(DoLockModesConflict(ternaryRelOpts[i].gen.lockmode,
+								   ternaryRelOpts[i].gen.lockmode));
+		j++;
+	}
+
 	for (i = 0; intRelOpts[i].gen.name; i++)
 	{
 		Assert(DoLockModesConflict(intRelOpts[i].gen.lockmode,
@@ -640,6 +653,14 @@ initialize_reloptions(void)
 		j++;
 	}
 
+	for (i = 0; ternaryRelOpts[i].gen.name; i++)
+	{
+		relOpts[j] = &ternaryRelOpts[i].gen;
+		relOpts[j]->type = RELOPT_TYPE_TERNARY;
+		relOpts[j]->namelen = strlen(relOpts[j]->name);
+		j++;
+	}
+
 	for (i = 0; intRelOpts[i].gen.name; i++)
 	{
 		relOpts[j] = &intRelOpts[i].gen;
@@ -800,6 +821,9 @@ allocate_reloption(bits32 kinds, int type, const char *name, const char *desc,
 		case RELOPT_TYPE_BOOL:
 			size = sizeof(relopt_bool);
 			break;
+		case RELOPT_TYPE_TERNARY:
+			size = sizeof(relopt_ternary);
+			break;
 		case RELOPT_TYPE_INT:
 			size = sizeof(relopt_int);
 			break;
@@ -883,6 +907,54 @@ add_local_bool_reloption(local_relopts *relopts, const char *name,
 	add_local_reloption(relopts, (relopt_gen *) newoption, offset);
 }
 
+/*
+ * init_ternary_reloption
+ *		Allocate and initialize a new ternary reloption
+ */
+static relopt_ternary *
+init_ternary_reloption(bits32 kinds, const char *name, const char *desc,
+					ternary default_val, LOCKMODE lockmode)
+{
+	relopt_ternary *newoption;
+
+	newoption = (relopt_ternary *) allocate_reloption(kinds,
+									RELOPT_TYPE_TERNARY, name, desc, lockmode);
+	newoption->default_val = default_val;
+
+	return newoption;
+}
+
+/*
+ * add_ternary_reloption
+ *		Add a new ternary reloption
+ */
+void
+add_ternary_reloption(bits32 kinds, const char *name, const char *desc,
+				   ternary default_val, LOCKMODE lockmode)
+{
+	relopt_ternary *newoption = init_ternary_reloption(kinds, name, desc,
+												 default_val, lockmode);
+
+	add_reloption((relopt_gen *) newoption);
+}
+
+/*
+ * add_local_ternary_reloption
+ *		Add a new ternary local reloption
+ *
+ * 'offset' is offset of ternary-typed field.
+ */
+void
+add_local_ternary_reloption(local_relopts *relopts, const char *name,
+							const char *desc, ternary default_val,
+							int offset)
+{
+	relopt_ternary *newoption = init_ternary_reloption(RELOPT_KIND_LOCAL,
+												name, desc,
+												default_val, 0);
+
+	add_local_reloption(relopts, (relopt_gen *) newoption, offset);
+}
 
 /*
  * init_real_reloption
@@ -1617,6 +1689,18 @@ parse_one_reloption(relopt_value *option, char *text_str, int text_len,
 									option->gen->name, value)));
 			}
 			break;
+		case RELOPT_TYPE_TERNARY:
+			{
+				bool b;
+				parsed = parse_bool(value, &b);
+				option->values.ternary_val = b ? TERNARY_TRUE : TERNARY_FALSE;
+				if (validate && !parsed)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("invalid value for ternary option \"%s\": %s",
+									option->gen->name, value)));
+			}
+			break;
 		case RELOPT_TYPE_INT:
 			{
 				relopt_int *optint = (relopt_int *) option->gen;
@@ -1780,17 +1864,6 @@ fillRelOptions(void *rdopts, Size basesize,
 				char	   *itempos = ((char *) rdopts) + elems[j].offset;
 				char	   *string_val;
 
-				/*
-				 * If isset_offset is provided, store whether the reloption is
-				 * set there.
-				 */
-				if (elems[j].isset_offset > 0)
-				{
-					char	   *setpos = ((char *) rdopts) + elems[j].isset_offset;
-
-					*(bool *) setpos = options[i].isset;
-				}
-
 				switch (options[i].gen->type)
 				{
 					case RELOPT_TYPE_BOOL:
@@ -1798,6 +1871,11 @@ fillRelOptions(void *rdopts, Size basesize,
 							options[i].values.bool_val :
 							((relopt_bool *) options[i].gen)->default_val;
 						break;
+					case RELOPT_TYPE_TERNARY:
+						*(ternary *) itempos = options[i].isset ?
+							options[i].values.ternary_val :
+							((relopt_ternary *) options[i].gen)->default_val;
+						break;
 					case RELOPT_TYPE_INT:
 						*(int *) itempos = options[i].isset ?
 							options[i].values.int_val :
@@ -1912,8 +1990,8 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		offsetof(StdRdOptions, parallel_workers)},
 		{"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
 		offsetof(StdRdOptions, vacuum_index_cleanup)},
-		{"vacuum_truncate", RELOPT_TYPE_BOOL,
-		offsetof(StdRdOptions, vacuum_truncate), offsetof(StdRdOptions, vacuum_truncate_set)},
+		{"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)}
 	};
@@ -1993,7 +2071,6 @@ build_local_reloptions(local_relopts *relopts, Datum options, bool validate)
 		elems[i].optname = opt->option->name;
 		elems[i].opttype = opt->option->type;
 		elems[i].offset = opt->offset;
-		elems[i].isset_offset = 0;	/* not supported for local relopts yet */
 
 		i++;
 	}
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 733ef40ae7..977babff54 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2221,9 +2221,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 	{
 		StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
 
-		if (opts && opts->vacuum_truncate_set)
+		if (opts && opts->vacuum_truncate != TERNARY_UNSET)
 		{
-			if (opts->vacuum_truncate)
+			if (opts->vacuum_truncate == TERNARY_TRUE)
 				params.truncate = VACOPTVALUE_ENABLED;
 			else
 				params.truncate = VACOPTVALUE_DISABLED;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index a604a4702c..a436697658 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -29,6 +29,7 @@
 typedef enum relopt_type
 {
 	RELOPT_TYPE_BOOL,
+	RELOPT_TYPE_TERNARY,  /* on, off, unset */
 	RELOPT_TYPE_INT,
 	RELOPT_TYPE_REAL,
 	RELOPT_TYPE_ENUM,
@@ -80,6 +81,7 @@ typedef struct relopt_value
 	union
 	{
 		bool		bool_val;
+		ternary		ternary_val;
 		int			int_val;
 		double		real_val;
 		int			enum_val;
@@ -94,6 +96,12 @@ typedef struct relopt_bool
 	bool		default_val;
 } relopt_bool;
 
+typedef struct relopt_rernary
+{
+	relopt_gen	gen;
+	int 		default_val;
+} relopt_ternary;
+
 typedef struct relopt_int
 {
 	relopt_gen	gen;
@@ -152,19 +160,6 @@ typedef struct
 	const char *optname;		/* option's name */
 	relopt_type opttype;		/* option's datatype */
 	int			offset;			/* offset of field in result struct */
-
-	/*
-	 * isset_offset is an optional offset of a field in the result struct that
-	 * stores whether the option is explicitly set for the relation or if it
-	 * just picked up the default value.  In most cases, this can be
-	 * accomplished by giving the reloption a special out-of-range default
-	 * value (e.g., some integer reloptions use -2), but this isn't always
-	 * possible.  For example, a Boolean reloption cannot be given an
-	 * out-of-range default, so we need another way to discover the source of
-	 * its value.  This offset is only used if given a value greater than
-	 * zero.
-	 */
-	int			isset_offset;
 } relopt_parse_elt;
 
 /* Local reloption definition */
@@ -195,6 +190,8 @@ typedef struct local_relopts
 extern relopt_kind add_reloption_kind(void);
 extern void add_bool_reloption(bits32 kinds, const char *name, const char *desc,
 							   bool default_val, LOCKMODE lockmode);
+extern void add_ternary_reloption(bits32 kinds, const char *name,
+					const char *desc, int default_val, LOCKMODE lockmode);
 extern void add_int_reloption(bits32 kinds, const char *name, const char *desc,
 							  int default_val, int min_val, int max_val,
 							  LOCKMODE lockmode);
@@ -214,6 +211,9 @@ extern void register_reloptions_validator(local_relopts *relopts,
 extern void add_local_bool_reloption(local_relopts *relopts, const char *name,
 									 const char *desc, bool default_val,
 									 int offset);
+extern void add_local_ternary_reloption(local_relopts *relopts,
+								const char *name, const char *desc,
+								ternary default_val, int offset);
 extern void add_local_int_reloption(local_relopts *relopts, const char *name,
 									const char *desc, int default_val,
 									int min_val, int max_val, int offset);
diff --git a/src/include/c.h b/src/include/c.h
index 39022f8a9d..9c59ccccf8 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -488,6 +488,22 @@ typedef void (*pg_funcptr_t) (void);
 
 #include <stdbool.h>
 
+/*
+ * ternary
+ *		Boolean value with an extrea "unset" option
+ *
+ * Ternary data type is used in relation options that can be "true", "false" or
+ * "unset". Since relation options are used deep inside the PostgreSQL code,
+ * this type is declared globally.
+*/
+
+typedef enum ternary
+{
+	TERNARY_FALSE = 0,
+	TERNARY_TRUE = 1,
+	TERNARY_UNSET = -1
+} ternary;
+
 
 /* ----------------------------------------------------------------
  *				Section 3:	standard system types
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index b552359915..7346d618f9 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -346,8 +346,7 @@ typedef struct StdRdOptions
 	bool		user_catalog_table; /* use as an additional catalog relation */
 	int			parallel_workers;	/* max number of parallel workers */
 	StdRdOptIndexCleanup vacuum_index_cleanup;	/* controls index vacuuming */
-	bool		vacuum_truncate;	/* enables vacuum to truncate a relation */
-	bool		vacuum_truncate_set;	/* whether vacuum_truncate is set */
+	ternary		vacuum_truncate;	/* enables vacuum to truncate a relation */
 
 	/*
 	 * Fraction of pages in a relation that vacuum can eagerly scan and fail
-- 
2.39.2

  [text/x-patch] v2-0003-Add-alias-to-be-used-as-unset-state.patch (12.0K, ../../11330814.CDJkKcVGEf@thinkpad-pgpro/4-v2-0003-Add-alias-to-be-used-as-unset-state.patch)
  download | inline diff:
From ac849b3fae94164ca07e178ebc3ac24af7071bfd Mon Sep 17 00:00:00 2001
From: Nikolay Shaplov <dhyan@nataraj.su>
Date: Thu, 4 Sep 2025 19:22:21 +0300
Subject: [PATCH v2 3/4] Add alias to be used as "unset" state.

Add `unset_alias` string parameter to ternary reloption definition. This will allow
user explicitly switch ternary option to "unset" state. Use this feature to
implement `vacuum_index_cleanup` and gist's `buffering` reloptions as ternary
reloptions
---
 src/backend/access/common/reloptions.c | 91 +++++++++++---------------
 src/backend/access/gist/gistbuild.c    |  4 +-
 src/backend/commands/vacuum.c          | 11 ++--
 src/include/access/gist_private.h      | 10 +--
 src/include/access/reloptions.h        |  7 +-
 src/include/utils/rel.h                | 10 +--
 src/test/regress/expected/gist.out     |  3 +-
 7 files changed, 54 insertions(+), 82 deletions(-)

diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index f543f11001..3637646641 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -170,6 +170,27 @@ static relopt_ternary ternaryRelOpts[] =
 			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
 			ShareUpdateExclusiveLock
 		},
+		NULL,
+		TERNARY_UNSET
+	},
+	{
+		{
+			"vacuum_index_cleanup",
+			"Controls index vacuuming and index cleanup",
+			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+			ShareUpdateExclusiveLock
+		},
+		"auto",
+		TERNARY_UNSET
+	},
+	{
+		{
+			"buffering",
+			"Enables buffering build for this GiST index",
+			RELOPT_KIND_GIST,
+			AccessExclusiveLock
+		},
+		"auto",
 		TERNARY_UNSET
 	},
 	/* list terminator */
@@ -489,30 +510,6 @@ static relopt_real realRelOpts[] =
 	{{NULL}}
 };
 
-/* values from StdRdOptIndexCleanup */
-static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
-{
-	{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
-	{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
-	{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
-	{"true", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
-	{"false", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
-	{"yes", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
-	{"no", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
-	{"1", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
-	{"0", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
-	{(const char *) NULL}		/* list terminator */
-};
-
-/* values from GistOptBufferingMode */
-static relopt_enum_elt_def gistBufferingOptValues[] =
-{
-	{"auto", GIST_OPTION_BUFFERING_AUTO},
-	{"on", GIST_OPTION_BUFFERING_ON},
-	{"off", GIST_OPTION_BUFFERING_OFF},
-	{(const char *) NULL}		/* list terminator */
-};
-
 /* values from ViewOptCheckOption */
 static relopt_enum_elt_def viewCheckOptValues[] =
 {
@@ -524,28 +521,6 @@ static relopt_enum_elt_def viewCheckOptValues[] =
 
 static relopt_enum enumRelOpts[] =
 {
-	{
-		{
-			"vacuum_index_cleanup",
-			"Controls index vacuuming and index cleanup",
-			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
-			ShareUpdateExclusiveLock
-		},
-		StdRdOptIndexCleanupValues,
-		STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
-		gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
-	},
-	{
-		{
-			"buffering",
-			"Enables buffering build for this GiST index",
-			RELOPT_KIND_GIST,
-			AccessExclusiveLock
-		},
-		gistBufferingOptValues,
-		GIST_OPTION_BUFFERING_AUTO,
-		gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
-	},
 	{
 		{
 			"check_option",
@@ -913,13 +888,14 @@ add_local_bool_reloption(local_relopts *relopts, const char *name,
  */
 static relopt_ternary *
 init_ternary_reloption(bits32 kinds, const char *name, const char *desc,
-					ternary default_val, LOCKMODE lockmode)
+			ternary default_val, const char* unset_alias, LOCKMODE lockmode)
 {
 	relopt_ternary *newoption;
 
 	newoption = (relopt_ternary *) allocate_reloption(kinds,
 									RELOPT_TYPE_TERNARY, name, desc, lockmode);
 	newoption->default_val = default_val;
+	newoption->unset_alias = unset_alias;
 
 	return newoption;
 }
@@ -930,10 +906,10 @@ init_ternary_reloption(bits32 kinds, const char *name, const char *desc,
  */
 void
 add_ternary_reloption(bits32 kinds, const char *name, const char *desc,
-				   ternary default_val, LOCKMODE lockmode)
+			ternary default_val, const char* unset_alias, LOCKMODE lockmode)
 {
 	relopt_ternary *newoption = init_ternary_reloption(kinds, name, desc,
-												 default_val, lockmode);
+											default_val, unset_alias, lockmode);
 
 	add_reloption((relopt_gen *) newoption);
 }
@@ -947,11 +923,11 @@ add_ternary_reloption(bits32 kinds, const char *name, const char *desc,
 void
 add_local_ternary_reloption(local_relopts *relopts, const char *name,
 							const char *desc, ternary default_val,
-							int offset)
+							const char* unset_alias, int offset)
 {
 	relopt_ternary *newoption = init_ternary_reloption(RELOPT_KIND_LOCAL,
 												name, desc,
-												default_val, 0);
+												default_val, unset_alias, 0);
 
 	add_local_reloption(relopts, (relopt_gen *) newoption, offset);
 }
@@ -1692,8 +1668,19 @@ parse_one_reloption(relopt_value *option, char *text_str, int text_len,
 		case RELOPT_TYPE_TERNARY:
 			{
 				bool b;
+				relopt_ternary *opt = (relopt_ternary *) option->gen;
+
 				parsed = parse_bool(value, &b);
 				option->values.ternary_val = b ? TERNARY_TRUE : TERNARY_FALSE;
+
+				if (!parsed && opt->unset_alias)
+				{
+					if (pg_strcasecmp(value, opt->unset_alias) == 0)
+					{
+						option->values.ternary_val = TERNARY_UNSET;
+						parsed = true;
+					}
+				}
 				if (validate && !parsed)
 					ereport(ERROR,
 							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1988,7 +1975,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		offsetof(StdRdOptions, user_catalog_table)},
 		{"parallel_workers", RELOPT_TYPE_INT,
 		offsetof(StdRdOptions, parallel_workers)},
-		{"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+		{"vacuum_index_cleanup", RELOPT_TYPE_TERNARY,
 		offsetof(StdRdOptions, vacuum_index_cleanup)},
 		{"vacuum_truncate", RELOPT_TYPE_TERNARY,
 		offsetof(StdRdOptions, vacuum_truncate)},
diff --git a/src/backend/access/gist/gistbuild.c b/src/backend/access/gist/gistbuild.c
index 9b2ec9815f..7f641f7825 100644
--- a/src/backend/access/gist/gistbuild.c
+++ b/src/backend/access/gist/gistbuild.c
@@ -213,9 +213,9 @@ gistbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	 */
 	if (options)
 	{
-		if (options->buffering_mode == GIST_OPTION_BUFFERING_ON)
+		if (options->buffering_mode == TERNARY_TRUE)
 			buildstate.buildMode = GIST_BUFFERING_STATS;
-		else if (options->buffering_mode == GIST_OPTION_BUFFERING_OFF)
+		else if (options->buffering_mode == TERNARY_FALSE)
 			buildstate.buildMode = GIST_BUFFERING_DISABLED;
 		else					/* must be "auto" */
 			buildstate.buildMode = GIST_BUFFERING_AUTO;
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 977babff54..61b4bdb682 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2175,22 +2175,21 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 	 */
 	if (params.index_cleanup == VACOPTVALUE_UNSPECIFIED)
 	{
-		StdRdOptIndexCleanup vacuum_index_cleanup;
+		ternary vacuum_index_cleanup;
 
 		if (rel->rd_options == NULL)
-			vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+			vacuum_index_cleanup = TERNARY_UNSET;
 		else
 			vacuum_index_cleanup =
 				((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
 
-		if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
+		if (vacuum_index_cleanup == TERNARY_UNSET)
 			params.index_cleanup = VACOPTVALUE_AUTO;
-		else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
+		else if (vacuum_index_cleanup == TERNARY_TRUE)
 			params.index_cleanup = VACOPTVALUE_ENABLED;
 		else
 		{
-			Assert(vacuum_index_cleanup ==
-				   STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
+			Assert(vacuum_index_cleanup == TERNARY_FALSE);
 			params.index_cleanup = VACOPTVALUE_DISABLED;
 		}
 	}
diff --git a/src/include/access/gist_private.h b/src/include/access/gist_private.h
index 39404ec7cd..a931c988fb 100644
--- a/src/include/access/gist_private.h
+++ b/src/include/access/gist_private.h
@@ -380,14 +380,6 @@ typedef struct GISTBuildBuffers
 	int			rootlevel;
 } GISTBuildBuffers;
 
-/* GiSTOptions->buffering_mode values */
-typedef enum GistOptBufferingMode
-{
-	GIST_OPTION_BUFFERING_AUTO,
-	GIST_OPTION_BUFFERING_ON,
-	GIST_OPTION_BUFFERING_OFF,
-} GistOptBufferingMode;
-
 /*
  * Storage type for GiST's reloptions
  */
@@ -395,7 +387,7 @@ typedef struct GiSTOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
-	GistOptBufferingMode buffering_mode;	/* buffering build mode */
+	ternary		buffering_mode;	/* buffering build mode */
 } GiSTOptions;
 
 /* gist.c */
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index a436697658..d2a1c7afb7 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -99,6 +99,7 @@ typedef struct relopt_bool
 typedef struct relopt_rernary
 {
 	relopt_gen	gen;
+	const char	*unset_alias; /* word that will be treaed as unset value */
 	int 		default_val;
 } relopt_ternary;
 
@@ -191,7 +192,8 @@ extern relopt_kind add_reloption_kind(void);
 extern void add_bool_reloption(bits32 kinds, const char *name, const char *desc,
 							   bool default_val, LOCKMODE lockmode);
 extern void add_ternary_reloption(bits32 kinds, const char *name,
-					const char *desc, int default_val, LOCKMODE lockmode);
+					const char *desc, ternary default_val,
+					const char* unset_alias, LOCKMODE lockmode);
 extern void add_int_reloption(bits32 kinds, const char *name, const char *desc,
 							  int default_val, int min_val, int max_val,
 							  LOCKMODE lockmode);
@@ -213,7 +215,8 @@ extern void add_local_bool_reloption(local_relopts *relopts, const char *name,
 									 int offset);
 extern void add_local_ternary_reloption(local_relopts *relopts,
 								const char *name, const char *desc,
-								ternary default_val, int offset);
+								ternary default_val, const char* unset_alias,
+								int offset);
 extern void add_local_int_reloption(local_relopts *relopts, const char *name,
 									const char *desc, int default_val,
 									int min_val, int max_val, int offset);
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 7346d618f9..95a18c6d16 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -329,14 +329,6 @@ typedef struct AutoVacOpts
 	float8		analyze_scale_factor;
 } AutoVacOpts;
 
-/* StdRdOptions->vacuum_index_cleanup values */
-typedef enum StdRdOptIndexCleanup
-{
-	STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
-	STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
-	STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
-} StdRdOptIndexCleanup;
-
 typedef struct StdRdOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
@@ -345,7 +337,7 @@ typedef struct StdRdOptions
 	AutoVacOpts autovacuum;		/* autovacuum-related options */
 	bool		user_catalog_table; /* use as an additional catalog relation */
 	int			parallel_workers;	/* max number of parallel workers */
-	StdRdOptIndexCleanup vacuum_index_cleanup;	/* controls index vacuuming */
+	ternary		vacuum_index_cleanup; /* controls index vacuuming */
 	ternary		vacuum_truncate;	/* enables vacuum to truncate a relation */
 
 	/*
diff --git a/src/test/regress/expected/gist.out b/src/test/regress/expected/gist.out
index c75bbb23b6..76751d1859 100644
--- a/src/test/regress/expected/gist.out
+++ b/src/test/regress/expected/gist.out
@@ -12,8 +12,7 @@ create index gist_pointidx4 on gist_point_tbl using gist(p) with (buffering = au
 drop index gist_pointidx2, gist_pointidx3, gist_pointidx4;
 -- Make sure bad values are refused
 create index gist_pointidx5 on gist_point_tbl using gist(p) with (buffering = invalid_value);
-ERROR:  invalid value for enum option "buffering": invalid_value
-DETAIL:  Valid values are "on", "off", and "auto".
+ERROR:  invalid value for ternary option "buffering": invalid_value
 create index gist_pointidx5 on gist_point_tbl using gist(p) with (fillfactor=9);
 ERROR:  value 9 out of bounds for option "fillfactor"
 DETAIL:  Valid values are between "10" and "100".
-- 
2.39.2

  [text/x-patch] v2-0004-Extra-tests.patch (12.3K, ../../11330814.CDJkKcVGEf@thinkpad-pgpro/5-v2-0004-Extra-tests.patch)
  download | inline diff:
From 3542824e57b171be13542ddc21288dc0490f0b22 Mon Sep 17 00:00:00 2001
From: Nikolay Shaplov <dhyan@nataraj.su>
Date: Thu, 4 Sep 2025 19:28:56 +0300
Subject: [PATCH v2 4/4] Extra tests

Add more tests for ternary reloptions in dummy_index_am module
---
 src/test/modules/dummy_index_am/README        |  1 +
 .../modules/dummy_index_am/dummy_index_am.c   | 36 +++++++++++++-----
 .../dummy_index_am/expected/reloptions.out    | 38 +++++++++++++++++--
 .../modules/dummy_index_am/sql/reloptions.sql | 16 ++++++++
 src/test/regress/expected/reloptions.out      |  4 +-
 src/test/regress/sql/reloptions.sql           |  4 +-
 6 files changed, 81 insertions(+), 18 deletions(-)

diff --git a/src/test/modules/dummy_index_am/README b/src/test/modules/dummy_index_am/README
index 61510f02fa..d80aff0db1 100644
--- a/src/test/modules/dummy_index_am/README
+++ b/src/test/modules/dummy_index_am/README
@@ -6,6 +6,7 @@ access method, whose code is kept a maximum simple.
 
 This includes tests for all relation option types:
 - boolean
+- ternary
 - enum
 - integer
 - real
diff --git a/src/test/modules/dummy_index_am/dummy_index_am.c b/src/test/modules/dummy_index_am/dummy_index_am.c
index 94ef639b6f..bf8446ddc6 100644
--- a/src/test/modules/dummy_index_am/dummy_index_am.c
+++ b/src/test/modules/dummy_index_am/dummy_index_am.c
@@ -22,7 +22,7 @@
 PG_MODULE_MAGIC;
 
 /* parse table for fillRelOptions */
-static relopt_parse_elt di_relopt_tab[6];
+static relopt_parse_elt di_relopt_tab[8];
 
 /* Kind of relation options for dummy index */
 static relopt_kind di_relopt_kind;
@@ -40,6 +40,8 @@ typedef struct DummyIndexOptions
 	int			option_int;
 	double		option_real;
 	bool		option_bool;
+	ternary		option_ternary1;
+	ternary		option_ternary2;
 	DummyAmEnum option_enum;
 	int			option_string_val_offset;
 	int			option_string_null_offset;
@@ -96,23 +98,37 @@ create_reloptions_table(void)
 	di_relopt_tab[2].opttype = RELOPT_TYPE_BOOL;
 	di_relopt_tab[2].offset = offsetof(DummyIndexOptions, option_bool);
 
+	add_ternary_reloption(di_relopt_kind, "option_ternary1",
+					   "First ternary option for dummy_index_am",
+					   TERNARY_UNSET, NULL, AccessExclusiveLock);
+	di_relopt_tab[3].optname = "option_ternary1";
+	di_relopt_tab[3].opttype = RELOPT_TYPE_TERNARY;
+	di_relopt_tab[3].offset = offsetof(DummyIndexOptions, option_ternary1);
+
+	add_ternary_reloption(di_relopt_kind, "option_ternary2",
+					   "Second ternary option for dummy_index_am",
+					   TERNARY_TRUE, "do_not_know_yet", AccessExclusiveLock);
+	di_relopt_tab[4].optname = "option_ternary2";
+	di_relopt_tab[4].opttype = RELOPT_TYPE_TERNARY;
+	di_relopt_tab[4].offset = offsetof(DummyIndexOptions, option_ternary2);
+
 	add_enum_reloption(di_relopt_kind, "option_enum",
 					   "Enum option for dummy_index_am",
 					   dummyAmEnumValues,
 					   DUMMY_AM_ENUM_ONE,
 					   "Valid values are \"one\" and \"two\".",
 					   AccessExclusiveLock);
-	di_relopt_tab[3].optname = "option_enum";
-	di_relopt_tab[3].opttype = RELOPT_TYPE_ENUM;
-	di_relopt_tab[3].offset = offsetof(DummyIndexOptions, option_enum);
+	di_relopt_tab[5].optname = "option_enum";
+	di_relopt_tab[5].opttype = RELOPT_TYPE_ENUM;
+	di_relopt_tab[5].offset = offsetof(DummyIndexOptions, option_enum);
 
 	add_string_reloption(di_relopt_kind, "option_string_val",
 						 "String option for dummy_index_am with non-NULL default",
 						 "DefaultValue", &validate_string_option,
 						 AccessExclusiveLock);
-	di_relopt_tab[4].optname = "option_string_val";
-	di_relopt_tab[4].opttype = RELOPT_TYPE_STRING;
-	di_relopt_tab[4].offset = offsetof(DummyIndexOptions,
+	di_relopt_tab[6].optname = "option_string_val";
+	di_relopt_tab[6].opttype = RELOPT_TYPE_STRING;
+	di_relopt_tab[6].offset = offsetof(DummyIndexOptions,
 									   option_string_val_offset);
 
 	/*
@@ -123,9 +139,9 @@ create_reloptions_table(void)
 						 NULL,	/* description */
 						 NULL, &validate_string_option,
 						 AccessExclusiveLock);
-	di_relopt_tab[5].optname = "option_string_null";
-	di_relopt_tab[5].opttype = RELOPT_TYPE_STRING;
-	di_relopt_tab[5].offset = offsetof(DummyIndexOptions,
+	di_relopt_tab[7].optname = "option_string_null";
+	di_relopt_tab[7].opttype = RELOPT_TYPE_STRING;
+	di_relopt_tab[7].offset = offsetof(DummyIndexOptions,
 									   option_string_null_offset);
 }
 
diff --git a/src/test/modules/dummy_index_am/expected/reloptions.out b/src/test/modules/dummy_index_am/expected/reloptions.out
index c873a80bb7..ad1b2ea639 100644
--- a/src/test/modules/dummy_index_am/expected/reloptions.out
+++ b/src/test/modules/dummy_index_am/expected/reloptions.out
@@ -18,6 +18,8 @@ SET client_min_messages TO 'notice';
 CREATE INDEX dummy_test_idx ON dummy_test_tab
   USING dummy_index_am (i) WITH (
   option_bool = false,
+  option_ternary1,
+  option_ternary2 = off,
   option_int = 5,
   option_real = 3.1,
   option_enum = 'two',
@@ -31,16 +33,20 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
          unnest         
 ------------------------
  option_bool=false
+ option_ternary1=true
+ option_ternary2=off
  option_int=5
  option_real=3.1
  option_enum=two
  option_string_val=null
  option_string_null=val
-(6 rows)
+(8 rows)
 
 -- ALTER INDEX .. SET
 ALTER INDEX dummy_test_idx SET (option_int = 10);
 ALTER INDEX dummy_test_idx SET (option_bool = true);
+ALTER INDEX dummy_test_idx SET (option_ternary1 = false);
+ALTER INDEX dummy_test_idx SET (option_ternary2 = Do_Not_Know_YET);
 ALTER INDEX dummy_test_idx SET (option_real = 3.2);
 ALTER INDEX dummy_test_idx SET (option_string_val = 'val2');
 ALTER INDEX dummy_test_idx SET (option_string_null = NULL);
@@ -49,19 +55,23 @@ ALTER INDEX dummy_test_idx SET (option_enum = 'three');
 ERROR:  invalid value for enum option "option_enum": three
 DETAIL:  Valid values are "one" and "two".
 SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
-         unnest          
--------------------------
+             unnest              
+---------------------------------
  option_int=10
  option_bool=true
+ option_ternary1=false
+ option_ternary2=do_not_know_yet
  option_real=3.2
  option_string_val=val2
  option_string_null=null
  option_enum=one
-(6 rows)
+(8 rows)
 
 -- ALTER INDEX .. RESET
 ALTER INDEX dummy_test_idx RESET (option_int);
 ALTER INDEX dummy_test_idx RESET (option_bool);
+ALTER INDEX dummy_test_idx RESET (option_ternary1);
+ALTER INDEX dummy_test_idx RESET (option_ternary2);
 ALTER INDEX dummy_test_idx RESET (option_real);
 ALTER INDEX dummy_test_idx RESET (option_enum);
 ALTER INDEX dummy_test_idx RESET (option_string_val);
@@ -100,6 +110,26 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 (1 row)
 
 ALTER INDEX dummy_test_idx RESET (option_bool);
+-- Ternary
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 4); -- error
+ERROR:  invalid value for ternary option "option_ternary1": 4
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 1); -- ok, as true
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 3.4); -- error
+ERROR:  invalid value for ternary option "option_ternary1": 3.4
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 'val4'); -- error
+ERROR:  invalid value for ternary option "option_ternary1": val4
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 'do_not_know_yet'); -- error. Valid for ternary2 not for ternary1
+ERROR:  invalid value for ternary option "option_ternary1": do_not_know_yet
+ALTER INDEX dummy_test_idx SET (option_ternary2 = 'do_not_know_yet'); -- ok
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
+             unnest              
+---------------------------------
+ option_ternary1=1
+ option_ternary2=do_not_know_yet
+(2 rows)
+
+ALTER INDEX dummy_test_idx RESET (option_ternary1);
+ALTER INDEX dummy_test_idx RESET (option_ternary2);
 -- Float
 ALTER INDEX dummy_test_idx SET (option_real = 4); -- ok
 ALTER INDEX dummy_test_idx SET (option_real = true); -- error
diff --git a/src/test/modules/dummy_index_am/sql/reloptions.sql b/src/test/modules/dummy_index_am/sql/reloptions.sql
index 6749d763e6..123540905a 100644
--- a/src/test/modules/dummy_index_am/sql/reloptions.sql
+++ b/src/test/modules/dummy_index_am/sql/reloptions.sql
@@ -18,6 +18,8 @@ SET client_min_messages TO 'notice';
 CREATE INDEX dummy_test_idx ON dummy_test_tab
   USING dummy_index_am (i) WITH (
   option_bool = false,
+  option_ternary1,
+  option_ternary2 = off,
   option_int = 5,
   option_real = 3.1,
   option_enum = 'two',
@@ -30,6 +32,8 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 -- ALTER INDEX .. SET
 ALTER INDEX dummy_test_idx SET (option_int = 10);
 ALTER INDEX dummy_test_idx SET (option_bool = true);
+ALTER INDEX dummy_test_idx SET (option_ternary1 = false);
+ALTER INDEX dummy_test_idx SET (option_ternary2 = Do_Not_Know_YET);
 ALTER INDEX dummy_test_idx SET (option_real = 3.2);
 ALTER INDEX dummy_test_idx SET (option_string_val = 'val2');
 ALTER INDEX dummy_test_idx SET (option_string_null = NULL);
@@ -40,6 +44,8 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 -- ALTER INDEX .. RESET
 ALTER INDEX dummy_test_idx RESET (option_int);
 ALTER INDEX dummy_test_idx RESET (option_bool);
+ALTER INDEX dummy_test_idx RESET (option_ternary1);
+ALTER INDEX dummy_test_idx RESET (option_ternary2);
 ALTER INDEX dummy_test_idx RESET (option_real);
 ALTER INDEX dummy_test_idx RESET (option_enum);
 ALTER INDEX dummy_test_idx RESET (option_string_val);
@@ -60,6 +66,16 @@ ALTER INDEX dummy_test_idx SET (option_bool = 3.4); -- error
 ALTER INDEX dummy_test_idx SET (option_bool = 'val4'); -- error
 SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 ALTER INDEX dummy_test_idx RESET (option_bool);
+-- Ternary
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 4); -- error
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 1); -- ok, as true
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 3.4); -- error
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 'val4'); -- error
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 'do_not_know_yet'); -- error. Valid for ternary2 not for ternary1
+ALTER INDEX dummy_test_idx SET (option_ternary2 = 'do_not_know_yet'); -- ok
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
+ALTER INDEX dummy_test_idx RESET (option_ternary1);
+ALTER INDEX dummy_test_idx RESET (option_ternary2);
 -- Float
 ALTER INDEX dummy_test_idx SET (option_real = 4); -- ok
 ALTER INDEX dummy_test_idx SET (option_real = true); -- error
diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index 1c99f79ab0..6e65cd5c3d 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -98,7 +98,7 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
  {fillfactor=13,autovacuum_enabled=false}
 (1 row)
 
--- Tests for future (FIXME) ternary options
+-- Tests for ternary options
 -- behave as boolean option: accept unassigned name and truncated value
 DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate);
@@ -116,7 +116,7 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
  {vacuum_truncate=fals}
 (1 row)
 
--- preferred "true" alias is used when storing
+-- preferred "true" alias is stored in pg_class
 DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=on);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index f5980dafcb..c99673db9e 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -59,7 +59,7 @@ UPDATE pg_class
 ALTER TABLE reloptions_test RESET (illegal_option);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 
--- Tests for future (FIXME) ternary options
+-- Tests for ternary options
 
 -- behave as boolean option: accept unassigned name and truncated value
 DROP TABLE reloptions_test;
@@ -70,7 +70,7 @@ DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate=FaLS);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 
--- preferred "true" alias is used when storing
+-- preferred "true" alias is stored in pg_class
 DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=on);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
-- 
2.39.2

  [application/pgp-signature] signature.asc (487B, ../../11330814.CDJkKcVGEf@thinkpad-pgpro/6-signature.asc)
  download

^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: [PATCH] ternary reloption type
  2025-09-04 17:07 Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
@ 2025-09-12 13:46 ` Timur Magomedov <t.magomedov@postgrespro.ru>
  2025-09-14 08:49   ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  0 siblings, 1 reply; 13+ messages in thread

From: Timur Magomedov @ 2025-09-12 13:46 UTC (permalink / raw)
  To: Nikolay Shaplov <dhyan@nataraj.su>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Álvaro Herrera <alvherre@alvh.no-ip.org>; Chris Travers <chris.travers@gmail.com>



Hello Nikolay!

Found a typo in reloptions.h, treaed -> treated.

Can ternary enum be added in a separate header file, say,
src/include/ternary.h instead of adding it to c.h? I'm just not sure if
c.h is it the right place for relation-options-specific code.
Of course, I can be wrong.

-- 
Regards,
Timur Magomedov






^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: [PATCH] ternary reloption type
  2025-09-04 17:07 Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2025-09-12 13:46 ` Re: [PATCH] ternary reloption type Timur Magomedov <t.magomedov@postgrespro.ru>
@ 2025-09-14 08:49   ` Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-16 15:14     ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
  0 siblings, 1 reply; 13+ messages in thread

From: Nikolay Shaplov @ 2025-09-14 08:49 UTC (permalink / raw)
  To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Álvaro Herrera <alvherre@alvh.no-ip.org>; Chris Travers <chris.travers@gmail.com>; Timur Magomedov <t.magomedov@postgrespro.ru>

В письме от пятница, 12 сентября 2025 г. 16:46:19 MSK пользователь Timur 
Magomedov написал:
> Hello Nikolay!
> 
> Found a typo in reloptions.h, treaed -> treated.
Oups. Fixed that in the attached version.

> Can ternary enum be added in a separate header file, say,
> src/include/ternary.h instead of adding it to c.h? I'm just not sure if
> c.h is it the right place for relation-options-specific code.
> Of course, I can be wrong.

I am not sure either. But my guess is that spamming into c.h is lesser crime 
then adding another useless header file.

Moreover, ternary value is not relation-options-specific, it is actually 
relation specific, if you think about it thoroughly. Relation code uses it, and 
there is no way to avoid that. 

Are there any other notions about the code?

I tried to make thongs more neat and more consistent here. Did I succeed?

-- 
Nikolay Shaplov aka Nataraj
Fuzzing Engineer at Postgres Professional
Matrix IM: @dhyan:nataraj.su

Attachments:

  [text/x-patch] v2a-0001-Add-ternary-reloption-type.patch (0B, ../../4047390.3Lj2Plt8kZ@thinkpad-pgpro/2-v2a-0001-Add-ternary-reloption-type.patch)
  download

^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: [PATCH] ternary reloption type
  2025-09-04 17:07 Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2025-09-12 13:46 ` Re: [PATCH] ternary reloption type Timur Magomedov <t.magomedov@postgrespro.ru>
  2025-09-14 08:49   ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
@ 2026-01-16 15:14     ` Álvaro Herrera <alvherre@alvh.no-ip.org>
  2026-01-16 21:52       ` Re: [PATCH] ternary reloption type Nathan Bossart <nathandbossart@gmail.com>
  2026-01-21 18:44       ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  0 siblings, 2 replies; 13+ messages in thread

From: Álvaro Herrera @ 2026-01-16 15:14 UTC (permalink / raw)
  To: Nikolay Shaplov <dhyan@nataraj.su>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Chris Travers <chris.travers@gmail.com>; Timur Magomedov <t.magomedov@postgrespro.ru>; Nathan Bossart <nathandbossart@gmail.com>

I took a quick look at 0001+0002 and I think it's quite reasonable.
Here it is again with some minor fixups.  (I'm omitting the further
patches for now, we can rebase them later.)

I'm CCing Nathan as committer of the vacuum_truncate_set stuff which
Nikolay so strongly disliked.  Any objections to going with this
approach?

Thanks,


(Please note that Gmail is sabotaging my kurilemu.de domain, so there's
significant delay in my emails to the list from that address.  I guess
I'm lucky that Nikolay decided to CC my old address in this thread.)

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"The problem with the future is that it keeps turning into the present"
(Hobbes)

Attachments:

  [text/x-diff] v3-0001-Introduce-ternary-reloptions.patch (15.6K, ../../202601161509.54de2afpgxpp@alvherre.pgsql/2-v3-0001-Introduce-ternary-reloptions.patch)
  download | inline diff:
From fe54ef4634f25bced3d8651da28f7728b53b019c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Fri, 16 Jan 2026 16:04:22 +0100
Subject: [PATCH v3] Introduce ternary reloptions

Introduce ternary reloption as a replacement for current
`vacuum_truncate` implementation. Remove the `vacuum_truncate_set`
separate flag and use TERNARY_UNSET instead.

This could also be used for other options such as `vacuum_index_cleanup`
and `buffering`, but lets get the scaffolding in first.

Discussion: https://postgr.es/m/3474141.usfYGdeWWP@thinkpad-pgpro
---
 src/backend/access/common/reloptions.c   | 137 ++++++++++++++++++-----
 src/backend/commands/vacuum.c            |   4 +-
 src/include/access/reloptions.h          |  27 ++---
 src/include/postgres.h                   |  15 +++
 src/include/utils/rel.h                  |   3 +-
 src/test/regress/expected/reloptions.out |  36 ++++++
 src/test/regress/sql/reloptions.sql      |  21 ++++
 src/tools/pgindent/typedefs.list         |   2 +
 8 files changed, 202 insertions(+), 43 deletions(-)

diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 0b83f98ed5f..6f1f577581a 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -40,9 +40,9 @@
  *
  * To add an option:
  *
- * (i) decide on a type (bool, integer, real, enum, string), name, default
- * value, upper and lower bounds (if applicable); for strings, consider a
- * validation routine.
+ * (i) decide on a type (bool, ternary, integer, real, enum, string), name,
+ * default value, upper and lower bounds (if applicable); for strings,
+ * consider a validation routine.
  * (ii) add a record below (or use add_<type>_reloption).
  * (iii) add it to the appropriate options struct (perhaps StdRdOptions)
  * (iv) add it to the appropriate handling routine (perhaps
@@ -147,15 +147,6 @@ static relopt_bool boolRelOpts[] =
 		},
 		false
 	},
-	{
-		{
-			"vacuum_truncate",
-			"Enables vacuum to truncate empty pages at the end of this table",
-			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
-			ShareUpdateExclusiveLock
-		},
-		true
-	},
 	{
 		{
 			"deduplicate_items",
@@ -170,6 +161,25 @@ static relopt_bool boolRelOpts[] =
 	{{NULL}}
 };
 
+static relopt_ternary ternaryRelOpts[] =
+{
+	{
+		{
+			"vacuum_truncate",
+			"Enables vacuum to truncate empty pages at the end of this table",
+			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+			ShareUpdateExclusiveLock
+		},
+		TERNARY_UNSET
+	},
+	/* list terminator */
+	{
+		{
+			NULL
+		}
+	}
+};
+
 static relopt_int intRelOpts[] =
 {
 	{
@@ -609,6 +619,13 @@ initialize_reloptions(void)
 								   boolRelOpts[i].gen.lockmode));
 		j++;
 	}
+	for (i = 0; ternaryRelOpts[i].gen.name; i++)
+	{
+		Assert(DoLockModesConflict(ternaryRelOpts[i].gen.lockmode,
+								   ternaryRelOpts[i].gen.lockmode));
+		j++;
+	}
+
 	for (i = 0; intRelOpts[i].gen.name; i++)
 	{
 		Assert(DoLockModesConflict(intRelOpts[i].gen.lockmode,
@@ -649,6 +666,14 @@ initialize_reloptions(void)
 		j++;
 	}
 
+	for (i = 0; ternaryRelOpts[i].gen.name; i++)
+	{
+		relOpts[j] = &ternaryRelOpts[i].gen;
+		relOpts[j]->type = RELOPT_TYPE_TERNARY;
+		relOpts[j]->namelen = strlen(relOpts[j]->name);
+		j++;
+	}
+
 	for (i = 0; intRelOpts[i].gen.name; i++)
 	{
 		relOpts[j] = &intRelOpts[i].gen;
@@ -809,6 +834,9 @@ allocate_reloption(bits32 kinds, int type, const char *name, const char *desc,
 		case RELOPT_TYPE_BOOL:
 			size = sizeof(relopt_bool);
 			break;
+		case RELOPT_TYPE_TERNARY:
+			size = sizeof(relopt_ternary);
+			break;
 		case RELOPT_TYPE_INT:
 			size = sizeof(relopt_int);
 			break;
@@ -892,6 +920,57 @@ add_local_bool_reloption(local_relopts *relopts, const char *name,
 	add_local_reloption(relopts, (relopt_gen *) newoption, offset);
 }
 
+/*
+ * init_ternary_reloption
+ *		Allocate and initialize a new ternary reloption
+ */
+static relopt_ternary *
+init_ternary_reloption(bits32 kinds, const char *name, const char *desc,
+					   pg_ternary default_val, LOCKMODE lockmode)
+{
+	relopt_ternary *newoption;
+
+	newoption = (relopt_ternary *)
+		allocate_reloption(kinds, RELOPT_TYPE_TERNARY, name, desc, lockmode);
+	newoption->default_val = default_val;
+
+	return newoption;
+}
+
+/*
+ * add_ternary_reloption
+ *		Add a new ternary reloption
+ */
+void
+add_ternary_reloption(bits32 kinds, const char *name, const char *desc,
+					  pg_ternary default_val, LOCKMODE lockmode)
+{
+	relopt_ternary *newoption;
+
+	newoption =
+		init_ternary_reloption(kinds, name, desc, default_val, lockmode);
+
+	add_reloption((relopt_gen *) newoption);
+}
+
+/*
+ * add_local_ternary_reloption
+ *		Add a new ternary local reloption
+ *
+ * 'offset' is offset of ternary-typed field.
+ */
+void
+add_local_ternary_reloption(local_relopts *relopts, const char *name,
+							const char *desc, pg_ternary default_val,
+							int offset)
+{
+	relopt_ternary *newoption;
+
+	newoption =
+		init_ternary_reloption(RELOPT_KIND_LOCAL, name, desc, default_val, 0);
+
+	add_local_reloption(relopts, (relopt_gen *) newoption, offset);
+}
 
 /*
  * init_real_reloption
@@ -1626,6 +1705,19 @@ parse_one_reloption(relopt_value *option, char *text_str, int text_len,
 									option->gen->name, value)));
 			}
 			break;
+		case RELOPT_TYPE_TERNARY:
+			{
+				bool		b;
+
+				parsed = parse_bool(value, &b);
+				option->values.ternary_val = b ? TERNARY_TRUE : TERNARY_FALSE;
+				if (validate && !parsed)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("invalid value for ternary option \"%s\": %s",
+									option->gen->name, value)));
+			}
+			break;
 		case RELOPT_TYPE_INT:
 			{
 				relopt_int *optint = (relopt_int *) option->gen;
@@ -1789,17 +1881,6 @@ fillRelOptions(void *rdopts, Size basesize,
 				char	   *itempos = ((char *) rdopts) + elems[j].offset;
 				char	   *string_val;
 
-				/*
-				 * If isset_offset is provided, store whether the reloption is
-				 * set there.
-				 */
-				if (elems[j].isset_offset > 0)
-				{
-					char	   *setpos = ((char *) rdopts) + elems[j].isset_offset;
-
-					*(bool *) setpos = options[i].isset;
-				}
-
 				switch (options[i].gen->type)
 				{
 					case RELOPT_TYPE_BOOL:
@@ -1807,6 +1888,11 @@ fillRelOptions(void *rdopts, Size basesize,
 							options[i].values.bool_val :
 							((relopt_bool *) options[i].gen)->default_val;
 						break;
+					case RELOPT_TYPE_TERNARY:
+						*(pg_ternary *) itempos = options[i].isset ?
+							options[i].values.ternary_val :
+							((relopt_ternary *) options[i].gen)->default_val;
+						break;
 					case RELOPT_TYPE_INT:
 						*(int *) itempos = options[i].isset ?
 							options[i].values.int_val :
@@ -1923,8 +2009,8 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		offsetof(StdRdOptions, parallel_workers)},
 		{"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
 		offsetof(StdRdOptions, vacuum_index_cleanup)},
-		{"vacuum_truncate", RELOPT_TYPE_BOOL,
-		offsetof(StdRdOptions, vacuum_truncate), offsetof(StdRdOptions, vacuum_truncate_set)},
+		{"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)}
 	};
@@ -2004,7 +2090,6 @@ build_local_reloptions(local_relopts *relopts, Datum options, bool validate)
 		elems[i].optname = opt->option->name;
 		elems[i].opttype = opt->option->type;
 		elems[i].offset = opt->offset;
-		elems[i].isset_offset = 0;	/* not supported for local relopts yet */
 
 		i++;
 	}
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index aa4fbec143f..696eab9bd97 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2224,9 +2224,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 	{
 		StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
 
-		if (opts && opts->vacuum_truncate_set)
+		if (opts && opts->vacuum_truncate != TERNARY_UNSET)
 		{
-			if (opts->vacuum_truncate)
+			if (opts->vacuum_truncate == TERNARY_TRUE)
 				params.truncate = VACOPTVALUE_ENABLED;
 			else
 				params.truncate = VACOPTVALUE_DISABLED;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index 2f08e1b0cf0..dfbef2babf2 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -29,6 +29,7 @@
 typedef enum relopt_type
 {
 	RELOPT_TYPE_BOOL,
+	RELOPT_TYPE_TERNARY,		/* on, off, unset */
 	RELOPT_TYPE_INT,
 	RELOPT_TYPE_REAL,
 	RELOPT_TYPE_ENUM,
@@ -80,6 +81,7 @@ typedef struct relopt_value
 	union
 	{
 		bool		bool_val;
+		pg_ternary	ternary_val;
 		int			int_val;
 		double		real_val;
 		int			enum_val;
@@ -94,6 +96,12 @@ typedef struct relopt_bool
 	bool		default_val;
 } relopt_bool;
 
+typedef struct relopt_ternary
+{
+	relopt_gen	gen;
+	int			default_val;
+} relopt_ternary;
+
 typedef struct relopt_int
 {
 	relopt_gen	gen;
@@ -152,19 +160,6 @@ typedef struct
 	const char *optname;		/* option's name */
 	relopt_type opttype;		/* option's datatype */
 	int			offset;			/* offset of field in result struct */
-
-	/*
-	 * isset_offset is an optional offset of a field in the result struct that
-	 * stores whether the option is explicitly set for the relation or if it
-	 * just picked up the default value.  In most cases, this can be
-	 * accomplished by giving the reloption a special out-of-range default
-	 * value (e.g., some integer reloptions use -2), but this isn't always
-	 * possible.  For example, a Boolean reloption cannot be given an
-	 * out-of-range default, so we need another way to discover the source of
-	 * its value.  This offset is only used if given a value greater than
-	 * zero.
-	 */
-	int			isset_offset;
 } relopt_parse_elt;
 
 /* Local reloption definition */
@@ -195,6 +190,9 @@ typedef struct local_relopts
 extern relopt_kind add_reloption_kind(void);
 extern void add_bool_reloption(bits32 kinds, const char *name, const char *desc,
 							   bool default_val, LOCKMODE lockmode);
+extern void add_ternary_reloption(bits32 kinds, const char *name,
+								  const char *desc, pg_ternary default_val,
+								  LOCKMODE lockmode);
 extern void add_int_reloption(bits32 kinds, const char *name, const char *desc,
 							  int default_val, int min_val, int max_val,
 							  LOCKMODE lockmode);
@@ -214,6 +212,9 @@ extern void register_reloptions_validator(local_relopts *relopts,
 extern void add_local_bool_reloption(local_relopts *relopts, const char *name,
 									 const char *desc, bool default_val,
 									 int offset);
+extern void add_local_ternary_reloption(local_relopts *relopts,
+										const char *name, const char *desc,
+										pg_ternary default_val, int offset);
 extern void add_local_int_reloption(local_relopts *relopts, const char *name,
 									const char *desc, int default_val,
 									int min_val, int max_val, int offset);
diff --git a/src/include/postgres.h b/src/include/postgres.h
index 7d93fbce709..47c88780776 100644
--- a/src/include/postgres.h
+++ b/src/include/postgres.h
@@ -543,6 +543,21 @@ Float8GetDatum(float8 X)
  * ----------------------------------------------------------------
  */
 
+/*
+ * pg_ternary
+ *		Boolean value with an extra "unset" value
+ *
+ * This enum can be used for values that want to distinguish between true,
+ * false, and unset.
+*/
+
+typedef enum pg_ternary
+{
+	TERNARY_FALSE = 0,
+	TERNARY_TRUE = 1,
+	TERNARY_UNSET = -1
+} pg_ternary;
+
 /*
  * NON_EXEC_STATIC: It's sometimes useful to define a variable or function
  * that is normally static but extern when using EXEC_BACKEND (see
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index d03ab247788..5feb18a5373 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -347,8 +347,7 @@ typedef struct StdRdOptions
 	bool		user_catalog_table; /* use as an additional catalog relation */
 	int			parallel_workers;	/* max number of parallel workers */
 	StdRdOptIndexCleanup vacuum_index_cleanup;	/* controls index vacuuming */
-	bool		vacuum_truncate;	/* enables vacuum to truncate a relation */
-	bool		vacuum_truncate_set;	/* whether vacuum_truncate is set */
+	pg_ternary		vacuum_truncate;	/* enables vacuum to truncate a relation */
 
 	/*
 	 * Fraction of pages in a relation that vacuum can eagerly scan and fail
diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index 9de19b4e3f1..1c99f79ab01 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -98,6 +98,42 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
  {fillfactor=13,autovacuum_enabled=false}
 (1 row)
 
+-- Tests for future (FIXME) ternary options
+-- behave as boolean option: accept unassigned name and truncated value
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+       reloptions       
+------------------------
+ {vacuum_truncate=true}
+(1 row)
+
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate=FaLS);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+       reloptions       
+------------------------
+ {vacuum_truncate=fals}
+(1 row)
+
+-- preferred "true" alias is used when storing
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=on);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+        reloptions         
+---------------------------
+ {vacuum_index_cleanup=on}
+(1 row)
+
+-- custom "third" value is available
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=auto);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+         reloptions          
+-----------------------------
+ {vacuum_index_cleanup=auto}
+(1 row)
+
 -- Test vacuum_truncate option
 DROP TABLE reloptions_test;
 CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index 24fbe0b478d..f5980dafcbc 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -59,6 +59,27 @@ UPDATE pg_class
 ALTER TABLE reloptions_test RESET (illegal_option);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 
+-- Tests for future (FIXME) ternary options
+
+-- behave as boolean option: accept unassigned name and truncated value
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate=FaLS);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
+-- preferred "true" alias is used when storing
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=on);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
+-- custom "third" value is available
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=auto);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
 -- Test vacuum_truncate option
 DROP TABLE reloptions_test;
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3f3a888fd0e..1c8610fd46c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3952,6 +3952,7 @@ pg_sha512_ctx
 pg_snapshot
 pg_special_case
 pg_stack_base_t
+pg_ternary
 pg_time_t
 pg_time_usec_t
 pg_tz
@@ -4079,6 +4080,7 @@ relopt_kind
 relopt_parse_elt
 relopt_real
 relopt_string
+relopt_ternary
 relopt_type
 relopt_value
 relopts_validator
-- 
2.47.3

^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: [PATCH] ternary reloption type
  2025-09-04 17:07 Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2025-09-12 13:46 ` Re: [PATCH] ternary reloption type Timur Magomedov <t.magomedov@postgrespro.ru>
  2025-09-14 08:49   ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-16 15:14     ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
@ 2026-01-16 21:52       ` Nathan Bossart <nathandbossart@gmail.com>
  1 sibling, 0 replies; 13+ messages in thread

From: Nathan Bossart @ 2026-01-16 21:52 UTC (permalink / raw)
  To: Álvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: Nikolay Shaplov <dhyan@nataraj.su>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Chris Travers <chris.travers@gmail.com>; Timur Magomedov <t.magomedov@postgrespro.ru>

On Fri, Jan 16, 2026 at 04:14:52PM +0100, Álvaro Herrera wrote:
> I'm CCing Nathan as committer of the vacuum_truncate_set stuff which
> Nikolay so strongly disliked.  Any objections to going with this
> approach?

Looks generally reasonable.

> This could also be used for other options such as `vacuum_index_cleanup`
> and `buffering`, but lets get the scaffolding in first.

Part of me wonders if we should just modify the Boolean relopt
implementation instead of using ternary only when needed.

> +				parsed = parse_bool(value, &b);
> +				option->values.ternary_val = b ? TERNARY_TRUE : TERNARY_FALSE;
> +				if (validate && !parsed)
> +					ereport(ERROR,
> +							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> +							 errmsg("invalid value for ternary option \"%s\": %s",
> +									option->gen->name, value)));

Shouldn't this say "invalid value for boolean option"?  IIUC the intent is
for ternary to be exactly like bool, except it defaults to an "unset" value
that can't be chosen by the user.  In that sense, I think "ternary" is kind
of a misnomer, but I wouldn't count this as an objection.

-- 
nathan





^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: [PATCH] ternary reloption type
  2025-09-04 17:07 Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2025-09-12 13:46 ` Re: [PATCH] ternary reloption type Timur Magomedov <t.magomedov@postgrespro.ru>
  2025-09-14 08:49   ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-16 15:14     ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
@ 2026-01-21 18:44       ` Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-21 19:23         ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
  1 sibling, 1 reply; 13+ messages in thread

From: Nikolay Shaplov @ 2026-01-21 18:44 UTC (permalink / raw)
  To: Álvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Chris Travers <chris.travers@gmail.com>; Timur Magomedov <t.magomedov@postgrespro.ru>; Nathan Bossart <nathandbossart@gmail.com>


On 16.01.2026 18:14, Álvaro Herrera wrote:
> I took a quick look at 0001+0002 and I think it's quite reasonable.
> Here it is again with some minor fixups.

Good. I like ternary -> pg_ternary change. That is reasonable. And 
postgres.h is better place for it then c.h.

>    (I'm omitting the further
> patches for now, we can rebase them later.)

I've rebased the rest of patches, and the whole patchset is in the 
attachment.

> I'm CCing Nathan as committer of the vacuum_truncate_set stuff which
> Nikolay so strongly disliked.  Any objections to going with this
> approach?

Ok, will quote him and reply below in the this letter.

> > This could also be used for other options such as `vacuum_index_cleanup`
> > and `buffering`, but lets get the scaffolding in first.
> Part of me wonders if we should just modify the Boolean relopt
> implementation instead of using ternary only when needed.
Ternary option, with it's third optional option is quite complex.

I'd rather not bother postgres developer with this complexity if they 
just want to add pure boolean option.

> >+				parsed = parse_bool(value, &b);
> >+				option->values.ternary_val = b ? TERNARY_TRUE : TERNARY_FALSE;
> >+				if (validate && !parsed)
> >+					ereport(ERROR,
> >+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> >+							 errmsg("invalid value for ternary option \"%s\": %s",
> >+									option->gen->name, value)));
> Shouldn't this say "invalid value for boolean option"?  IIUC the intent is
> for ternary to be exactly like bool, except it defaults to an "unset" value
> that can't be chosen by the user.  In that sense, I think "ternary" is kind
> of a misnomer, but I wouldn't count this as an objection.
If you look at buffering option for example, you will see it does not 
behave as pure boolean option.
"auto" value can be explicitly set by user.

Error reporting is not strong part of my patch, I would agree with that.

What if we give two different error reports in two different cases:

Give boolean-like error report when option does not have explicit alias 
for third option, and more verbose
error message, listing all available options in the case when third 
option have explicit alias?

like

"invalid value for boolean option \"%s\": %s",

and

"invalid value for option \"%s\": %s",
"valid values are 'on', 'off' and '%s'"


we may actually say nothing about option being ternary here

Attachments:

  [text/x-patch] v3a-0004-Extra-tests.patch (12.3K, ../../3c4118a2-ec6b-4ea9-89af-233e1a2892ce@nataraj.su/3-v3a-0004-Extra-tests.patch)
  download | inline diff:
From 0da64249b33d3f2162081346c09bc50bcc546c0a Mon Sep 17 00:00:00 2001
From: Nikolay Shaplov <dhyan@nataraj.su>
Date: Thu, 4 Sep 2025 19:28:56 +0300
Subject: [PATCH v3a 4/4] Extra tests

Add more tests for ternary reloptions in dummy_index_am module
---
 src/test/modules/dummy_index_am/README        |  1 +
 .../modules/dummy_index_am/dummy_index_am.c   | 36 +++++++++++++-----
 .../dummy_index_am/expected/reloptions.out    | 38 +++++++++++++++++--
 .../modules/dummy_index_am/sql/reloptions.sql | 16 ++++++++
 src/test/regress/expected/reloptions.out      |  4 +-
 src/test/regress/sql/reloptions.sql           |  4 +-
 6 files changed, 81 insertions(+), 18 deletions(-)

diff --git a/src/test/modules/dummy_index_am/README b/src/test/modules/dummy_index_am/README
index 61510f02fae..d80aff0db19 100644
--- a/src/test/modules/dummy_index_am/README
+++ b/src/test/modules/dummy_index_am/README
@@ -6,6 +6,7 @@ access method, whose code is kept a maximum simple.
 
 This includes tests for all relation option types:
 - boolean
+- ternary
 - enum
 - integer
 - real
diff --git a/src/test/modules/dummy_index_am/dummy_index_am.c b/src/test/modules/dummy_index_am/dummy_index_am.c
index 9eb8f0a6c63..81d212b85a3 100644
--- a/src/test/modules/dummy_index_am/dummy_index_am.c
+++ b/src/test/modules/dummy_index_am/dummy_index_am.c
@@ -22,7 +22,7 @@
 PG_MODULE_MAGIC;
 
 /* parse table for fillRelOptions */
-static relopt_parse_elt di_relopt_tab[6];
+static relopt_parse_elt di_relopt_tab[8];
 
 /* Kind of relation options for dummy index */
 static relopt_kind di_relopt_kind;
@@ -40,6 +40,8 @@ typedef struct DummyIndexOptions
 	int			option_int;
 	double		option_real;
 	bool		option_bool;
+	pg_ternary	option_ternary1;
+	pg_ternary	option_ternary2;
 	DummyAmEnum option_enum;
 	int			option_string_val_offset;
 	int			option_string_null_offset;
@@ -96,23 +98,37 @@ create_reloptions_table(void)
 	di_relopt_tab[2].opttype = RELOPT_TYPE_BOOL;
 	di_relopt_tab[2].offset = offsetof(DummyIndexOptions, option_bool);
 
+	add_ternary_reloption(di_relopt_kind, "option_ternary1",
+					   "First ternary option for dummy_index_am",
+					   TERNARY_UNSET, NULL, AccessExclusiveLock);
+	di_relopt_tab[3].optname = "option_ternary1";
+	di_relopt_tab[3].opttype = RELOPT_TYPE_TERNARY;
+	di_relopt_tab[3].offset = offsetof(DummyIndexOptions, option_ternary1);
+
+	add_ternary_reloption(di_relopt_kind, "option_ternary2",
+					   "Second ternary option for dummy_index_am",
+					   TERNARY_TRUE, "do_not_know_yet", AccessExclusiveLock);
+	di_relopt_tab[4].optname = "option_ternary2";
+	di_relopt_tab[4].opttype = RELOPT_TYPE_TERNARY;
+	di_relopt_tab[4].offset = offsetof(DummyIndexOptions, option_ternary2);
+
 	add_enum_reloption(di_relopt_kind, "option_enum",
 					   "Enum option for dummy_index_am",
 					   dummyAmEnumValues,
 					   DUMMY_AM_ENUM_ONE,
 					   "Valid values are \"one\" and \"two\".",
 					   AccessExclusiveLock);
-	di_relopt_tab[3].optname = "option_enum";
-	di_relopt_tab[3].opttype = RELOPT_TYPE_ENUM;
-	di_relopt_tab[3].offset = offsetof(DummyIndexOptions, option_enum);
+	di_relopt_tab[5].optname = "option_enum";
+	di_relopt_tab[5].opttype = RELOPT_TYPE_ENUM;
+	di_relopt_tab[5].offset = offsetof(DummyIndexOptions, option_enum);
 
 	add_string_reloption(di_relopt_kind, "option_string_val",
 						 "String option for dummy_index_am with non-NULL default",
 						 "DefaultValue", &validate_string_option,
 						 AccessExclusiveLock);
-	di_relopt_tab[4].optname = "option_string_val";
-	di_relopt_tab[4].opttype = RELOPT_TYPE_STRING;
-	di_relopt_tab[4].offset = offsetof(DummyIndexOptions,
+	di_relopt_tab[6].optname = "option_string_val";
+	di_relopt_tab[6].opttype = RELOPT_TYPE_STRING;
+	di_relopt_tab[6].offset = offsetof(DummyIndexOptions,
 									   option_string_val_offset);
 
 	/*
@@ -123,9 +139,9 @@ create_reloptions_table(void)
 						 NULL,	/* description */
 						 NULL, &validate_string_option,
 						 AccessExclusiveLock);
-	di_relopt_tab[5].optname = "option_string_null";
-	di_relopt_tab[5].opttype = RELOPT_TYPE_STRING;
-	di_relopt_tab[5].offset = offsetof(DummyIndexOptions,
+	di_relopt_tab[7].optname = "option_string_null";
+	di_relopt_tab[7].opttype = RELOPT_TYPE_STRING;
+	di_relopt_tab[7].offset = offsetof(DummyIndexOptions,
 									   option_string_null_offset);
 }
 
diff --git a/src/test/modules/dummy_index_am/expected/reloptions.out b/src/test/modules/dummy_index_am/expected/reloptions.out
index c873a80bb75..ad1b2ea6397 100644
--- a/src/test/modules/dummy_index_am/expected/reloptions.out
+++ b/src/test/modules/dummy_index_am/expected/reloptions.out
@@ -18,6 +18,8 @@ SET client_min_messages TO 'notice';
 CREATE INDEX dummy_test_idx ON dummy_test_tab
   USING dummy_index_am (i) WITH (
   option_bool = false,
+  option_ternary1,
+  option_ternary2 = off,
   option_int = 5,
   option_real = 3.1,
   option_enum = 'two',
@@ -31,16 +33,20 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
          unnest         
 ------------------------
  option_bool=false
+ option_ternary1=true
+ option_ternary2=off
  option_int=5
  option_real=3.1
  option_enum=two
  option_string_val=null
  option_string_null=val
-(6 rows)
+(8 rows)
 
 -- ALTER INDEX .. SET
 ALTER INDEX dummy_test_idx SET (option_int = 10);
 ALTER INDEX dummy_test_idx SET (option_bool = true);
+ALTER INDEX dummy_test_idx SET (option_ternary1 = false);
+ALTER INDEX dummy_test_idx SET (option_ternary2 = Do_Not_Know_YET);
 ALTER INDEX dummy_test_idx SET (option_real = 3.2);
 ALTER INDEX dummy_test_idx SET (option_string_val = 'val2');
 ALTER INDEX dummy_test_idx SET (option_string_null = NULL);
@@ -49,19 +55,23 @@ ALTER INDEX dummy_test_idx SET (option_enum = 'three');
 ERROR:  invalid value for enum option "option_enum": three
 DETAIL:  Valid values are "one" and "two".
 SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
-         unnest          
--------------------------
+             unnest              
+---------------------------------
  option_int=10
  option_bool=true
+ option_ternary1=false
+ option_ternary2=do_not_know_yet
  option_real=3.2
  option_string_val=val2
  option_string_null=null
  option_enum=one
-(6 rows)
+(8 rows)
 
 -- ALTER INDEX .. RESET
 ALTER INDEX dummy_test_idx RESET (option_int);
 ALTER INDEX dummy_test_idx RESET (option_bool);
+ALTER INDEX dummy_test_idx RESET (option_ternary1);
+ALTER INDEX dummy_test_idx RESET (option_ternary2);
 ALTER INDEX dummy_test_idx RESET (option_real);
 ALTER INDEX dummy_test_idx RESET (option_enum);
 ALTER INDEX dummy_test_idx RESET (option_string_val);
@@ -100,6 +110,26 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 (1 row)
 
 ALTER INDEX dummy_test_idx RESET (option_bool);
+-- Ternary
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 4); -- error
+ERROR:  invalid value for ternary option "option_ternary1": 4
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 1); -- ok, as true
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 3.4); -- error
+ERROR:  invalid value for ternary option "option_ternary1": 3.4
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 'val4'); -- error
+ERROR:  invalid value for ternary option "option_ternary1": val4
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 'do_not_know_yet'); -- error. Valid for ternary2 not for ternary1
+ERROR:  invalid value for ternary option "option_ternary1": do_not_know_yet
+ALTER INDEX dummy_test_idx SET (option_ternary2 = 'do_not_know_yet'); -- ok
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
+             unnest              
+---------------------------------
+ option_ternary1=1
+ option_ternary2=do_not_know_yet
+(2 rows)
+
+ALTER INDEX dummy_test_idx RESET (option_ternary1);
+ALTER INDEX dummy_test_idx RESET (option_ternary2);
 -- Float
 ALTER INDEX dummy_test_idx SET (option_real = 4); -- ok
 ALTER INDEX dummy_test_idx SET (option_real = true); -- error
diff --git a/src/test/modules/dummy_index_am/sql/reloptions.sql b/src/test/modules/dummy_index_am/sql/reloptions.sql
index 6749d763e6a..123540905af 100644
--- a/src/test/modules/dummy_index_am/sql/reloptions.sql
+++ b/src/test/modules/dummy_index_am/sql/reloptions.sql
@@ -18,6 +18,8 @@ SET client_min_messages TO 'notice';
 CREATE INDEX dummy_test_idx ON dummy_test_tab
   USING dummy_index_am (i) WITH (
   option_bool = false,
+  option_ternary1,
+  option_ternary2 = off,
   option_int = 5,
   option_real = 3.1,
   option_enum = 'two',
@@ -30,6 +32,8 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 -- ALTER INDEX .. SET
 ALTER INDEX dummy_test_idx SET (option_int = 10);
 ALTER INDEX dummy_test_idx SET (option_bool = true);
+ALTER INDEX dummy_test_idx SET (option_ternary1 = false);
+ALTER INDEX dummy_test_idx SET (option_ternary2 = Do_Not_Know_YET);
 ALTER INDEX dummy_test_idx SET (option_real = 3.2);
 ALTER INDEX dummy_test_idx SET (option_string_val = 'val2');
 ALTER INDEX dummy_test_idx SET (option_string_null = NULL);
@@ -40,6 +44,8 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 -- ALTER INDEX .. RESET
 ALTER INDEX dummy_test_idx RESET (option_int);
 ALTER INDEX dummy_test_idx RESET (option_bool);
+ALTER INDEX dummy_test_idx RESET (option_ternary1);
+ALTER INDEX dummy_test_idx RESET (option_ternary2);
 ALTER INDEX dummy_test_idx RESET (option_real);
 ALTER INDEX dummy_test_idx RESET (option_enum);
 ALTER INDEX dummy_test_idx RESET (option_string_val);
@@ -60,6 +66,16 @@ ALTER INDEX dummy_test_idx SET (option_bool = 3.4); -- error
 ALTER INDEX dummy_test_idx SET (option_bool = 'val4'); -- error
 SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 ALTER INDEX dummy_test_idx RESET (option_bool);
+-- Ternary
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 4); -- error
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 1); -- ok, as true
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 3.4); -- error
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 'val4'); -- error
+ALTER INDEX dummy_test_idx SET (option_ternary1 = 'do_not_know_yet'); -- error. Valid for ternary2 not for ternary1
+ALTER INDEX dummy_test_idx SET (option_ternary2 = 'do_not_know_yet'); -- ok
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
+ALTER INDEX dummy_test_idx RESET (option_ternary1);
+ALTER INDEX dummy_test_idx RESET (option_ternary2);
 -- Float
 ALTER INDEX dummy_test_idx SET (option_real = 4); -- ok
 ALTER INDEX dummy_test_idx SET (option_real = true); -- error
diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index 1c99f79ab01..6e65cd5c3da 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -98,7 +98,7 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
  {fillfactor=13,autovacuum_enabled=false}
 (1 row)
 
--- Tests for future (FIXME) ternary options
+-- Tests for ternary options
 -- behave as boolean option: accept unassigned name and truncated value
 DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate);
@@ -116,7 +116,7 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
  {vacuum_truncate=fals}
 (1 row)
 
--- preferred "true" alias is used when storing
+-- preferred "true" alias is stored in pg_class
 DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=on);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index f5980dafcbc..c99673db9ec 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -59,7 +59,7 @@ UPDATE pg_class
 ALTER TABLE reloptions_test RESET (illegal_option);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 
--- Tests for future (FIXME) ternary options
+-- Tests for ternary options
 
 -- behave as boolean option: accept unassigned name and truncated value
 DROP TABLE reloptions_test;
@@ -70,7 +70,7 @@ DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate=FaLS);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 
--- preferred "true" alias is used when storing
+-- preferred "true" alias is stored in pg_class
 DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=on);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
-- 
2.47.3



  [text/x-patch] v3a-0003-Add-alias-to-be-used-as-unset-state.patch (12.4K, ../../3c4118a2-ec6b-4ea9-89af-233e1a2892ce@nataraj.su/4-v3a-0003-Add-alias-to-be-used-as-unset-state.patch)
  download | inline diff:
From a801a6766ce6b3777c2b1bc3e7194165ff77385c Mon Sep 17 00:00:00 2001
From: Nikolay Shaplov <dhyan@nataraj.su>
Date: Thu, 4 Sep 2025 19:22:21 +0300
Subject: [PATCH v3a 3/4] Add alias to be used as "unset" state.

Add `unset_alias` string parameter to ternary reloption definition. This will allow
user explicitly switch ternary option to "unset" state. Use this feature to
implement `vacuum_index_cleanup` and gist's `buffering` reloptions as ternary
reloptions
---
 src/backend/access/common/reloptions.c | 94 +++++++++++---------------
 src/backend/access/gist/gistbuild.c    |  4 +-
 src/backend/commands/vacuum.c          | 11 ++-
 src/include/access/gist_private.h      | 10 +--
 src/include/access/reloptions.h        | 10 +--
 src/include/utils/rel.h                | 12 +---
 src/test/regress/expected/gist.out     |  3 +-
 7 files changed, 57 insertions(+), 87 deletions(-)

diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 6f1f577581a..57c0a5e7ee0 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -170,6 +170,27 @@ static relopt_ternary ternaryRelOpts[] =
 			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
 			ShareUpdateExclusiveLock
 		},
+		NULL,
+		TERNARY_UNSET
+	},
+	{
+		{
+			"vacuum_index_cleanup",
+			"Controls index vacuuming and index cleanup",
+			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+			ShareUpdateExclusiveLock
+		},
+		"auto",
+		TERNARY_UNSET
+	},
+	{
+		{
+			"buffering",
+			"Enables buffering build for this GiST index",
+			RELOPT_KIND_GIST,
+			AccessExclusiveLock
+		},
+		"auto",
 		TERNARY_UNSET
 	},
 	/* list terminator */
@@ -502,30 +523,6 @@ static relopt_real realRelOpts[] =
 	{{NULL}}
 };
 
-/* values from StdRdOptIndexCleanup */
-static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
-{
-	{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
-	{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
-	{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
-	{"true", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
-	{"false", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
-	{"yes", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
-	{"no", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
-	{"1", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
-	{"0", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
-	{(const char *) NULL}		/* list terminator */
-};
-
-/* values from GistOptBufferingMode */
-static relopt_enum_elt_def gistBufferingOptValues[] =
-{
-	{"auto", GIST_OPTION_BUFFERING_AUTO},
-	{"on", GIST_OPTION_BUFFERING_ON},
-	{"off", GIST_OPTION_BUFFERING_OFF},
-	{(const char *) NULL}		/* list terminator */
-};
-
 /* values from ViewOptCheckOption */
 static relopt_enum_elt_def viewCheckOptValues[] =
 {
@@ -537,28 +534,6 @@ static relopt_enum_elt_def viewCheckOptValues[] =
 
 static relopt_enum enumRelOpts[] =
 {
-	{
-		{
-			"vacuum_index_cleanup",
-			"Controls index vacuuming and index cleanup",
-			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
-			ShareUpdateExclusiveLock
-		},
-		StdRdOptIndexCleanupValues,
-		STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
-		gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
-	},
-	{
-		{
-			"buffering",
-			"Enables buffering build for this GiST index",
-			RELOPT_KIND_GIST,
-			AccessExclusiveLock
-		},
-		gistBufferingOptValues,
-		GIST_OPTION_BUFFERING_AUTO,
-		gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
-	},
 	{
 		{
 			"check_option",
@@ -926,13 +901,14 @@ add_local_bool_reloption(local_relopts *relopts, const char *name,
  */
 static relopt_ternary *
 init_ternary_reloption(bits32 kinds, const char *name, const char *desc,
-					   pg_ternary default_val, LOCKMODE lockmode)
+			 pg_ternary default_val, const char* unset_alias, LOCKMODE lockmode)
 {
 	relopt_ternary *newoption;
 
 	newoption = (relopt_ternary *)
 		allocate_reloption(kinds, RELOPT_TYPE_TERNARY, name, desc, lockmode);
 	newoption->default_val = default_val;
+	newoption->unset_alias = unset_alias;
 
 	return newoption;
 }
@@ -943,12 +919,12 @@ init_ternary_reloption(bits32 kinds, const char *name, const char *desc,
  */
 void
 add_ternary_reloption(bits32 kinds, const char *name, const char *desc,
-					  pg_ternary default_val, LOCKMODE lockmode)
+		  pg_ternary default_val, const char* unset_alias,  LOCKMODE lockmode)
 {
 	relopt_ternary *newoption;
 
-	newoption =
-		init_ternary_reloption(kinds, name, desc, default_val, lockmode);
+	newoption = init_ternary_reloption(kinds, name, desc, default_val,
+													unset_alias, lockmode);
 
 	add_reloption((relopt_gen *) newoption);
 }
@@ -962,12 +938,12 @@ add_ternary_reloption(bits32 kinds, const char *name, const char *desc,
 void
 add_local_ternary_reloption(local_relopts *relopts, const char *name,
 							const char *desc, pg_ternary default_val,
-							int offset)
+							const char* unset_alias, int offset)
 {
 	relopt_ternary *newoption;
 
-	newoption =
-		init_ternary_reloption(RELOPT_KIND_LOCAL, name, desc, default_val, 0);
+	newoption = init_ternary_reloption(RELOPT_KIND_LOCAL, name, desc,
+												default_val, unset_alias, 0);
 
 	add_local_reloption(relopts, (relopt_gen *) newoption, offset);
 }
@@ -1708,9 +1684,19 @@ parse_one_reloption(relopt_value *option, char *text_str, int text_len,
 		case RELOPT_TYPE_TERNARY:
 			{
 				bool		b;
+				relopt_ternary *opt = (relopt_ternary *) option->gen;
 
 				parsed = parse_bool(value, &b);
 				option->values.ternary_val = b ? TERNARY_TRUE : TERNARY_FALSE;
+
+				if (!parsed && opt->unset_alias)
+				{
+					if (pg_strcasecmp(value, opt->unset_alias) == 0)
+					{
+						option->values.ternary_val = TERNARY_UNSET;
+						parsed = true;
+					}
+				}
 				if (validate && !parsed)
 					ereport(ERROR,
 							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -2007,7 +1993,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		offsetof(StdRdOptions, user_catalog_table)},
 		{"parallel_workers", RELOPT_TYPE_INT,
 		offsetof(StdRdOptions, parallel_workers)},
-		{"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+		{"vacuum_index_cleanup", RELOPT_TYPE_TERNARY,
 		offsetof(StdRdOptions, vacuum_index_cleanup)},
 		{"vacuum_truncate", RELOPT_TYPE_TERNARY,
 		offsetof(StdRdOptions, vacuum_truncate)},
diff --git a/src/backend/access/gist/gistbuild.c b/src/backend/access/gist/gistbuild.c
index 7f57c787f4c..77c26d99919 100644
--- a/src/backend/access/gist/gistbuild.c
+++ b/src/backend/access/gist/gistbuild.c
@@ -213,9 +213,9 @@ gistbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	 */
 	if (options)
 	{
-		if (options->buffering_mode == GIST_OPTION_BUFFERING_ON)
+		if (options->buffering_mode == TERNARY_TRUE)
 			buildstate.buildMode = GIST_BUFFERING_STATS;
-		else if (options->buffering_mode == GIST_OPTION_BUFFERING_OFF)
+		else if (options->buffering_mode == TERNARY_FALSE)
 			buildstate.buildMode = GIST_BUFFERING_DISABLED;
 		else					/* must be "auto" */
 			buildstate.buildMode = GIST_BUFFERING_AUTO;
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 696eab9bd97..dbb5c067d27 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2178,22 +2178,21 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 	 */
 	if (params.index_cleanup == VACOPTVALUE_UNSPECIFIED)
 	{
-		StdRdOptIndexCleanup vacuum_index_cleanup;
+		pg_ternary vacuum_index_cleanup;
 
 		if (rel->rd_options == NULL)
-			vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+			vacuum_index_cleanup = TERNARY_UNSET;
 		else
 			vacuum_index_cleanup =
 				((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
 
-		if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
+		if (vacuum_index_cleanup == TERNARY_UNSET)
 			params.index_cleanup = VACOPTVALUE_AUTO;
-		else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
+		else if (vacuum_index_cleanup == TERNARY_TRUE)
 			params.index_cleanup = VACOPTVALUE_ENABLED;
 		else
 		{
-			Assert(vacuum_index_cleanup ==
-				   STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
+			Assert(vacuum_index_cleanup == TERNARY_FALSE);
 			params.index_cleanup = VACOPTVALUE_DISABLED;
 		}
 	}
diff --git a/src/include/access/gist_private.h b/src/include/access/gist_private.h
index 552f605c0aa..6db5f0a1c9c 100644
--- a/src/include/access/gist_private.h
+++ b/src/include/access/gist_private.h
@@ -380,14 +380,6 @@ typedef struct GISTBuildBuffers
 	int			rootlevel;
 } GISTBuildBuffers;
 
-/* GiSTOptions->buffering_mode values */
-typedef enum GistOptBufferingMode
-{
-	GIST_OPTION_BUFFERING_AUTO,
-	GIST_OPTION_BUFFERING_ON,
-	GIST_OPTION_BUFFERING_OFF,
-} GistOptBufferingMode;
-
 /*
  * Storage type for GiST's reloptions
  */
@@ -395,7 +387,7 @@ typedef struct GiSTOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
-	GistOptBufferingMode buffering_mode;	/* buffering build mode */
+	pg_ternary	buffering_mode;	/* buffering build mode */
 } GiSTOptions;
 
 /* gist.c */
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index dfbef2babf2..8ea38562986 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -99,6 +99,7 @@ typedef struct relopt_bool
 typedef struct relopt_ternary
 {
 	relopt_gen	gen;
+	const char	*unset_alias; /* word that will be treated as unset value */
 	int			default_val;
 } relopt_ternary;
 
@@ -191,8 +192,8 @@ extern relopt_kind add_reloption_kind(void);
 extern void add_bool_reloption(bits32 kinds, const char *name, const char *desc,
 							   bool default_val, LOCKMODE lockmode);
 extern void add_ternary_reloption(bits32 kinds, const char *name,
-								  const char *desc, pg_ternary default_val,
-								  LOCKMODE lockmode);
+					const char *desc, pg_ternary default_val,
+					const char* unset_alias, LOCKMODE lockmode);
 extern void add_int_reloption(bits32 kinds, const char *name, const char *desc,
 							  int default_val, int min_val, int max_val,
 							  LOCKMODE lockmode);
@@ -213,8 +214,9 @@ extern void add_local_bool_reloption(local_relopts *relopts, const char *name,
 									 const char *desc, bool default_val,
 									 int offset);
 extern void add_local_ternary_reloption(local_relopts *relopts,
-										const char *name, const char *desc,
-										pg_ternary default_val, int offset);
+								const char *name, const char *desc,
+								pg_ternary default_val, const char* unset_alias,
+								int offset);
 extern void add_local_int_reloption(local_relopts *relopts, const char *name,
 									const char *desc, int default_val,
 									int min_val, int max_val, int offset);
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 5feb18a5373..db6b043d2d0 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -330,14 +330,6 @@ typedef struct AutoVacOpts
 	float8		analyze_scale_factor;
 } AutoVacOpts;
 
-/* StdRdOptions->vacuum_index_cleanup values */
-typedef enum StdRdOptIndexCleanup
-{
-	STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
-	STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
-	STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
-} StdRdOptIndexCleanup;
-
 typedef struct StdRdOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
@@ -346,8 +338,8 @@ typedef struct StdRdOptions
 	AutoVacOpts autovacuum;		/* autovacuum-related options */
 	bool		user_catalog_table; /* use as an additional catalog relation */
 	int			parallel_workers;	/* max number of parallel workers */
-	StdRdOptIndexCleanup vacuum_index_cleanup;	/* controls index vacuuming */
-	pg_ternary		vacuum_truncate;	/* enables vacuum to truncate a relation */
+	pg_ternary	vacuum_index_cleanup; /* controls index vacuuming */
+	pg_ternary	vacuum_truncate;	/* enables vacuum to truncate a relation */
 
 	/*
 	 * Fraction of pages in a relation that vacuum can eagerly scan and fail
diff --git a/src/test/regress/expected/gist.out b/src/test/regress/expected/gist.out
index c75bbb23b6e..76751d1859a 100644
--- a/src/test/regress/expected/gist.out
+++ b/src/test/regress/expected/gist.out
@@ -12,8 +12,7 @@ create index gist_pointidx4 on gist_point_tbl using gist(p) with (buffering = au
 drop index gist_pointidx2, gist_pointidx3, gist_pointidx4;
 -- Make sure bad values are refused
 create index gist_pointidx5 on gist_point_tbl using gist(p) with (buffering = invalid_value);
-ERROR:  invalid value for enum option "buffering": invalid_value
-DETAIL:  Valid values are "on", "off", and "auto".
+ERROR:  invalid value for ternary option "buffering": invalid_value
 create index gist_pointidx5 on gist_point_tbl using gist(p) with (fillfactor=9);
 ERROR:  value 9 out of bounds for option "fillfactor"
 DETAIL:  Valid values are between "10" and "100".
-- 
2.47.3



  [text/x-patch] v3a-0002-Introduce-ternary-reloptions.patch (12.2K, ../../3c4118a2-ec6b-4ea9-89af-233e1a2892ce@nataraj.su/5-v3a-0002-Introduce-ternary-reloptions.patch)
  download | inline diff:
From 7f420d57c962e8b5bc4d2d0751cf249ae9297983 Mon Sep 17 00:00:00 2001
From: Nikolay Shaplov <dhyan@nataraj.su>
Date: Tue, 20 Jan 2026 18:22:39 +0300
Subject: [PATCH v3a 2/4] Introduce ternary reloptions

Introduce ternary reloption as a replacement for current `vacuum_truncate`
implementation. Remove `vacuum_truncate_set` additional flag and using
`TERNARY_UNSET` value instead.
---
 src/backend/access/common/reloptions.c | 137 ++++++++++++++++++++-----
 src/backend/commands/vacuum.c          |   4 +-
 src/include/access/reloptions.h        |  27 ++---
 src/include/postgres.h                 |  15 +++
 src/include/utils/rel.h                |   3 +-
 src/tools/pgindent/typedefs.list       |   2 +
 6 files changed, 145 insertions(+), 43 deletions(-)

diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 0b83f98ed5f..6f1f577581a 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -40,9 +40,9 @@
  *
  * To add an option:
  *
- * (i) decide on a type (bool, integer, real, enum, string), name, default
- * value, upper and lower bounds (if applicable); for strings, consider a
- * validation routine.
+ * (i) decide on a type (bool, ternary, integer, real, enum, string), name,
+ * default value, upper and lower bounds (if applicable); for strings,
+ * consider a validation routine.
  * (ii) add a record below (or use add_<type>_reloption).
  * (iii) add it to the appropriate options struct (perhaps StdRdOptions)
  * (iv) add it to the appropriate handling routine (perhaps
@@ -147,15 +147,6 @@ static relopt_bool boolRelOpts[] =
 		},
 		false
 	},
-	{
-		{
-			"vacuum_truncate",
-			"Enables vacuum to truncate empty pages at the end of this table",
-			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
-			ShareUpdateExclusiveLock
-		},
-		true
-	},
 	{
 		{
 			"deduplicate_items",
@@ -170,6 +161,25 @@ static relopt_bool boolRelOpts[] =
 	{{NULL}}
 };
 
+static relopt_ternary ternaryRelOpts[] =
+{
+	{
+		{
+			"vacuum_truncate",
+			"Enables vacuum to truncate empty pages at the end of this table",
+			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+			ShareUpdateExclusiveLock
+		},
+		TERNARY_UNSET
+	},
+	/* list terminator */
+	{
+		{
+			NULL
+		}
+	}
+};
+
 static relopt_int intRelOpts[] =
 {
 	{
@@ -609,6 +619,13 @@ initialize_reloptions(void)
 								   boolRelOpts[i].gen.lockmode));
 		j++;
 	}
+	for (i = 0; ternaryRelOpts[i].gen.name; i++)
+	{
+		Assert(DoLockModesConflict(ternaryRelOpts[i].gen.lockmode,
+								   ternaryRelOpts[i].gen.lockmode));
+		j++;
+	}
+
 	for (i = 0; intRelOpts[i].gen.name; i++)
 	{
 		Assert(DoLockModesConflict(intRelOpts[i].gen.lockmode,
@@ -649,6 +666,14 @@ initialize_reloptions(void)
 		j++;
 	}
 
+	for (i = 0; ternaryRelOpts[i].gen.name; i++)
+	{
+		relOpts[j] = &ternaryRelOpts[i].gen;
+		relOpts[j]->type = RELOPT_TYPE_TERNARY;
+		relOpts[j]->namelen = strlen(relOpts[j]->name);
+		j++;
+	}
+
 	for (i = 0; intRelOpts[i].gen.name; i++)
 	{
 		relOpts[j] = &intRelOpts[i].gen;
@@ -809,6 +834,9 @@ allocate_reloption(bits32 kinds, int type, const char *name, const char *desc,
 		case RELOPT_TYPE_BOOL:
 			size = sizeof(relopt_bool);
 			break;
+		case RELOPT_TYPE_TERNARY:
+			size = sizeof(relopt_ternary);
+			break;
 		case RELOPT_TYPE_INT:
 			size = sizeof(relopt_int);
 			break;
@@ -892,6 +920,57 @@ add_local_bool_reloption(local_relopts *relopts, const char *name,
 	add_local_reloption(relopts, (relopt_gen *) newoption, offset);
 }
 
+/*
+ * init_ternary_reloption
+ *		Allocate and initialize a new ternary reloption
+ */
+static relopt_ternary *
+init_ternary_reloption(bits32 kinds, const char *name, const char *desc,
+					   pg_ternary default_val, LOCKMODE lockmode)
+{
+	relopt_ternary *newoption;
+
+	newoption = (relopt_ternary *)
+		allocate_reloption(kinds, RELOPT_TYPE_TERNARY, name, desc, lockmode);
+	newoption->default_val = default_val;
+
+	return newoption;
+}
+
+/*
+ * add_ternary_reloption
+ *		Add a new ternary reloption
+ */
+void
+add_ternary_reloption(bits32 kinds, const char *name, const char *desc,
+					  pg_ternary default_val, LOCKMODE lockmode)
+{
+	relopt_ternary *newoption;
+
+	newoption =
+		init_ternary_reloption(kinds, name, desc, default_val, lockmode);
+
+	add_reloption((relopt_gen *) newoption);
+}
+
+/*
+ * add_local_ternary_reloption
+ *		Add a new ternary local reloption
+ *
+ * 'offset' is offset of ternary-typed field.
+ */
+void
+add_local_ternary_reloption(local_relopts *relopts, const char *name,
+							const char *desc, pg_ternary default_val,
+							int offset)
+{
+	relopt_ternary *newoption;
+
+	newoption =
+		init_ternary_reloption(RELOPT_KIND_LOCAL, name, desc, default_val, 0);
+
+	add_local_reloption(relopts, (relopt_gen *) newoption, offset);
+}
 
 /*
  * init_real_reloption
@@ -1626,6 +1705,19 @@ parse_one_reloption(relopt_value *option, char *text_str, int text_len,
 									option->gen->name, value)));
 			}
 			break;
+		case RELOPT_TYPE_TERNARY:
+			{
+				bool		b;
+
+				parsed = parse_bool(value, &b);
+				option->values.ternary_val = b ? TERNARY_TRUE : TERNARY_FALSE;
+				if (validate && !parsed)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("invalid value for ternary option \"%s\": %s",
+									option->gen->name, value)));
+			}
+			break;
 		case RELOPT_TYPE_INT:
 			{
 				relopt_int *optint = (relopt_int *) option->gen;
@@ -1789,17 +1881,6 @@ fillRelOptions(void *rdopts, Size basesize,
 				char	   *itempos = ((char *) rdopts) + elems[j].offset;
 				char	   *string_val;
 
-				/*
-				 * If isset_offset is provided, store whether the reloption is
-				 * set there.
-				 */
-				if (elems[j].isset_offset > 0)
-				{
-					char	   *setpos = ((char *) rdopts) + elems[j].isset_offset;
-
-					*(bool *) setpos = options[i].isset;
-				}
-
 				switch (options[i].gen->type)
 				{
 					case RELOPT_TYPE_BOOL:
@@ -1807,6 +1888,11 @@ fillRelOptions(void *rdopts, Size basesize,
 							options[i].values.bool_val :
 							((relopt_bool *) options[i].gen)->default_val;
 						break;
+					case RELOPT_TYPE_TERNARY:
+						*(pg_ternary *) itempos = options[i].isset ?
+							options[i].values.ternary_val :
+							((relopt_ternary *) options[i].gen)->default_val;
+						break;
 					case RELOPT_TYPE_INT:
 						*(int *) itempos = options[i].isset ?
 							options[i].values.int_val :
@@ -1923,8 +2009,8 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		offsetof(StdRdOptions, parallel_workers)},
 		{"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
 		offsetof(StdRdOptions, vacuum_index_cleanup)},
-		{"vacuum_truncate", RELOPT_TYPE_BOOL,
-		offsetof(StdRdOptions, vacuum_truncate), offsetof(StdRdOptions, vacuum_truncate_set)},
+		{"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)}
 	};
@@ -2004,7 +2090,6 @@ build_local_reloptions(local_relopts *relopts, Datum options, bool validate)
 		elems[i].optname = opt->option->name;
 		elems[i].opttype = opt->option->type;
 		elems[i].offset = opt->offset;
-		elems[i].isset_offset = 0;	/* not supported for local relopts yet */
 
 		i++;
 	}
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index aa4fbec143f..696eab9bd97 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2224,9 +2224,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 	{
 		StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
 
-		if (opts && opts->vacuum_truncate_set)
+		if (opts && opts->vacuum_truncate != TERNARY_UNSET)
 		{
-			if (opts->vacuum_truncate)
+			if (opts->vacuum_truncate == TERNARY_TRUE)
 				params.truncate = VACOPTVALUE_ENABLED;
 			else
 				params.truncate = VACOPTVALUE_DISABLED;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index 2f08e1b0cf0..dfbef2babf2 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -29,6 +29,7 @@
 typedef enum relopt_type
 {
 	RELOPT_TYPE_BOOL,
+	RELOPT_TYPE_TERNARY,		/* on, off, unset */
 	RELOPT_TYPE_INT,
 	RELOPT_TYPE_REAL,
 	RELOPT_TYPE_ENUM,
@@ -80,6 +81,7 @@ typedef struct relopt_value
 	union
 	{
 		bool		bool_val;
+		pg_ternary	ternary_val;
 		int			int_val;
 		double		real_val;
 		int			enum_val;
@@ -94,6 +96,12 @@ typedef struct relopt_bool
 	bool		default_val;
 } relopt_bool;
 
+typedef struct relopt_ternary
+{
+	relopt_gen	gen;
+	int			default_val;
+} relopt_ternary;
+
 typedef struct relopt_int
 {
 	relopt_gen	gen;
@@ -152,19 +160,6 @@ typedef struct
 	const char *optname;		/* option's name */
 	relopt_type opttype;		/* option's datatype */
 	int			offset;			/* offset of field in result struct */
-
-	/*
-	 * isset_offset is an optional offset of a field in the result struct that
-	 * stores whether the option is explicitly set for the relation or if it
-	 * just picked up the default value.  In most cases, this can be
-	 * accomplished by giving the reloption a special out-of-range default
-	 * value (e.g., some integer reloptions use -2), but this isn't always
-	 * possible.  For example, a Boolean reloption cannot be given an
-	 * out-of-range default, so we need another way to discover the source of
-	 * its value.  This offset is only used if given a value greater than
-	 * zero.
-	 */
-	int			isset_offset;
 } relopt_parse_elt;
 
 /* Local reloption definition */
@@ -195,6 +190,9 @@ typedef struct local_relopts
 extern relopt_kind add_reloption_kind(void);
 extern void add_bool_reloption(bits32 kinds, const char *name, const char *desc,
 							   bool default_val, LOCKMODE lockmode);
+extern void add_ternary_reloption(bits32 kinds, const char *name,
+								  const char *desc, pg_ternary default_val,
+								  LOCKMODE lockmode);
 extern void add_int_reloption(bits32 kinds, const char *name, const char *desc,
 							  int default_val, int min_val, int max_val,
 							  LOCKMODE lockmode);
@@ -214,6 +212,9 @@ extern void register_reloptions_validator(local_relopts *relopts,
 extern void add_local_bool_reloption(local_relopts *relopts, const char *name,
 									 const char *desc, bool default_val,
 									 int offset);
+extern void add_local_ternary_reloption(local_relopts *relopts,
+										const char *name, const char *desc,
+										pg_ternary default_val, int offset);
 extern void add_local_int_reloption(local_relopts *relopts, const char *name,
 									const char *desc, int default_val,
 									int min_val, int max_val, int offset);
diff --git a/src/include/postgres.h b/src/include/postgres.h
index 7d93fbce709..47c88780776 100644
--- a/src/include/postgres.h
+++ b/src/include/postgres.h
@@ -543,6 +543,21 @@ Float8GetDatum(float8 X)
  * ----------------------------------------------------------------
  */
 
+/*
+ * pg_ternary
+ *		Boolean value with an extra "unset" value
+ *
+ * This enum can be used for values that want to distinguish between true,
+ * false, and unset.
+*/
+
+typedef enum pg_ternary
+{
+	TERNARY_FALSE = 0,
+	TERNARY_TRUE = 1,
+	TERNARY_UNSET = -1
+} pg_ternary;
+
 /*
  * NON_EXEC_STATIC: It's sometimes useful to define a variable or function
  * that is normally static but extern when using EXEC_BACKEND (see
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index d03ab247788..5feb18a5373 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -347,8 +347,7 @@ typedef struct StdRdOptions
 	bool		user_catalog_table; /* use as an additional catalog relation */
 	int			parallel_workers;	/* max number of parallel workers */
 	StdRdOptIndexCleanup vacuum_index_cleanup;	/* controls index vacuuming */
-	bool		vacuum_truncate;	/* enables vacuum to truncate a relation */
-	bool		vacuum_truncate_set;	/* whether vacuum_truncate is set */
+	pg_ternary		vacuum_truncate;	/* enables vacuum to truncate a relation */
 
 	/*
 	 * Fraction of pages in a relation that vacuum can eagerly scan and fail
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3f3a888fd0e..1c8610fd46c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3952,6 +3952,7 @@ pg_sha512_ctx
 pg_snapshot
 pg_special_case
 pg_stack_base_t
+pg_ternary
 pg_time_t
 pg_time_usec_t
 pg_tz
@@ -4079,6 +4080,7 @@ relopt_kind
 relopt_parse_elt
 relopt_real
 relopt_string
+relopt_ternary
 relopt_type
 relopt_value
 relopts_validator
-- 
2.47.3



  [text/x-patch] v3a-0001-Add-ternary-reloption-type.patch (4.8K, ../../3c4118a2-ec6b-4ea9-89af-233e1a2892ce@nataraj.su/6-v3a-0001-Add-ternary-reloption-type.patch)
  download | inline diff:
From f5584b4e09ec65fe267ec6b27ff97b387707d5cd Mon Sep 17 00:00:00 2001
From: Nikolay Shaplov <dhyan@nataraj.su>
Date: Tue, 20 Jan 2026 18:02:40 +0300
Subject: [PATCH v3a 1/4] Add ternary reloption type

There is a tendency for boolean reloptions in PostgreSQL code, one with "on"
and "off" values, to be replaced with options with "on", "off",
"[use_global_settings]" behaviour.

For `vacuum_index_cleanup" and gist's `buffering` reloption such behavior have
been implemented as enum-tyoe option.

For vacuum_truncate this behaviour have been umplemented by adding additional
`is_set` flag to `bytea` representation of the reloptions.

Both solutions looks like workaround hacks to implement option with three
available states.

This patch introduce "ternary" reloption type, that behave like bool option,
but also has an additional "unset" state. This state may be reachable only by
not setting or RESETting an option, like in `vacuum_truncate` option, or it
may have some text alias that allow user to explicitly set it, like "auto" in
`vacuum_index_cleanup` or gist's `buffering`

`vacuum_truncate`, `vacuum_index_cleanup` and gist's `buffering` reloptions are
reimplemented as ternary  reloptions without significant behaviour changes.

---

This patch is split into four parts, to help reviewer grasp the login behind
it. I guess it is better to commit it as a single commit

---

Part1: Add regression tests that would be tests for ternary reloptions in
future, but now it checks the behaviour of reloptions that would become
ternary. There behaviour should not change, so adding tests before changing
anything. These tests should pass before applying the patch, and after it.
---
 src/test/regress/expected/reloptions.out | 36 ++++++++++++++++++++++++
 src/test/regress/sql/reloptions.sql      | 21 ++++++++++++++
 2 files changed, 57 insertions(+)

diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index 9de19b4e3f1..1c99f79ab01 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -98,6 +98,42 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
  {fillfactor=13,autovacuum_enabled=false}
 (1 row)
 
+-- Tests for future (FIXME) ternary options
+-- behave as boolean option: accept unassigned name and truncated value
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+       reloptions       
+------------------------
+ {vacuum_truncate=true}
+(1 row)
+
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate=FaLS);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+       reloptions       
+------------------------
+ {vacuum_truncate=fals}
+(1 row)
+
+-- preferred "true" alias is used when storing
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=on);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+        reloptions         
+---------------------------
+ {vacuum_index_cleanup=on}
+(1 row)
+
+-- custom "third" value is available
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=auto);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+         reloptions          
+-----------------------------
+ {vacuum_index_cleanup=auto}
+(1 row)
+
 -- Test vacuum_truncate option
 DROP TABLE reloptions_test;
 CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index 24fbe0b478d..f5980dafcbc 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -59,6 +59,27 @@ UPDATE pg_class
 ALTER TABLE reloptions_test RESET (illegal_option);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 
+-- Tests for future (FIXME) ternary options
+
+-- behave as boolean option: accept unassigned name and truncated value
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate=FaLS);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
+-- preferred "true" alias is used when storing
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=on);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
+-- custom "third" value is available
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=auto);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
 -- Test vacuum_truncate option
 DROP TABLE reloptions_test;
 
-- 
2.47.3



^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: [PATCH] ternary reloption type
  2025-09-04 17:07 Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2025-09-12 13:46 ` Re: [PATCH] ternary reloption type Timur Magomedov <t.magomedov@postgrespro.ru>
  2025-09-14 08:49   ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-16 15:14     ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
  2026-01-21 18:44       ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
@ 2026-01-21 19:23         ` Álvaro Herrera <alvherre@alvh.no-ip.org>
  2026-02-05 13:08           ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-08-17 13:03           ` Re: [PATCH] ternary reloption type Peter Eisentraut <peter@eisentraut.org>
  0 siblings, 2 replies; 13+ messages in thread

From: Álvaro Herrera @ 2026-01-21 19:23 UTC (permalink / raw)
  To: Nikolay Shaplov <dhyan@nataraj.su>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Chris Travers <chris.travers@gmail.com>; Timur Magomedov <t.magomedov@postgrespro.ru>; Nathan Bossart <nathandbossart@gmail.com>

On 2026-Jan-21, Nikolay Shaplov wrote:

> On 16.01.2026 18:14, Álvaro Herrera wrote:
> > I took a quick look at 0001+0002 and I think it's quite reasonable.
> > Here it is again with some minor fixups.
> 
> Good. I like ternary -> pg_ternary change. That is reasonable. And
> postgres.h is better place for it then c.h.

I further changed TERNARY_TRUE and so on to have a PG_ prefix also; it's
not impossible that there's userland code somewhere outside Postgres
that uses those symbol names, so let's avoid a collision.

> I've rebased the rest of patches, and the whole patchset is in the
> attachment.

I'm not a fan of how these commit messages are structured.  You explain
the point of the whole patch series in the commit message for 0001, but
that one only adds some tests for the existing behavior.  If I were to
commit that, it would make no sense in the overall Postgres commit
history.  So I have squashed 0001 with 0002, and also with the fraction
of 0004 that includes tests for the feature in 0002.  I think it's
strange to submit those tests in 0004, when the other half of the tests
are for the feature in 0003.  I recommend to consider how you structure
your patch splits so that they would make sense in the Postgres commit
history assuming they are committed on separate days, and that there are
multiple other commits in between.

I do agree with Nathan that there seems to be little point in the
message saying this new type is different from Boolean.  The user can
still say only "on" or "off".  Even with your "alias" proposal, there
will only be a mechanism to let the system choose between those two
values, but it will still be one or the other.  There's no provision to
have the system behave as if the user set the value to half.

Another thing I did is remove default_val for ternaries.  As far as I
can see, it makes no sense.  If your reloption defaults to either on or
off, then it's just a Boolean, right?  It can no longer be unset,
because if you unset it, then it becomes the default.

Anyway, I have pushed the first part, after rewriting the commit
message.  You can resubmit the rest after rebasing on the current tree.
I have also marked the commitfest item as committed.  Please create a
new one for the next part.

Thanks,

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/
"El sentido de las cosas no viene de las cosas, sino de
las inteligencias que las aplican a sus problemas diarios
en busca del progreso." (Ernesto Hernández-Novich)





^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: [PATCH] ternary reloption type
  2025-09-04 17:07 Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2025-09-12 13:46 ` Re: [PATCH] ternary reloption type Timur Magomedov <t.magomedov@postgrespro.ru>
  2025-09-14 08:49   ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-16 15:14     ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
  2026-01-21 18:44       ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-21 19:23         ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
@ 2026-02-05 13:08           ` Nikolay Shaplov <dhyan@nataraj.su>
  2026-05-10 11:09             ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  1 sibling, 1 reply; 13+ messages in thread

From: Nikolay Shaplov @ 2026-02-05 13:08 UTC (permalink / raw)
  To: Álvaro Herrera <alvherre@alvh.no-ip.org>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Chris Travers <chris.travers@gmail.com>; Timur Magomedov <t.magomedov@postgrespro.ru>; Nathan Bossart <nathandbossart@gmail.com>


On 21.01.2026 22:23, Álvaro Herrera wrote:
>> I've rebased the rest of patches, and the whole patchset is in the
>> attachment.
> I'm not a fan of how these commit messages are structured.  You explain
> the point of the whole patch series in the commit message for 0001, but
> that one only adds some tests for the existing behavior.  If I were to
> commit that, it would make no sense in the overall Postgres commit
> history.  So I have squashed 0001 with 0002, and also with the fraction
> of 0004 that includes tests for the feature in 0002.

I guess that comes from my frustration with big reloptions patch. I've 
been  looking
for a way to make iy more easy to read and understand. My thought was 
that if
I split them into logical "layers" showing the  development of the 
patch's idea, it
would be more easy to understand them as a whole. And they were intended
to be squashed before commit, may be I should state that more clearly.

> I think it's
> strange to submit those tests in 0004, when the other half of the tests
> are for the feature in 0003.

Yeah, here you are right, I should add tests both to 0003 and 0004. I 
did not think
they can be committed separately

> I recommend to consider how you structure
> your patch splits so that they would make sense in the Postgres commit
> history assuming they are committed on separate days, and that there are
> multiple other commits in between.

Second part of ternary options patch is consist of one piece, so now it 
is not a problem,
and as for big reloptions patch, I think we can discuss it later. I 
doubt it is readable
when it is provided in single piece.

> I do agree with Nathan that there seems to be little point in the
> message saying this new type is different from Boolean.  The user can
> still say only "on" or "off".  Even with your "alias" proposal, there
> will only be a mechanism to let the system choose between those two
> values, but it will still be one or the other.  There's no provision to
> have the system behave as if the user set the value to half.

For ternary options with explicit "third" value, I added another error 
message, it says nothing
about option type, it just lists possible values. This can be good solution.

> Another thing I did is remove default_val for ternaries.  As far as I
> can see, it makes no sense.  If your reloption defaults to either on or
> off, then it's just a Boolean, right?  It can no longer be unset,
> because if you unset it, then it becomes the default.
For the part you've committed that is correct. But with explicit "third" 
option, you can't tell
for sure which value is default. Like:
prefer_XXXX_optimization: yes/no/never
It can be implemented as ternary option, but default value here can be 
"yes".
I would not try to predict what behavior option developer will need, and 
try to provide all
possibilities.
That's why I put default value back. But I will not be much upset if you 
still decide to remove it.
Or I can remove it myself if you insist. We can add it later when 
someone runs into this "never" case.
> Anyway, I have pushed the first part, after rewriting the commit
> message.

Thanks! Now the world is better place, from my point of view.

> You can resubmit the rest after rebasing on the current tree.
> I have also marked the commitfest item as committed.  Please create a
> new one for the next part.
I've reworked second part of the patch, It is in the attachment, and I 
am going to create new commitfest record for it. Thank you for your work.

Attachments:

  [text/x-patch] v1a-0001-Convert-vacuum_index_cleanup-and-gist-s-bufferin.patch (23.0K, ../../20b95759-b5d6-4d46-9ab5-3ca54a9b58be@nataraj.su/3-v1a-0001-Convert-vacuum_index_cleanup-and-gist-s-bufferin.patch)
  download | inline diff:
From 3403340bc1b6c52c012fbbe392dcd7ab53a39780 Mon Sep 17 00:00:00 2001
From: Nikolay Shaplov <dhyan@nataraj.su>
Date: Thu, 4 Sep 2025 19:22:21 +0300
Subject: [PATCH v1a 1/1] Convert `vacuum_index_cleanup` and gist's `buffering`
 reloptions from enum to ternary type

Enum reloptions `vacuum_index_cleanup` and gist's `buffering` behaves almost
like ternary options, except that their third "unset" value can be explicitly
set by specifying keyword "auto".

Here we add `unset_alias` into ternary option definition, that will permit
explicitly setting third "unest" value, and convert `vacuum_index_cleanup`
and `buffering` reloptions to ternary type.
---
 src/backend/access/common/reloptions.c        | 111 +++++++++---------
 src/backend/access/gist/gistbuild.c           |   4 +-
 src/backend/commands/vacuum.c                 |  11 +-
 src/include/access/gist_private.h             |  10 +-
 src/include/access/reloptions.h               |  11 +-
 src/include/utils/rel.h                       |  10 +-
 src/test/modules/dummy_index_am/README        |   3 +-
 .../modules/dummy_index_am/dummy_index_am.c   |  11 +-
 .../dummy_index_am/expected/reloptions.out    |  27 +++--
 .../modules/dummy_index_am/sql/reloptions.sql |   7 ++
 src/test/regress/expected/gist.out            |   2 +-
 src/test/regress/expected/reloptions.out      |  18 +++
 src/test/regress/sql/reloptions.sql           |  10 ++
 13 files changed, 139 insertions(+), 96 deletions(-)

diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 237ab8d0ed9..2ca45c98662 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -173,7 +173,29 @@ static relopt_ternary ternaryRelOpts[] =
 			"Enables vacuum to truncate empty pages at the end of this table",
 			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
 			ShareUpdateExclusiveLock
-		}
+		},
+		NULL,
+		PG_TERNARY_UNSET
+	},
+	{
+		{
+			"vacuum_index_cleanup",
+			"Controls index vacuuming and index cleanup",
+			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+			ShareUpdateExclusiveLock
+		},
+		"auto",
+		PG_TERNARY_UNSET
+	},
+	{
+		{
+			"buffering",
+			"Enables buffering build for this GiST index",
+			RELOPT_KIND_GIST,
+			AccessExclusiveLock
+		},
+		"auto",
+		PG_TERNARY_UNSET
 	},
 	/* list terminator */
 	{
@@ -505,30 +527,6 @@ static relopt_real realRelOpts[] =
 	{{NULL}}
 };
 
-/* values from StdRdOptIndexCleanup */
-static relopt_enum_elt_def StdRdOptIndexCleanupValues[] =
-{
-	{"auto", STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO},
-	{"on", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
-	{"off", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
-	{"true", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
-	{"false", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
-	{"yes", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
-	{"no", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
-	{"1", STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON},
-	{"0", STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF},
-	{(const char *) NULL}		/* list terminator */
-};
-
-/* values from GistOptBufferingMode */
-static relopt_enum_elt_def gistBufferingOptValues[] =
-{
-	{"auto", GIST_OPTION_BUFFERING_AUTO},
-	{"on", GIST_OPTION_BUFFERING_ON},
-	{"off", GIST_OPTION_BUFFERING_OFF},
-	{(const char *) NULL}		/* list terminator */
-};
-
 /* values from ViewOptCheckOption */
 static relopt_enum_elt_def viewCheckOptValues[] =
 {
@@ -540,28 +538,6 @@ static relopt_enum_elt_def viewCheckOptValues[] =
 
 static relopt_enum enumRelOpts[] =
 {
-	{
-		{
-			"vacuum_index_cleanup",
-			"Controls index vacuuming and index cleanup",
-			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
-			ShareUpdateExclusiveLock
-		},
-		StdRdOptIndexCleanupValues,
-		STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO,
-		gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
-	},
-	{
-		{
-			"buffering",
-			"Enables buffering build for this GiST index",
-			RELOPT_KIND_GIST,
-			AccessExclusiveLock
-		},
-		gistBufferingOptValues,
-		GIST_OPTION_BUFFERING_AUTO,
-		gettext_noop("Valid values are \"on\", \"off\", and \"auto\".")
-	},
 	{
 		{
 			"check_option",
@@ -929,12 +905,14 @@ add_local_bool_reloption(local_relopts *relopts, const char *name,
  */
 static relopt_ternary *
 init_ternary_reloption(bits32 kinds, const char *name, const char *desc,
-					   LOCKMODE lockmode)
+			 pg_ternary default_val, const char* unset_alias, LOCKMODE lockmode)
 {
 	relopt_ternary *newoption;
 
 	newoption = (relopt_ternary *)
 		allocate_reloption(kinds, RELOPT_TYPE_TERNARY, name, desc, lockmode);
+	newoption->default_val = default_val;
+	newoption->unset_alias = unset_alias;
 
 	return newoption;
 }
@@ -945,12 +923,12 @@ init_ternary_reloption(bits32 kinds, const char *name, const char *desc,
  */
 void
 add_ternary_reloption(bits32 kinds, const char *name, const char *desc,
-					  LOCKMODE lockmode)
+		  pg_ternary default_val, const char* unset_alias,  LOCKMODE lockmode)
 {
 	relopt_ternary *newoption;
 
-	newoption =
-		init_ternary_reloption(kinds, name, desc, lockmode);
+	newoption = init_ternary_reloption(kinds, name, desc, default_val,
+													unset_alias, lockmode);
 
 	add_reloption((relopt_gen *) newoption);
 }
@@ -963,12 +941,13 @@ add_ternary_reloption(bits32 kinds, const char *name, const char *desc,
  */
 void
 add_local_ternary_reloption(local_relopts *relopts, const char *name,
-							const char *desc, int offset)
+							const char *desc, pg_ternary default_val,
+							const char* unset_alias, int offset)
 {
 	relopt_ternary *newoption;
 
-	newoption =
-		init_ternary_reloption(RELOPT_KIND_LOCAL, name, desc, 0);
+	newoption = init_ternary_reloption(RELOPT_KIND_LOCAL, name, desc,
+												default_val, unset_alias, 0);
 
 	add_local_reloption(relopts, (relopt_gen *) newoption, offset);
 }
@@ -1709,15 +1688,35 @@ parse_one_reloption(relopt_value *option, char *text_str, int text_len,
 		case RELOPT_TYPE_TERNARY:
 			{
 				bool		b;
+				relopt_ternary *opt = (relopt_ternary *) option->gen;
 
 				parsed = parse_bool(value, &b);
 				option->ternary_val = b ? PG_TERNARY_TRUE :
 					PG_TERNARY_FALSE;
-				if (validate && !parsed)
+
+				/* If no "unset alias" set, this option behaves almost like
+				 * boolean, so report error accordingly */
+				if (!opt->unset_alias && validate && !parsed)
 					ereport(ERROR,
 							errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 							errmsg("invalid value for boolean option \"%s\": %s",
 								   option->gen->name, value));
+
+				if (!parsed && opt->unset_alias)
+				{
+					if (pg_strcasecmp(value, opt->unset_alias) == 0)
+					{
+						option->ternary_val = PG_TERNARY_UNSET;
+						parsed = true;
+					}
+				}
+				if (validate && !parsed)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							errmsg("invalid value for option \"%s\": %s",
+									option->gen->name, value),
+							errdetail("Valid values are \"on\", \"off\", and \"%s\".",
+										opt->unset_alias)));
 			}
 			break;
 		case RELOPT_TYPE_INT:
@@ -2008,7 +2007,7 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		offsetof(StdRdOptions, user_catalog_table)},
 		{"parallel_workers", RELOPT_TYPE_INT,
 		offsetof(StdRdOptions, parallel_workers)},
-		{"vacuum_index_cleanup", RELOPT_TYPE_ENUM,
+		{"vacuum_index_cleanup", RELOPT_TYPE_TERNARY,
 		offsetof(StdRdOptions, vacuum_index_cleanup)},
 		{"vacuum_truncate", RELOPT_TYPE_TERNARY,
 		offsetof(StdRdOptions, vacuum_truncate)},
diff --git a/src/backend/access/gist/gistbuild.c b/src/backend/access/gist/gistbuild.c
index 7f57c787f4c..4031a07de0e 100644
--- a/src/backend/access/gist/gistbuild.c
+++ b/src/backend/access/gist/gistbuild.c
@@ -213,9 +213,9 @@ gistbuild(Relation heap, Relation index, IndexInfo *indexInfo)
 	 */
 	if (options)
 	{
-		if (options->buffering_mode == GIST_OPTION_BUFFERING_ON)
+		if (options->buffering_mode == PG_TERNARY_TRUE)
 			buildstate.buildMode = GIST_BUFFERING_STATS;
-		else if (options->buffering_mode == GIST_OPTION_BUFFERING_OFF)
+		else if (options->buffering_mode == PG_TERNARY_FALSE)
 			buildstate.buildMode = GIST_BUFFERING_DISABLED;
 		else					/* must be "auto" */
 			buildstate.buildMode = GIST_BUFFERING_AUTO;
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 03932f45c8a..895a7d2e3a1 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -2178,22 +2178,21 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 	 */
 	if (params.index_cleanup == VACOPTVALUE_UNSPECIFIED)
 	{
-		StdRdOptIndexCleanup vacuum_index_cleanup;
+		pg_ternary vacuum_index_cleanup;
 
 		if (rel->rd_options == NULL)
-			vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
+			vacuum_index_cleanup = PG_TERNARY_UNSET;
 		else
 			vacuum_index_cleanup =
 				((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
 
-		if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
+		if (vacuum_index_cleanup == PG_TERNARY_UNSET)
 			params.index_cleanup = VACOPTVALUE_AUTO;
-		else if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON)
+		else if (vacuum_index_cleanup == PG_TERNARY_TRUE)
 			params.index_cleanup = VACOPTVALUE_ENABLED;
 		else
 		{
-			Assert(vacuum_index_cleanup ==
-				   STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF);
+			Assert(vacuum_index_cleanup == PG_TERNARY_FALSE);
 			params.index_cleanup = VACOPTVALUE_DISABLED;
 		}
 	}
diff --git a/src/include/access/gist_private.h b/src/include/access/gist_private.h
index 552f605c0aa..6db5f0a1c9c 100644
--- a/src/include/access/gist_private.h
+++ b/src/include/access/gist_private.h
@@ -380,14 +380,6 @@ typedef struct GISTBuildBuffers
 	int			rootlevel;
 } GISTBuildBuffers;
 
-/* GiSTOptions->buffering_mode values */
-typedef enum GistOptBufferingMode
-{
-	GIST_OPTION_BUFFERING_AUTO,
-	GIST_OPTION_BUFFERING_ON,
-	GIST_OPTION_BUFFERING_OFF,
-} GistOptBufferingMode;
-
 /*
  * Storage type for GiST's reloptions
  */
@@ -395,7 +387,7 @@ typedef struct GiSTOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
 	int			fillfactor;		/* page fill factor in percent (0..100) */
-	GistOptBufferingMode buffering_mode;	/* buffering build mode */
+	pg_ternary	buffering_mode;	/* buffering build mode */
 } GiSTOptions;
 
 /* gist.c */
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index 0bd17b30ca7..40ce291c894 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -99,7 +99,8 @@ typedef struct relopt_bool
 typedef struct relopt_ternary
 {
 	relopt_gen	gen;
-	/* ternaries have no default_val: otherwise they'd just be bools */
+	const char	*unset_alias; /* word that will be treated as unset value */
+	int			default_val;
 } relopt_ternary;
 
 typedef struct relopt_int
@@ -191,7 +192,8 @@ extern relopt_kind add_reloption_kind(void);
 extern void add_bool_reloption(bits32 kinds, const char *name, const char *desc,
 							   bool default_val, LOCKMODE lockmode);
 extern void add_ternary_reloption(bits32 kinds, const char *name,
-								  const char *desc, LOCKMODE lockmode);
+					const char *desc, pg_ternary default_val,
+					const char* unset_alias, LOCKMODE lockmode);
 extern void add_int_reloption(bits32 kinds, const char *name, const char *desc,
 							  int default_val, int min_val, int max_val,
 							  LOCKMODE lockmode);
@@ -212,8 +214,9 @@ extern void add_local_bool_reloption(local_relopts *relopts, const char *name,
 									 const char *desc, bool default_val,
 									 int offset);
 extern void add_local_ternary_reloption(local_relopts *relopts,
-										const char *name, const char *desc,
-										int offset);
+								const char *name, const char *desc,
+								pg_ternary default_val, const char* unset_alias,
+								int offset);
 extern void add_local_int_reloption(local_relopts *relopts, const char *name,
 									const char *desc, int default_val,
 									int min_val, int max_val, int offset);
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 236830f6b93..db6b043d2d0 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -330,14 +330,6 @@ typedef struct AutoVacOpts
 	float8		analyze_scale_factor;
 } AutoVacOpts;
 
-/* StdRdOptions->vacuum_index_cleanup values */
-typedef enum StdRdOptIndexCleanup
-{
-	STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO = 0,
-	STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF,
-	STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON,
-} StdRdOptIndexCleanup;
-
 typedef struct StdRdOptions
 {
 	int32		vl_len_;		/* varlena header (do not touch directly!) */
@@ -346,7 +338,7 @@ typedef struct StdRdOptions
 	AutoVacOpts autovacuum;		/* autovacuum-related options */
 	bool		user_catalog_table; /* use as an additional catalog relation */
 	int			parallel_workers;	/* max number of parallel workers */
-	StdRdOptIndexCleanup vacuum_index_cleanup;	/* controls index vacuuming */
+	pg_ternary	vacuum_index_cleanup; /* controls index vacuuming */
 	pg_ternary	vacuum_truncate;	/* enables vacuum to truncate a relation */
 
 	/*
diff --git a/src/test/modules/dummy_index_am/README b/src/test/modules/dummy_index_am/README
index 604d823c2e4..d80aff0db19 100644
--- a/src/test/modules/dummy_index_am/README
+++ b/src/test/modules/dummy_index_am/README
@@ -5,7 +5,8 @@ Dummy index AM is a module for testing any facility usable by an index
 access method, whose code is kept a maximum simple.
 
 This includes tests for all relation option types:
-- boolean & ternary
+- boolean
+- ternary
 - enum
 - integer
 - real
diff --git a/src/test/modules/dummy_index_am/dummy_index_am.c b/src/test/modules/dummy_index_am/dummy_index_am.c
index 31f8d2b8161..e8f6dcac9de 100644
--- a/src/test/modules/dummy_index_am/dummy_index_am.c
+++ b/src/test/modules/dummy_index_am/dummy_index_am.c
@@ -41,6 +41,7 @@ typedef struct DummyIndexOptions
 	double		option_real;
 	bool		option_bool;
 	pg_ternary	option_ternary_1;
+	pg_ternary	option_ternary_2;
 	DummyAmEnum option_enum;
 	int			option_string_val_offset;
 	int			option_string_null_offset;
@@ -104,12 +105,20 @@ create_reloptions_table(void)
 
 	add_ternary_reloption(di_relopt_kind, "option_ternary_1",
 						  "One ternary option for dummy_index_am",
-						  AccessExclusiveLock);
+					   PG_TERNARY_UNSET, NULL, AccessExclusiveLock);
 	di_relopt_tab[i].optname = "option_ternary_1";
 	di_relopt_tab[i].opttype = RELOPT_TYPE_TERNARY;
 	di_relopt_tab[i].offset = offsetof(DummyIndexOptions, option_ternary_1);
 	i++;
 
+	add_ternary_reloption(di_relopt_kind, "option_ternary_2",
+					   "Second ternary option for dummy_index_am",
+					   PG_TERNARY_TRUE, "do_not_know_yet", AccessExclusiveLock);
+	di_relopt_tab[i].optname = "option_ternary_2";
+	di_relopt_tab[i].opttype = RELOPT_TYPE_TERNARY;
+	di_relopt_tab[i].offset = offsetof(DummyIndexOptions, option_ternary_2);
+	i++;
+
 	add_enum_reloption(di_relopt_kind, "option_enum",
 					   "Enum option for dummy_index_am",
 					   dummyAmEnumValues,
diff --git a/src/test/modules/dummy_index_am/expected/reloptions.out b/src/test/modules/dummy_index_am/expected/reloptions.out
index 3b06d514995..90abbe2e373 100644
--- a/src/test/modules/dummy_index_am/expected/reloptions.out
+++ b/src/test/modules/dummy_index_am/expected/reloptions.out
@@ -19,6 +19,7 @@ CREATE INDEX dummy_test_idx ON dummy_test_tab
   USING dummy_index_am (i) WITH (
   option_bool = false,
   option_ternary_1,
+  option_ternary_2 = off,
   option_int = 5,
   option_real = 3.1,
   option_enum = 'two',
@@ -33,17 +34,19 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 ------------------------
  option_bool=false
  option_ternary_1=true
+ option_ternary_2=off
  option_int=5
  option_real=3.1
  option_enum=two
  option_string_val=null
  option_string_null=val
-(7 rows)
+(8 rows)
 
 -- ALTER INDEX .. SET
 ALTER INDEX dummy_test_idx SET (option_int = 10);
 ALTER INDEX dummy_test_idx SET (option_bool = true);
 ALTER INDEX dummy_test_idx SET (option_ternary_1 = false);
+ALTER INDEX dummy_test_idx SET (option_ternary_2 = Do_Not_Know_YET);
 ALTER INDEX dummy_test_idx SET (option_real = 3.2);
 ALTER INDEX dummy_test_idx SET (option_string_val = 'val2');
 ALTER INDEX dummy_test_idx SET (option_string_null = NULL);
@@ -52,21 +55,23 @@ ALTER INDEX dummy_test_idx SET (option_enum = 'three');
 ERROR:  invalid value for enum option "option_enum": three
 DETAIL:  Valid values are "one" and "two".
 SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
-         unnest          
--------------------------
+              unnest              
+----------------------------------
  option_int=10
  option_bool=true
  option_ternary_1=false
+ option_ternary_2=do_not_know_yet
  option_real=3.2
  option_string_val=val2
  option_string_null=null
  option_enum=one
-(7 rows)
+(8 rows)
 
 -- ALTER INDEX .. RESET
 ALTER INDEX dummy_test_idx RESET (option_int);
 ALTER INDEX dummy_test_idx RESET (option_bool);
 ALTER INDEX dummy_test_idx RESET (option_ternary_1);
+ALTER INDEX dummy_test_idx RESET (option_ternary_2);
 ALTER INDEX dummy_test_idx RESET (option_real);
 ALTER INDEX dummy_test_idx RESET (option_enum);
 ALTER INDEX dummy_test_idx RESET (option_string_val);
@@ -113,13 +118,21 @@ ALTER INDEX dummy_test_idx SET (option_ternary_1 = 3.4); -- error
 ERROR:  invalid value for boolean option "option_ternary_1": 3.4
 ALTER INDEX dummy_test_idx SET (option_ternary_1 = 'val4'); -- error
 ERROR:  invalid value for boolean option "option_ternary_1": val4
+ALTER INDEX dummy_test_idx SET (option_ternary_1 = 'do_not_know_yet'); -- error. Valid for ternary2 not for ternary1
+ERROR:  invalid value for boolean option "option_ternary_1": do_not_know_yet
+ALTER INDEX dummy_test_idx SET (option_ternary_2 = 'do_not_know_yet'); -- ok
+ALTER INDEX dummy_test_idx SET (option_ternary_2 = 'illegal_value'); -- error
+ERROR:  invalid value for option "option_ternary_2": illegal_value
+DETAIL:  Valid values are "on", "off", and "do_not_know_yet".
 SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
-       unnest       
---------------------
+              unnest              
+----------------------------------
  option_ternary_1=1
-(1 row)
+ option_ternary_2=do_not_know_yet
+(2 rows)
 
 ALTER INDEX dummy_test_idx RESET (option_ternary_1);
+ALTER INDEX dummy_test_idx RESET (option_ternary_2);
 -- Float
 ALTER INDEX dummy_test_idx SET (option_real = 4); -- ok
 ALTER INDEX dummy_test_idx SET (option_real = true); -- error
diff --git a/src/test/modules/dummy_index_am/sql/reloptions.sql b/src/test/modules/dummy_index_am/sql/reloptions.sql
index 2cdff0820f6..f8b985055c1 100644
--- a/src/test/modules/dummy_index_am/sql/reloptions.sql
+++ b/src/test/modules/dummy_index_am/sql/reloptions.sql
@@ -19,6 +19,7 @@ CREATE INDEX dummy_test_idx ON dummy_test_tab
   USING dummy_index_am (i) WITH (
   option_bool = false,
   option_ternary_1,
+  option_ternary_2 = off,
   option_int = 5,
   option_real = 3.1,
   option_enum = 'two',
@@ -32,6 +33,7 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 ALTER INDEX dummy_test_idx SET (option_int = 10);
 ALTER INDEX dummy_test_idx SET (option_bool = true);
 ALTER INDEX dummy_test_idx SET (option_ternary_1 = false);
+ALTER INDEX dummy_test_idx SET (option_ternary_2 = Do_Not_Know_YET);
 ALTER INDEX dummy_test_idx SET (option_real = 3.2);
 ALTER INDEX dummy_test_idx SET (option_string_val = 'val2');
 ALTER INDEX dummy_test_idx SET (option_string_null = NULL);
@@ -43,6 +45,7 @@ SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 ALTER INDEX dummy_test_idx RESET (option_int);
 ALTER INDEX dummy_test_idx RESET (option_bool);
 ALTER INDEX dummy_test_idx RESET (option_ternary_1);
+ALTER INDEX dummy_test_idx RESET (option_ternary_2);
 ALTER INDEX dummy_test_idx RESET (option_real);
 ALTER INDEX dummy_test_idx RESET (option_enum);
 ALTER INDEX dummy_test_idx RESET (option_string_val);
@@ -68,8 +71,12 @@ ALTER INDEX dummy_test_idx SET (option_ternary_1 = 4); -- error
 ALTER INDEX dummy_test_idx SET (option_ternary_1 = 1); -- ok, as true
 ALTER INDEX dummy_test_idx SET (option_ternary_1 = 3.4); -- error
 ALTER INDEX dummy_test_idx SET (option_ternary_1 = 'val4'); -- error
+ALTER INDEX dummy_test_idx SET (option_ternary_1 = 'do_not_know_yet'); -- error. Valid for ternary2 not for ternary1
+ALTER INDEX dummy_test_idx SET (option_ternary_2 = 'do_not_know_yet'); -- ok
+ALTER INDEX dummy_test_idx SET (option_ternary_2 = 'illegal_value'); -- error
 SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
 ALTER INDEX dummy_test_idx RESET (option_ternary_1);
+ALTER INDEX dummy_test_idx RESET (option_ternary_2);
 -- Float
 ALTER INDEX dummy_test_idx SET (option_real = 4); -- ok
 ALTER INDEX dummy_test_idx SET (option_real = true); -- error
diff --git a/src/test/regress/expected/gist.out b/src/test/regress/expected/gist.out
index c75bbb23b6e..19dc547d5f8 100644
--- a/src/test/regress/expected/gist.out
+++ b/src/test/regress/expected/gist.out
@@ -12,7 +12,7 @@ create index gist_pointidx4 on gist_point_tbl using gist(p) with (buffering = au
 drop index gist_pointidx2, gist_pointidx3, gist_pointidx4;
 -- Make sure bad values are refused
 create index gist_pointidx5 on gist_point_tbl using gist(p) with (buffering = invalid_value);
-ERROR:  invalid value for enum option "buffering": invalid_value
+ERROR:  invalid value for option "buffering": invalid_value
 DETAIL:  Valid values are "on", "off", and "auto".
 create index gist_pointidx5 on gist_point_tbl using gist(p) with (fillfactor=9);
 ERROR:  value 9 out of bounds for option "fillfactor"
diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index e3a974f2611..6e65cd5c3da 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -116,6 +116,24 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
  {vacuum_truncate=fals}
 (1 row)
 
+-- preferred "true" alias is stored in pg_class
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=on);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+        reloptions         
+---------------------------
+ {vacuum_index_cleanup=on}
+(1 row)
+
+-- custom "third" value is available
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=auto);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+         reloptions          
+-----------------------------
+ {vacuum_index_cleanup=auto}
+(1 row)
+
 -- Test vacuum_truncate option
 DROP TABLE reloptions_test;
 CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index 680c8bf8614..c99673db9ec 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -70,6 +70,16 @@ DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_truncate=FaLS);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 
+-- preferred "true" alias is stored in pg_class
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=on);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
+-- custom "third" value is available
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=auto);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
 -- Test vacuum_truncate option
 DROP TABLE reloptions_test;
 
-- 
2.47.3



^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: [PATCH] ternary reloption type
  2025-09-04 17:07 Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2025-09-12 13:46 ` Re: [PATCH] ternary reloption type Timur Magomedov <t.magomedov@postgrespro.ru>
  2025-09-14 08:49   ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-16 15:14     ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
  2026-01-21 18:44       ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-21 19:23         ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
  2026-02-05 13:08           ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
@ 2026-05-10 11:09             ` Nikolay Shaplov <dhyan@nataraj.su>
  0 siblings, 0 replies; 13+ messages in thread

From: Nikolay Shaplov @ 2026-05-10 11:09 UTC (permalink / raw)
  To: Álvaro Herrera <alvherre@alvh.no-ip.org>; pgsql-hackers@lists.postgresql.org; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Chris Travers <chris.travers@gmail.com>; Timur Magomedov <t.magomedov@postgrespro.ru>; Nathan Bossart <nathandbossart@gmail.com>; Nikolay Shaplov <dhyan@nataraj.su>

В письме от четверг, 5 февраля 2026 г. 16:08:59 Москва, стандартное время 
пользователь Nikolay Shaplov написал:

Here hoes a rebased version of second part of ternary patch that changes 
`vacuum_index_cleanup` and GiST's `buffering`  reloptions to ternary type

-- 
Nikolay Shaplov aka Nataraj
Fuzzing Engineer at Postgres Professional
Matrix IM: @dhyan:nataraj.su


Attachments:

  [text/x-patch] v2-0001-Convert-vacuum_index_cleanup-and-GiST-s-buffering.patch (0B, ../../8720506.NyiUUSuA9g@thinkpad-pgpro/2-v2-0001-Convert-vacuum_index_cleanup-and-GiST-s-buffering.patch)
  download

^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: [PATCH] ternary reloption type
  2025-09-04 17:07 Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2025-09-12 13:46 ` Re: [PATCH] ternary reloption type Timur Magomedov <t.magomedov@postgrespro.ru>
  2025-09-14 08:49   ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-16 15:14     ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
  2026-01-21 18:44       ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-21 19:23         ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
@ 2026-08-17 13:03           ` Peter Eisentraut <peter@eisentraut.org>
  2026-08-17 14:16             ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-08-17 15:16             ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
  1 sibling, 2 replies; 13+ messages in thread

From: Peter Eisentraut @ 2026-08-17 13:03 UTC (permalink / raw)
  To: Álvaro Herrera <alvherre@alvh.no-ip.org>; Nikolay Shaplov <dhyan@nataraj.su>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Chris Travers <chris.travers@gmail.com>; Timur Magomedov <t.magomedov@postgrespro.ru>; Nathan Bossart <nathandbossart@gmail.com>

On 21.01.26 20:23, Álvaro Herrera wrote:
> On 2026-Jan-21, Nikolay Shaplov wrote:
> 
>> On 16.01.2026 18:14, Álvaro Herrera wrote:
>>> I took a quick look at 0001+0002 and I think it's quite reasonable.
>>> Here it is again with some minor fixups.
>> Good. I like ternary -> pg_ternary change. That is reasonable. And
>> postgres.h is better place for it then c.h.
> I further changed TERNARY_TRUE and so on to have a PG_ prefix also; it's
> not impossible that there's userland code somewhere outside Postgres
> that uses those symbol names, so let's avoid a collision.

I don't like that pg_ternary was added to postgres.h.

There are, depending on how you count, a few to many other ternary types 
used throughout the tree, and it's not clear why this one should be the 
standard one now.  At least if so that should have involved some 
discussion and analysis on the other ones.  There are also some 
tradeoffs about how this type should be designed.  This particular one 
uses 0 and 1 for false and true, and -1 for unset.  Others use 0 for 
unset and other values for false and true.  Maybe this choice is useful 
for this particular use, but we shouldn't impose it on everyone.

Independent of that, I don't understand why this was put into postgres.h 
instead of c.h.  It's not particular to backend code, as far as I can tell.

I think it would be better to rename this to something like 
relopt_ternary and move it to access/reloptions.h.

If we want to consolidate all ternary types, that might be useful, but 
it should be an explicit discussion.








^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: [PATCH] ternary reloption type
  2025-09-04 17:07 Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2025-09-12 13:46 ` Re: [PATCH] ternary reloption type Timur Magomedov <t.magomedov@postgrespro.ru>
  2025-09-14 08:49   ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-16 15:14     ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
  2026-01-21 18:44       ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-21 19:23         ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
  2026-08-17 13:03           ` Re: [PATCH] ternary reloption type Peter Eisentraut <peter@eisentraut.org>
@ 2026-08-17 14:16             ` Nikolay Shaplov <dhyan@nataraj.su>
  1 sibling, 0 replies; 13+ messages in thread

From: Nikolay Shaplov @ 2026-08-17 14:16 UTC (permalink / raw)
  To: Álvaro Herrera <alvherre@alvh.no-ip.org>; Peter Eisentraut <peter@eisentraut.org>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Chris Travers <chris.travers@gmail.com>; Timur Magomedov <t.magomedov@postgrespro.ru>; Nathan Bossart <nathandbossart@gmail.com>

В письме от понедельник, 17 августа 2026 г. 16:03:12 Москва, стандартное время 
пользователь Peter Eisentraut написал:

> > I don't like that pg_ternary was added to postgres.h. 

That's understandable.

> There are, depending on how you count, a few to many other ternary types
> used throughout the tree, and it's not clear why this one should be the
> standard one now.  At least if so that should have involved some
> discussion and analysis on the other ones.  There are also some
> tradeoffs about how this type should be designed.  This particular one
> uses 0 and 1 for false and true, and -1 for unset.  Others use 0 for
> unset and other values for false and true.  Maybe this choice is useful
> for this particular use, but we shouldn't impose it on everyone.

The idea was that from the reloptions point of view, ternary is a boolean with 
one extra possibility. This comes about purely historically, because the 
current and future ternary options are born from boolean options, so it's 
convenient to keep the values that encode explicit 'yes' and 'no', so that the 
corresponding fields in the database don't have to be updated when switching 
from boolean to ternary. With this encoding, everything will keep working the 
way it did without any pg_catalog update.

As for the 'third' value, using an enum seemed reasonable in this case, and 
then you have to pick one specific value. If 0 and 1 are already taken, then -1 
seems like the logical option.
	
When developing this patch, I wasn't aware of the existence of other ternary-
logic implementations in Postgres. I'm not against bringing these 
implementations to a common style. But in the case of reloptions, we're 
constrained by the fact that the data is already stored on disk and it's 
better not to change it. If the other ternary values are used only in memory, 
then it might be right to bring them to the same data type as the one used in 
options. If you share a list of the other places where ternary logic is also 
used, we'll all have a chance to look at it and assess how justified bringing 
them to a common style would be.

> Independent of that, I don't understand why this was put into postgres.h
> instead of c.h.  It's not particular to backend code, as far as I can tell.

> I think it would be better to rename this to something like
> relopt_ternary and move it to access/reloptions.h.

If it were up to me, I'd keep the definition of pg_ternary in access/
reloptions.h and not interfere with the core Postgres code. Unfortunately, 
though, one of the ternary options value is located in the StdRdOptions struct 
defined in include/utils/rel.h, so the pg_ternary type has to be defined in some 
very global place. Which one exactly is debatable. In the original version of 
the patch I put it in c.h. When committing, Álvaro moved it to postgres.h. I 
concluded that Álvaro knows better where it should be. I don't have an opinion 
of my own on this question — the main thing for me is that pg_ternary be 
defined in a header file that can be included in utils/rel.h.

I guess some logic behind it might be like this: We using pg_ternary name, not 
just ternary, because some other library header might also want to define 
ternary. And since this type has pg_ suffix postgres.h seems to be better place 
to store it, than c.h. pg_ means it is related to postgres. Things from  c.h 
are not postgres related.

> If we want to consolidate all ternary types, that might be useful, but
> it should be an explicit discussion.

I think we want. Me at least. Let's discuss it.

-- 
Nikolay Shaplov aka Nataraj
Fuzzing Engineer at Postgres Professional
Matrix IM: @dhyan:nataraj.su

Attachments:

  [application/pgp-signature] signature.asc (487B, ../../3425885.aeNJFYEL58@thinkpad-pgpro/2-signature.asc)
  download

^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: [PATCH] ternary reloption type
  2025-09-04 17:07 Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2025-09-12 13:46 ` Re: [PATCH] ternary reloption type Timur Magomedov <t.magomedov@postgrespro.ru>
  2025-09-14 08:49   ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-16 15:14     ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
  2026-01-21 18:44       ` Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
  2026-01-21 19:23         ` Re: [PATCH] ternary reloption type Álvaro Herrera <alvherre@alvh.no-ip.org>
  2026-08-17 13:03           ` Re: [PATCH] ternary reloption type Peter Eisentraut <peter@eisentraut.org>
@ 2026-08-17 15:16             ` Álvaro Herrera <alvherre@alvh.no-ip.org>
  1 sibling, 0 replies; 13+ messages in thread

From: Álvaro Herrera @ 2026-08-17 15:16 UTC (permalink / raw)
  To: Peter Eisentraut <peter@eisentraut.org>; +Cc: Nikolay Shaplov <dhyan@nataraj.su>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>; Chris Travers <chris.travers@gmail.com>; Timur Magomedov <t.magomedov@postgrespro.ru>; Nathan Bossart <nathandbossart@gmail.com>

On 2026-Aug-17, Peter Eisentraut wrote:

> There are, depending on how you count, a few to many other ternary types
> used throughout the tree, and it's not clear why this one should be the
> standard one now.

I asked Claude which ternaries we have.  The response listed three, and
it started with:

  pg_ternary — src/include/postgres.h
  The canonical/general-purpose one. Values: PG_TERNARY_FALSE (0),
  PG_TERNARY_TRUE (1), PG_TERNARY_UNSET (-1). Comment explicitly describes
  it as a boolean with an extra "unset" value.

It's already considered the canonical one!  That's a great start.

It then said
  trivalue — src/bin/pg_dump/pg_backup.h
  Used by pg_dump / client tools for command-line options. Values:
  TRI_DEFAULT, TRI_NO, TRI_YES.

  PGTernaryBool — src/interfaces/libpq/libpq-int.h (and an identical
  copy in src/interfaces/libpq-oauth/oauth-utils.h)
  A libpq internal "boolean plus not-known" for GUCs it may have to
  fetch. Values: PG_BOOL_UNKNOWN (0), PG_BOOL_YES, PG_BOOL_NO.

That's the complete list it produced.

> At least if so that should have involved some discussion
> and analysis on the other ones.  There are also some tradeoffs about how
> this type should be designed.  This particular one uses 0 and 1 for false
> and true, and -1 for unset.  Others use 0 for unset and other values for
> false and true.  Maybe this choice is useful for this particular use, but we
> shouldn't impose it on everyone.

I think you're talking about the libpq one (PGTernaryBool), which dates
back to commit ee28cacf619f and was defined in libpq-int.h.

> Independent of that, I don't understand why this was put into postgres.h
> instead of c.h.  It's not particular to backend code, as far as I can tell.

Right, it's not.  It felt a bit out of place in c.h to me, and I didn't
see the argument for exposing it wider than postgres.h, but at the same
time it seemed to me that a notion this common can perfectly well use a
single central definition rather than have each module define the same
thing.

We have a handful of enums all called "trivalue" in various clients
programs, with the same definitions, and that doesn't seem great to me --
quite the opposite in fact.  If we move pg_ternary to c.h and add
aliases TRI_YES / NO / DEFAULT, then we can remove the repetitive enum
typedefs and we'd probably be in a better position.

> I think it would be better to rename this to something like relopt_ternary
> and move it to access/reloptions.h.

I'm not sure what we gain from doing that.  If there's generalized
opposition to having it in postgres.h, I'm open to renaming it as
suggested and moving it there.

> If we want to consolidate all ternary types, that might be useful, but it
> should be an explicit discussion.

The others I found were:

/*
 * Represents whether a header line must match the actual names
 * (which implies "true"), and whether it should be present.
 */
#define COPY_HEADER_MATCH   -1
#define COPY_HEADER_FALSE   0
#define COPY_HEADER_TRUE    1

and

#define GIN_FALSE       0       /* item is not present / does not match */
#define GIN_TRUE        1       /* item is present / matches */
#define GIN_MAYBE       2       /* don't know if item is present / don't know
                                 * if matches */

and it didn't seem that they had semantics similar enough to make them
use the new enum.

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/
"Sallah, I said NO camels! That's FIVE camels; can't you count?"
(Indiana Jones)






^ permalink  raw  reply  [nested|flat] 13+ messages in thread

* Re: [PATCH] ternary reloption type
@ 2026-05-18 19:30 Andrey Rachitskiy <pl0h0yp1@gmail.com>
  0 siblings, 0 replies; 13+ messages in thread

From: Andrey Rachitskiy @ 2026-05-18 19:30 UTC (permalink / raw)
  To: pgsql-hackers@lists.postgresql.org; +Cc: dhyan@nataraj.su; alvherre@alvh.no-ip.org; chris.travers@gmail.com; nathandbossart@gmail.com

Hi, Nikolay!
Here is a brief review and suggestions for the patch.

```
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -98,8 +98,8 @@ typedef struct relopt_bool
 typedef struct relopt_ternary
 {
        relopt_gen      gen;
-       const char      *unset_alias; /* word that will be treated as unset value */
-       int                     default_val;
+       const char      *unset_alias;   /* word treated as unset, or NULL */
+       pg_ternary      default_val;
 } relopt_ternary;

 typedef struct relopt_int
```
The v2 patch added default_val but typed it as int. The field holds PG_TERNARY_TRUE/FALSE/UNSET, so it should use pg_ternary, same as ternary_val in relopt_value and the add_ternary_reloption() parameters.


```
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -915,7 +915,8 @@ add_local_bool_reloption(local_relopts *relopts, const char *name,
  */
 static relopt_ternary *
 init_ternary_reloption(uint32 kinds, const char *name, const char *desc,
-                        pg_ternary default_val, const char* unset_alias, LOCKMODE lockmode)
+                                          pg_ternary default_val, const char *unset_alias,
+                                          LOCKMODE lockmode)
 {
        relopt_ternary *newoption;

@@ -933,7 +934,8 @@ init_ternary_reloption(uint32 kinds, const char *name, const char *desc,
  */
 void
 add_ternary_reloption(uint32 kinds, const char *name, const char *desc,
-                 pg_ternary default_val, const char* unset_alias,  LOCKMODE lockmode)
+                                         pg_ternary default_val, const char *unset_alias,
+                                         LOCKMODE lockmode)
 {
        relopt_ternary *newoption;

@@ -952,7 +954,7 @@ add_ternary_reloption(uint32 kinds, const char *name, const char *desc,
 void
 add_local_ternary_reloption(local_relopts *relopts, const char *name,
                                                        const char *desc, pg_ternary default_val,
-                                                       const char* unset_alias, int offset)
+                                                       const char *unset_alias, int offset)
 {
        relopt_ternary *newoption;

--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -191,8 +191,8 @@ extern relopt_kind add_reloption_kind(void);
 extern void add_bool_reloption(uint32 kinds, const char *name, const char *desc,
                                                           bool default_val, LOCKMODE lockmode);
 extern void add_ternary_reloption(uint32 kinds, const char *name,
-                                       const char *desc, pg_ternary default_val,
-                                       const char* unset_alias, LOCKMODE lockmode);
+                                                                 const char *desc, pg_ternary default_val,
+                                                                 const char *unset_alias, LOCKMODE lockmode);
 extern void add_int_reloption(uint32 kinds, const char *name, const char *desc,
                                                          int default_val, int min_val, int max_val,
                                                          LOCKMODE lockmode);
@@ -213,9 +213,9 @@ extern void add_local_bool_reloption(local_relopts *relopts, const char *name,
                                                                         const char *desc, bool default_val,
                                                                         int offset);
 extern void add_local_ternary_reloption(local_relopts *relopts,
-                                                               const char *name, const char *desc,
-                                                               pg_ternary default_val, const char* unset_alias,
-                                                               int offset);
+                                                                               const char *name, const char *desc,
+                                                                               pg_ternary default_val,
+                                                                               const char *unset_alias, int offset);
 extern void add_local_int_reloption(local_relopts *relopts, const char *name,
                                                                        const char *desc, int default_val,
                                                                        int min_val, int max_val, int offset);
```
Formatting only (const char * and prototype alignment), no semantic change.


```
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -1723,10 +1725,10 @@ parse_one_reloption(relopt_value *option, char *text_str, int text_len,
                                if (validate && !parsed)
                                        ereport(ERROR,
                                                        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-                                                       errmsg("invalid value for option \"%s\": %s",
+                                                        errmsg("invalid value for option \"%s\": %s",
                                                                        option->gen->name, value),
-                                                       errdetail("Valid values are \"on\", \"off\", and \"%s\".",
-                                                                               opt->unset_alias)));
+                                                        errdetail("Valid values are \"on\", \"off\", \"true\", \"false\", \"yes\", \"no\", \"1\", \"0\", and \"%s\".",
+                                                                          opt->unset_alias)));
                        }
                        break;
                case RELOPT_TYPE_INT:

--- a/src/test/modules/dummy_index_am/expected/reloptions.out
+++ b/src/test/modules/dummy_index_am/expected/reloptions.out
@@ -123,7 +123,7 @@ ERROR:  invalid value for boolean option "option_ternary_1": do_not_know_yet
 ALTER INDEX dummy_test_idx SET (option_ternary_2 = 'do_not_know_yet'); -- ok
 ALTER INDEX dummy_test_idx SET (option_ternary_2 = 'illegal_value'); -- error
 ERROR:  invalid value for option "option_ternary_2": illegal_value
-DETAIL:  Valid values are "on", "off", and "do_not_know_yet".
+DETAIL:  Valid values are "on", "off", "true", "false", "yes", "no", "1", "0", and "do_not_know_yet".
 SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
               unnest
 ----------------------------------
```
Ternary values are parsed with parse_bool() and then unset_alias, so a rejection should tell the user the full set of valid spellings. The previous enum-based vacuum_index_cleanup error effectively did that; we restore the same level of detail for the ternary path.


```
@@ -1901,7 +1903,8 @@ fillRelOptions(void *rdopts, Size basesize,
                                                break;
                                        case RELOPT_TYPE_TERNARY:
                                                *(pg_ternary *) itempos = options[i].isset ?
-                                                       options[i].ternary_val : PG_TERNARY_UNSET;
+                                                       options[i].ternary_val :
+                                                       ((relopt_ternary *) options[i].gen)->default_val;
                                                break;
                                        case RELOPT_TYPE_INT:
                                                *(int *) itempos = options[i].isset ?
```
parseRelOptions() returns every option of a kind, with isset=false when it is not in pg_class.reloptions. For bool/int/enum we already apply default_val in that case; ternary was still hardcoding PG_TERNARY_UNSET, so default_val on relopt_ternary was unused.
Built-in ternaries (vacuum_truncate, vacuum_index_cleanup, buffering) all have default_val == PG_TERNARY_UNSET, so behavior for them is unchanged. This matters for AMs that call add_ternary_reloption() with another default (dummy_index_am option_ternary_2).


```
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -913,7 +913,8 @@ gistoptions(Datum reloptions, bool validate)
 {
        static const relopt_parse_elt tab[] = {
                {"fillfactor", RELOPT_TYPE_INT, offsetof(GiSTOptions, fillfactor)},
-               {"buffering", RELOPT_TYPE_ENUM, offsetof(GiSTOptions, buffering_mode)}
+               {"buffering", RELOPT_TYPE_TERNARY,
+               offsetof(GiSTOptions, buffering_mode)}
        };
```
buffering is registered globally as a ternary reloption; the local relopt_parse_elt table still said ENUM after v2. fillRelOptions() uses options[i].gen->type from the global registry, so this was not a runtime bug, but the table was wrong and confusing for anyone reading the GiST options path.


```
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -134,6 +134,23 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
  {vacuum_index_cleanup=auto}
 (1 row)

+-- boolean synonyms are accepted
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=true);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+         reloptions
+-----------------------------
+ {vacuum_index_cleanup=true}
+(1 row)
+
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=false);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+          reloptions
+------------------------------
+ {vacuum_index_cleanup=false}
+(1 row)
+

--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -80,6 +80,14 @@ DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=auto);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;

+-- boolean synonyms are accepted
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=true);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=false);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
 -- Test vacuum_truncate option
 DROP TABLE reloptions_test;
```
After moving vacuum_index_cleanup from enum to ternary, we still accept true/false via parse_bool(). These tests lock that in and show the value is stored in pg_class.reloptions as given (not normalized to on/off).


```
--- a/src/test/regress/expected/gist.out
+++ b/src/test/regress/expected/gist.out
@@ -9,11 +9,12 @@ create index gist_pointidx on gist_point_tbl using gist(p);
 create index gist_pointidx2 on gist_point_tbl using gist(p) with (buffering = on, fillfactor=50);
 create index gist_pointidx3 on gist_point_tbl using gist(p) with (buffering = off);
 create index gist_pointidx4 on gist_point_tbl using gist(p) with (buffering = auto);
-drop index gist_pointidx2, gist_pointidx3, gist_pointidx4;
+create index gist_pointidx4b on gist_point_tbl using gist(p) with (buffering = true);
+drop index gist_pointidx2, gist_pointidx3, gist_pointidx4, gist_pointidx4b;
 -- Make sure bad values are refused
 create index gist_pointidx5 on gist_point_tbl using gist(p) with (buffering = invalid_value);
 ERROR:  invalid value for option "buffering": invalid_value
-DETAIL:  Valid values are "on", "off", and "auto".
+DETAIL:  Valid values are "on", "off", "true", "false", "yes", "no", "1", "0", and "auto".
 create index gist_pointidx5 on gist_point_tbl using gist(p) with (fillfactor=9);
 ERROR:  value 9 out of bounds for option "fillfactor"
 DETAIL:  Valid values are between "10" and "100".

--- a/src/test/regress/sql/gist.sql
+++ b/src/test/regress/sql/gist.sql
@@ -11,7 +11,8 @@ create index gist_pointidx on gist_point_tbl using gist(p);
 create index gist_pointidx2 on gist_point_tbl using gist(p) with (buffering = on, fillfactor=50);
 create index gist_pointidx3 on gist_point_tbl using gist(p) with (buffering = off);
 create index gist_pointidx4 on gist_point_tbl using gist(p) with (buffering = auto);
-drop index gist_pointidx2, gist_pointidx3, gist_pointidx4;
+create index gist_pointidx4b on gist_point_tbl using gist(p) with (buffering = true);
+drop index gist_pointidx2, gist_pointidx3, gist_pointidx4, gist_pointidx4b;
```
Same idea for GiST buffering: true should work as a synonym for on. Expected output updated for the new index and the extended DETAIL line on invalid input.


------
Regards,
Andrey Rachitskiy


> 18 мая 2026 г., в 19:11, Nikolay Shaplov <dhyan@nataraj.su> написал(а):
> 
> -- 
> Nikolay Shaplov aka Nataraj
> Fuzzing Engineer at Postgres Professional
> Matrix IM: @dhyan:nataraj.su
> 
> Отправитель: Nikolay Shaplov <dhyan@nataraj.su>
> Тема: Ответ: [PATCH] ternary reloption type
> Дата: 10 мая 2026 г. в 16:09:28 GMT+5
> Кому: Álvaro Herrera <alvherre@alvh.no-ip.org>, pgsql-hackers@lists.postgresql.org
> Копия: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>, Chris Travers <chris.travers@gmail.com>, Timur Magomedov <t.magomedov@postgrespro.ru>, Nathan Bossart <nathandbossart@gmail.com>, Nikolay Shaplov <dhyan@nataraj.su>
> 
> 
> В письме от четверг, 5 февраля 2026 г. 16:08:59 Москва, стандартное время 
> пользователь Nikolay Shaplov написал:
> 
> Here hoes a rebased version of second part of ternary patch that changes 
> `vacuum_index_cleanup` and GiST's `buffering`  reloptions to ternary type
> 
> -- 
> Nikolay Shaplov aka Nataraj
> Fuzzing Engineer at Postgres Professional
> Matrix IM: @dhyan:nataraj.su

> 

Attachments:

  [application/octet-stream] review_ternary-options-part2___v2.patch (9.6K, ../../5673948B-91DF-422A-A5DC-DCD7E1DA87DA@gmail.com/3-review_ternary-options-part2___v2.patch)
  download | inline diff:
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 7094016a9c..b11220119d 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -915,7 +915,8 @@ add_local_bool_reloption(local_relopts *relopts, const char *name,
  */
 static relopt_ternary *
 init_ternary_reloption(uint32 kinds, const char *name, const char *desc,
-			 pg_ternary default_val, const char* unset_alias, LOCKMODE lockmode)
+					   pg_ternary default_val, const char *unset_alias,
+					   LOCKMODE lockmode)
 {
 	relopt_ternary *newoption;
 
@@ -933,7 +934,8 @@ init_ternary_reloption(uint32 kinds, const char *name, const char *desc,
  */
 void
 add_ternary_reloption(uint32 kinds, const char *name, const char *desc,
-		  pg_ternary default_val, const char* unset_alias,  LOCKMODE lockmode)
+					  pg_ternary default_val, const char *unset_alias,
+					  LOCKMODE lockmode)
 {
 	relopt_ternary *newoption;
 
@@ -952,7 +954,7 @@ add_ternary_reloption(uint32 kinds, const char *name, const char *desc,
 void
 add_local_ternary_reloption(local_relopts *relopts, const char *name,
 							const char *desc, pg_ternary default_val,
-							const char* unset_alias, int offset)
+							const char *unset_alias, int offset)
 {
 	relopt_ternary *newoption;
 
@@ -1723,10 +1725,10 @@ parse_one_reloption(relopt_value *option, char *text_str, int text_len,
 				if (validate && !parsed)
 					ereport(ERROR,
 							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-							errmsg("invalid value for option \"%s\": %s",
+							 errmsg("invalid value for option \"%s\": %s",
 									option->gen->name, value),
-							errdetail("Valid values are \"on\", \"off\", and \"%s\".",
-										opt->unset_alias)));
+							 errdetail("Valid values are \"on\", \"off\", \"true\", \"false\", \"yes\", \"no\", \"1\", \"0\", and \"%s\".",
+									   opt->unset_alias)));
 			}
 			break;
 		case RELOPT_TYPE_INT:
@@ -1901,7 +1903,8 @@ fillRelOptions(void *rdopts, Size basesize,
 						break;
 					case RELOPT_TYPE_TERNARY:
 						*(pg_ternary *) itempos = options[i].isset ?
-							options[i].ternary_val : PG_TERNARY_UNSET;
+							options[i].ternary_val :
+							((relopt_ternary *) options[i].gen)->default_val;
 						break;
 					case RELOPT_TYPE_INT:
 						*(int *) itempos = options[i].isset ?
diff --git a/src/backend/access/gist/gistutil.c b/src/backend/access/gist/gistutil.c
index 0f58f61879..2d69c3549b 100644
--- a/src/backend/access/gist/gistutil.c
+++ b/src/backend/access/gist/gistutil.c
@@ -913,7 +913,8 @@ gistoptions(Datum reloptions, bool validate)
 {
 	static const relopt_parse_elt tab[] = {
 		{"fillfactor", RELOPT_TYPE_INT, offsetof(GiSTOptions, fillfactor)},
-		{"buffering", RELOPT_TYPE_ENUM, offsetof(GiSTOptions, buffering_mode)}
+		{"buffering", RELOPT_TYPE_TERNARY,
+		offsetof(GiSTOptions, buffering_mode)}
 	};
 
 	return (bytea *) build_reloptions(reloptions, validate,
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index 1c5ea85e76..599a99ec2e 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -98,8 +98,8 @@ typedef struct relopt_bool
 typedef struct relopt_ternary
 {
 	relopt_gen	gen;
-	const char	*unset_alias; /* word that will be treated as unset value */
-	int			default_val;
+	const char	*unset_alias;	/* word treated as unset, or NULL */
+	pg_ternary	default_val;
 } relopt_ternary;
 
 typedef struct relopt_int
@@ -191,8 +191,8 @@ extern relopt_kind add_reloption_kind(void);
 extern void add_bool_reloption(uint32 kinds, const char *name, const char *desc,
 							   bool default_val, LOCKMODE lockmode);
 extern void add_ternary_reloption(uint32 kinds, const char *name,
-					const char *desc, pg_ternary default_val,
-					const char* unset_alias, LOCKMODE lockmode);
+								  const char *desc, pg_ternary default_val,
+								  const char *unset_alias, LOCKMODE lockmode);
 extern void add_int_reloption(uint32 kinds, const char *name, const char *desc,
 							  int default_val, int min_val, int max_val,
 							  LOCKMODE lockmode);
@@ -213,9 +213,9 @@ extern void add_local_bool_reloption(local_relopts *relopts, const char *name,
 									 const char *desc, bool default_val,
 									 int offset);
 extern void add_local_ternary_reloption(local_relopts *relopts,
-								const char *name, const char *desc,
-								pg_ternary default_val, const char* unset_alias,
-								int offset);
+										const char *name, const char *desc,
+										pg_ternary default_val,
+										const char *unset_alias, int offset);
 extern void add_local_int_reloption(local_relopts *relopts, const char *name,
 									const char *desc, int default_val,
 									int min_val, int max_val, int offset);
diff --git a/src/test/modules/dummy_index_am/expected/reloptions.out b/src/test/modules/dummy_index_am/expected/reloptions.out
index 90abbe2e37..ea07de1d6e 100644
--- a/src/test/modules/dummy_index_am/expected/reloptions.out
+++ b/src/test/modules/dummy_index_am/expected/reloptions.out
@@ -123,7 +123,7 @@ ERROR:  invalid value for boolean option "option_ternary_1": do_not_know_yet
 ALTER INDEX dummy_test_idx SET (option_ternary_2 = 'do_not_know_yet'); -- ok
 ALTER INDEX dummy_test_idx SET (option_ternary_2 = 'illegal_value'); -- error
 ERROR:  invalid value for option "option_ternary_2": illegal_value
-DETAIL:  Valid values are "on", "off", and "do_not_know_yet".
+DETAIL:  Valid values are "on", "off", "true", "false", "yes", "no", "1", "0", and "do_not_know_yet".
 SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_idx';
               unnest              
 ----------------------------------
diff --git a/src/test/regress/expected/gist.out b/src/test/regress/expected/gist.out
index 19dc547d5f..ea2acb8fbe 100644
--- a/src/test/regress/expected/gist.out
+++ b/src/test/regress/expected/gist.out
@@ -9,11 +9,12 @@ create index gist_pointidx on gist_point_tbl using gist(p);
 create index gist_pointidx2 on gist_point_tbl using gist(p) with (buffering = on, fillfactor=50);
 create index gist_pointidx3 on gist_point_tbl using gist(p) with (buffering = off);
 create index gist_pointidx4 on gist_point_tbl using gist(p) with (buffering = auto);
-drop index gist_pointidx2, gist_pointidx3, gist_pointidx4;
+create index gist_pointidx4b on gist_point_tbl using gist(p) with (buffering = true);
+drop index gist_pointidx2, gist_pointidx3, gist_pointidx4, gist_pointidx4b;
 -- Make sure bad values are refused
 create index gist_pointidx5 on gist_point_tbl using gist(p) with (buffering = invalid_value);
 ERROR:  invalid value for option "buffering": invalid_value
-DETAIL:  Valid values are "on", "off", and "auto".
+DETAIL:  Valid values are "on", "off", "true", "false", "yes", "no", "1", "0", and "auto".
 create index gist_pointidx5 on gist_point_tbl using gist(p) with (fillfactor=9);
 ERROR:  value 9 out of bounds for option "fillfactor"
 DETAIL:  Valid values are between "10" and "100".
diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index 6e65cd5c3d..d85eb68380 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -134,6 +134,23 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
  {vacuum_index_cleanup=auto}
 (1 row)
 
+-- boolean synonyms are accepted
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=true);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+         reloptions          
+-----------------------------
+ {vacuum_index_cleanup=true}
+(1 row)
+
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=false);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+          reloptions          
+------------------------------
+ {vacuum_index_cleanup=false}
+(1 row)
+
 -- Test vacuum_truncate option
 DROP TABLE reloptions_test;
 CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
diff --git a/src/test/regress/sql/gist.sql b/src/test/regress/sql/gist.sql
index 6f1fc65f12..0683574719 100644
--- a/src/test/regress/sql/gist.sql
+++ b/src/test/regress/sql/gist.sql
@@ -11,7 +11,8 @@ create index gist_pointidx on gist_point_tbl using gist(p);
 create index gist_pointidx2 on gist_point_tbl using gist(p) with (buffering = on, fillfactor=50);
 create index gist_pointidx3 on gist_point_tbl using gist(p) with (buffering = off);
 create index gist_pointidx4 on gist_point_tbl using gist(p) with (buffering = auto);
-drop index gist_pointidx2, gist_pointidx3, gist_pointidx4;
+create index gist_pointidx4b on gist_point_tbl using gist(p) with (buffering = true);
+drop index gist_pointidx2, gist_pointidx3, gist_pointidx4, gist_pointidx4b;
 
 -- Make sure bad values are refused
 create index gist_pointidx5 on gist_point_tbl using gist(p) with (buffering = invalid_value);
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index c99673db9e..d4ba177090 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -80,6 +80,14 @@ DROP TABLE reloptions_test;
 CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=auto);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 
+-- boolean synonyms are accepted
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=true);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+DROP TABLE reloptions_test;
+CREATE TABLE reloptions_test(i INT) WITH (vacuum_index_cleanup=false);
+SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
+
 -- Test vacuum_truncate option
 DROP TABLE reloptions_test;
 

  [text/x-patch] v2-0001-Convert-vacuum_index_cleanup-and-GiST-s-buffering.patch (487B, ../../5673948B-91DF-422A-A5DC-DCD7E1DA87DA@gmail.com/5-v2-0001-Convert-vacuum_index_cleanup-and-GiST-s-buffering.patch)
  download | inline diff:
-----BEGIN PGP SIGNATURE-----

iQEzBAABCgAdFiEE+sk3ebqQKlezKOi8PMbfuIHAGpgFAmoAZ2gACgkQPMbfuIHA
GpirwwgAnT+MgmIE5m1ayaChqoJfg0n4gzUrk5r4Mo7FVDBt+NgYvHwsV6NCtxC4
fPuprZV17jGDQPaJ0HKFtuOmJqa3ozoTzFm4n6l/rn5x3HV6IBrPGwy3X9QV57Wk
cvhTBSzgyy7GhJB20718fbgaFoUnUhI/ieQODJzPecbdUdgle6bn6gnkRiUK72Kr
/Hu8qc15vuyh/GLJUINAM/m2goNVTEb6mVcZh6zoV1FZfGYvw4X5+sNO+beg8NkW
9UZUJkIr0qolAPS1zstrPHk0MG5YfL2PR54AK7wXCT2ZUQgtlks3ghvbZINMCEmO
x1AFchU+EQSS4RzdiSF5NgIcJYfpsw==
=Qsjj
-----END PGP SIGNATURE-----

^ permalink  raw  reply  [nested|flat] 13+ messages in thread


end of thread, other threads:[~2026-08-17 15:16 UTC | newest]

Thread overview: 13+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2025-09-04 17:07 Re: [PATCH] ternary reloption type Nikolay Shaplov <dhyan@nataraj.su>
2025-09-12 13:46 ` Timur Magomedov <t.magomedov@postgrespro.ru>
2025-09-14 08:49   ` Nikolay Shaplov <dhyan@nataraj.su>
2026-01-16 15:14     ` Álvaro Herrera <alvherre@alvh.no-ip.org>
2026-01-16 21:52       ` Nathan Bossart <nathandbossart@gmail.com>
2026-01-21 18:44       ` Nikolay Shaplov <dhyan@nataraj.su>
2026-01-21 19:23         ` Álvaro Herrera <alvherre@alvh.no-ip.org>
2026-02-05 13:08           ` Nikolay Shaplov <dhyan@nataraj.su>
2026-05-10 11:09             ` Nikolay Shaplov <dhyan@nataraj.su>
2026-08-17 13:03           ` Peter Eisentraut <peter@eisentraut.org>
2026-08-17 14:16             ` Nikolay Shaplov <dhyan@nataraj.su>
2026-08-17 15:16             ` Álvaro Herrera <alvherre@alvh.no-ip.org>
2026-05-18 19:30 Re: [PATCH] ternary reloption type Andrey Rachitskiy <pl0h0yp1@gmail.com>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox