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

Attachment: review_ternary-options-part2___v2.patch
Description: Binary data


18 мая 2026 г., в 19:11, Nikolay Shaplov <[email protected]> написал(а):

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

Отправитель: Nikolay Shaplov <[email protected]>
Тема: Ответ: [PATCH] ternary reloption type
Дата: 10 мая 2026 г. в 16:09:28 GMT+5
Кому: Álvaro Herrera <[email protected]>, [email protected]
Копия: PostgreSQL Hackers <[email protected]>, Chris Travers <[email protected]>, Timur Magomedov <[email protected]>, Nathan Bossart <[email protected]>, Nikolay Shaplov <[email protected]>


В письме от четверг, 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
-----BEGIN PGP SIGNATURE-----

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



Reply via email to