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. -- Julien Tachoires
>From ceb99fc9cb49eb5bca7ef35dd2afc767b5d2abf1 Mon Sep 17 00:00:00 2001 From: Julien Tachoires <jul...@tachoires.me> 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 table AM can be dropped (if not supported by the new table AM) via: DROP option, or, updated via: SET option 'value'. Before this commit, tables using different table AMs than heap were able to use heap's reloptions (fillfactor, toast_tuple_target, etc...). Now, this is not the case anymore: if the table AM 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 11d1bc7dbe1..1b4dd023877 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -77,7 +77,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>] [, ... ] ) @@ -755,7 +755,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 @@ -773,6 +773,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 e5c034d724e..4420d4c83cd 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -1552,7 +1552,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 645b5c00467..8de07d3d266 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); @@ -2048,7 +2051,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) @@ -2078,6 +2082,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 table AM 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 24d3765aa20..e9a1cb4ba1e 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" @@ -2701,6 +2702,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 10624353b0a..d067b9a0be9 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -657,6 +657,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); @@ -906,24 +908,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; @@ -1026,6 +1010,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, @@ -5507,6 +5514,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 */ @@ -16024,6 +16034,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. */ @@ -16081,7 +16223,7 @@ ATExecSetRelOptions(Relation rel, List *defList, AlterTableType operation, { case RELKIND_RELATION: 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 0fc502a3a40..16ac2ea8260 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -2919,6 +2919,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 2513a8ef8a6..aff14a71585 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -332,6 +332,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, @@ -346,7 +347,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, @@ -2033,7 +2034,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); @@ -2106,7 +2108,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; @@ -2364,6 +2367,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) { @@ -2689,7 +2693,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; @@ -2698,7 +2703,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; @@ -2721,6 +2726,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; @@ -2741,7 +2747,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 9f54a9e72b7..8771e8d9846 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -469,6 +469,7 @@ RelationParseRelOptions(Relation relation, HeapTuple tuple) { bytea *options; amoptions_function amoptsfn; + reloptions_function reloptsfn; relation->rd_options = NULL; @@ -480,13 +481,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: @@ -498,7 +504,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 dfbb4c85460..37f51d0f1c2 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" @@ -237,7 +238,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, @@ -251,6 +253,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 b8cb1e744ad..f7ec0ed57bc 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 dd22b5efdfd..8e42f394107 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 8d3ec3528f30a5fcacc2930249d2e20c0ad325bd Mon Sep 17 00:00:00 2001 From: Julien Tachoires <jul...@tachoires.me> 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 table AM 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 | 581 ++++++++++++++++++ .../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, 948 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..bc9beba195a --- /dev/null +++ b/src/test/modules/dummy_table_am/dummy_table_am.c @@ -0,0 +1,581 @@ +/*------------------------------------------------------------------------- + * + * 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_tuple(TableScanDesc scan, TupleTableSlot *slot, + bool *recheck, uint64 *lossy_pages, + uint64 *exact_pages) +{ + 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_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..0b947500ead --- /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..47fb4862c6c --- /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