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
From 0da64249b33d3f2162081346c09bc50bcc546c0a Mon Sep 17 00:00:00 2001
From: Nikolay Shaplov <[email protected]>
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
From a801a6766ce6b3777c2b1bc3e7194165ff77385c Mon Sep 17 00:00:00 2001
From: Nikolay Shaplov <[email protected]>
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
From 7f420d57c962e8b5bc4d2d0751cf249ae9297983 Mon Sep 17 00:00:00 2001
From: Nikolay Shaplov <[email protected]>
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
From f5584b4e09ec65fe267ec6b27ff97b387707d5cd Mon Sep 17 00:00:00 2001
From: Nikolay Shaplov <[email protected]>
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