Hi,
With the help of the new TAM routine 'relation_options', table access
methods can with this patch define their own reloptions
parser/validator.
These reloptions can be set via the following commands:
1. CREATE TABLE ... USING table_am
WITH (option1='value1', option2='value2');
2. ALTER TABLE ...
SET (option1 'value1', option2 'value2');
3. ALTER TABLE ... SET ACCESS METHOD table_am
OPTIONS (option1 'value1', option2 'value2');
When changing table's access method, the settings inherited from the
former TAM can be dropped (if not supported by the new TAM) via: DROP
option, or, updated via: SET option 'value'.
Currently, tables using different TAMs than heap are able to use heap's
reloptions (fillfactor, toast_tuple_target, etc...). With this patch
applied, this is not the case anymore: if the TAM needs to have access
to similar settings to heap ones, they have to explicitly define them.
The 2nd patch file includes a new test module 'dummy_table_am' which
implements a dummy table access method utilized to exercise TAM
reloptions. This test module is strongly based on what we already have
in 'dummy_index_am'. 'dummy_table_am' provides a complete example of TAM
reloptions definition.
This work is directly derived from SadhuPrasad's patch here [2]. Others
attempts were posted here [1] and here [3].
[1]
https://www.postgresql.org/message-id/flat/429fb58fa3218221bb17c7bf9e70e1aa6cfc6b5d.camel%40j-davis.com
[2]
https://www.postgresql.org/message-id/flat/caff0-cg4kzhdtyhmsonwixnzj16gwzpduxan8yf7pddub+g...@mail.gmail.com
[3]
https://www.postgresql.org/message-id/flat/AMUA1wBBBxfc3tKRLLdU64rb.1.1683276279979.Hmail.wuhao%40hashdata.cn
--
Julien Tachoires
>From 8968bb1cf92e373523377c79ff42e76dc9fc20ed Mon Sep 17 00:00:00 2001
From: Julien Tachoires <[email protected]>
Date: Sat, 1 Mar 2025 17:59:49 +0100
Subject: [PATCH 1/2] Allow table AMs to define their own reloptions
With the help of the new routine 'relation_options', table access
methods can now define their own reloptions.
These options can be set via the following commands:
1. CREATE TABLE ... USING table_am
WITH (option1='value1', option2='value2');
2. ALTER TABLE ...
SET (option1 'value1', option2 'value2');
3. ALTER TABLE ... SET ACCESS METHOD table_am
OPTIONS (option1 'value1', option2 'value2');
When changing table's access method, the settings from the former
TAM can be dropped (if not supported by the new TAM) via:
DROP option, or, updated via: SET option 'value'.
Before this commit, tables using different TAMs than heap were able
to use heap's reloptions (fillfactor, toast_tuple_target, etc...).
Now, this is not the case anymore: if the TAM needs to have access
to settings similar to heap ones, they must explicitly define them.
This work is directly derived from SadhuPrasad's patch named:
v4-0001-PATCH-V4-Per-table-storage-parameters-for-TableAM.patch
---
doc/src/sgml/ref/alter_table.sgml | 13 +-
doc/src/sgml/ref/create_table.sgml | 3 +-
src/backend/access/common/reloptions.c | 66 ++++++++-
src/backend/access/heap/heapam_handler.c | 2 +
src/backend/commands/foreigncmds.c | 2 +-
src/backend/commands/tablecmds.c | 180 ++++++++++++++++++++---
src/backend/parser/gram.y | 9 ++
src/backend/postmaster/autovacuum.c | 18 ++-
src/backend/utils/cache/relcache.c | 11 +-
src/include/access/reloptions.h | 6 +-
src/include/access/tableam.h | 10 ++
src/include/commands/defrem.h | 1 +
12 files changed, 286 insertions(+), 35 deletions(-)
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 8e56b8e59b0..e38200e20d2 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -76,7 +76,7 @@ ALTER TABLE [ IF EXISTS ] <replaceable class="parameter">name</replaceable>
CLUSTER ON <replaceable class="parameter">index_name</replaceable>
SET WITHOUT CLUSTER
SET WITHOUT OIDS
- SET ACCESS METHOD { <replaceable class="parameter">new_access_method</replaceable> | DEFAULT }
+ SET ACCESS METHOD { <replaceable class="parameter">new_access_method</replaceable> | DEFAULT } [ OPTIONS ( [ ADD | SET | DROP ] <replaceable class="parameter">option</replaceable> ['<replaceable class="parameter">value</replaceable>'] [, ... ] ) ]
SET TABLESPACE <replaceable class="parameter">new_tablespace</replaceable>
SET { LOGGED | UNLOGGED }
SET ( <replaceable class="parameter">storage_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] )
@@ -734,7 +734,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
</varlistentry>
<varlistentry id="sql-altertable-desc-set-access-method">
- <term><literal>SET ACCESS METHOD</literal></term>
+ <term><literal>SET ACCESS METHOD { <replaceable class="parameter">new_access_method</replaceable> | DEFAULT } [ OPTIONS ( [ ADD | SET | DROP ] <replaceable class="parameter">option</replaceable> ['<replaceable class="parameter">value</replaceable>'] [, ... ] ) ]</literal></term>
<listitem>
<para>
This form changes the access method of the table by rewriting it
@@ -752,6 +752,15 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
causing future partitions to default to
<varname>default_table_access_method</varname>.
</para>
+ <para>
+ Specifying <literal>OPTIONS</literal> allows to change options for
+ the table when changing the table access method.
+ <literal>ADD</literal>, <literal>SET</literal>, and
+ <literal>DROP</literal> specify the action to be performed.
+ <literal>ADD</literal> is assumed if no operation is explicitly
+ specified. Option names must be unique; names and values are also
+ validated using the table access method's library.
+ </para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 0a3e520f215..96ecb2ee060 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1548,7 +1548,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
Storage parameters for
indexes are documented in <xref linkend="sql-createindex"/>.
The storage parameters currently
- available for tables are listed below. For many of these parameters, as
+ available for tables are listed below. Each table may have different set of storage
+ parameters through different access methods. For many of these parameters, as
shown, there is an additional parameter with the same name prefixed with
<literal>toast.</literal>, which controls the behavior of the
table's secondary <acronym>TOAST</acronym> table, if any
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 59fb53e7707..93561936043 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -25,6 +25,7 @@
#include "access/reloptions.h"
#include "access/spgist_private.h"
#include "catalog/pg_type.h"
+#include "catalog/pg_am.h"
#include "commands/defrem.h"
#include "commands/tablespace.h"
#include "nodes/makefuncs.h"
@@ -34,6 +35,7 @@
#include "utils/guc.h"
#include "utils/memutils.h"
#include "utils/rel.h"
+#include "utils/syscache.h"
/*
* Contents of pg_class.reloptions
@@ -1396,7 +1398,7 @@ untransformRelOptions(Datum options)
*/
bytea *
extractRelOptions(HeapTuple tuple, TupleDesc tupdesc,
- amoptions_function amoptions)
+ amoptions_function amoptions, reloptions_function reloptsfun)
{
bytea *options;
bool isnull;
@@ -1418,7 +1420,8 @@ extractRelOptions(HeapTuple tuple, TupleDesc tupdesc,
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
case RELKIND_MATVIEW:
- options = heap_reloptions(classForm->relkind, datum, false);
+ options = table_reloptions(reloptsfun, InvalidOid, classForm->relkind,
+ datum, false);
break;
case RELKIND_PARTITIONED_TABLE:
options = partitioned_table_reloptions(datum, false);
@@ -2036,7 +2039,8 @@ view_reloptions(Datum reloptions, bool validate)
}
/*
- * Parse options for heaps, views and toast tables.
+ * Parse options for heaps, views and toast tables. This is the implementation
+ * of relOptions for the access method heap.
*/
bytea *
heap_reloptions(char relkind, Datum reloptions, bool validate)
@@ -2066,6 +2070,62 @@ heap_reloptions(char relkind, Datum reloptions, bool validate)
}
+/*
+ * Parse options for tables.
+ *
+ * reloptsfun Table AM's option parser function. Can be NULL if amid is
+ * valid. In this case we load the new TAM and use its option
+ * parser function.
+ * amid New table AM's Oid if any.
+ * relkind relation kind
+ * reloptions options as text[] datum
+ * validate error flag
+ */
+bytea *
+table_reloptions(reloptions_function reloptsfun, Oid amid, char relkind,
+ Datum reloptions, bool validate)
+{
+ /* amid and reloptsfun are mutually exclusive */
+ Assert((!OidIsValid(amid) && (reloptsfun != NULL)) || \
+ (OidIsValid(amid) && (reloptsfun == NULL)));
+
+ /* Parse/validate options using reloptsfun */
+ if (!OidIsValid(amid) && reloptsfun != NULL)
+ {
+ /* Assume function is strict */
+ if (!PointerIsValid(DatumGetPointer(reloptions)))
+ return NULL;
+
+ return reloptsfun(relkind, reloptions, validate);
+ }
+ /* Parse/validate options using the API of the new Table AM */
+ else if (OidIsValid(amid) && (reloptsfun == NULL))
+ {
+ const TableAmRoutine *routine;
+ HeapTuple atuple;
+ Form_pg_am aform;
+
+ atuple = SearchSysCache1(AMOID, ObjectIdGetDatum(amid));
+
+ if (!HeapTupleIsValid(atuple))
+ elog(ERROR, "cache lookup failed for access method %u", amid);
+
+ aform = (Form_pg_am) GETSTRUCT(atuple);
+ routine = GetTableAmRoutine(aform->amhandler);
+ ReleaseSysCache(atuple);
+
+ if (routine->relation_options != NULL)
+ return routine->relation_options(relkind, reloptions, validate);
+
+ return NULL;
+ }
+ else
+ {
+ /* Should not happen */
+ return NULL;
+ }
+}
+
/*
* Parse options for indexes.
*
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index e78682c3cef..23451c5af92 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -24,6 +24,7 @@
#include "access/heaptoast.h"
#include "access/multixact.h"
#include "access/rewriteheap.h"
+#include "access/reloptions.h"
#include "access/syncscan.h"
#include "access/tableam.h"
#include "access/tsmapi.h"
@@ -2678,6 +2679,7 @@ static const TableAmRoutine heapam_methods = {
.index_build_range_scan = heapam_index_build_range_scan,
.index_validate_scan = heapam_index_validate_scan,
+ .relation_options = heap_reloptions,
.relation_size = table_block_relation_size,
.relation_needs_toast_table = heapam_relation_needs_toast_table,
.relation_toast_am = heapam_relation_toast_am,
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index c14e038d54f..9dab5dfb999 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -62,7 +62,7 @@ static void import_error_callback(void *arg);
* processing, hence any validation should be done before this
* conversion.
*/
-static Datum
+Datum
optionListToArray(List *options)
{
ArrayBuildState *astate = NULL;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ce7d115667e..660de70fe9f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -635,6 +635,8 @@ static void ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel,
const char *tablespacename, LOCKMODE lockmode);
static void ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode);
static void ATExecSetTableSpaceNoStorage(Relation rel, Oid newTableSpace);
+static void ATExecSetAccessMethodOptions(Relation rel, List *defList, AlterTableType operation,
+ LOCKMODE lockmode, Oid newAccessMethodId);
static void ATExecSetRelOptions(Relation rel, List *defList,
AlterTableType operation,
LOCKMODE lockmode);
@@ -884,24 +886,6 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
if (!OidIsValid(ownerId))
ownerId = GetUserId();
- /*
- * Parse and validate reloptions, if any.
- */
- reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, validnsps,
- true, false);
-
- switch (relkind)
- {
- case RELKIND_VIEW:
- (void) view_reloptions(reloptions, true);
- break;
- case RELKIND_PARTITIONED_TABLE:
- (void) partitioned_table_reloptions(reloptions, true);
- break;
- default:
- (void) heap_reloptions(relkind, reloptions, true);
- }
-
if (stmt->ofTypename)
{
AclResult aclresult;
@@ -1016,6 +1000,29 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
accessMethodId = get_table_am_oid(default_table_access_method, false);
}
+ /*
+ * Parse and validate reloptions, if any.
+ */
+ reloptions = transformRelOptions((Datum) 0, stmt->options, NULL, validnsps,
+ true, false);
+ switch (relkind)
+ {
+ case RELKIND_VIEW:
+ (void) view_reloptions(reloptions, true);
+ break;
+ case RELKIND_PARTITIONED_TABLE:
+ (void) partitioned_table_reloptions(reloptions, true);
+ break;
+ case RELKIND_RELATION:
+ case RELKIND_TOASTVALUE:
+ case RELKIND_MATVIEW:
+ (void) table_reloptions(NULL, accessMethodId, relkind, reloptions,
+ true);
+ break;
+ default:
+ (void) heap_reloptions(relkind, reloptions, true);
+ }
+
/*
* Create the relation. Inherited defaults and CHECK constraints are
* passed in for immediate handling --- since they don't need parsing,
@@ -5497,6 +5504,9 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
tab->chgAccessMethod)
ATExecSetAccessMethodNoStorage(rel, tab->newAccessMethod);
+
+ ATExecSetAccessMethodOptions(rel, (List *) cmd->def, cmd->subtype,
+ lockmode, tab->newAccessMethod);
break;
case AT_SetTableSpace: /* SET TABLESPACE */
@@ -15690,6 +15700,138 @@ ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel, const char *tablespacen
tab->newTableSpace = tablespaceId;
}
+/* SET, ADD or DROP options in ALTER TABLE SET ACCESS METHOD */
+static void
+ATExecSetAccessMethodOptions(Relation rel, List *options, AlterTableType operation,
+ LOCKMODE lockmode, Oid newAccessMethodId)
+{
+ Oid relid;
+ Relation pgclass;
+ HeapTuple tuple;
+ HeapTuple newtuple;
+ Datum datum;
+ bool isnull;
+ Datum newOptions;
+ Datum repl_val[Natts_pg_class];
+ bool repl_null[Natts_pg_class];
+ bool repl_repl[Natts_pg_class];
+ List *resultOptions;
+ ListCell *optcell;
+
+ pgclass = table_open(RelationRelationId, RowExclusiveLock);
+
+ /* Fetch heap tuple */
+ relid = RelationGetRelid(rel);
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ /* Get the old reloptions */
+ datum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_reloptions, &isnull);
+
+ if (isnull)
+ datum = PointerGetDatum(NULL);
+
+ resultOptions = untransformRelOptions(datum);
+
+ foreach(optcell, options)
+ {
+ DefElem *od = lfirst(optcell);
+ ListCell *cell;
+
+ /* Search in existing options */
+ foreach(cell, resultOptions)
+ {
+ DefElem *def = lfirst(cell);
+
+ if (strcmp(def->defname, od->defname) == 0)
+ break;
+ }
+
+ /*
+ * It is possible to perform multiple SET/DROP actions on the same
+ * option. The standard permits this, as long as the options to be
+ * added are unique. Note that an unspecified action is taken to be
+ * ADD.
+ */
+ switch (od->defaction)
+ {
+ case DEFELEM_DROP:
+ if (!cell)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("option \"%s\" not found",
+ od->defname)));
+ resultOptions = list_delete_cell(resultOptions, cell);
+ break;
+
+ case DEFELEM_SET:
+ if (!cell)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("option \"%s\" not found",
+ od->defname)));
+ lfirst(cell) = od;
+ break;
+
+ case DEFELEM_ADD:
+ case DEFELEM_UNSPEC:
+ if (cell)
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_OBJECT),
+ errmsg("option \"%s\" provided more than once",
+ od->defname)));
+ resultOptions = lappend(resultOptions, od);
+ break;
+
+ default:
+ elog(ERROR, "unrecognized action %d on option \"%s\"",
+ (int) od->defaction, od->defname);
+ break;
+ }
+ }
+
+ newOptions = optionListToArray(resultOptions);
+
+ /*
+ * If the new table access method was not explicitly defined, then use the
+ * default one.
+ */
+ if (!OidIsValid(newAccessMethodId))
+ newAccessMethodId = get_table_am_oid(default_table_access_method, false);
+
+ /* Validate new options via the new Table Access Method API */
+ (void) table_reloptions(NULL, newAccessMethodId, rel->rd_rel->relkind,
+ newOptions, true);
+
+ /* Initialize buffers for new tuple values */
+ memset(repl_val, 0, sizeof(repl_val));
+ memset(repl_null, false, sizeof(repl_null));
+ memset(repl_repl, false, sizeof(repl_repl));
+
+ if (newOptions != (Datum) 0)
+ repl_val[Anum_pg_class_reloptions - 1] = newOptions;
+ else
+ repl_null[Anum_pg_class_reloptions - 1] = true;
+
+ repl_repl[Anum_pg_class_reloptions - 1] = true;
+
+ /* Everything looks good - update the tuple */
+ newtuple = heap_modify_tuple(tuple, RelationGetDescr(pgclass),
+ repl_val, repl_null, repl_repl);
+
+ CatalogTupleUpdate(pgclass, &newtuple->t_self, newtuple);
+
+ InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel),
+ InvalidOid);
+
+ ReleaseSysCache(tuple);
+
+ table_close(pgclass, RowExclusiveLock);
+
+ heap_freetuple(newtuple);
+}
+
/*
* Set, reset, or replace reloptions.
*/
@@ -15747,7 +15889,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation,
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
case RELKIND_MATVIEW:
- (void) heap_reloptions(rel->rd_rel->relkind, newOptions, true);
+ rel->rd_tableam->relation_options(rel->rd_rel->relkind, newOptions, true);
break;
case RELKIND_PARTITIONED_TABLE:
(void) partitioned_table_reloptions(newOptions, true);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7d99c9355c6..9f38463626f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -2901,6 +2901,15 @@ alter_table_cmd:
n->name = $4;
$$ = (Node *) n;
}
+ /* ALTER TABLE <name> SET ACCESS METHOD <amname> [OPTIONS]*/
+ | SET ACCESS METHOD name alter_generic_options
+ {
+ AlterTableCmd *n = makeNode(AlterTableCmd);
+ n->subtype = AT_SetAccessMethod;
+ n->name = $4;
+ n->def = (Node *) $5;
+ $$ = (Node *)n;
+ }
/* ALTER TABLE <name> SET TABLESPACE <tablespacename> */
| SET TABLESPACE name
{
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index ddb303f5201..20058327297 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -331,6 +331,7 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
+ reloptions_function reloptions,
int effective_multixact_freeze_max_age);
static void recheck_relation_needs_vacanalyze(Oid relid, AutoVacOpts *avopts,
Form_pg_class classForm,
@@ -345,7 +346,7 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
+ TupleDesc pg_class_desc, reloptions_function reloptions);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -2031,7 +2032,8 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = extract_autovac_opts(tuple, pg_class_desc,
+ classRel->rd_tableam->relation_options);
tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared,
relid);
@@ -2104,7 +2106,8 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = extract_autovac_opts(tuple, pg_class_desc,
+ classRel->rd_tableam->relation_options);
if (relopts == NULL)
{
av_relation *hentry;
@@ -2362,6 +2365,7 @@ do_autovacuum(void)
*/
MemoryContextSwitchTo(AutovacMemCxt);
tab = table_recheck_autovac(relid, table_toast_map, pg_class_desc,
+ classRel->rd_tableam->relation_options,
effective_multixact_freeze_max_age);
if (tab == NULL)
{
@@ -2687,7 +2691,8 @@ deleted2:
* be a risk; fortunately, it doesn't.
*/
static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
+extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc,
+ reloptions_function reloptions)
{
bytea *relopts;
AutoVacOpts *av;
@@ -2696,7 +2701,7 @@ extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
+ relopts = extractRelOptions(tup, pg_class_desc, NULL, reloptions);
if (relopts == NULL)
return NULL;
@@ -2719,6 +2724,7 @@ extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
static autovac_table *
table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
+ reloptions_function reloptions,
int effective_multixact_freeze_max_age)
{
Form_pg_class classForm;
@@ -2739,7 +2745,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
+ avopts = extract_autovac_opts(classTup, pg_class_desc, reloptions);
if (classForm->relkind == RELKIND_TOASTVALUE &&
avopts == NULL && table_toast_map != NULL)
{
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 398114373e9..b6c309c8bd2 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -466,6 +466,7 @@ RelationParseRelOptions(Relation relation, HeapTuple tuple)
{
bytea *options;
amoptions_function amoptsfn;
+ reloptions_function reloptsfn;
relation->rd_options = NULL;
@@ -477,13 +478,18 @@ RelationParseRelOptions(Relation relation, HeapTuple tuple)
{
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
- case RELKIND_VIEW:
case RELKIND_MATVIEW:
+ reloptsfn = relation->rd_tableam->relation_options;
+ amoptsfn = NULL;
+ break;
+ case RELKIND_VIEW:
case RELKIND_PARTITIONED_TABLE:
+ reloptsfn = NULL;
amoptsfn = NULL;
break;
case RELKIND_INDEX:
case RELKIND_PARTITIONED_INDEX:
+ reloptsfn = NULL;
amoptsfn = relation->rd_indam->amoptions;
break;
default:
@@ -495,7 +501,8 @@ RelationParseRelOptions(Relation relation, HeapTuple tuple)
* we might not have any other for pg_class yet (consider executing this
* code for pg_class itself)
*/
- options = extractRelOptions(tuple, GetPgClassDescriptor(), amoptsfn);
+ options = extractRelOptions(tuple, GetPgClassDescriptor(),
+ amoptsfn, reloptsfn);
/*
* Copy parsed data into CacheMemoryContext. To guard against the
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index 43445cdcc6c..d0ef7918856 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -21,6 +21,7 @@
#include "access/amapi.h"
#include "access/htup.h"
+#include "access/tableam.h"
#include "access/tupdesc.h"
#include "nodes/pg_list.h"
#include "storage/lock.h"
@@ -224,7 +225,8 @@ extern Datum transformRelOptions(Datum oldOptions, List *defList,
bool acceptOidsOff, bool isReset);
extern List *untransformRelOptions(Datum options);
extern bytea *extractRelOptions(HeapTuple tuple, TupleDesc tupdesc,
- amoptions_function amoptions);
+ amoptions_function amoptions,
+ reloptions_function reloptsfun);
extern void *build_reloptions(Datum reloptions, bool validate,
relopt_kind kind,
Size relopt_struct_size,
@@ -238,6 +240,8 @@ extern bytea *default_reloptions(Datum reloptions, bool validate,
extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate);
extern bytea *view_reloptions(Datum reloptions, bool validate);
extern bytea *partitioned_table_reloptions(Datum reloptions, bool validate);
+extern bytea *table_reloptions(reloptions_function reloptsfun, Oid amid, char relkind,
+ Datum reloptions, bool validate);
extern bytea *index_reloptions(amoptions_function amoptions, Datum reloptions,
bool validate);
extern bytea *attribute_reloptions(Datum reloptions, bool validate);
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 131c050c15f..79ad91d201c 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -276,6 +276,14 @@ typedef void (*IndexBuildCallback) (Relation index,
bool tupleIsAlive,
void *state);
+/*
+ * Callback in charge of parsing and validating the table reloptions.
+ * It returns parsed options in bytea format.
+ */
+typedef bytea *(*reloptions_function) (char relkind,
+ Datum reloptions,
+ bool validate);
+
/*
* API struct for a table AM. Note this must be allocated in a
* server-lifetime manner, typically as a static const struct, which then gets
@@ -715,6 +723,8 @@ typedef struct TableAmRoutine
* ------------------------------------------------------------------------
*/
+ reloptions_function relation_options;
+
/*
* See table_relation_size().
*
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index 6d9348bac80..cd0aaaa0b93 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -136,6 +136,7 @@ extern ObjectAddress AlterUserMapping(AlterUserMappingStmt *stmt);
extern Oid RemoveUserMapping(DropUserMappingStmt *stmt);
extern void CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid);
extern void ImportForeignSchema(ImportForeignSchemaStmt *stmt);
+extern Datum optionListToArray(List *options);
extern Datum transformGenericOptions(Oid catalogId,
Datum oldOptions,
List *options,
--
2.39.5
>From 993694f7c610c23e8b5ebf99ab501b1aede87bb9 Mon Sep 17 00:00:00 2001
From: Julien Tachoires <[email protected]>
Date: Sat, 1 Mar 2025 20:50:13 +0100
Subject: [PATCH 2/2] Add the "dummy_table_am" test module
This test module is in charge of testing TAM reloptions. It's very
similar to what we do in dummy_index_am as we have to exercise the
exact same kind of feature.
---
src/test/modules/Makefile | 1 +
src/test/modules/dummy_table_am/Makefile | 20 +
src/test/modules/dummy_table_am/README | 14 +
.../dummy_table_am/dummy_table_am--1.0.sql | 13 +
.../modules/dummy_table_am/dummy_table_am.c | 588 ++++++++++++++++++
.../dummy_table_am/dummy_table_am.control | 5 +
.../dummy_table_am/expected/reloptions.out | 181 ++++++
src/test/modules/dummy_table_am/meson.build | 33 +
.../modules/dummy_table_am/sql/reloptions.sql | 99 +++
src/test/modules/meson.build | 1 +
10 files changed, 955 insertions(+)
create mode 100644 src/test/modules/dummy_table_am/Makefile
create mode 100644 src/test/modules/dummy_table_am/README
create mode 100644 src/test/modules/dummy_table_am/dummy_table_am--1.0.sql
create mode 100644 src/test/modules/dummy_table_am/dummy_table_am.c
create mode 100644 src/test/modules/dummy_table_am/dummy_table_am.control
create mode 100644 src/test/modules/dummy_table_am/expected/reloptions.out
create mode 100644 src/test/modules/dummy_table_am/meson.build
create mode 100644 src/test/modules/dummy_table_am/sql/reloptions.sql
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 4e4be3fa511..8fe2a2904d6 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -9,6 +9,7 @@ SUBDIRS = \
commit_ts \
delay_execution \
dummy_index_am \
+ dummy_table_am \
dummy_seclabel \
libpq_pipeline \
oauth_validator \
diff --git a/src/test/modules/dummy_table_am/Makefile b/src/test/modules/dummy_table_am/Makefile
new file mode 100644
index 00000000000..94837dff392
--- /dev/null
+++ b/src/test/modules/dummy_table_am/Makefile
@@ -0,0 +1,20 @@
+# src/test/modules/dummy_table_am/Makefile
+
+MODULES = dummy_table_am
+
+EXTENSION = dummy_table_am
+DATA = dummy_table_am--1.0.sql
+PGFILEDESC = "dummy_table_am - table access method template"
+
+REGRESS = reloptions
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/dummy_table_am
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/dummy_table_am/README b/src/test/modules/dummy_table_am/README
new file mode 100644
index 00000000000..50cf08ee3b1
--- /dev/null
+++ b/src/test/modules/dummy_table_am/README
@@ -0,0 +1,14 @@
+Dummy Table AM
+==============
+
+Dummy table AM is a module for testing any facility usable by a table
+access method, whose code is kept a maximum simple.
+
+This includes tests for all relation option types:
+- boolean
+- enum
+- integer
+- real
+- strings (with and without NULL as default)
+
+It also includes tests related to unrecognized options.
diff --git a/src/test/modules/dummy_table_am/dummy_table_am--1.0.sql b/src/test/modules/dummy_table_am/dummy_table_am--1.0.sql
new file mode 100644
index 00000000000..12ad3ad174b
--- /dev/null
+++ b/src/test/modules/dummy_table_am/dummy_table_am--1.0.sql
@@ -0,0 +1,13 @@
+/* src/test/modules/dummy_table_am/dummy_table_am--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION dummy_table_am" to load this file. \quit
+
+CREATE FUNCTION dummy_table_am_handler(internal)
+RETURNS table_am_handler
+AS 'MODULE_PATHNAME'
+LANGUAGE C;
+
+-- Access method
+CREATE ACCESS METHOD dummy_table_am TYPE TABLE HANDLER dummy_table_am_handler;
+COMMENT ON ACCESS METHOD dummy_table_am IS 'Dummy Table Access Method';
diff --git a/src/test/modules/dummy_table_am/dummy_table_am.c b/src/test/modules/dummy_table_am/dummy_table_am.c
new file mode 100644
index 00000000000..a473bc7dd9b
--- /dev/null
+++ b/src/test/modules/dummy_table_am/dummy_table_am.c
@@ -0,0 +1,588 @@
+/*-------------------------------------------------------------------------
+ *
+ * dummy_table_am.c
+ * Table AM templae main file
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/test/modules/dummy_table_am/dummy_table_am.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+
+#include "access/hio.h"
+#include "access/relscan.h"
+#include "access/reloptions.h"
+#include "access/tableam.h"
+#include "access/sdir.h"
+#include "access/skey.h"
+#include "executor/tuptable.h"
+#include "utils/relcache.h"
+#include "utils/snapshot.h"
+
+
+PG_MODULE_MAGIC;
+
+/* Base structures for scans */
+typedef struct DummyScanDescData
+{
+ TableScanDescData rs_base; /* AM independent part of the descriptor */
+
+ /* Add more fields here as needed by the AM. */
+} DummyScanDescData;
+typedef struct DummyScanDescData *DummyScanDesc;
+
+/* parse table for fillRelOptions */
+static relopt_parse_elt dt_relopt_tab[7];
+
+/* Kind of relation options for dummy index */
+static relopt_kind dt_relopt_kind;
+
+typedef enum DummyAmEnum
+{
+ DUMMY_AM_ENUM_ONE,
+ DUMMY_AM_ENUM_TWO,
+} DummyAmEnum;
+
+/* Dummy table options */
+typedef struct DummyTableOptions
+{
+ int32 vl_len_; /* varlena header (do not touch directly!) */
+ int option_int;
+ double option_real;
+ bool option_bool;
+ DummyAmEnum option_enum;
+ int option_string_val_offset;
+ int option_string_null_offset;
+ int fillfactor;
+} DummyTableOptions;
+
+static relopt_enum_elt_def dummyAmEnumValues[] =
+{
+ {"one", DUMMY_AM_ENUM_ONE},
+ {"two", DUMMY_AM_ENUM_TWO},
+ {(const char *) NULL} /* list terminator */
+};
+
+/* ------------------------------------------------------------------------
+ * Dummy Access Method Interface
+ * ------------------------------------------------------------------------
+ */
+
+static const TupleTableSlotOps *
+dummy_slot_callbacks(Relation relation)
+{
+ return &TTSOpsMinimalTuple;
+}
+
+static TableScanDesc
+dummy_scan_begin(Relation relation, Snapshot snapshot, int nkeys, ScanKey key,
+ ParallelTableScanDesc parallel_scan, uint32 flags)
+{
+ DummyScanDesc scan;
+
+ scan = (DummyScanDesc) palloc(sizeof(DummyScanDescData));
+
+ scan->rs_base.rs_rd = relation;
+ scan->rs_base.rs_snapshot = snapshot;
+ scan->rs_base.rs_nkeys = nkeys;
+ scan->rs_base.rs_flags = flags;
+ scan->rs_base.rs_parallel = parallel_scan;
+
+ return (TableScanDesc) scan;
+}
+
+static void
+dummy_scan_end(TableScanDesc sscan)
+{
+ DummyScanDesc scan = (DummyScanDesc) sscan;
+
+ pfree(scan);
+
+ return;
+}
+
+static void
+dummy_scan_rescan(TableScanDesc sscan, ScanKey key, bool set_params,
+ bool allow_strat, bool allow_sync, bool allow_pagemode)
+{
+ return;
+}
+
+static bool
+dummy_scan_getnextslot(TableScanDesc sscan, ScanDirection direction,
+ TupleTableSlot *slot)
+{
+ return true;
+}
+
+static void
+dummy_scan_set_tidrange(TableScanDesc sscan, ItemPointer mintid,
+ ItemPointer maxtid)
+{
+ return;
+}
+
+static bool
+dummy_scan_getnextslot_tidrange(TableScanDesc sscan, ScanDirection direction,
+ TupleTableSlot *slot)
+{
+ return true;
+}
+
+static Size
+dummy_parallelscan_estimate(Relation rel)
+{
+ return 0;
+}
+
+static Size
+dummy_parallelscan_initialize(Relation rel, ParallelTableScanDesc pscan)
+{
+ return 0;
+}
+
+static void
+dummy_parallelscan_reinitialize(Relation rel, ParallelTableScanDesc pscan)
+{
+ return;
+}
+
+static IndexFetchTableData *
+dummy_index_fetch_begin(Relation rel)
+{
+ return NULL;
+}
+
+static void
+dummy_index_fetch_reset(IndexFetchTableData *scan)
+{
+ return;
+}
+
+static void
+dummy_index_fetch_end(IndexFetchTableData *scan)
+{
+ return;
+}
+
+static bool
+dummy_index_fetch_tuple(struct IndexFetchTableData *scan, ItemPointer tid,
+ Snapshot snapshot, TupleTableSlot *slot,
+ bool *call_again, bool *all_dead)
+{
+ return true;
+}
+
+static void
+dummy_tuple_insert(Relation relation, TupleTableSlot *slot, CommandId cid,
+ int options, BulkInsertStateData *bistate)
+{
+ DummyTableOptions *relopts;
+
+ relopts = (DummyTableOptions *) relation->rd_options;
+
+ elog(NOTICE, "option_int=%d, option_real=%f, option_bool=%d, option_enum=%d",
+ relopts->option_int, relopts->option_real, relopts->option_bool, relopts->option_enum);
+
+ return;
+}
+
+static void
+dummy_tuple_insert_speculative(Relation relation, TupleTableSlot *slot,
+ CommandId cid, int options,
+ BulkInsertStateData *bistate, uint32 specToken)
+{
+ return;
+}
+
+static void
+dummy_tuple_complete_speculative(Relation relation, TupleTableSlot *slot,
+ uint32 specToken, bool succeeded)
+{
+ return;
+}
+
+static void
+dummy_multi_insert(Relation relation, TupleTableSlot **slots, int ntuples,
+ CommandId cid, int options, BulkInsertStateData *bistate)
+{
+ return;
+}
+
+static TM_Result
+dummy_tuple_delete(Relation relation, ItemPointer tid, CommandId cid,
+ Snapshot snapshot, Snapshot crosscheck, bool wait,
+ TM_FailureData *tmfd, bool changingPart)
+{
+ return TM_Ok;
+}
+
+static TM_Result
+dummy_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot,
+ CommandId cid, Snapshot snapshot, Snapshot crosscheck,
+ bool wait, TM_FailureData *tmfd,
+ LockTupleMode *lockmode, TU_UpdateIndexes *update_indexes)
+{
+ return TM_Ok;
+}
+
+static TM_Result
+dummy_tuple_lock(Relation relation, ItemPointer tid, Snapshot snapshot,
+ TupleTableSlot *slot, CommandId cid, LockTupleMode mode,
+ LockWaitPolicy wait_policy, uint8 flags,
+ TM_FailureData *tmfd)
+{
+ return TM_Ok;
+}
+
+static bool
+dummy_fetch_row_version(Relation relation, ItemPointer tid,
+ Snapshot snapshot, TupleTableSlot *slot)
+{
+ return false;
+}
+
+static void
+dummy_get_latest_tid(TableScanDesc sscan, ItemPointer tid)
+{
+ return;
+}
+
+static bool
+dummy_tuple_tid_valid(TableScanDesc scan, ItemPointer tid)
+{
+ return false;
+}
+
+static bool
+dummy_tuple_satisfies_snapshot(Relation rel, TupleTableSlot *slot,
+ Snapshot snapshot)
+{
+ return false;
+}
+
+static TransactionId
+dummy_index_delete_tuples(Relation rel, TM_IndexDeleteOp *delstate)
+{
+ return InvalidTransactionId;
+}
+
+static void
+dummy_relation_set_new_filelocator(Relation rel,
+ const RelFileLocator *newrlocator,
+ char persistence,
+ TransactionId *freezeXid,
+ MultiXactId *minmulti)
+{
+ return;
+}
+
+static void
+dummy_relation_nontransactional_truncate(Relation rel)
+{
+ return;
+}
+
+static void
+dummy_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
+{
+ return;
+}
+
+static void
+dummy_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
+ Relation OldIndex, bool use_sort,
+ TransactionId OldestXmin,
+ TransactionId *xid_cutoff,
+ MultiXactId *multi_cutoff,
+ double *num_tuples,
+ double *tups_vacuumed,
+ double *tups_recently_dead)
+{
+ return;
+}
+
+static void
+dummy_relation_vacuum(Relation rel, struct VacuumParams *params,
+ BufferAccessStrategy bstrategy)
+{
+ return;
+}
+
+static bool
+dummy_scan_analyze_next_block(TableScanDesc scan, ReadStream *stream)
+{
+ return false;
+}
+
+static bool
+dummy_scan_analyze_next_tuple(TableScanDesc scan, TransactionId OldestXmin,
+ double *liverows, double *deadrows,
+ TupleTableSlot *slot)
+{
+ return false;
+}
+
+static double
+dummy_index_build_range_scan(Relation heapRelation,
+ Relation indexRelation,
+ struct IndexInfo *indexInfo,
+ bool allow_sync,
+ bool anyvisible,
+ bool progress,
+ BlockNumber start_blockno,
+ BlockNumber numblocks,
+ IndexBuildCallback callback,
+ void *callback_state,
+ TableScanDesc scan)
+{
+ return 0;
+}
+
+static void
+dummy_index_validate_scan(Relation heapRelation,
+ Relation indexRelation,
+ struct IndexInfo *indexInfo,
+ Snapshot snapshot,
+ struct ValidateIndexState *state)
+{
+ return;
+}
+
+static uint64
+dummy_relation_size(Relation rel, ForkNumber forkNumber)
+{
+ return 0;
+}
+
+static bool
+dummy_relation_needs_toast_table(Relation rel)
+{
+ return false;
+}
+
+static Oid
+dummy_relation_toast_am(Relation rel)
+{
+ return InvalidOid;
+}
+
+static void
+dummy_relation_fetch_toast_slice(Relation toastrel, Oid valueid, int32 attrsize,
+ int32 sliceoffset, int32 slicelength,
+ struct varlena *result)
+{
+ return;
+}
+
+static void
+dummy_relation_estimate_size(Relation rel, int32 *attr_widths,
+ BlockNumber *pages, double *tuples,
+ double *allvisfrac)
+{
+ return;
+}
+
+static bool
+dummy_scan_bitmap_next_block(TableScanDesc scan, BlockNumber *blockno,
+ bool *recheck, uint64 *lossy_pages,
+ uint64 *exact_pages)
+{
+ return false;
+}
+
+static bool
+dummy_scan_bitmap_next_tuple(TableScanDesc scan, TupleTableSlot *slot)
+{
+ return false;
+}
+
+static bool
+dummy_scan_sample_next_block(TableScanDesc scan, struct SampleScanState *scanstate)
+{
+ return false;
+}
+
+static bool
+dummy_scan_sample_next_tuple(TableScanDesc scan, struct SampleScanState *scanstate,
+ TupleTableSlot *slot)
+{
+ return false;
+}
+
+static bytea *
+dummy_relation_options(char relkind, Datum reloptions, bool validate)
+{
+ return (bytea *) build_reloptions(reloptions, validate,
+ dt_relopt_kind,
+ sizeof(DummyTableOptions),
+ dt_relopt_tab, lengthof(dt_relopt_tab));
+}
+
+/*
+ * Validation function for string relation options.
+ */
+static void
+validate_string_option(const char *value)
+{
+ ereport(NOTICE,
+ (errmsg("new option value for string parameter %s",
+ value ? value : "NULL")));
+}
+
+/*
+ * This function creates a full set of relation option types,
+ * with various patterns.
+ */
+static void
+create_reloptions_table(void)
+{
+ dt_relopt_kind = add_reloption_kind();
+
+ add_int_reloption(dt_relopt_kind, "option_int",
+ "Integer option for dummy_table_am",
+ 10, -10, 100, AccessExclusiveLock);
+ dt_relopt_tab[0].optname = "option_int";
+ dt_relopt_tab[0].opttype = RELOPT_TYPE_INT;
+ dt_relopt_tab[0].offset = offsetof(DummyTableOptions, option_int);
+
+ add_real_reloption(dt_relopt_kind, "option_real",
+ "Real option for dummy_table_am",
+ 3.1415, -10, 100, AccessExclusiveLock);
+ dt_relopt_tab[1].optname = "option_real";
+ dt_relopt_tab[1].opttype = RELOPT_TYPE_REAL;
+ dt_relopt_tab[1].offset = offsetof(DummyTableOptions, option_real);
+
+ add_bool_reloption(dt_relopt_kind, "option_bool",
+ "Boolean option for dummy_table_am",
+ true, AccessExclusiveLock);
+ dt_relopt_tab[2].optname = "option_bool";
+ dt_relopt_tab[2].opttype = RELOPT_TYPE_BOOL;
+ dt_relopt_tab[2].offset = offsetof(DummyTableOptions, option_bool);
+
+ add_enum_reloption(dt_relopt_kind, "option_enum",
+ "Enum option for dummy_table_am",
+ dummyAmEnumValues,
+ DUMMY_AM_ENUM_ONE,
+ "Valid values are \"one\" and \"two\".",
+ AccessExclusiveLock);
+ dt_relopt_tab[3].optname = "option_enum";
+ dt_relopt_tab[3].opttype = RELOPT_TYPE_ENUM;
+ dt_relopt_tab[3].offset = offsetof(DummyTableOptions, option_enum);
+
+ add_string_reloption(dt_relopt_kind, "option_string_val",
+ "String option for dummy_table_am with non-NULL default",
+ "DefaultValue", &validate_string_option,
+ AccessExclusiveLock);
+ dt_relopt_tab[4].optname = "option_string_val";
+ dt_relopt_tab[4].opttype = RELOPT_TYPE_STRING;
+ dt_relopt_tab[4].offset = offsetof(DummyTableOptions,
+ option_string_val_offset);
+
+ /*
+ * String option for dummy_table_am with NULL default, and without
+ * description.
+ */
+ add_string_reloption(dt_relopt_kind, "option_string_null",
+ NULL, /* description */
+ NULL, &validate_string_option,
+ AccessExclusiveLock);
+ dt_relopt_tab[5].optname = "option_string_null";
+ dt_relopt_tab[5].opttype = RELOPT_TYPE_STRING;
+ dt_relopt_tab[5].offset = offsetof(DummyTableOptions,
+ option_string_null_offset);
+
+ /*
+ * fillfactor will be used to check reloption conversion when changing
+ * table access method between heap AM and dummy_table_am.
+ */
+ add_int_reloption(dt_relopt_kind, "fillfactor",
+ "Fillfactor option for dummy_table_am",
+ 10, 0, 90, AccessExclusiveLock);
+ dt_relopt_tab[6].optname = "fillfactor";
+ dt_relopt_tab[6].opttype = RELOPT_TYPE_INT;
+ dt_relopt_tab[6].offset = offsetof(DummyTableOptions, fillfactor);
+}
+
+
+/*
+ * Table Access Method API
+ */
+static const TableAmRoutine dummy_table_am_methods = {
+ .type = T_TableAmRoutine,
+
+ .slot_callbacks = dummy_slot_callbacks,
+ .scan_begin = dummy_scan_begin,
+ .scan_end = dummy_scan_end,
+ .scan_rescan = dummy_scan_rescan,
+ .scan_getnextslot = dummy_scan_getnextslot,
+
+ .scan_set_tidrange = dummy_scan_set_tidrange,
+ .scan_getnextslot_tidrange = dummy_scan_getnextslot_tidrange,
+
+ .parallelscan_estimate = dummy_parallelscan_estimate,
+ .parallelscan_initialize = dummy_parallelscan_initialize,
+ .parallelscan_reinitialize = dummy_parallelscan_reinitialize,
+
+ .index_fetch_begin = dummy_index_fetch_begin,
+ .index_fetch_reset = dummy_index_fetch_reset,
+ .index_fetch_end = dummy_index_fetch_end,
+ .index_fetch_tuple = dummy_index_fetch_tuple,
+
+ .tuple_insert = dummy_tuple_insert,
+ .tuple_insert_speculative = dummy_tuple_insert_speculative,
+ .tuple_complete_speculative = dummy_tuple_complete_speculative,
+ .multi_insert = dummy_multi_insert,
+ .tuple_delete = dummy_tuple_delete,
+ .tuple_update = dummy_tuple_update,
+ .tuple_lock = dummy_tuple_lock,
+
+ .tuple_fetch_row_version = dummy_fetch_row_version,
+ .tuple_get_latest_tid = dummy_get_latest_tid,
+ .tuple_tid_valid = dummy_tuple_tid_valid,
+ .tuple_satisfies_snapshot = dummy_tuple_satisfies_snapshot,
+ .index_delete_tuples = dummy_index_delete_tuples,
+
+ .relation_set_new_filelocator = dummy_relation_set_new_filelocator,
+ .relation_nontransactional_truncate = dummy_relation_nontransactional_truncate,
+ .relation_copy_data = dummy_relation_copy_data,
+ .relation_copy_for_cluster = dummy_relation_copy_for_cluster,
+ .relation_vacuum = dummy_relation_vacuum,
+ .scan_analyze_next_block = dummy_scan_analyze_next_block,
+ .scan_analyze_next_tuple = dummy_scan_analyze_next_tuple,
+ .index_build_range_scan = dummy_index_build_range_scan,
+ .index_validate_scan = dummy_index_validate_scan,
+
+ .relation_size = dummy_relation_size,
+ .relation_needs_toast_table = dummy_relation_needs_toast_table,
+ .relation_toast_am = dummy_relation_toast_am,
+ .relation_fetch_toast_slice = dummy_relation_fetch_toast_slice,
+ .relation_estimate_size = dummy_relation_estimate_size,
+ .relation_options = dummy_relation_options,
+
+ .scan_bitmap_next_block = dummy_scan_bitmap_next_block,
+ .scan_bitmap_next_tuple = dummy_scan_bitmap_next_tuple,
+ .scan_sample_next_block = dummy_scan_sample_next_block,
+ .scan_sample_next_tuple = dummy_scan_sample_next_tuple
+};
+
+PG_FUNCTION_INFO_V1(dummy_table_am_handler);
+
+Datum
+dummy_table_am_handler(PG_FUNCTION_ARGS)
+{
+ PG_RETURN_POINTER(&dummy_table_am_methods);
+}
+
+void
+_PG_init(void)
+{
+ create_reloptions_table();
+}
diff --git a/src/test/modules/dummy_table_am/dummy_table_am.control b/src/test/modules/dummy_table_am/dummy_table_am.control
new file mode 100644
index 00000000000..08f2f868d49
--- /dev/null
+++ b/src/test/modules/dummy_table_am/dummy_table_am.control
@@ -0,0 +1,5 @@
+# dummy_table_am extension
+comment = 'dummy_table_am - table access method template'
+default_version = '1.0'
+module_pathname = '$libdir/dummy_table_am'
+relocatable = true
diff --git a/src/test/modules/dummy_table_am/expected/reloptions.out b/src/test/modules/dummy_table_am/expected/reloptions.out
new file mode 100644
index 00000000000..4c08ac4e3ac
--- /dev/null
+++ b/src/test/modules/dummy_table_am/expected/reloptions.out
@@ -0,0 +1,181 @@
+-- Tests for relation options
+CREATE EXTENSION dummy_table_am;
+CREATE TABLE dummy_test_tab (i int4) USING dummy_table_am;
+-- Silence validation checks for strings
+SET client_min_messages TO 'warning';
+-- Test with default values.
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+ unnest
+--------
+(0 rows)
+
+DROP TABLE dummy_test_tab;
+-- Test with full set of options.
+-- Allow validation checks for strings
+SET client_min_messages TO 'notice';
+CREATE TABLE dummy_test_tab (i int4)
+ USING dummy_table_am WITH (
+ option_bool = false,
+ option_int = 5,
+ option_real = 3.1,
+ option_enum = 'two',
+ option_string_val = NULL,
+ option_string_null = 'val');
+NOTICE: new option value for string parameter null
+NOTICE: new option value for string parameter val
+-- Silence again validation checks for strings until the end of the test.
+SET client_min_messages TO 'warning';
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+ unnest
+------------------------
+ option_bool=false
+ option_int=5
+ option_real=3.1
+ option_enum=two
+ option_string_val=null
+ option_string_null=val
+(6 rows)
+
+-- ALTER TABLE .. SET
+ALTER TABLE dummy_test_tab SET (option_int = 10);
+ALTER TABLE dummy_test_tab SET (option_bool = true);
+ALTER TABLE dummy_test_tab SET (option_real = 3.2);
+ALTER TABLE dummy_test_tab SET (option_string_val = 'val2');
+ALTER TABLE dummy_test_tab SET (option_string_null = NULL);
+ALTER TABLE dummy_test_tab SET (option_enum = 'one');
+ALTER TABLE dummy_test_tab 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_tab';
+ unnest
+-------------------------
+ option_int=10
+ option_bool=true
+ option_real=3.2
+ option_string_val=val2
+ option_string_null=null
+ option_enum=one
+(6 rows)
+
+-- ALTER TABLE .. RESET
+ALTER TABLE dummy_test_tab RESET (option_int);
+ALTER TABLE dummy_test_tab RESET (option_bool);
+ALTER TABLE dummy_test_tab RESET (option_real);
+ALTER TABLE dummy_test_tab RESET (option_enum);
+ALTER TABLE dummy_test_tab RESET (option_string_val);
+ALTER TABLE dummy_test_tab RESET (option_string_null);
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+ unnest
+--------
+(0 rows)
+
+-- Cross-type checks for reloption values
+-- Integer
+ALTER TABLE dummy_test_tab SET (option_int = 3.3); -- ok
+ALTER TABLE dummy_test_tab SET (option_int = true); -- error
+ERROR: invalid value for integer option "option_int": true
+ALTER TABLE dummy_test_tab SET (option_int = 'val3'); -- error
+ERROR: invalid value for integer option "option_int": val3
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+ unnest
+----------------
+ option_int=3.3
+(1 row)
+
+ALTER TABLE dummy_test_tab RESET (option_int);
+-- Boolean
+ALTER TABLE dummy_test_tab SET (option_bool = 4); -- error
+ERROR: invalid value for boolean option "option_bool": 4
+ALTER TABLE dummy_test_tab SET (option_bool = 1); -- ok, as true
+ALTER TABLE dummy_test_tab SET (option_bool = 3.4); -- error
+ERROR: invalid value for boolean option "option_bool": 3.4
+ALTER TABLE dummy_test_tab SET (option_bool = 'val4'); -- error
+ERROR: invalid value for boolean option "option_bool": val4
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+ unnest
+---------------
+ option_bool=1
+(1 row)
+
+ALTER TABLE dummy_test_tab RESET (option_bool);
+-- Float
+ALTER TABLE dummy_test_tab SET (option_real = 4); -- ok
+ALTER TABLE dummy_test_tab SET (option_real = true); -- error
+ERROR: invalid value for floating point option "option_real": true
+ALTER TABLE dummy_test_tab SET (option_real = 'val5'); -- error
+ERROR: invalid value for floating point option "option_real": val5
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+ unnest
+---------------
+ option_real=4
+(1 row)
+
+ALTER TABLE dummy_test_tab RESET (option_real);
+-- Enum
+ALTER TABLE dummy_test_tab SET (option_enum = 'one'); -- ok
+ALTER TABLE dummy_test_tab SET (option_enum = 0); -- error
+ERROR: invalid value for enum option "option_enum": 0
+DETAIL: Valid values are "one" and "two".
+ALTER TABLE dummy_test_tab SET (option_enum = true); -- error
+ERROR: invalid value for enum option "option_enum": true
+DETAIL: Valid values are "one" and "two".
+ALTER TABLE dummy_test_tab SET (option_enum = 'three'); -- error
+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_tab';
+ unnest
+-----------------
+ option_enum=one
+(1 row)
+
+ALTER TABLE dummy_test_tab RESET (option_enum);
+-- String
+ALTER TABLE dummy_test_tab SET (option_string_val = 4); -- ok
+ALTER TABLE dummy_test_tab SET (option_string_val = 3.5); -- ok
+ALTER TABLE dummy_test_tab SET (option_string_val = true); -- ok, as "true"
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+ unnest
+------------------------
+ option_string_val=true
+(1 row)
+
+ALTER TABLE dummy_test_tab RESET (option_string_val);
+DROP TABLE dummy_test_tab;
+-- ALTER TABLE SET ACCESS METHOD OPTIONS
+CREATE TABLE heap_tab (i INT4) WITH (fillfactor=100, toast_tuple_target=1000);
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'heap_tab';
+ unnest
+-------------------------
+ fillfactor=100
+ toast_tuple_target=1000
+(2 rows)
+
+-- error: fillfactor is out of bounds: maximum value from the new table am is 90
+ALTER TABLE heap_tab SET ACCESS METHOD dummy_table_am;
+ERROR: value 100 out of bounds for option "fillfactor"
+DETAIL: Valid values are between "0" and "90".
+-- error: toast_tuple_target does not exist in the new table AM
+ALTER TABLE heap_tab SET ACCESS METHOD dummy_table_am OPTIONS (SET fillfactor '50');
+ERROR: unrecognized parameter "toast_tuple_target"
+-- error: adding is not possible when the parameter is already defined in source reloptions
+ALTER TABLE heap_tab SET ACCESS METHOD dummy_table_am OPTIONS (ADD fillfactor '50');
+ERROR: option "fillfactor" provided more than once
+-- error: the specified option we want to drop does not exist
+ALTER TABLE heap_tab SET ACCESS METHOD dummy_table_am OPTIONS (DROP does_not_exist);
+ERROR: option "does_not_exist" not found
+-- error: adding unrecognized parameter
+ALTER TABLE heap_tab SET ACCESS METHOD dummy_table_am OPTIONS (SET fillfactor '50', DROP toast_tuple_target, ADD unrecognized 'foo');
+ERROR: unrecognized parameter "unrecognized"
+-- ok
+ALTER TABLE heap_tab SET ACCESS METHOD dummy_table_am OPTIONS (DROP fillfactor, DROP toast_tuple_target, option_int '1', option_bool 'true', option_real '0.001', option_enum 'one', option_string_val 'hello');
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'heap_tab';
+ unnest
+-------------------------
+ option_int=1
+ option_bool=true
+ option_real=0.001
+ option_enum=one
+ option_string_val=hello
+(5 rows)
+
+DROP TABLE heap_tab;
diff --git a/src/test/modules/dummy_table_am/meson.build b/src/test/modules/dummy_table_am/meson.build
new file mode 100644
index 00000000000..6b197b15ffa
--- /dev/null
+++ b/src/test/modules/dummy_table_am/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2022-2025, PostgreSQL Global Development Group
+
+dummy_table_am_sources = files(
+ 'dummy_table_am.c',
+)
+
+if host_system == 'windows'
+ dummy_table_am_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+ '--NAME', 'dummy_table_am',
+ '--FILEDESC', 'dummy_table_am - table access method template',])
+endif
+
+dummy_table_am = shared_module('dummy_table_am',
+ dummy_table_am_sources,
+ kwargs: pg_test_mod_args,
+)
+test_install_libs += dummy_table_am
+
+test_install_data += files(
+ 'dummy_table_am.control',
+ 'dummy_table_am--1.0.sql',
+)
+
+tests += {
+ 'name': 'dummy_table_am',
+ 'sd': meson.current_source_dir(),
+ 'bd': meson.current_build_dir(),
+ 'regress': {
+ 'sql': [
+ 'reloptions',
+ ],
+ },
+}
diff --git a/src/test/modules/dummy_table_am/sql/reloptions.sql b/src/test/modules/dummy_table_am/sql/reloptions.sql
new file mode 100644
index 00000000000..ce02533d42e
--- /dev/null
+++ b/src/test/modules/dummy_table_am/sql/reloptions.sql
@@ -0,0 +1,99 @@
+-- Tests for relation options
+CREATE EXTENSION dummy_table_am;
+
+CREATE TABLE dummy_test_tab (i int4) USING dummy_table_am;
+
+-- Silence validation checks for strings
+SET client_min_messages TO 'warning';
+
+-- Test with default values.
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+DROP TABLE dummy_test_tab;
+
+-- Test with full set of options.
+-- Allow validation checks for strings
+SET client_min_messages TO 'notice';
+CREATE TABLE dummy_test_tab (i int4)
+ USING dummy_table_am WITH (
+ option_bool = false,
+ option_int = 5,
+ option_real = 3.1,
+ option_enum = 'two',
+ option_string_val = NULL,
+ option_string_null = 'val');
+-- Silence again validation checks for strings until the end of the test.
+SET client_min_messages TO 'warning';
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+
+-- ALTER TABLE .. SET
+ALTER TABLE dummy_test_tab SET (option_int = 10);
+ALTER TABLE dummy_test_tab SET (option_bool = true);
+ALTER TABLE dummy_test_tab SET (option_real = 3.2);
+ALTER TABLE dummy_test_tab SET (option_string_val = 'val2');
+ALTER TABLE dummy_test_tab SET (option_string_null = NULL);
+ALTER TABLE dummy_test_tab SET (option_enum = 'one');
+ALTER TABLE dummy_test_tab SET (option_enum = 'three');
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+
+-- ALTER TABLE .. RESET
+ALTER TABLE dummy_test_tab RESET (option_int);
+ALTER TABLE dummy_test_tab RESET (option_bool);
+ALTER TABLE dummy_test_tab RESET (option_real);
+ALTER TABLE dummy_test_tab RESET (option_enum);
+ALTER TABLE dummy_test_tab RESET (option_string_val);
+ALTER TABLE dummy_test_tab RESET (option_string_null);
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+
+-- Cross-type checks for reloption values
+-- Integer
+ALTER TABLE dummy_test_tab SET (option_int = 3.3); -- ok
+ALTER TABLE dummy_test_tab SET (option_int = true); -- error
+ALTER TABLE dummy_test_tab SET (option_int = 'val3'); -- error
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+ALTER TABLE dummy_test_tab RESET (option_int);
+-- Boolean
+ALTER TABLE dummy_test_tab SET (option_bool = 4); -- error
+ALTER TABLE dummy_test_tab SET (option_bool = 1); -- ok, as true
+ALTER TABLE dummy_test_tab SET (option_bool = 3.4); -- error
+ALTER TABLE dummy_test_tab SET (option_bool = 'val4'); -- error
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+ALTER TABLE dummy_test_tab RESET (option_bool);
+-- Float
+ALTER TABLE dummy_test_tab SET (option_real = 4); -- ok
+ALTER TABLE dummy_test_tab SET (option_real = true); -- error
+ALTER TABLE dummy_test_tab SET (option_real = 'val5'); -- error
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+ALTER TABLE dummy_test_tab RESET (option_real);
+-- Enum
+ALTER TABLE dummy_test_tab SET (option_enum = 'one'); -- ok
+ALTER TABLE dummy_test_tab SET (option_enum = 0); -- error
+ALTER TABLE dummy_test_tab SET (option_enum = true); -- error
+ALTER TABLE dummy_test_tab SET (option_enum = 'three'); -- error
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+ALTER TABLE dummy_test_tab RESET (option_enum);
+-- String
+ALTER TABLE dummy_test_tab SET (option_string_val = 4); -- ok
+ALTER TABLE dummy_test_tab SET (option_string_val = 3.5); -- ok
+ALTER TABLE dummy_test_tab SET (option_string_val = true); -- ok, as "true"
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'dummy_test_tab';
+ALTER TABLE dummy_test_tab RESET (option_string_val);
+
+DROP TABLE dummy_test_tab;
+
+-- ALTER TABLE SET ACCESS METHOD OPTIONS
+CREATE TABLE heap_tab (i INT4) WITH (fillfactor=100, toast_tuple_target=1000);
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'heap_tab';
+-- error: fillfactor is out of bounds: maximum value from the new table am is 90
+ALTER TABLE heap_tab SET ACCESS METHOD dummy_table_am;
+-- error: toast_tuple_target does not exist in the new table AM
+ALTER TABLE heap_tab SET ACCESS METHOD dummy_table_am OPTIONS (SET fillfactor '50');
+-- error: adding is not possible when the parameter is already defined in source reloptions
+ALTER TABLE heap_tab SET ACCESS METHOD dummy_table_am OPTIONS (ADD fillfactor '50');
+-- error: the specified option we want to drop does not exist
+ALTER TABLE heap_tab SET ACCESS METHOD dummy_table_am OPTIONS (DROP does_not_exist);
+-- error: adding unrecognized parameter
+ALTER TABLE heap_tab SET ACCESS METHOD dummy_table_am OPTIONS (SET fillfactor '50', DROP toast_tuple_target, ADD unrecognized 'foo');
+-- ok
+ALTER TABLE heap_tab SET ACCESS METHOD dummy_table_am OPTIONS (DROP fillfactor, DROP toast_tuple_target, option_int '1', option_bool 'true', option_real '0.001', option_enum 'one', option_string_val 'hello');
+SELECT unnest(reloptions) FROM pg_class WHERE relname = 'heap_tab';
+DROP TABLE heap_tab;
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 2b057451473..28398254df7 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -4,6 +4,7 @@ subdir('brin')
subdir('commit_ts')
subdir('delay_execution')
subdir('dummy_index_am')
+subdir('dummy_table_am')
subdir('dummy_seclabel')
subdir('gin')
subdir('injection_points')
--
2.39.5