On Thu, Jul 16, 2026 at 9:37 AM Rafia Sabih <[email protected]> wrote:
> > > On Thu, 4 Jun 2026 at 04:32, Andrew Dunstan <[email protected]> wrote: > >> >> On 2025-05-26 Mo 7:06 AM, Julien Tachoires wrote: >> > On Sat, Mar 29, 2025 at 08:46:01AM +0100, Julien Tachoires wrote: >> >> On Sun, Mar 02, 2025 at 02:23:54PM +0100, Julien Tachoires wrote: >> >>> On Sun, Mar 02, 2025 at 09:56:41AM +0100, Julien Tachoires wrote: >> >>>> 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 >> >>> Please find a new version including minor fixes: 'TAM' terms are >> >>> replaced by 'table AM' >> >> Please find a new rebased version. >> > New rebased version. >> > >> >> >> This thread seems to have gone dormant, sadly. Here's a fresh attempt I >> made (with assistance from Claude), before I was aware of the existence >> of this and other efforts. I think it meets most of the previous >> objections, and is in line with what we do for Index AMs. >> >> I was testing this patch and found this issue, > CREATE EXTENSION dummy_table_am; > CREATE TABLE t (a int) USING dummy_table_am WITH (option_int = 42); > ALTER TABLE t SET ACCESS METHOD heap; > SELECT reloptions FROM pg_class WHERE oid = 't'::regclass; > reloptions > ----------------- > {option_int=42} > (1 row) > ALTER TABLE t SET (fillfactor = 50); > 2026-07-16 19:01:36.060 IST [95058] ERROR: unrecognized parameter > "option_int" > 2026-07-16 19:01:36.060 IST [95058] STATEMENT: ALTER TABLE t SET > (fillfactor = 50); > ERROR: unrecognized parameter "option_int" > > Basically, the option is left behind after changing the tableam to heap, I > think that isn't intentional. > >> >> > Thanks for reviewing. Good catch! Basically the code had an early exit in the validation code that shouldn't have been there. v2 attached with a fix for that, plus a regression test covering this direction (dummy_table_am -> heap with a still-set AM-specific option), which I verified fails against v1 and passes with the fix. I also folded the two independent access-method-resolution code paths in DefineRelation into one and added the new test module's typedefs to typedefs.list cheers andrew
From fde77afbef3cb1cc53a60c069b5fb332c67ce945 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan <[email protected]> Date: Tue, 12 May 2026 09:03:33 -0400 Subject: [PATCH v2] Add amoptions callback to table access methods Give table access methods the same option-extension story that index access methods already have. TableAmRoutine gets an optional amoptions field of type amoptions_function (same signature as IndexAmRoutine.amoptions). A matching table_reloptions() entry point dispatches to it: bytea * table_reloptions(amoptions_function amoptions, char relkind, Datum reloptions, bool validate) { if (amoptions != NULL) return amoptions(reloptions, validate); return heap_reloptions(relkind, reloptions, validate); } When an AM supplies an amoptions parser it owns the option set entirely: it may accept all standard heap options, only a subset, or add its own; the returned bytea is stored verbatim in Relation->rd_options, so the AM dictates the layout its other callbacks read. When amoptions is NULL the result is the standard StdRdOptions layout, identical to today. DefineRelation and ATExecSetRelOptions are updated to call table_reloptions() for RELKIND_RELATION and RELKIND_MATVIEW. At CREATE TABLE the AM is resolved early (explicit USING clause, partition parent, or default_table_access_method) so its amoptions can be consulted during reloption validation; at ALTER TABLE SET the AM is read from rel->rd_tableam, with one twist: if SET ACCESS METHOD is queued in the same statement, the new AM's parser is used so that users can write ALTER TABLE t SET ACCESS METHOD x, SET (foo = bar) where foo is recognised by x but not by the current AM. RelationParseRelOptions and extractRelOptions are likewise routed through table_reloptions when a table relation is opened, so AMs see their own parsed struct in rd_options. ALTER TABLE ... SET ACCESS METHOD also runs a final reloption revalidation after all phase-2 subcommands have committed: the relation's resulting reloptions are checked against the new AM's parser so that pre-existing options the new AM does not recognise fail the statement with a clear message rather than being silently dropped at the next relcache load. Users can clear such options in the same statement, e.g. ALTER TABLE t SET ACCESS METHOD x, RESET (fillfactor); Because an AM that supplies amoptions owns its rd_options layout, core macros that previously assumed rd_options was always StdRdOptions (RelationGetFillFactor, RelationGetToastTupleTarget, RelationIsUsedAsCatalogTable, RelationGetParallelWorkers) now gate on a new helper RelationHasStdRdOptions(). The same gating is applied to the direct (StdRdOptions *) casts in vacuum.c and index.c so a custom layout is never reinterpreted as StdRdOptions. Heap registers no amoptions (the field is NULL), so its behaviour is unchanged. A companion helper, add_reloption_to_kind(name, kind), extends an existing reloption registration with an additional kind bit: void add_reloption_to_kind(const char *name, relopt_kind kind); Extensions that want their AM-specific parser to accept standard options (fillfactor, parallel_workers, autovacuum_*, vacuum_truncate) that core registers only for RELOPT_KIND_HEAP can now call this once per option in _PG_init instead of duplicating each definition under the new kind. Errors if no option with that name has been registered. A new test module src/test/modules/dummy_table_am demonstrates the API: it wraps the heap AM and only overrides amoptions, so the relation behaves as a heap table but accepts a custom reloption set. The accompanying regress test exercises CREATE/ALTER/RESET round-trips, SET ACCESS METHOD revalidation (both the "options-incompatible-with-new-AM" failure path and the "RESET-in-same-statement" success path), and partitioned-table inheritance of an AM-specific option set. Together the two additions are exactly the surface an extension table AM needs to register its own reloptions in its own RELOPT_KIND_* namespace -- AM-specific options no longer have to pollute StdRdOptions / RELOPT_KIND_HEAP to be parseable. --- doc/src/sgml/ref/alter_table.sgml | 19 ++ doc/src/sgml/tableam.sgml | 67 ++++++ src/backend/access/common/reloptions.c | 87 +++++++- src/backend/catalog/index.c | 3 +- src/backend/commands/tablecmds.c | 199 +++++++++++++++--- src/backend/commands/vacuum.c | 7 +- src/backend/utils/cache/relcache.c | 4 +- src/include/access/reloptions.h | 3 + src/include/access/tableam.h | 32 +++ src/include/utils/rel.h | 25 ++- src/test/modules/Makefile | 1 + src/test/modules/dummy_table_am/Makefile | 20 ++ src/test/modules/dummy_table_am/README | 21 ++ .../dummy_table_am/dummy_table_am--1.0.sql | 13 ++ .../modules/dummy_table_am/dummy_table_am.c | 166 +++++++++++++++ .../dummy_table_am/dummy_table_am.control | 5 + .../dummy_table_am/expected/reloptions.out | 182 ++++++++++++++++ src/test/modules/dummy_table_am/meson.build | 33 +++ .../modules/dummy_table_am/sql/reloptions.sql | 105 +++++++++ src/test/modules/meson.build | 1 + src/tools/pgindent/typedefs.list | 2 + 21 files changed, 959 insertions(+), 36 deletions(-) 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/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index ff7071bef5b..4ff4ea9096d 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -813,6 +813,25 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM causing future partitions to default to <varname>default_table_access_method</varname>. </para> + <para> + The new access method must accept every storage parameter + currently set on the table. An access method may define its own + set of parameters, so a parameter that was legal under the old + access method is not necessarily recognized by the new one; if any + such parameter remains, <command>ALTER TABLE</command> raises an + error rather than silently dropping the value. The unwanted + parameters can be cleared in the same statement, for example: +<programlisting> +ALTER TABLE measurement + SET ACCESS METHOD columnar, + RESET (fillfactor); +</programlisting> + Validation is performed once, after all storage-parameter + sub-commands in the statement have been applied, so the order of + <literal>SET</literal>, <literal>RESET</literal>, and + <literal>SET ACCESS METHOD</literal> within the same + <command>ALTER TABLE</command> does not matter. + </para> </listitem> </varlistentry> diff --git a/doc/src/sgml/tableam.sgml b/doc/src/sgml/tableam.sgml index 9ccf5b739ed..d47c12177ab 100644 --- a/doc/src/sgml/tableam.sgml +++ b/doc/src/sgml/tableam.sgml @@ -152,4 +152,71 @@ my_tableam_handler(PG_FUNCTION_ARGS) its implementation. </para> + <sect1 id="tableam-reloptions"> + <title>Table Access Method Storage Parameters</title> + + <para> + A table access method may define its own set of storage parameters + (reloptions) by supplying an <structfield>amoptions</structfield> + callback in its <structname>TableAmRoutine</structname>. The callback + has the same signature as the corresponding index AM callback; it is + invoked at <command>CREATE TABLE</command> and + <command>ALTER TABLE</command> time to parse and validate the option + set, and at relation open time (with <literal>validate = false</literal>) + to build the in-memory representation stored in + <structfield>Relation->rd_options</structfield>. An AM that does not + supply an <structfield>amoptions</structfield> callback inherits the + standard heap parser and the <structname>StdRdOptions</structname> + layout. + </para> + + <para> + When the AM provides its own parser it owns the option set entirely: + it may accept all standard heap options, only a subset, or define + parameters of its own. The bytea returned from the callback is + stored verbatim in <structfield>rd_options</structfield>, so the AM + also dictates the in-memory layout that its other callbacks read. + </para> + + <para> + The parser is expected to validate user-supplied values, but + <emphasis>must not silently rewrite them</emphasis>. In particular + it must not coerce out-of-range values to a default, drop unknown + options when <literal>validate = true</literal>, or substitute a + different unit; the user must be able to verify with + <command>SELECT reloptions FROM pg_class</command> that the values + they supplied are what the relation will use. Out-of-range or + unknown options should be reported with + <function>ereport(ERROR)</function>. + </para> + + <para> + To honour an option that the core code already registers for + <literal>RELOPT_KIND_HEAP</literal> (for example + <literal>fillfactor</literal> or the <literal>autovacuum_*</literal> + family), call <function>add_reloption_to_kind()</function> once per + option in the module's <function>_PG_init</function>. This extends + the existing registration with the AM's own kind without forcing + the AM to re-declare each option. + </para> + + <para> + <command>ALTER TABLE ... SET ACCESS METHOD</command> revalidates the + relation's current storage parameters against the new access + method's parser after all <literal>SET</literal>, + <literal>RESET</literal>, and <literal>REPLACE</literal> + sub-commands in the same statement have been applied. A parameter + that is not accepted by the new AM raises an error; the user can + clear such parameters in the same statement (see <xref + linkend="sql-altertable"/>). + </para> + + <para> + See <filename>src/test/modules/dummy_table_am</filename> for a + minimal example that exercises both + <structfield>amoptions</structfield> and + <function>add_reloption_to_kind()</function>. + </para> + </sect1> + </chapter> diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c index 3e832c3797e..84f67e87646 100644 --- a/src/backend/access/common/reloptions.c +++ b/src/backend/access/common/reloptions.c @@ -24,6 +24,7 @@ #include "access/nbtree.h" #include "access/reloptions.h" #include "access/spgist_private.h" +#include "access/tableam.h" #include "catalog/pg_type.h" #include "commands/defrem.h" #include "commands/tablespace.h" @@ -749,6 +750,44 @@ add_reloption_kind(void) return (relopt_kind) last_assigned_kind; } +/* + * add_reloption_to_kind + * Extend an already-registered reloption so it is also accepted for + * the given kind. + * + * Useful for table access methods that want their own RELOPT_KIND_* + * parser to accept standard options (fillfactor, parallel_workers, + * autovacuum_*, etc.) that core registers only for RELOPT_KIND_HEAP. + * Without this, every AM that wants the standard option set would + * have to re-register each option under its own kind. + * + * 'name' must match an existing option; 'kind' is OR'ed into that + * option's kinds mask. Errors if no option with that name exists. + */ +void +add_reloption_to_kind(const char *name, relopt_kind kind) +{ + int namelen = strlen(name); + int i; + + if (need_initialization) + initialize_reloptions(); + + for (i = 0; relOpts[i]; i++) + { + if (relOpts[i]->namelen == namelen && + strncmp(relOpts[i]->name, name, namelen) == 0) + { + relOpts[i]->kinds |= kind; + return; + } + } + + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("reloption \"%s\" does not exist", name))); +} + /* * add_reloption * Add an already-created custom reloption to the list, and recompute the @@ -1516,8 +1555,11 @@ extractRelOptions(HeapTuple tuple, TupleDesc tupdesc, switch (classForm->relkind) { case RELKIND_RELATION: - case RELKIND_TOASTVALUE: case RELKIND_MATVIEW: + options = table_reloptions(amoptions, classForm->relkind, + datum, false); + break; + case RELKIND_TOASTVALUE: options = heap_reloptions(classForm->relkind, datum, false); break; case RELKIND_PARTITIONED_TABLE: @@ -2187,6 +2229,49 @@ heap_reloptions(char relkind, Datum reloptions, bool validate) } } +/* + * Parse options for a table relation, dispatching to the access method's + * own option parser when it supplies one. + * + * amoptions the table AM's option parser, or NULL to fall back to the + * standard heap parser for this relkind. + * relkind the relation's kind. + * reloptions options as a text[] datum. + * validate error flag for unknown options or bad values. + * + * When amoptions is non-NULL the AM owns the option set: it may accept + * all standard heap options, only a subset, or define its own. The + * returned bytea is laid out as the AM dictates (it is stored verbatim + * in Relation->rd_options). When amoptions is NULL the result is the + * standard StdRdOptions layout. + */ +bytea * +table_reloptions(amoptions_function amoptions, char relkind, + Datum reloptions, bool validate) +{ + if (amoptions != NULL) + return amoptions(reloptions, validate); + return heap_reloptions(relkind, reloptions, validate); +} + +/* + * Returns true when the relation's rd_options buffer is laid out as + * StdRdOptions. Used by the rel.h accessor macros (RelationGetFillFactor, + * RelationIsUsedAsCatalogTable, ...) to gate StdRdOptions casts so that a + * table access method which supplies its own amoptions callback (and + * therefore owns the rd_options layout) does not have its bytes + * misinterpreted. + */ +bool +RelationHasStdRdOptions(Relation relation) +{ + if (relation->rd_options == NULL) + return false; + if (relation->rd_tableam == NULL) + return false; + return relation->rd_tableam->amoptions == NULL; +} + /* * Parse options for indexes. diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c index 81bba4beac7..1230be892b4 100644 --- a/src/backend/catalog/index.c +++ b/src/backend/catalog/index.c @@ -2871,7 +2871,8 @@ index_update_stats(Relation rel, { if (AutoVacuumingActive()) { - StdRdOptions *options = (StdRdOptions *) rel->rd_options; + StdRdOptions *options = RelationHasStdRdOptions(rel) ? + (StdRdOptions *) rel->rd_options : NULL; if (options != NULL && !options->autovacuum.enabled) update_stats = false; diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index cb93c3e935a..86d821e0c55 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -724,9 +724,11 @@ 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 ATValidateAccessMethodOptions(List **wqueue); static void ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, - LOCKMODE lockmode); + LOCKMODE lockmode, + Oid newAccessMethodId); static void ATExecEnableDisableTrigger(Relation rel, const char *trigname, char fires_when, bool skip_system, bool recurse, LOCKMODE lockmode); @@ -981,6 +983,30 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, if (!OidIsValid(ownerId)) ownerId = GetUserId(); + /* + * For relations with table AM and partitioned tables, select access + * method to use: an explicitly indicated one, or (in the case of a + * partitioned table) the parent's, if it has one. Done ahead of + * reloptions validation below so that RELKIND_RELATION/RELKIND_MATVIEW + * reloptions can be checked against the AM's own parser. + */ + if (stmt->accessMethod != NULL) + { + Assert(RELKIND_HAS_TABLE_AM(relkind) || relkind == RELKIND_PARTITIONED_TABLE); + accessMethodId = get_table_am_oid(stmt->accessMethod, false); + } + else if (RELKIND_HAS_TABLE_AM(relkind) || relkind == RELKIND_PARTITIONED_TABLE) + { + if (stmt->partbound) + { + Assert(list_length(inheritOids) == 1); + accessMethodId = get_rel_relam(linitial_oid(inheritOids)); + } + + if (RELKIND_HAS_TABLE_AM(relkind) && !OidIsValid(accessMethodId)) + accessMethodId = get_table_am_oid(default_table_access_method, false); + } + /* * Parse and validate reloptions, if any. */ @@ -995,6 +1021,27 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, case RELKIND_PARTITIONED_TABLE: (void) partitioned_table_reloptions(reloptions, true); break; + case RELKIND_RELATION: + case RELKIND_MATVIEW: + { + amoptions_function amoptions = NULL; + + if (OidIsValid(accessMethodId)) + { + HeapTuple tuple; + + tuple = SearchSysCache1(AMOID, ObjectIdGetDatum(accessMethodId)); + if (HeapTupleIsValid(tuple)) + { + Form_pg_am amform = (Form_pg_am) GETSTRUCT(tuple); + + amoptions = GetTableAmRoutine(amform->amhandler)->amoptions; + ReleaseSysCache(tuple); + } + } + (void) table_reloptions(amoptions, relkind, reloptions, true); + } + break; default: (void) heap_reloptions(relkind, reloptions, true); } @@ -1081,28 +1128,6 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId, TupleDescFinalize(descriptor); - /* - * For relations with table AM and partitioned tables, select access - * method to use: an explicitly indicated one, or (in the case of a - * partitioned table) the parent's, if it has one. - */ - if (stmt->accessMethod != NULL) - { - Assert(RELKIND_HAS_TABLE_AM(relkind) || relkind == RELKIND_PARTITIONED_TABLE); - accessMethodId = get_table_am_oid(stmt->accessMethod, false); - } - else if (RELKIND_HAS_TABLE_AM(relkind) || relkind == RELKIND_PARTITIONED_TABLE) - { - if (stmt->partbound) - { - Assert(list_length(inheritOids) == 1); - accessMethodId = get_rel_relam(linitial_oid(inheritOids)); - } - - if (RELKIND_HAS_TABLE_AM(relkind) && !OidIsValid(accessMethodId)) - accessMethodId = get_table_am_oid(default_table_access_method, false); - } - /* * Create the relation. Inherited defaults and CHECK constraints are * passed in for immediate handling --- since they don't need parsing, @@ -4985,6 +5010,18 @@ ATController(AlterTableStmt *parsetree, /* Phase 2: update system catalogs */ ATRewriteCatalogs(&wqueue, lockmode, context); + /* + * After all phase-2 subcommands have committed any SET / RESET / REPLACE + * option changes to pg_class, but before any rewrite, ensure the final + * reloptions are accepted by the access method the relation will use once + * the ALTER TABLE finishes. This catches the case where SET ACCESS + * METHOD changes the AM and leaves pre-existing reloptions in pg_class + * that the new AM does not recognise; without this check the new AM's + * option parser would be called with validate=false at relcache load time + * and silently ignore them. + */ + ATValidateAccessMethodOptions(&wqueue); + /* Phase 3: scan/rewrite tables as needed, and run afterStmts */ ATRewriteTables(parsetree, &wqueue, lockmode, context); } @@ -5656,7 +5693,17 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, case AT_SetRelOptions: /* SET (...) */ case AT_ResetRelOptions: /* RESET (...) */ case AT_ReplaceRelOptions: /* replace entire option list */ - ATExecSetRelOptions(rel, (List *) cmd->def, cmd->subtype, lockmode); + + /* + * If SET ACCESS METHOD is queued in the same ALTER TABLE, the + * reloptions in pg_class will be parsed by the new AM after the + * statement finishes; tell ATExecSetRelOptions to validate + * against that AM rather than the relation's current AM. This + * lets a user write ALTER TABLE t SET ACCESS METHOD x, SET (foo = + * bar) where foo is recognised by x but not by the current AM. + */ + ATExecSetRelOptions(rel, (List *) cmd->def, cmd->subtype, lockmode, + tab->chgAccessMethod ? tab->newAccessMethod : InvalidOid); break; case AT_EnableTrig: /* ENABLE TRIGGER name */ ATExecEnableDisableTrigger(rel, cmd->name, @@ -17344,12 +17391,89 @@ ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel, const char *tablespacen tab->newTableSpace = tablespaceId; } +/* + * Re-validate pg_class.reloptions for every work-queue entry whose access + * method is being changed. Called between phase 2 (catalog updates) and + * phase 3 (table rewrites): SET / RESET / REPLACE subcommands have already + * been committed to pg_class, and tab->newAccessMethod identifies the AM + * the relation will use once the ALTER TABLE finishes. + * + * The check exists because relcache.c calls the AM's option parser with + * validate=false at relation open: any pre-existing reloption that the + * new AM does not recognise would otherwise be silently dropped from the + * parsed StdRdOptions / AM-specific options struct, leaving the user + * unable to tell that the option is no longer in effect. Failing the + * ALTER TABLE here with a clear message lets the user RESET the option + * in the same statement and re-run. + */ +static void +ATValidateAccessMethodOptions(List **wqueue) +{ + ListCell *ltab; + + foreach(ltab, *wqueue) + { + AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab); + HeapTuple amtup; + HeapTuple reltup; + Form_pg_am amform; + Form_pg_class relform; + amoptions_function amoptions; + Datum reloptions; + bool isnull; + Oid amoid; + + if (!tab->chgAccessMethod) + continue; + + /* + * Partitioned tables may reset the AM to "default" (InvalidOid); each + * partition then chooses its own AM at create time, so there is no + * per-relation AM whose parser to consult here. + */ + amoid = tab->newAccessMethod; + if (!OidIsValid(amoid)) + continue; + + amtup = SearchSysCache1(AMOID, ObjectIdGetDatum(amoid)); + if (!HeapTupleIsValid(amtup)) + elog(ERROR, "cache lookup failed for access method %u", amoid); + amform = (Form_pg_am) GETSTRUCT(amtup); + amoptions = GetTableAmRoutine(amform->amhandler)->amoptions; + ReleaseSysCache(amtup); + + /* + * Always revalidate, even when the new AM has no amoptions of its + * own: table_reloptions() then falls back to heap_reloptions(), which + * is exactly the check needed to catch options that only the + * relation's previous (AM-specific) parser accepted. + */ + reltup = SearchSysCache1(RELOID, ObjectIdGetDatum(tab->relid)); + if (!HeapTupleIsValid(reltup)) + elog(ERROR, "cache lookup failed for relation %u", tab->relid); + relform = (Form_pg_class) GETSTRUCT(reltup); + reloptions = SysCacheGetAttr(RELOID, reltup, + Anum_pg_class_reloptions, &isnull); + if (!isnull) + (void) table_reloptions(amoptions, relform->relkind, + reloptions, true); + ReleaseSysCache(reltup); + } +} + /* * Set, reset, or replace reloptions. + * + * newAccessMethodId, if valid, names the table access method whose option + * parser should validate the resulting reloptions. This is used when SET + * ACCESS METHOD is queued in the same ALTER TABLE so that the new options + * are checked against the AM the relation will use after the statement + * finishes, not the AM it has now. Pass InvalidOid to use the relation's + * current access method. */ static void ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, - LOCKMODE lockmode) + LOCKMODE lockmode, Oid newAccessMethodId) { Oid relid; Relation pgclass; @@ -17401,7 +17525,30 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, { case RELKIND_RELATION: case RELKIND_MATVIEW: - (void) heap_reloptions(rel->rd_rel->relkind, newOptions, true); + { + amoptions_function amoptions; + + if (OidIsValid(newAccessMethodId)) + { + HeapTuple amtup; + Form_pg_am amform; + + amtup = SearchSysCache1(AMOID, + ObjectIdGetDatum(newAccessMethodId)); + if (!HeapTupleIsValid(amtup)) + elog(ERROR, "cache lookup failed for access method %u", + newAccessMethodId); + amform = (Form_pg_am) GETSTRUCT(amtup); + amoptions = GetTableAmRoutine(amform->amhandler)->amoptions; + ReleaseSysCache(amtup); + } + else + amoptions = (rel->rd_tableam ? + rel->rd_tableam->amoptions : NULL); + + (void) table_reloptions(amoptions, rel->rd_rel->relkind, + newOptions, true); + } break; case RELKIND_PARTITIONED_TABLE: (void) partitioned_table_reloptions(newOptions, true); diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index 38539a6fd3d..e83364def5c 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -2190,7 +2190,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params, { StdRdOptIndexCleanup vacuum_index_cleanup; - if (rel->rd_options == NULL) + if (!RelationHasStdRdOptions(rel)) vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO; else vacuum_index_cleanup = @@ -2221,7 +2221,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params, * Check if the vacuum_max_eager_freeze_failure_rate table storage * parameter was specified. This overrides the GUC value. */ - if (rel->rd_options != NULL && + if (RelationHasStdRdOptions(rel) && ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0) params.max_eager_freeze_failure_rate = ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate; @@ -2232,7 +2232,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params, */ if (params.truncate == VACOPTVALUE_UNSPECIFIED) { - StdRdOptions *opts = (StdRdOptions *) rel->rd_options; + StdRdOptions *opts = RelationHasStdRdOptions(rel) ? + (StdRdOptions *) rel->rd_options : NULL; if (opts && opts->vacuum_truncate != PG_TERNARY_UNSET) { diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index 19c4ff6e75e..dc1e7745e42 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -483,9 +483,11 @@ RelationParseRelOptions(Relation relation, HeapTuple tuple) switch (relation->rd_rel->relkind) { case RELKIND_RELATION: + case RELKIND_MATVIEW: + amoptsfn = relation->rd_tableam ? relation->rd_tableam->amoptions : NULL; + break; case RELKIND_TOASTVALUE: case RELKIND_VIEW: - case RELKIND_MATVIEW: case RELKIND_PARTITIONED_TABLE: amoptsfn = NULL; break; diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h index e8cb7f7a627..1282bccc77f 100644 --- a/src/include/access/reloptions.h +++ b/src/include/access/reloptions.h @@ -187,6 +187,7 @@ typedef struct local_relopts (char *)(optstruct) + (optstruct)->member) extern relopt_kind add_reloption_kind(void); +extern void add_reloption_to_kind(const char *name, relopt_kind kind); 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, @@ -248,6 +249,8 @@ extern void *build_local_reloptions(local_relopts *relopts, Datum options, extern bytea *default_reloptions(Datum reloptions, bool validate, relopt_kind kind); extern bytea *heap_reloptions(char relkind, Datum reloptions, bool validate); +extern bytea *table_reloptions(amoptions_function amoptions, 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 *index_reloptions(amoptions_function amoptions, Datum reloptions, diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h index f2c36696bca..15e46d7995a 100644 --- a/src/include/access/tableam.h +++ b/src/include/access/tableam.h @@ -17,6 +17,7 @@ #ifndef TABLEAM_H #define TABLEAM_H +#include "access/amapi.h" #include "access/relscan.h" #include "access/sdir.h" #include "access/xact.h" @@ -324,6 +325,37 @@ typedef struct TableAmRoutine NodeTag type; + /* ------------------------------------------------------------------------ + * Reloption parsing. + * ------------------------------------------------------------------------ + */ + + /* + * Parse and validate AM-specific reloptions. Optional: when NULL, the + * caller falls back to the standard heap reloption parser + * (default_reloptions with RELOPT_KIND_HEAP) and the result is laid out + * as StdRdOptions. + * + * When non-NULL, the AM owns the option set entirely. It is free to + * accept all standard heap options, only a subset, or to add its own. The + * returned bytea must begin with a VARSIZE header and is stored in + * Relation->rd_options, so the AM dictates the in-memory layout that its + * other callbacks read. Core code that reads StdRdOptions fields out of + * rd_options (RelationGetFillFactor, RelationIsUsedAsCatalogTable, ...) + * gates on RelationHasStdRdOptions(), so a custom layout will not be + * misinterpreted. + * + * The callback validates user-supplied values but must not silently + * rewrite them: a user inspecting pg_class.reloptions must see exactly + * what they passed in. Out-of-range or unknown options should be + * reported with ereport(ERROR) when validate is true. + * + * Signature matches the index AM's amoptions callback so the same helper + * machinery (add_string_reloption, add_int_reloption, etc.) can be used. + */ + amoptions_function amoptions; + + /* ------------------------------------------------------------------------ * Slot related callbacks. * ------------------------------------------------------------------------ diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 89c159b133f..b88544c9617 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -361,12 +361,29 @@ typedef struct StdRdOptions #define HEAP_MIN_FILLFACTOR 10 #define HEAP_DEFAULT_FILLFACTOR 100 +/* + * RelationHasStdRdOptions + * Returns true when the relation's rd_options buffer is laid out as + * StdRdOptions, i.e. it was produced by the standard heap reloption + * parser. A table access method that supplies its own amoptions + * callback owns its rd_options layout and is not required to expose + * StdRdOptions fields; macros that read those fields must check this + * first to avoid reading garbage data. For indexes and other + * relkinds rd_options is in an AM-specific layout, so this returns + * false for them. + * + * Defined as a function (in reloptions.c) rather than a macro + * because the test needs the full TableAmRoutine struct definition, + * which would create an #include cycle if pulled into rel.h. + */ +extern bool RelationHasStdRdOptions(Relation relation); + /* * RelationGetToastTupleTarget * Returns the relation's toast_tuple_target. Note multiple eval of argument! */ #define RelationGetToastTupleTarget(relation, defaulttarg) \ - ((relation)->rd_options ? \ + (RelationHasStdRdOptions(relation) ? \ ((StdRdOptions *) (relation)->rd_options)->toast_tuple_target : (defaulttarg)) /* @@ -374,7 +391,7 @@ typedef struct StdRdOptions * Returns the relation's fillfactor. Note multiple eval of argument! */ #define RelationGetFillFactor(relation, defaultff) \ - ((relation)->rd_options ? \ + (RelationHasStdRdOptions(relation) ? \ ((StdRdOptions *) (relation)->rd_options)->fillfactor : (defaultff)) /* @@ -397,7 +414,7 @@ typedef struct StdRdOptions * from the pov of logical decoding. Note multiple eval of argument! */ #define RelationIsUsedAsCatalogTable(relation) \ - ((relation)->rd_options && \ + (RelationHasStdRdOptions(relation) && \ ((relation)->rd_rel->relkind == RELKIND_RELATION || \ (relation)->rd_rel->relkind == RELKIND_MATVIEW) ? \ ((StdRdOptions *) (relation)->rd_options)->user_catalog_table : false) @@ -408,7 +425,7 @@ typedef struct StdRdOptions * Note multiple eval of argument! */ #define RelationGetParallelWorkers(relation, defaultpw) \ - ((relation)->rd_options ? \ + (RelationHasStdRdOptions(relation) ? \ ((StdRdOptions *) (relation)->rd_options)->parallel_workers : (defaultpw)) /* ViewOptions->check_option values */ diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 0a74ab5c86f..223005fdf98 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -10,6 +10,7 @@ SUBDIRS = \ delay_execution \ dummy_index_am \ dummy_seclabel \ + dummy_table_am \ index \ 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..a234a1f107f --- /dev/null +++ b/src/test/modules/dummy_table_am/README @@ -0,0 +1,21 @@ +Dummy Table AM +============== + +Dummy table AM is a module for testing the table access method +amoptions callback and the add_reloption_to_kind() helper. It +delegates all storage and scan callbacks to the heap AM and only +swaps in its own option parser, so a relation created with USING +dummy_table_am behaves like a heap table but accepts a different +set of reloptions: + + - "fillfactor" (inherited from the core heap registration via + add_reloption_to_kind) + - "option_int" (integer) + - "option_real" (real) + - "option_bool" (boolean) + - "option_enum" (enum, one|two) + +Standard heap options such as parallel_workers, autovacuum_*, and +toast_tuple_target are intentionally NOT accepted, to exercise the +"AM rejects an unknown option" path in ALTER TABLE ... SET ACCESS +METHOD revalidation. 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..2e295b95845 --- /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 dthandler(internal) +RETURNS table_am_handler +AS 'MODULE_PATHNAME' +LANGUAGE C; + +-- Access method +CREATE ACCESS METHOD dummy_table_am TYPE TABLE HANDLER dthandler; +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..834a9acd5bb --- /dev/null +++ b/src/test/modules/dummy_table_am/dummy_table_am.c @@ -0,0 +1,166 @@ +/*------------------------------------------------------------------------- + * + * dummy_table_am.c + * Table AM template main file. + * + * This module exists primarily to demonstrate and exercise the table AM + * amoptions callback and the add_reloption_to_kind() helper. Storage + * and scan callbacks are delegated to the heap AM, so a relation + * created with USING dummy_table_am behaves like a heap table; only the + * reloption surface differs. + * + * Portions Copyright (c) 1996-2026, 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 "access/reloptions.h" +#include "access/tableam.h" +#include "fmgr.h" + +PG_MODULE_MAGIC; + +/* Parse table for build_reloptions */ +static relopt_parse_elt dt_relopt_tab[5]; + +/* Kind of relation options for dummy table */ +static relopt_kind dt_relopt_kind; + +typedef enum DummyTableEnum +{ + DUMMY_TABLE_ENUM_ONE, + DUMMY_TABLE_ENUM_TWO, +} DummyTableEnum; + +/* + * Dummy table options. + * + * The first two fields are the standard heap options (fillfactor + + * autovacuum_enabled) that we inherit by calling add_reloption_to_kind() + * on the matching names; the remaining ones are AM-specific options. + */ +typedef struct DummyTableOptions +{ + int32 vl_len_; /* varlena header (do not touch directly!) */ + int fillfactor; + int option_int; + double option_real; + bool option_bool; + DummyTableEnum option_enum; +} DummyTableOptions; + +static relopt_enum_elt_def dummyTableEnumValues[] = +{ + {"one", DUMMY_TABLE_ENUM_ONE}, + {"two", DUMMY_TABLE_ENUM_TWO}, + {(const char *) NULL} /* list terminator */ +}; + +PG_FUNCTION_INFO_V1(dthandler); + +/* + * Register a relopt_kind for this AM and populate the parse table. + */ +static void +create_reloptions_table(void) +{ + int i = 0; + + dt_relopt_kind = add_reloption_kind(); + + /* + * Accept the standard "fillfactor" option (registered by core for + * RELOPT_KIND_HEAP only) under our own kind. This is the canonical use + * of add_reloption_to_kind(): an AM that wants to honour an existing + * core-registered option without duplicating its definition. + */ + add_reloption_to_kind("fillfactor", dt_relopt_kind); + dt_relopt_tab[i].optname = "fillfactor"; + dt_relopt_tab[i].opttype = RELOPT_TYPE_INT; + dt_relopt_tab[i].offset = offsetof(DummyTableOptions, fillfactor); + i++; + + add_int_reloption(dt_relopt_kind, "option_int", + "Integer option for dummy_table_am", + 10, -10, 100, AccessExclusiveLock); + dt_relopt_tab[i].optname = "option_int"; + dt_relopt_tab[i].opttype = RELOPT_TYPE_INT; + dt_relopt_tab[i].offset = offsetof(DummyTableOptions, option_int); + i++; + + add_real_reloption(dt_relopt_kind, "option_real", + "Real option for dummy_table_am", + 3.1415, -10, 100, AccessExclusiveLock); + dt_relopt_tab[i].optname = "option_real"; + dt_relopt_tab[i].opttype = RELOPT_TYPE_REAL; + dt_relopt_tab[i].offset = offsetof(DummyTableOptions, option_real); + i++; + + add_bool_reloption(dt_relopt_kind, "option_bool", + "Boolean option for dummy_table_am", + true, AccessExclusiveLock); + dt_relopt_tab[i].optname = "option_bool"; + dt_relopt_tab[i].opttype = RELOPT_TYPE_BOOL; + dt_relopt_tab[i].offset = offsetof(DummyTableOptions, option_bool); + i++; + + add_enum_reloption(dt_relopt_kind, "option_enum", + "Enum option for dummy_table_am", + dummyTableEnumValues, + DUMMY_TABLE_ENUM_ONE, + "Valid values are \"one\" and \"two\".", + AccessExclusiveLock); + dt_relopt_tab[i].optname = "option_enum"; + dt_relopt_tab[i].opttype = RELOPT_TYPE_ENUM; + dt_relopt_tab[i].offset = offsetof(DummyTableOptions, option_enum); + i++; +} + +/* + * Parse reloptions for dummy_table_am. + * + * Returning DummyTableOptions tells the caller (relcache.c) to store + * exactly that layout in Relation->rd_options. + */ +static bytea * +dtoptions(Datum reloptions, bool validate) +{ + return (bytea *) build_reloptions(reloptions, validate, + dt_relopt_kind, + sizeof(DummyTableOptions), + dt_relopt_tab, lengthof(dt_relopt_tab)); +} + +/* + * Handler for table AM. + * + * All storage-side callbacks are inherited from heap; we only swap in + * our own amoptions so that the AM owns its reloption set. This keeps + * the example focused on the new API without duplicating the heap AM. + */ +Datum +dthandler(PG_FUNCTION_ARGS) +{ + static TableAmRoutine routine; + static bool initialized = false; + + if (!initialized) + { + memcpy(&routine, GetHeapamTableAmRoutine(), sizeof(routine)); + routine.amoptions = dtoptions; + initialized = true; + } + + PG_RETURN_POINTER(&routine); +} + +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..0015ef0df51 --- /dev/null +++ b/src/test/modules/dummy_table_am/expected/reloptions.out @@ -0,0 +1,182 @@ +-- Tests for the table AM amoptions callback and add_reloption_to_kind() +CREATE EXTENSION dummy_table_am; +-- Sanity: CREATE TABLE with AM-specific options succeeds and round-trips +CREATE TABLE dummy_t (a int) USING dummy_table_am + WITH (option_int = 17, option_real = 2.5, option_bool = false, + option_enum = 'two', fillfactor = 60); +SELECT reloptions FROM pg_class + WHERE oid = 'dummy_t'::regclass ORDER BY reloptions; + reloptions +--------------------------------------------------------------------------------- + {option_int=17,option_real=2.5,option_bool=false,option_enum=two,fillfactor=60} +(1 row) + +-- AM-specific option ranges are enforced (option_int allows -10..100) +CREATE TABLE dummy_oor (a int) USING dummy_table_am WITH (option_int = 9999); +ERROR: value 9999 out of bounds for option "option_int" +DETAIL: Valid values are between "-10" and "100". +-- Unknown options are rejected at CREATE TABLE time +CREATE TABLE dummy_bad (a int) USING dummy_table_am WITH (parallel_workers = 4); +ERROR: unrecognized parameter "parallel_workers" +-- Default values land in pg_class only when the user did not set them +CREATE TABLE dummy_defaults (a int) USING dummy_table_am; +SELECT reloptions FROM pg_class WHERE oid = 'dummy_defaults'::regclass; + reloptions +------------ + +(1 row) + +DROP TABLE dummy_defaults; +-- ALTER TABLE ... SET (...) with AM-specific option +ALTER TABLE dummy_t SET (option_int = 42); +SELECT reloptions FROM pg_class WHERE oid = 'dummy_t'::regclass; + reloptions +--------------------------------------------------------------------------------- + {option_real=2.5,option_bool=false,option_enum=two,fillfactor=60,option_int=42} +(1 row) + +-- ALTER TABLE ... SET (...) with an unknown option errors +ALTER TABLE dummy_t SET (parallel_workers = 4); +ERROR: unrecognized parameter "parallel_workers" +-- ALTER TABLE ... RESET (option) round-trips +ALTER TABLE dummy_t RESET (option_int); +SELECT reloptions FROM pg_class WHERE oid = 'dummy_t'::regclass; + reloptions +------------------------------------------------------------------- + {option_real=2.5,option_bool=false,option_enum=two,fillfactor=60} +(1 row) + +-- SET ACCESS METHOD to an AM with no amoptions of its own (heap) must +-- still revalidate: an option that only the *current* AM understands +-- (option_int) must not be silently left behind when switching away. +ALTER TABLE dummy_t RESET (option_real, option_bool, option_enum); +ALTER TABLE dummy_t SET (option_int = 42); +ALTER TABLE dummy_t SET ACCESS METHOD heap; +ERROR: unrecognized parameter "option_int" +-- Confirm nothing changed +SELECT amname FROM pg_class c JOIN pg_am a ON a.oid = c.relam + WHERE c.oid = 'dummy_t'::regclass; + amname +---------------- + dummy_table_am +(1 row) + +SELECT reloptions FROM pg_class WHERE oid = 'dummy_t'::regclass; + reloptions +------------------------------- + {fillfactor=60,option_int=42} +(1 row) + +-- Succeeds once the offending option is cleared in the same statement. +ALTER TABLE dummy_t SET ACCESS METHOD heap, RESET (option_int); +SELECT amname FROM pg_class c JOIN pg_am a ON a.oid = c.relam + WHERE c.oid = 'dummy_t'::regclass; + amname +-------- + heap +(1 row) + +SELECT reloptions FROM pg_class WHERE oid = 'dummy_t'::regclass; + reloptions +----------------- + {fillfactor=60} +(1 row) + +-- SET ACCESS METHOD revalidation: +-- moving a heap table that has standard heap options not accepted by the +-- new AM (parallel_workers) into dummy_table_am must fail with a clear +-- message and must NOT silently drop the option. +CREATE TABLE heap_t (a int) WITH (fillfactor = 70, parallel_workers = 4); +SELECT reloptions FROM pg_class WHERE oid = 'heap_t'::regclass; + reloptions +------------------------------------ + {fillfactor=70,parallel_workers=4} +(1 row) + +ALTER TABLE heap_t SET ACCESS METHOD dummy_table_am; +ERROR: unrecognized parameter "parallel_workers" +-- Confirm nothing changed +SELECT amname FROM pg_class c JOIN pg_am a ON a.oid = c.relam + WHERE c.oid = 'heap_t'::regclass; + amname +-------- + heap +(1 row) + +SELECT reloptions FROM pg_class WHERE oid = 'heap_t'::regclass; + reloptions +------------------------------------ + {fillfactor=70,parallel_workers=4} +(1 row) + +-- After RESETing the offending option in the same statement the swap +-- succeeds; fillfactor survives because dummy_table_am inherits it via +-- add_reloption_to_kind(). +ALTER TABLE heap_t SET ACCESS METHOD dummy_table_am, RESET (parallel_workers); +SELECT amname FROM pg_class c JOIN pg_am a ON a.oid = c.relam + WHERE c.oid = 'heap_t'::regclass; + amname +---------------- + dummy_table_am +(1 row) + +SELECT reloptions FROM pg_class WHERE oid = 'heap_t'::regclass; + reloptions +----------------- + {fillfactor=70} +(1 row) + +-- Going back to heap still works: heap accepts fillfactor. +ALTER TABLE heap_t SET ACCESS METHOD heap; +SELECT amname FROM pg_class c JOIN pg_am a ON a.oid = c.relam + WHERE c.oid = 'heap_t'::regclass; + amname +-------- + heap +(1 row) + +-- SET ACCESS METHOD + SET (...) of an option that only the new AM accepts. +CREATE TABLE heap_to_dt (a int); +ALTER TABLE heap_to_dt SET ACCESS METHOD dummy_table_am, SET (option_int = 25); +SELECT amname FROM pg_class c JOIN pg_am a ON a.oid = c.relam + WHERE c.oid = 'heap_to_dt'::regclass; + amname +---------------- + dummy_table_am +(1 row) + +SELECT reloptions FROM pg_class WHERE oid = 'heap_to_dt'::regclass; + reloptions +----------------- + {option_int=25} +(1 row) + +-- Partitioned-table inheritance: AM declared on the parent partition flows +-- to partitions that don't override it. Partitioned tables themselves +-- cannot carry reloptions; the test verifies the AM lookup that +-- DefineRelation does for partitions. +CREATE TABLE parted (a int) PARTITION BY RANGE (a) USING dummy_table_am; +CREATE TABLE parted_p1 PARTITION OF parted FOR VALUES FROM (0) TO (100) + WITH (option_int = 11); +SELECT c.relname, + (SELECT amname FROM pg_am WHERE oid = c.relam) AS amname, + c.reloptions + FROM pg_class c + WHERE c.oid IN ('parted'::regclass, 'parted_p1'::regclass) + ORDER BY c.relname; + relname | amname | reloptions +-----------+----------------+----------------- + parted | dummy_table_am | + parted_p1 | dummy_table_am | {option_int=11} +(2 rows) + +-- A partition that explicitly chooses heap must reject options that are +-- only known to the parent's AM. +CREATE TABLE parted_p2 PARTITION OF parted FOR VALUES FROM (100) TO (200) + USING heap WITH (option_int = 9); +ERROR: unrecognized parameter "option_int" +DROP TABLE parted; +DROP TABLE heap_to_dt; +DROP TABLE heap_t; +DROP TABLE dummy_t; +DROP EXTENSION dummy_table_am; 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..ad3fa2410cc --- /dev/null +++ b/src/test/modules/dummy_table_am/meson.build @@ -0,0 +1,33 @@ +# Copyright (c) 2026, 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..20785d7834a --- /dev/null +++ b/src/test/modules/dummy_table_am/sql/reloptions.sql @@ -0,0 +1,105 @@ +-- Tests for the table AM amoptions callback and add_reloption_to_kind() +CREATE EXTENSION dummy_table_am; + +-- Sanity: CREATE TABLE with AM-specific options succeeds and round-trips +CREATE TABLE dummy_t (a int) USING dummy_table_am + WITH (option_int = 17, option_real = 2.5, option_bool = false, + option_enum = 'two', fillfactor = 60); +SELECT reloptions FROM pg_class + WHERE oid = 'dummy_t'::regclass ORDER BY reloptions; + +-- AM-specific option ranges are enforced (option_int allows -10..100) +CREATE TABLE dummy_oor (a int) USING dummy_table_am WITH (option_int = 9999); + +-- Unknown options are rejected at CREATE TABLE time +CREATE TABLE dummy_bad (a int) USING dummy_table_am WITH (parallel_workers = 4); + +-- Default values land in pg_class only when the user did not set them +CREATE TABLE dummy_defaults (a int) USING dummy_table_am; +SELECT reloptions FROM pg_class WHERE oid = 'dummy_defaults'::regclass; +DROP TABLE dummy_defaults; + +-- ALTER TABLE ... SET (...) with AM-specific option +ALTER TABLE dummy_t SET (option_int = 42); +SELECT reloptions FROM pg_class WHERE oid = 'dummy_t'::regclass; + +-- ALTER TABLE ... SET (...) with an unknown option errors +ALTER TABLE dummy_t SET (parallel_workers = 4); + +-- ALTER TABLE ... RESET (option) round-trips +ALTER TABLE dummy_t RESET (option_int); +SELECT reloptions FROM pg_class WHERE oid = 'dummy_t'::regclass; + +-- SET ACCESS METHOD to an AM with no amoptions of its own (heap) must +-- still revalidate: an option that only the *current* AM understands +-- (option_int) must not be silently left behind when switching away. +ALTER TABLE dummy_t RESET (option_real, option_bool, option_enum); +ALTER TABLE dummy_t SET (option_int = 42); +ALTER TABLE dummy_t SET ACCESS METHOD heap; +-- Confirm nothing changed +SELECT amname FROM pg_class c JOIN pg_am a ON a.oid = c.relam + WHERE c.oid = 'dummy_t'::regclass; +SELECT reloptions FROM pg_class WHERE oid = 'dummy_t'::regclass; +-- Succeeds once the offending option is cleared in the same statement. +ALTER TABLE dummy_t SET ACCESS METHOD heap, RESET (option_int); +SELECT amname FROM pg_class c JOIN pg_am a ON a.oid = c.relam + WHERE c.oid = 'dummy_t'::regclass; +SELECT reloptions FROM pg_class WHERE oid = 'dummy_t'::regclass; + +-- SET ACCESS METHOD revalidation: +-- moving a heap table that has standard heap options not accepted by the +-- new AM (parallel_workers) into dummy_table_am must fail with a clear +-- message and must NOT silently drop the option. +CREATE TABLE heap_t (a int) WITH (fillfactor = 70, parallel_workers = 4); +SELECT reloptions FROM pg_class WHERE oid = 'heap_t'::regclass; +ALTER TABLE heap_t SET ACCESS METHOD dummy_table_am; +-- Confirm nothing changed +SELECT amname FROM pg_class c JOIN pg_am a ON a.oid = c.relam + WHERE c.oid = 'heap_t'::regclass; +SELECT reloptions FROM pg_class WHERE oid = 'heap_t'::regclass; + +-- After RESETing the offending option in the same statement the swap +-- succeeds; fillfactor survives because dummy_table_am inherits it via +-- add_reloption_to_kind(). +ALTER TABLE heap_t SET ACCESS METHOD dummy_table_am, RESET (parallel_workers); +SELECT amname FROM pg_class c JOIN pg_am a ON a.oid = c.relam + WHERE c.oid = 'heap_t'::regclass; +SELECT reloptions FROM pg_class WHERE oid = 'heap_t'::regclass; + +-- Going back to heap still works: heap accepts fillfactor. +ALTER TABLE heap_t SET ACCESS METHOD heap; +SELECT amname FROM pg_class c JOIN pg_am a ON a.oid = c.relam + WHERE c.oid = 'heap_t'::regclass; + +-- SET ACCESS METHOD + SET (...) of an option that only the new AM accepts. +CREATE TABLE heap_to_dt (a int); +ALTER TABLE heap_to_dt SET ACCESS METHOD dummy_table_am, SET (option_int = 25); +SELECT amname FROM pg_class c JOIN pg_am a ON a.oid = c.relam + WHERE c.oid = 'heap_to_dt'::regclass; +SELECT reloptions FROM pg_class WHERE oid = 'heap_to_dt'::regclass; + +-- Partitioned-table inheritance: AM declared on the parent partition flows +-- to partitions that don't override it. Partitioned tables themselves +-- cannot carry reloptions; the test verifies the AM lookup that +-- DefineRelation does for partitions. +CREATE TABLE parted (a int) PARTITION BY RANGE (a) USING dummy_table_am; +CREATE TABLE parted_p1 PARTITION OF parted FOR VALUES FROM (0) TO (100) + WITH (option_int = 11); +SELECT c.relname, + (SELECT amname FROM pg_am WHERE oid = c.relam) AS amname, + c.reloptions + FROM pg_class c + WHERE c.oid IN ('parted'::regclass, 'parted_p1'::regclass) + ORDER BY c.relname; + +-- A partition that explicitly chooses heap must reject options that are +-- only known to the parent's AM. +CREATE TABLE parted_p2 PARTITION OF parted FOR VALUES FROM (100) TO (200) + USING heap WITH (option_int = 9); + +DROP TABLE parted; +DROP TABLE heap_to_dt; +DROP TABLE heap_t; +DROP TABLE dummy_t; + +DROP EXTENSION dummy_table_am; diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index 4bca42bb370..07b6b24a5ab 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -5,6 +5,7 @@ subdir('commit_ts') subdir('delay_execution') subdir('dummy_index_am') subdir('dummy_seclabel') +subdir('dummy_table_am') subdir('gin') subdir('index') subdir('injection_points') diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 56c1f997f88..e6c6443fdf9 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -710,6 +710,8 @@ DropSubscriptionStmt DropTableSpaceStmt DropUserMappingStmt DropdbStmt +DummyTableEnum +DummyTableOptions DumpComponents DumpId DumpOptions -- 2.43.0
