From e467df19b0807ca4351be5c9ad246d3959929c5f Mon Sep 17 00:00:00 2001 From: Akshay Joshi Date: Tue, 2 Jun 2026 14:18:40 +0530 Subject: [PATCH v14] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements The function reconstructs the CREATE TABLE statement for an ordinary or partitioned table, followed by the ALTER TABLE / CREATE INDEX / CREATE RULE / CREATE STATISTICS statements needed to restore its full definition. Each statement is returned as a separate row. Supported per-column features: data type with type modifiers, COLLATE, STORAGE, COMPRESSION (pglz / lz4), GENERATED ALWAYS AS (expr) STORED/VIRTUAL, GENERATED ALWAYS|BY DEFAULT AS IDENTITY (with sequence options), DEFAULT, NOT NULL (including named NOT NULL constraints), and per-column attoptions emitted as ALTER COLUMN SET (...). Supported table-level features: UNLOGGED, INHERITS, PARTITION BY (RANGE / LIST / HASH parents), PARTITION OF parent FOR VALUES (FROM/TO, WITH modulus/remainder, DEFAULT), USING table access method, WITH (reloptions), TABLESPACE, and inline CHECK constraints in the CREATE TABLE body. Supported sub-objects (re-using existing deparse helpers from ruleutils.c): indexes (including partial and functional) via pg_get_indexdef_ddl; constraints (PRIMARY KEY with WITHOUT OVERLAPS for temporal keys, UNIQUE with NULLS NOT DISTINCT and INCLUDE columns, FOREIGN KEY with ON DELETE/UPDATE referential actions and MATCH clause, NOT ENFORCED foreign keys, EXCLUDE, named NOT NULL) via pg_get_constraintdef_body; rules via pg_get_ruledef_ddl; extended statistics via pg_get_statisticsobjdef_ddl; REPLICA IDENTITY NOTHING/FULL/USING INDEX; ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY; and child-local DEFAULT overrides on inheritance/partition children. DDL for partition children of a partitioned-table parent is appended after the parent by default. The function signature follows the named-parameter convention established by pg_get_role_ddl(), pg_get_tablespace_ddl(), and pg_get_database_ddl(): pg_get_table_ddl(relation regclass, pretty boolean DEFAULT false, owner boolean DEFAULT true, tablespace boolean DEFAULT true, schema_qualified boolean DEFAULT true, only_kinds text[] DEFAULT NULL, except_kinds text[] DEFAULT NULL) pretty controls pretty-printed output. owner controls emission of the ALTER TABLE ... OWNER TO statement. tablespace controls the TABLESPACE clause on CREATE TABLE. schema_qualified controls whether object names are emitted with their schema prefix: when true (the default) the active search_path is temporarily narrowed to pg_catalog so every deparse helper produces fully-qualified names; when false it is narrowed to the target table's own schema so same-schema references come out unqualified while cross-schema references remain qualified for correctness. Temporary tables are never schema-qualified regardless of this setting: the TEMPORARY keyword already places them in pg_temp, and emitting pg_temp_NN.relname would produce non-replayable DDL. Object-class filtering uses two mutually-exclusive text-array parameters, only_kinds and except_kinds. When only_kinds is set, only the listed kinds are emitted; when except_kinds is set, every kind except the listed ones is emitted; when neither is set every kind is emitted. Each array element is matched case-insensitively with leading/trailing whitespace trimmed; an unrecognized name raises an error. The kind vocabulary is: table, index, primary_key, unique, check, foreign_key, exclusion, rule, statistics, rls, replica_identity, partition trigger and policy are accepted in the vocabulary but currently produce no output; they are reserved for when standalone pg_get_trigger_ddl() and pg_get_policy_ddl() helpers become available. NOT NULL is not part of the vocabulary: it is always emitted to prevent producing schemas that silently accept NULLs the source would reject. When replica_identity is in the active filter and the table's REPLICA IDENTITY USING INDEX references an index that the filter would suppress, the function raises an error before emitting any output, so the generated DDL never references an index it did not produce. Default omission convention: every optional clause is omitted when its value matches what the server would reapply on round-trip, including type-default COLLATE, per-type STORAGE, the auto-generated identity sequence name and parameter defaults, heap access method, default REPLICA IDENTITY, disabled RLS toggles, empty reloptions, and the default tablespace. Author: Akshay Joshi Reviewed-by: Marcos Pegoraro Reviewed-by: Zsolt Parragi Reviewed-by: Kyotaro Horiguchi Reviewed-by: Chao Li Reviewed-by: Rui Zhao --- doc/src/sgml/func/func-info.sgml | 138 ++ src/backend/catalog/pg_inherits.c | 93 + src/backend/commands/tablecmds.c | 30 +- src/backend/utils/adt/ddlutils.c | 1959 ++++++++++++++++- src/backend/utils/adt/ruleutils.c | 87 +- src/include/catalog/pg_inherits.h | 1 + src/include/catalog/pg_proc.dat | 8 + src/include/commands/tablecmds.h | 3 + src/include/utils/ruleutils.h | 4 + .../regress/expected/pg_get_table_ddl.out | 1274 +++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/pg_get_table_ddl.sql | 740 +++++++ src/tools/pgindent/typedefs.list | 2 + 13 files changed, 4324 insertions(+), 17 deletions(-) create mode 100644 src/test/regress/expected/pg_get_table_ddl.out create mode 100644 src/test/regress/sql/pg_get_table_ddl.sql diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index 69ef3857cfa..fa2431bef4a 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -3960,6 +3960,144 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} is false, the OWNER clause is omitted. + + + + pg_get_table_ddl + + pg_get_table_ddl + ( relation regclass + , pretty boolean + DEFAULT false + , owner boolean + DEFAULT true + , tablespace boolean + DEFAULT true + , schema_qualified boolean + DEFAULT true + , only_kinds text[] + DEFAULT NULL + , except_kinds text[] + DEFAULT NULL ) + setof text + + + Reconstructs the CREATE TABLE statement for the + specified ordinary or partitioned table, followed by the + ALTER TABLE, CREATE INDEX, + CREATE RULE, and CREATE STATISTICS + statements needed to recreate the table's columns, constraints, + indexes, rules, extended statistics, and row-level security flags. + Inherited columns and constraints are emitted by the parent table's + DDL and are not duplicated on inheritance children or partitions. + Each statement is returned as a separate row. + When pretty is true, the output is + pretty-printed. When owner is false, the + ALTER TABLE ... OWNER TO statement is omitted. + When tablespace is false, the + TABLESPACE clause is omitted from the + CREATE TABLE statement. The + only_kinds and except_kinds + parameters described below control which object classes are emitted. + + + The only_kinds and except_kinds + parameters each take a text array of object-class kind names + and are mutually exclusive (specifying both raises an + error). When only_kinds is set, only the + listed kinds are emitted; when except_kinds is + set, every kind except the listed ones is + emitted. When neither is set (the default), every kind is + emitted. Leading and trailing whitespace in each array element + is ignored and matching is case-insensitive; an unrecognized + kind name raises an error. + The kind vocabulary is + table, + index, + primary_key, + unique, + check, + foreign_key, + exclusion, + rule, + statistics, + trigger, + policy, + rls (the + ENABLE/FORCE ROW LEVEL SECURITY + toggles), + replica_identity, and + partition (the DDL for each direct + partition child of a partitioned-table parent). + Note that trigger and policy + are accepted in the vocabulary but currently produce no output; + they are reserved for future use when standalone + pg_get_trigger_ddl and + pg_get_policy_ddl helpers become available. The + table kind groups the + CREATE TABLE statement together with the + related per-table ALTER TABLE passes: + OWNER TO, child-default + SET DEFAULT, and per-column + SET (attoptions). + NOT NULL constraints are not part of the + vocabulary; they are always emitted as part of the table to + avoid producing schemas that silently accept + NULL values the source would have rejected. + For example, the second pass of a two-pass schema clone that + adds cross-table foreign keys after data has loaded can be + written as + only_kinds => ARRAY['foreign_key'], and the + pub/sub-style clone that keeps a primary key but drops every + other constraint can be written as + except_kinds => ARRAY['unique','check','foreign_key','exclusion']. + + + When the table has + REPLICA IDENTITY USING INDEX and the + replica_identity kind is in the active + filter, the kind that emits the referenced index + (primary_key, unique, + exclusion, or index) must + also be in the filter. Otherwise the emitted + ALTER TABLE ... REPLICA IDENTITY USING INDEX + would reference an index the same DDL never produced, and the + function reports an error before emitting any statements. + + + The schema_qualified parameter (default + true) controls whether the target table's own + schema is included in the generated DDL. When set to + false, the table name is emitted unqualified + in the CREATE TABLE and every subsequent + ALTER TABLE, CREATE INDEX, + CREATE RULE, and + CREATE STATISTICS statement. References to + objects in the same schema as the target table (inheritance + parents, partition parents, identity sequences, and any + same-schema object the deparse helpers happen to mention) are + also emitted unqualified, so the script can be replayed under a + different search_path to recreate the table + in another schema. Cross-schema references (for example a + foreign key target in a different schema) remain qualified for + correctness. + + + All three forms of CREATE TABLE are supported: + the ordinary column-list form, the typed-table form + (OF type_name, with + per-column WITH OPTIONS overrides for local + defaults, NOT NULL, and CHECK + constraints), and the PARTITION OF form. + TEMPORARY and UNLOGGED + persistence modes are emitted from + relpersistence. For temporary tables + registered in the current session, the + ON COMMIT DELETE ROWS and + ON COMMIT DROP clauses are emitted; the default + ON COMMIT PRESERVE ROWS is omitted. + + diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c index 4b9802aafcc..897151bd6d1 100644 --- a/src/backend/catalog/pg_inherits.c +++ b/src/backend/catalog/pg_inherits.c @@ -236,6 +236,99 @@ find_inheritance_children_extended(Oid parentrelId, bool omit_detached, } +/* + * find_inheritance_parents + * + * Returns a list containing the OIDs of all relations that the relation with + * OID 'relid' inherits *directly* from, in inhseqno order (the order in which + * they should appear in an INHERITS clause). + * + * The specified lock type is acquired on each parent relation (but not on the + * given rel; caller should already have locked it). If lockmode is NoLock + * then no locks are acquired, but caller must beware of race conditions + * against possible DROPs of parent relations. + * + * Partition children also have a pg_inherits entry pointing to the + * partitioned parent; callers that distinguish INHERITS from PARTITION OF + * must check relispartition themselves. + */ +List * +find_inheritance_parents(Oid relid, LOCKMODE lockmode) +{ + List *list = NIL; + Relation relation; + SysScanDesc scan; + ScanKeyData key[1]; + HeapTuple inheritsTuple; + Oid inhparent; + Oid *oidarr; + int maxoids, + numoids, + i; + + /* + * Scan pg_inherits via the (inhrelid, inhseqno) index so that the rows + * come out in inhseqno order, which is the order required for the + * INHERITS clause. + */ + maxoids = 8; + oidarr = (Oid *) palloc(maxoids * sizeof(Oid)); + numoids = 0; + + relation = table_open(InheritsRelationId, AccessShareLock); + + ScanKeyInit(&key[0], + Anum_pg_inherits_inhrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + + scan = systable_beginscan(relation, InheritsRelidSeqnoIndexId, true, + NULL, 1, key); + + while ((inheritsTuple = systable_getnext(scan)) != NULL) + { + inhparent = ((Form_pg_inherits) GETSTRUCT(inheritsTuple))->inhparent; + if (numoids >= maxoids) + { + maxoids *= 2; + oidarr = (Oid *) repalloc(oidarr, maxoids * sizeof(Oid)); + } + oidarr[numoids++] = inhparent; + } + + systable_endscan(scan); + + table_close(relation, AccessShareLock); + + /* + * Acquire locks and build the result list. Unlike + * find_inheritance_children we do *not* sort by OID: callers need the + * seqno-ordered traversal. + */ + for (i = 0; i < numoids; i++) + { + inhparent = oidarr[i]; + + if (lockmode != NoLock) + { + LockRelationOid(inhparent, lockmode); + + if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(inhparent))) + { + UnlockRelationOid(inhparent, lockmode); + continue; + } + } + + list = lappend_oid(list, inhparent); + } + + pfree(oidarr); + + return list; +} + + /* * find_all_inheritors - * Returns a list of relation OIDs including the given rel plus diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 472db112fa7..2e108564de0 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -746,7 +746,6 @@ static ObjectAddress ATExecSetCompression(Relation rel, const char *column, Node *newValue, LOCKMODE lockmode); static void index_copy_data(Relation rel, RelFileLocator newrlocator); -static const char *storage_name(char c); static void RangeVarCallbackForDropRelation(const RangeVar *rel, Oid relOid, Oid oldRelOid, void *arg); @@ -2522,7 +2521,7 @@ truncate_check_activity(Relation rel) * storage_name * returns the name corresponding to a typstorage/attstorage enum value */ -static const char * +const char * storage_name(char c) { switch (c) @@ -19776,6 +19775,33 @@ remove_on_commit_action(Oid relid) } } +/* + * Look up the registered ON COMMIT action for a relation. + * + * Returns ONCOMMIT_NOOP when nothing was registered, which also covers + * temporary tables created with the default ON COMMIT PRESERVE ROWS + * behavior (register_on_commit_action() skips those, since no action is + * needed at commit). Entries marked for deletion in the current + * transaction are ignored. + */ +OnCommitAction +get_on_commit_action(Oid relid) +{ + ListCell *l; + + foreach(l, on_commits) + { + OnCommitItem *oc = (OnCommitItem *) lfirst(l); + + if (oc->relid != relid) + continue; + if (oc->deleting_subid != InvalidSubTransactionId) + continue; + return oc->oncommit; + } + return ONCOMMIT_NOOP; +} + /* * Perform ON COMMIT actions. * diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c index a70f1c28655..b56f59f0f41 100644 --- a/src/backend/utils/adt/ddlutils.c +++ b/src/backend/utils/adt/ddlutils.c @@ -5,7 +5,7 @@ * * This file contains the pg_get_*_ddl family of functions that generate * DDL statements to recreate database objects such as roles, tablespaces, - * and databases, along with common infrastructure for option parsing and + * databases, and tables, along with common infrastructure for * pretty-printing. * * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group @@ -21,30 +21,89 @@ #include "access/genam.h" #include "access/htup_details.h" #include "access/table.h" +#include "access/toast_compression.h" +#include "catalog/namespace.h" +#include "catalog/partition.h" +#include "catalog/pg_am.h" #include "catalog/pg_auth_members.h" #include "catalog/pg_authid.h" +#include "catalog/pg_class.h" #include "catalog/pg_collation.h" +#include "catalog/pg_constraint.h" #include "catalog/pg_database.h" #include "catalog/pg_db_role_setting.h" +#include "catalog/pg_inherits.h" +#include "catalog/pg_policy.h" +#include "catalog/pg_sequence.h" +#include "catalog/pg_statistic_ext.h" #include "catalog/pg_tablespace.h" +#include "catalog/pg_trigger.h" +#include "commands/defrem.h" +#include "commands/tablecmds.h" #include "commands/tablespace.h" -#include "common/relpath.h" #include "funcapi.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "utils/acl.h" +#include "utils/array.h" #include "utils/builtins.h" #include "utils/datetime.h" #include "utils/fmgroids.h" #include "utils/guc.h" #include "utils/lsyscache.h" -#include "utils/pg_locale.h" #include "utils/rel.h" #include "utils/ruleutils.h" #include "utils/syscache.h" #include "utils/timestamp.h" #include "utils/varlena.h" +/* + * Object-class kinds that the only / except options on + * pg_get_table_ddl can filter on. Members are stored as integers in a + * Bitmapset on TableDdlContext. Keep table_ddl_kind_names[] in sync + * with the order of additions here. + */ +typedef enum TableDdlKind +{ + TABLE_DDL_KIND_TABLE, + TABLE_DDL_KIND_INDEX, + TABLE_DDL_KIND_PRIMARY_KEY, + TABLE_DDL_KIND_UNIQUE, + TABLE_DDL_KIND_CHECK, + TABLE_DDL_KIND_FOREIGN_KEY, + TABLE_DDL_KIND_EXCLUSION, + TABLE_DDL_KIND_RULE, + TABLE_DDL_KIND_STATISTICS, + TABLE_DDL_KIND_TRIGGER, + TABLE_DDL_KIND_POLICY, + TABLE_DDL_KIND_RLS, + TABLE_DDL_KIND_REPLICA_IDENTITY, + TABLE_DDL_KIND_PARTITION, +} TableDdlKind; + +static const struct +{ + const char *name; + TableDdlKind kind; +} table_ddl_kind_names[] = +{ + {"table", TABLE_DDL_KIND_TABLE}, + {"index", TABLE_DDL_KIND_INDEX}, + {"primary_key", TABLE_DDL_KIND_PRIMARY_KEY}, + {"unique", TABLE_DDL_KIND_UNIQUE}, + {"check", TABLE_DDL_KIND_CHECK}, + {"foreign_key", TABLE_DDL_KIND_FOREIGN_KEY}, + {"exclusion", TABLE_DDL_KIND_EXCLUSION}, + {"rule", TABLE_DDL_KIND_RULE}, + {"statistics", TABLE_DDL_KIND_STATISTICS}, + {"trigger", TABLE_DDL_KIND_TRIGGER}, + {"policy", TABLE_DDL_KIND_POLICY}, + {"rls", TABLE_DDL_KIND_RLS}, + {"replica_identity", TABLE_DDL_KIND_REPLICA_IDENTITY}, + {"partition", TABLE_DDL_KIND_PARTITION}, +}; + +static Bitmapset *parse_kind_array(const char *paramname, ArrayType *arr); static void append_ddl_option(StringInfo buf, bool pretty, int indent, const char *fmt, ...) pg_attribute_printf(4, 5); @@ -57,6 +116,183 @@ static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid); static List *pg_get_database_ddl_internal(Oid dbid, bool pretty, bool no_owner, bool no_tablespace); +/* + * Per-column cache of locally-declared NOT NULL constraints. Built once + * by collect_local_not_null() and consulted by the column-emit helpers + * and the post-CREATE constraint loop. Entries with conoid == InvalidOid + * mean the column has no local NOT NULL constraint row in pg_constraint + * (the column may still have attnotnull=true on a pre-PG-18-upgraded + * catalog, in which case emit plain inline NOT NULL). + */ +typedef struct LocalNotNullEntry +{ + Oid conoid; + char *name; + bool is_auto; /* matches "__not_null" */ + bool no_inherit; +} LocalNotNullEntry; + +/* + * Working context threaded through the per-pass helpers below. Inputs + * (caller-provided option flags) are filled in once at the top of + * pg_get_table_ddl_internal and treated as read-only thereafter. Derived + * fields (qualname, nn_entries, skip_notnull_oids) are computed once + * during setup. Each pass appends to ctx->buf and pushes the finished + * statement onto ctx->statements via append_stmt(). + */ +typedef struct TableDdlContext +{ + Relation rel; + Oid relid; + bool pretty; + bool no_owner; + bool no_tablespace; + bool schema_qualified; + + /* + * Object-class filtering. If only_kinds is non-NULL, only the + * kinds in that set are emitted; if except_kinds is non-NULL, all + * kinds *except* those in that set are emitted; if both are NULL, + * every kind is emitted (the default). The two are mutually + * exclusive at the user-facing layer (the SRF entry rejects + * specifying both). + */ + Bitmapset *only_kinds; + Bitmapset *except_kinds; + + /* Derived during setup */ + Oid base_namespace; + char *qualname; + int save_nestlevel; /* >= 0 if we narrowed search_path */ + LocalNotNullEntry *nn_entries; + List *skip_notnull_oids; + + /* Mutable working state */ + StringInfoData buf; + List *statements; +} TableDdlContext; + +static List *pg_get_table_ddl_internal(TableDdlContext *ctx); +static bool is_kind_included(const TableDdlContext *ctx, TableDdlKind kind); +static void append_column_defs(StringInfo buf, Relation rel, bool pretty, + bool include_check, + bool schema_qualified, + LocalNotNullEntry *nn_entries); +static void append_typed_column_overrides(StringInfo buf, Relation rel, + bool pretty, bool include_check, + LocalNotNullEntry *nn_entries); +static void append_inline_check_constraints(StringInfo buf, Relation rel, + bool pretty, bool *first); +static char *find_attrdef_text(Relation rel, AttrNumber attnum, + List **dpcontext); +static char *lookup_relname_for_emit(Oid relid, bool schema_qualified, + Oid base_namespace); +static LocalNotNullEntry *collect_local_not_null(Relation rel, + List **skip_oids); + +static void append_stmt(TableDdlContext *ctx); +static void emit_create_table_stmt(TableDdlContext *ctx); +static void emit_owner_stmt(TableDdlContext *ctx); +static void emit_child_default_overrides(TableDdlContext *ctx); +static void emit_attoptions(TableDdlContext *ctx); +static void emit_indexes(TableDdlContext *ctx); +static void emit_local_constraints(TableDdlContext *ctx); +static void emit_rules(TableDdlContext *ctx); +static void emit_statistics(TableDdlContext *ctx); +static void emit_replica_identity(TableDdlContext *ctx); +static void emit_rls_toggles(TableDdlContext *ctx); +static void emit_partition_children(TableDdlContext *ctx); + + +/* + * parse_kind_array + * Parse a text[] of object-class kind names into a Bitmapset of + * TableDdlKind values. + * + * Each element is matched case-insensitively; surrounding whitespace is + * stripped. NULL elements are rejected. Unknown kind names raise an error + * citing the supplied parameter name. An empty array is also rejected. + * Duplicate entries are silently de-duplicated by the Bitmapset. + */ +static Bitmapset * +parse_kind_array(const char *paramname, ArrayType *arr) +{ + Datum *elems; + bool *nulls; + int nelems; + Bitmapset *result = NULL; + + deconstruct_array_builtin(arr, TEXTOID, &elems, &nulls, &nelems); + + if (nelems == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("parameter \"%s\" must specify at least one kind", + paramname))); + + for (int i = 0; i < nelems; i++) + { + char *raw; + char *token; + char *end; + bool found = false; + + if (nulls[i]) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("parameter \"%s\" must not contain NULL elements", + paramname))); + + raw = text_to_cstring(DatumGetTextPP(elems[i])); + + /* Trim leading whitespace. */ + token = raw; + while (*token == ' ' || *token == '\t') + token++; + + /* Trim trailing whitespace. */ + end = token + strlen(token); + while (end > token && (end[-1] == ' ' || end[-1] == '\t')) + end--; + *end = '\0'; + + for (size_t j = 0; j < lengthof(table_ddl_kind_names); j++) + { + if (pg_strcasecmp(token, table_ddl_kind_names[j].name) == 0) + { + result = bms_add_member(result, + (int) table_ddl_kind_names[j].kind); + found = true; + break; + } + } + + if (!found) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unrecognized kind \"%s\" in parameter \"%s\"", + token, paramname))); + + pfree(raw); + } + + return result; +} + +/* + * is_kind_included + * Determine whether DDL for the given object-class kind should be + * emitted under the current options. + */ +static bool +is_kind_included(const TableDdlContext *ctx, TableDdlKind kind) +{ + if (ctx->only_kinds != NULL) + return bms_is_member((int) kind, ctx->only_kinds); + if (ctx->except_kinds != NULL) + return !bms_is_member((int) kind, ctx->except_kinds); + return true; +} /* * Helper to append a formatted string with optional pretty-printing. @@ -975,3 +1211,1720 @@ pg_get_database_ddl(PG_FUNCTION_ARGS) SRF_RETURN_DONE(funcctx); } } + +/* + * lookup_relname_for_emit + * Return either the schema-qualified or the bare quoted name of a + * relation, depending on the schema_qualified flag. + * + * Temporary relations are never schema-qualified regardless of + * schema_qualified: the TEMPORARY keyword in CREATE TEMPORARY TABLE + * already places the table in the session's temp schema, so emitting + * pg_temp_NN.relname would produce DDL that cannot be replayed. + * + * When schema_qualified is true the schema-qualified name is always + * returned for non-temporary relations. When false, the bare relname + * is returned only if the target relation lives in base_namespace (the + * namespace of the table whose DDL is being generated); otherwise the + * schema-qualified form is returned, because cross-schema references + * (for example an inheritance parent or foreign key target in a + * different schema) are not safe to omit. + * + * This replaces the unsafe pattern + * quote_qualified_identifier(get_namespace_name(get_rel_namespace(oid)), + * get_rel_name(oid)) + * which dereferences NULL when a concurrent transaction has dropped the + * referenced relation (or its schema) between when we cached its OID and + * when we ask the syscache for its name. Holding AccessShareLock on a + * dependent relation makes this race vanishingly unlikely in practice, but + * we still defend against it because the alternative is a SIGSEGV. + * + * Caller is responsible for pfree()ing the result. + */ +static char * +lookup_relname_for_emit(Oid relid, bool schema_qualified, Oid base_namespace) +{ + HeapTuple tp; + Form_pg_class reltup; + char *nspname; + char *result; + + tp = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); + if (!HeapTupleIsValid(tp)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("relation with OID %u does not exist", relid), + errdetail("It may have been concurrently dropped."))); + + reltup = (Form_pg_class) GETSTRUCT(tp); + + /* + * Temporary relations are never schema-qualified: the TEMPORARY keyword + * already places them in pg_temp, and pg_temp_NN.relname cannot be + * replayed in any other session. + */ + if (isTempNamespace(reltup->relnamespace)) + { + result = pstrdup(quote_identifier(NameStr(reltup->relname))); + ReleaseSysCache(tp); + return result; + } + + /* Bare name only when caller asked and target is in the base namespace. */ + if (!schema_qualified && reltup->relnamespace == base_namespace) + { + result = pstrdup(quote_identifier(NameStr(reltup->relname))); + ReleaseSysCache(tp); + return result; + } + + nspname = get_namespace_name(reltup->relnamespace); + if (nspname == NULL) + { + Oid nspoid = reltup->relnamespace; + + ReleaseSysCache(tp); + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("schema with OID %u does not exist", nspoid), + errdetail("It may have been concurrently dropped."))); + } + + result = quote_qualified_identifier(nspname, NameStr(reltup->relname)); + + pfree(nspname); + ReleaseSysCache(tp); + + return result; +} + +/* + * collect_local_not_null + * Scan pg_constraint once for locally-declared NOT NULL constraints + * on rel, returning a palloc'd array indexed by attnum (1..natts). + * + * Entries with conoid==InvalidOid mean "no local NOT NULL row for this + * column". When the constraint name does not match the auto-generated + * pattern "__not_null", the constraint OID is + * appended to *skip_oids so the post-CREATE constraint loop can avoid + * re-emitting it as ALTER TABLE ... ADD CONSTRAINT - the column-emit + * pass will produce it inline as "CONSTRAINT name NOT NULL" instead. + */ +static LocalNotNullEntry * +collect_local_not_null(Relation rel, List **skip_oids) +{ + TupleDesc tupdesc = RelationGetDescr(rel); + int natts = tupdesc->natts; + LocalNotNullEntry *entries; + const char *relname = RelationGetRelationName(rel); + Relation conRel; + SysScanDesc conScan; + ScanKeyData conKey; + HeapTuple conTup; + + entries = (LocalNotNullEntry *) palloc0(sizeof(LocalNotNullEntry) * + (natts + 1)); + + conRel = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&conKey, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + conScan = systable_beginscan(conRel, ConstraintRelidTypidNameIndexId, + true, NULL, 1, &conKey); + + while (HeapTupleIsValid(conTup = systable_getnext(conScan))) + { + Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup); + Datum conkeyDat; + bool conkeyNull; + ArrayType *conkeyArr; + int16 *conkeyVals; + int attnum; + Form_pg_attribute att; + char autoname[NAMEDATALEN]; + + if (con->contype != CONSTRAINT_NOTNULL) + continue; + if (!con->conislocal) + continue; + + conkeyDat = heap_getattr(conTup, Anum_pg_constraint_conkey, + RelationGetDescr(conRel), &conkeyNull); + if (conkeyNull) + continue; + conkeyArr = DatumGetArrayTypeP(conkeyDat); + + /* + * Defend against a malformed conkey: a NOT NULL constraint is always + * a single-column 1-D int2 array, but a corrupted catalog or a future + * patch that stores wider conkeys mustn't trip us into reading past + * the array header. + */ + if (ARR_NDIM(conkeyArr) != 1 || + ARR_DIMS(conkeyArr)[0] < 1 || + ARR_HASNULL(conkeyArr) || + ARR_ELEMTYPE(conkeyArr) != INT2OID) + continue; + + conkeyVals = (int16 *) ARR_DATA_PTR(conkeyArr); + attnum = conkeyVals[0]; + if (attnum < 1 || attnum > natts) + continue; + + att = TupleDescAttr(tupdesc, attnum - 1); + snprintf(autoname, sizeof(autoname), "%s_%s_not_null", + relname, NameStr(att->attname)); + + entries[attnum].conoid = con->oid; + entries[attnum].name = pstrdup(NameStr(con->conname)); + entries[attnum].is_auto = (strcmp(NameStr(con->conname), autoname) == 0); + entries[attnum].no_inherit = con->connoinherit; + + /* + * Inline emission of NOT NULL only happens for columns that the + * column list actually emits, i.e. attislocal columns. For those + * the inline pass already materializes the constraint (either as + * "CONSTRAINT name NOT NULL" for user-named, or as a bare + * "NOT NULL" that PG re-creates under the auto-name pattern), so + * the post-CREATE constraint loop must not emit a second + * ALTER TABLE ... ADD CONSTRAINT for the same column - PG only + * allows one NOT NULL constraint per column and rejects the + * second with a name mismatch whenever the saved auto-name no + * longer matches the current table name (e.g. after a rename, or + * when CREATE TABLE uniquifies the auto-name to dodge a sequence + * collision). For a locally-declared NOT NULL sitting on an + * inherited (non-local) column the inline path never fires, so + * leave the OID out of skip_oids and let the post-CREATE loop + * emit ALTER TABLE ... ADD CONSTRAINT. + */ + if (att->attislocal && skip_oids != NULL) + *skip_oids = lappend_oid(*skip_oids, con->oid); + } + systable_endscan(conScan); + table_close(conRel, AccessShareLock); + + return entries; +} + +/* + * find_attrdef_text + * Return the deparsed DEFAULT/GENERATED expression for attnum on rel, + * or NULL if no entry exists in TupleConstr->defval. + * + * The caller passes a List ** so that the deparse context is built lazily + * and reused across calls (deparse_context_for is not cheap). Returned + * string is palloc'd in the current memory context; caller pfree's it. + */ +static char * +find_attrdef_text(Relation rel, AttrNumber attnum, List **dpcontext) +{ + TupleConstr *constr = RelationGetDescr(rel)->constr; + + if (constr == NULL) + return NULL; + + for (int j = 0; j < constr->num_defval; j++) + { + if (constr->defval[j].adnum != attnum) + continue; + + if (*dpcontext == NIL) + *dpcontext = deparse_context_for(RelationGetRelationName(rel), + RelationGetRelid(rel)); + + return deparse_expression(stringToNode(constr->defval[j].adbin), + *dpcontext, false, false); + } + return NULL; +} + +/* + * append_inline_check_constraints + * Emit each locally-declared CHECK constraint on rel as + * "CONSTRAINT name ", separated by ',' from any + * previously-emitted column or constraint. + * + * *first tracks whether anything has been emitted on this list yet, so the + * caller can chain column emission and constraint emission through the same + * buffer. Inherited CHECK constraints (!conislocal) come from the parent's + * DDL and aren't repeated here. + */ +static void +append_inline_check_constraints(StringInfo buf, Relation rel, bool pretty, + bool *first) +{ + Relation conRel; + SysScanDesc conScan; + ScanKeyData conKey; + HeapTuple conTup; + + conRel = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&conKey, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(rel))); + conScan = systable_beginscan(conRel, ConstraintRelidTypidNameIndexId, + true, NULL, 1, &conKey); + + while (HeapTupleIsValid(conTup = systable_getnext(conScan))) + { + Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup); + Datum defDatum; + char *defbody; + + if (con->contype != CONSTRAINT_CHECK) + continue; + if (!con->conislocal) + continue; + + if (!*first) + appendStringInfoChar(buf, ','); + if (pretty) + appendStringInfoString(buf, "\n "); + else if (!*first) + appendStringInfoChar(buf, ' '); + *first = false; + + defDatum = OidFunctionCall1(F_PG_GET_CONSTRAINTDEF_OID, + ObjectIdGetDatum(con->oid)); + defbody = TextDatumGetCString(defDatum); + appendStringInfo(buf, "CONSTRAINT %s %s", + quote_identifier(NameStr(con->conname)), + defbody); + pfree(defbody); + } + systable_endscan(conScan); + table_close(conRel, AccessShareLock); +} + +/* + * append_column_defs + * Append the comma-separated column definition list for a table. + * + * Emits each non-dropped, locally-declared column as + * name type [COLLATE x] [STORAGE s] [COMPRESSION c] + * [GENERATED ... | DEFAULT e] [NOT NULL] + * followed by any locally-declared inline CHECK constraints. Optional + * clauses are omitted when their value matches what the system would + * reapply on round-trip (e.g. type-default COLLATE, type-default STORAGE). + */ +static void +append_column_defs(StringInfo buf, Relation rel, bool pretty, + bool include_check, + bool schema_qualified, + LocalNotNullEntry *nn_entries) +{ + TupleDesc tupdesc = RelationGetDescr(rel); + Oid base_namespace = RelationGetNamespace(rel); + List *dpcontext = NIL; + bool first = true; + + for (int i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + char *typstr; + + if (att->attisdropped) + continue; + + /* + * Columns inherited from a parent are covered by the INHERITS clause, + * not the column list, unless the child redeclared them locally + * (attislocal=true). + */ + if (!att->attislocal) + continue; + + if (!first) + appendStringInfoChar(buf, ','); + if (pretty) + appendStringInfoString(buf, "\n "); + else if (!first) + appendStringInfoChar(buf, ' '); + first = false; + + appendStringInfoString(buf, quote_identifier(NameStr(att->attname))); + appendStringInfoChar(buf, ' '); + + typstr = format_type_with_typemod(att->atttypid, att->atttypmod); + appendStringInfoString(buf, typstr); + pfree(typstr); + + /* COLLATE clause, only if it differs from the type's default. */ + if (OidIsValid(att->attcollation) && + att->attcollation != get_typcollation(att->atttypid)) + appendStringInfo(buf, " COLLATE %s", + generate_collation_name(att->attcollation)); + + /* STORAGE clause, only if it differs from the type's default. */ + if (att->attstorage != get_typstorage(att->atttypid)) + appendStringInfo(buf, " STORAGE %s", storage_name(att->attstorage)); + + /* COMPRESSION clause, only if explicitly set on the column. */ + if (CompressionMethodIsValid(att->attcompression)) + appendStringInfo(buf, " COMPRESSION %s", + GetCompressionMethodName(att->attcompression)); + + /* + * Look up the default/generated expression text up front; generated + * columns have atthasdef=true with an entry in pg_attrdef just like + * regular defaults. + */ + { + char *defexpr = NULL; + + if (att->atthasdef) + defexpr = find_attrdef_text(rel, att->attnum, &dpcontext); + + /* GENERATED / IDENTITY / DEFAULT are mutually exclusive. */ + if (att->attgenerated == ATTRIBUTE_GENERATED_STORED && defexpr) + appendStringInfo(buf, " GENERATED ALWAYS AS (%s) STORED", defexpr); + else if (att->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL && defexpr) + appendStringInfo(buf, " GENERATED ALWAYS AS (%s) VIRTUAL", defexpr); + else if (att->attidentity == ATTRIBUTE_IDENTITY_ALWAYS || + att->attidentity == ATTRIBUTE_IDENTITY_BY_DEFAULT) + { + const char *idkind = + (att->attidentity == ATTRIBUTE_IDENTITY_ALWAYS) + ? "ALWAYS" : "BY DEFAULT"; + Oid seqid = getIdentitySequence(rel, att->attnum, true); + + appendStringInfo(buf, " GENERATED %s AS IDENTITY", idkind); + + /* + * Emit only the sequence options that differ from their + * defaults - mirroring pg_get_database_ddl's pattern of + * omitting values that the system would reapply on its own. + */ + if (OidIsValid(seqid)) + { + HeapTuple seqTup = SearchSysCache1(SEQRELID, + ObjectIdGetDatum(seqid)); + + if (HeapTupleIsValid(seqTup)) + { + Form_pg_sequence seq = (Form_pg_sequence) GETSTRUCT(seqTup); + StringInfoData opts; + bool first_opt = true; + int64 def_min, + def_max, + def_start; + int64 typ_min, + typ_max; + + /* + * Per-type bounds for the sequence's underlying + * integer type. Defaults to int8 if the column type + * is something else (shouldn't happen for IDENTITY, + * but be defensive). + */ + switch (att->atttypid) + { + case INT2OID: + typ_min = PG_INT16_MIN; + typ_max = PG_INT16_MAX; + break; + case INT4OID: + typ_min = PG_INT32_MIN; + typ_max = PG_INT32_MAX; + break; + default: + typ_min = PG_INT64_MIN; + typ_max = PG_INT64_MAX; + break; + } + + if (seq->seqincrement > 0) + { + def_min = 1; + def_max = typ_max; + def_start = def_min; + } + else + { + def_min = typ_min; + def_max = -1; + def_start = def_max; + } + + initStringInfo(&opts); + + /* + * SEQUENCE NAME - omit when it matches the implicit + * "__seq" pattern in the same + * schema, since CREATE TABLE will regenerate that + * exact name. The sequence is an INTERNAL dependency + * of the column, so the lock we hold on the table + * also pins it, but the lookup helper still defends + * against a missing pg_class row. + */ + { + HeapTuple seqClassTup; + Form_pg_class seqClass; + char autoname[NAMEDATALEN]; + + seqClassTup = SearchSysCache1(RELOID, + ObjectIdGetDatum(seqid)); + if (!HeapTupleIsValid(seqClassTup)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("identity sequence with OID %u does not exist", + seqid), + errdetail("It may have been concurrently dropped."))); + seqClass = (Form_pg_class) GETSTRUCT(seqClassTup); + + snprintf(autoname, sizeof(autoname), "%s_%s_seq", + RelationGetRelationName(rel), + NameStr(att->attname)); + if (seqClass->relnamespace != RelationGetNamespace(rel) || + strcmp(NameStr(seqClass->relname), autoname) != 0) + { + char *seqQual = + lookup_relname_for_emit(seqid, + schema_qualified, + base_namespace); + + appendStringInfo(&opts, "%sSEQUENCE NAME %s", + first_opt ? "" : " ", seqQual); + first_opt = false; + pfree(seqQual); + } + ReleaseSysCache(seqClassTup); + } + + if (seq->seqstart != def_start) + { + appendStringInfo(&opts, "%sSTART WITH " INT64_FORMAT, + first_opt ? "" : " ", seq->seqstart); + first_opt = false; + } + if (seq->seqincrement != 1) + { + appendStringInfo(&opts, "%sINCREMENT BY " INT64_FORMAT, + first_opt ? "" : " ", seq->seqincrement); + first_opt = false; + } + if (seq->seqmin != def_min) + { + appendStringInfo(&opts, "%sMINVALUE " INT64_FORMAT, + first_opt ? "" : " ", seq->seqmin); + first_opt = false; + } + if (seq->seqmax != def_max) + { + appendStringInfo(&opts, "%sMAXVALUE " INT64_FORMAT, + first_opt ? "" : " ", seq->seqmax); + first_opt = false; + } + if (seq->seqcache != 1) + { + appendStringInfo(&opts, "%sCACHE " INT64_FORMAT, + first_opt ? "" : " ", seq->seqcache); + first_opt = false; + } + if (seq->seqcycle) + { + appendStringInfo(&opts, "%sCYCLE", first_opt ? "" : " "); + first_opt = false; + } + + if (!first_opt) + appendStringInfo(buf, " (%s)", opts.data); + + pfree(opts.data); + ReleaseSysCache(seqTup); + } + } + } + else if (defexpr) + appendStringInfo(buf, " DEFAULT %s", defexpr); + + if (defexpr) + pfree(defexpr); + } + + if (att->attnotnull) + { + LocalNotNullEntry *nn = &nn_entries[att->attnum]; + + if (nn->name != NULL && !nn->is_auto) + appendStringInfo(buf, " CONSTRAINT %s NOT NULL", + quote_identifier(nn->name)); + else + appendStringInfoString(buf, " NOT NULL"); + if (nn->name != NULL && nn->no_inherit) + appendStringInfoString(buf, " NO INHERIT"); + } + } + + /* + * Table-level CHECK constraints - emitted inline in the CREATE TABLE body + * so they appear alongside the columns (the pg_dump shape). The + * constraint loop later in pg_get_table_ddl_internal skips CHECK + * constraints to avoid double-emission. + */ + if (include_check) + append_inline_check_constraints(buf, rel, pretty, &first); +} + +/* + * append_typed_column_overrides + * For a typed table (CREATE TABLE ... OF type_name), append the + * optional "(col WITH OPTIONS ..., ...)" list carrying locally + * applied per-column overrides - DEFAULT, NOT NULL, and any locally + * declared CHECK constraints. + * + * Columns whose type is fully dictated by reloftype emit nothing. The + * parenthesised list is suppressed entirely when no column needs an + * override and there are no locally-declared CHECK constraints, matching + * the canonical "CREATE TABLE x OF t;" shape. + */ +static void +append_typed_column_overrides(StringInfo buf, Relation rel, bool pretty, + bool include_check, + LocalNotNullEntry *nn_entries) +{ + TupleDesc tupdesc = RelationGetDescr(rel); + List *dpcontext = NIL; + StringInfoData inner; + bool first = true; + + initStringInfo(&inner); + + for (int i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + char *defexpr = NULL; + bool has_default; + bool has_notnull; + + if (att->attisdropped) + continue; + + if (att->atthasdef) + defexpr = find_attrdef_text(rel, att->attnum, &dpcontext); + + has_default = (defexpr != NULL); + has_notnull = att->attnotnull; + + if (!has_default && !has_notnull) + { + if (defexpr) + pfree(defexpr); + continue; + } + + if (!first) + appendStringInfoChar(&inner, ','); + if (pretty) + appendStringInfoString(&inner, "\n "); + else if (!first) + appendStringInfoChar(&inner, ' '); + first = false; + + appendStringInfo(&inner, "%s WITH OPTIONS", + quote_identifier(NameStr(att->attname))); + if (has_default) + appendStringInfo(&inner, " DEFAULT %s", defexpr); + if (has_notnull) + { + LocalNotNullEntry *nn = &nn_entries[att->attnum]; + + if (nn->name != NULL && !nn->is_auto) + appendStringInfo(&inner, " CONSTRAINT %s NOT NULL", + quote_identifier(nn->name)); + else + appendStringInfoString(&inner, " NOT NULL"); + if (nn->name != NULL && nn->no_inherit) + appendStringInfoString(&inner, " NO INHERIT"); + } + + if (defexpr) + pfree(defexpr); + } + + /* + * Locally-declared CHECK constraints on a typed table belong in the + * column-list parentheses, same as for an untyped table. The out-of-line + * constraint loop later still skips CHECKs. + */ + if (include_check) + append_inline_check_constraints(&inner, rel, pretty, &first); + + if (!first) + { + appendStringInfoString(buf, " ("); + appendStringInfoString(buf, inner.data); + if (pretty) + appendStringInfoString(buf, "\n)"); + else + appendStringInfoChar(buf, ')'); + } + pfree(inner.data); +} + +/* + * append_stmt + * Push ctx->buf onto ctx->statements. + * + * Used for all DDL emissions. When schema_qualified is false, the + * active search_path has already been narrowed to the base schema, so + * ruleutils helpers (pg_get_indexdef_ddl, pg_get_ruledef_ddl, + * pg_get_constraintdef_body, pg_get_statisticsobjdef_ddl) produce + * unqualified names for same-schema objects automatically. + */ +static void +append_stmt(TableDdlContext *ctx) +{ + ctx->statements = lappend(ctx->statements, pstrdup(ctx->buf.data)); +} + +/* + * emit_create_table_stmt + * Build the leading CREATE TABLE statement, including persistence + * (TEMPORARY / UNLOGGED), body (column list / OF type_name / + * PARTITION OF parent), INHERITS, PARTITION BY, USING method, + * WITH (reloptions), TABLESPACE, and ON COMMIT. + */ +static void +emit_create_table_stmt(TableDdlContext *ctx) +{ + Relation rel = ctx->rel; + char relpersistence = rel->rd_rel->relpersistence; + char relkind = rel->rd_rel->relkind; + bool is_typed = OidIsValid(rel->rd_rel->reloftype); + HeapTuple classtup; + Datum reloptDatum; + bool reloptIsnull; + + classtup = SearchSysCache1(RELOID, ObjectIdGetDatum(ctx->relid)); + if (!HeapTupleIsValid(classtup)) + elog(ERROR, "cache lookup failed for relation %u", ctx->relid); + + reloptDatum = SysCacheGetAttr(RELOID, classtup, + Anum_pg_class_reloptions, &reloptIsnull); + + resetStringInfo(&ctx->buf); + appendStringInfoString(&ctx->buf, "CREATE "); + if (relpersistence == RELPERSISTENCE_TEMP) + appendStringInfoString(&ctx->buf, "TEMPORARY "); + else if (relpersistence == RELPERSISTENCE_UNLOGGED) + appendStringInfoString(&ctx->buf, "UNLOGGED "); + appendStringInfo(&ctx->buf, "TABLE %s", ctx->qualname); + + if (rel->rd_rel->relispartition) + { + Oid parentOid = get_partition_parent(ctx->relid, true); + char *parentQual = lookup_relname_for_emit(parentOid, + ctx->schema_qualified, + ctx->base_namespace); + char *parentRelname = get_rel_name(parentOid); + Datum boundDatum; + bool boundIsnull; + char *forValues = NULL; + char *boundStr = NULL; + + if (parentRelname == NULL) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("partition parent with OID %u does not exist", + parentOid), + errdetail("It may have been concurrently dropped."))); + + boundDatum = SysCacheGetAttr(RELOID, classtup, + Anum_pg_class_relpartbound, &boundIsnull); + if (!boundIsnull) + { + Node *boundNode; + List *dpcontext; + + boundStr = TextDatumGetCString(boundDatum); + boundNode = stringToNode(boundStr); + dpcontext = deparse_context_for(parentRelname, parentOid); + forValues = deparse_expression(boundNode, dpcontext, false, false); + } + + appendStringInfo(&ctx->buf, " PARTITION OF %s %s", + parentQual, forValues ? forValues : "DEFAULT"); + if (forValues) + pfree(forValues); + if (boundStr) + pfree(boundStr); + pfree(parentQual); + pfree(parentRelname); + } + else if (is_typed) + { + char *typname = format_type_be_qualified(rel->rd_rel->reloftype); + + appendStringInfo(&ctx->buf, " OF %s", typname); + pfree(typname); + + append_typed_column_overrides(&ctx->buf, rel, ctx->pretty, + is_kind_included(ctx, TABLE_DDL_KIND_CHECK), + ctx->nn_entries); + } + else + { + List *parents; + ListCell *lc; + bool first; + + appendStringInfoString(&ctx->buf, " ("); + + append_column_defs(&ctx->buf, rel, ctx->pretty, + is_kind_included(ctx, TABLE_DDL_KIND_CHECK), + ctx->schema_qualified, ctx->nn_entries); + + if (ctx->pretty) + appendStringInfoString(&ctx->buf, "\n)"); + else + appendStringInfoChar(&ctx->buf, ')'); + + parents = find_inheritance_parents(ctx->relid, NoLock); + if (parents != NIL) + { + appendStringInfoString(&ctx->buf, " INHERITS ("); + first = true; + foreach(lc, parents) + { + Oid poid = lfirst_oid(lc); + char *pname = lookup_relname_for_emit(poid, + ctx->schema_qualified, + ctx->base_namespace); + + if (!first) + appendStringInfoString(&ctx->buf, ", "); + first = false; + appendStringInfoString(&ctx->buf, pname); + pfree(pname); + } + appendStringInfoChar(&ctx->buf, ')'); + list_free(parents); + } + } + + if (relkind == RELKIND_PARTITIONED_TABLE) + { + Datum partkeyDatum; + char *partkey; + + partkeyDatum = OidFunctionCall1(F_PG_GET_PARTKEYDEF, + ObjectIdGetDatum(ctx->relid)); + partkey = TextDatumGetCString(partkeyDatum); + appendStringInfo(&ctx->buf, " PARTITION BY %s", partkey); + pfree(partkey); + } + + if (OidIsValid(rel->rd_rel->relam) && + rel->rd_rel->relam != HEAP_TABLE_AM_OID) + { + char *amname = get_am_name(rel->rd_rel->relam); + + if (amname != NULL) + { + appendStringInfo(&ctx->buf, " USING %s", + quote_identifier(amname)); + pfree(amname); + } + } + + if (!reloptIsnull) + { + appendStringInfoString(&ctx->buf, " WITH ("); + get_reloptions(&ctx->buf, reloptDatum); + appendStringInfoChar(&ctx->buf, ')'); + } + + ReleaseSysCache(classtup); + + if (!ctx->no_tablespace && OidIsValid(rel->rd_rel->reltablespace)) + { + char *tsname = get_tablespace_name(rel->rd_rel->reltablespace); + + if (tsname != NULL) + { + appendStringInfo(&ctx->buf, " TABLESPACE %s", + quote_identifier(tsname)); + pfree(tsname); + } + } + + if (relpersistence == RELPERSISTENCE_TEMP) + { + OnCommitAction oc = get_on_commit_action(ctx->relid); + + if (oc == ONCOMMIT_DELETE_ROWS) + appendStringInfoString(&ctx->buf, " ON COMMIT DELETE ROWS"); + else if (oc == ONCOMMIT_DROP) + appendStringInfoString(&ctx->buf, " ON COMMIT DROP"); + } + + appendStringInfoChar(&ctx->buf, ';'); + append_stmt(ctx); +} + +/* + * emit_owner_stmt + * ALTER TABLE qualname OWNER TO role. + */ +static void +emit_owner_stmt(TableDdlContext *ctx) +{ + char *owner; + + if (ctx->no_owner) + return; + + owner = GetUserNameFromId(ctx->rel->rd_rel->relowner, false); + resetStringInfo(&ctx->buf); + appendStringInfo(&ctx->buf, "ALTER TABLE %s OWNER TO %s;", + ctx->qualname, quote_identifier(owner)); + append_stmt(ctx); + pfree(owner); +} + +/* + * emit_child_default_overrides + * ALTER TABLE qualname ALTER COLUMN col SET DEFAULT expr - one per + * inherited (attislocal=false) non-generated column carrying a + * locally-set default. Generated columns are skipped: their + * expression is inherited automatically and SET DEFAULT would + * fail at replay. + */ +static void +emit_child_default_overrides(TableDdlContext *ctx) +{ + TupleDesc tupdesc = RelationGetDescr(ctx->rel); + List *dpcontext = NIL; + + for (int i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + char *defstr; + + if (att->attisdropped || att->attislocal || !att->atthasdef) + continue; + if (att->attgenerated != '\0') + continue; + + defstr = find_attrdef_text(ctx->rel, att->attnum, &dpcontext); + if (defstr == NULL) + continue; + + resetStringInfo(&ctx->buf); + appendStringInfo(&ctx->buf, + "ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s;", + ctx->qualname, + quote_identifier(NameStr(att->attname)), + defstr); + append_stmt(ctx); + pfree(defstr); + } +} + +/* + * emit_attoptions + * ALTER TABLE qualname ALTER COLUMN col SET (...) - one per column + * with non-null pg_attribute.attoptions. The inline form of these + * options isn't available in CREATE TABLE, so they come out here. + */ +static void +emit_attoptions(TableDdlContext *ctx) +{ + TupleDesc tupdesc = RelationGetDescr(ctx->rel); + + for (int i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupdesc, i); + HeapTuple attTup; + Datum optDatum; + bool optIsnull; + + if (att->attisdropped) + continue; + + attTup = SearchSysCache2(ATTNUM, + ObjectIdGetDatum(ctx->relid), + Int16GetDatum(att->attnum)); + if (!HeapTupleIsValid(attTup)) + continue; + + optDatum = SysCacheGetAttr(ATTNUM, attTup, + Anum_pg_attribute_attoptions, &optIsnull); + if (!optIsnull) + { + resetStringInfo(&ctx->buf); + appendStringInfo(&ctx->buf, + "ALTER TABLE %s ALTER COLUMN %s SET (", + ctx->qualname, + quote_identifier(NameStr(att->attname))); + get_reloptions(&ctx->buf, optDatum); + appendStringInfoString(&ctx->buf, ");"); + append_stmt(ctx); + } + ReleaseSysCache(attTup); + } +} + +/* + * emit_indexes + * CREATE INDEX per non-constraint-backed index on the relation. + * Indexes that back PK / UNIQUE / EXCLUDE constraints are emitted + * out-of-line by emit_local_constraints (the ALTER TABLE ... ADD + * CONSTRAINT statement creates the index implicitly). + */ +static void +emit_indexes(TableDdlContext *ctx) +{ + List *indexoids; + ListCell *lc; + + if (!is_kind_included(ctx, TABLE_DDL_KIND_INDEX)) + return; + + indexoids = RelationGetIndexList(ctx->rel); + foreach(lc, indexoids) + { + Oid idxoid = lfirst_oid(lc); + char *idxdef; + + if (OidIsValid(get_index_constraint(idxoid))) + continue; + + idxdef = pg_get_indexdef_ddl(idxoid); + resetStringInfo(&ctx->buf); + appendStringInfo(&ctx->buf, "%s;", idxdef); + append_stmt(ctx); + pfree(idxdef); + } + list_free(indexoids); +} + +/* + * emit_local_constraints + * ALTER TABLE ... ADD CONSTRAINT for each locally-defined constraint + * on the relation. Inherited constraints (conislocal=false) come + * from the parent's DDL. CHECK constraints are emitted inline for + * regular/typed tables (skip here) but out-of-line for partition + * children (no column list to live in). Local NOT NULL + * constraints on attislocal columns - whether user-named or + * matching the auto-name pattern - are emitted inline by the + * column-emit helpers and are skipped via skip_notnull_oids, + * because PG only allows one NOT NULL per column and would reject + * a second ALTER TABLE ... ADD CONSTRAINT. Partition children + * have no column list, so their NOT NULLs fall through here. + * + * Each contype is gated on the matching kind in the only / except + * vocabulary, so callers can produce e.g. an FK-only pass + * (only => 'foreign_key') or a pub/sub clone that keeps only + * the primary key (except => 'unique,check,foreign_key,exclusion'). + * NOT NULL is intentionally not in the vocabulary - always emitted + * so cloned schemas don't silently accept NULLs the source would + * have rejected. + */ +static void +emit_local_constraints(TableDdlContext *ctx) +{ + Relation conRel; + SysScanDesc conScan; + ScanKeyData conKey; + HeapTuple conTup; + bool is_partition = ctx->rel->rd_rel->relispartition; + + conRel = table_open(ConstraintRelationId, AccessShareLock); + ScanKeyInit(&conKey, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(ctx->relid)); + conScan = systable_beginscan(conRel, ConstraintRelidTypidNameIndexId, + true, NULL, 1, &conKey); + + while (HeapTupleIsValid(conTup = systable_getnext(conScan))) + { + Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup); + + if (!con->conislocal) + continue; + + /* + * Each contype is gated on its kind in the only / except + * vocabulary. CHECK is also skipped for non-partition relations + * because append_inline_check_constraints emits those inline in + * the CREATE TABLE body; partition children have no column list + * to live in, so they come through this loop. NOT NULL is not a + * filterable kind - emitted unconditionally to avoid producing + * schemas that silently accept NULLs the source would have + * rejected. + */ + switch (con->contype) + { + case CONSTRAINT_PRIMARY: + if (!is_kind_included(ctx, TABLE_DDL_KIND_PRIMARY_KEY)) + continue; + break; + case CONSTRAINT_UNIQUE: + if (!is_kind_included(ctx, TABLE_DDL_KIND_UNIQUE)) + continue; + break; + case CONSTRAINT_CHECK: + if (!is_kind_included(ctx, TABLE_DDL_KIND_CHECK)) + continue; + + /* + * For non-partition relations, CHECK is normally emitted + * inline in the CREATE TABLE body by + * append_inline_check_constraints, so we skip it here to + * avoid double emission. But when KIND_TABLE is not in + * the active filter (e.g. only=>'check' or a clone that + * targets only sub-objects), the inline pass never runs; + * fall through and emit each CHECK via ALTER TABLE so the + * user actually gets the constraint they asked for. + */ + if (!is_partition && + is_kind_included(ctx, TABLE_DDL_KIND_TABLE)) + continue; + break; + case CONSTRAINT_FOREIGN: + if (!is_kind_included(ctx, TABLE_DDL_KIND_FOREIGN_KEY)) + continue; + break; + case CONSTRAINT_EXCLUSION: + if (!is_kind_included(ctx, TABLE_DDL_KIND_EXCLUSION)) + continue; + break; + case CONSTRAINT_NOTNULL: + /* + * Out-of-line NOT NULL is conceptually part of the + * table definition: gating it on KIND_TABLE means an + * only=foreign_key second pass does not re-emit + * NOT NULLs that the first pass already created, + * while the no-options default still emits them. + */ + if (!is_kind_included(ctx, TABLE_DDL_KIND_TABLE)) + continue; + if (!is_partition && + list_member_oid(ctx->skip_notnull_oids, con->oid)) + continue; + break; + default: + /* + * Any future contype the vocabulary does not yet cover: + * fall through and emit it via pg_get_constraintdef_command, + * matching the original loop's "emit unless filtered" + * behavior. This is unreachable in current PG (every + * contype above is enumerated) but kept defensive against + * a new contype being introduced. + */ + break; + } + + { + char *conbody = pg_get_constraintdef_body(con->oid); + + resetStringInfo(&ctx->buf); + appendStringInfo(&ctx->buf, "ALTER TABLE %s ADD CONSTRAINT %s %s;", + ctx->qualname, + quote_identifier(NameStr(con->conname)), + conbody); + append_stmt(ctx); + pfree(conbody); + } + } + systable_endscan(conScan); + table_close(conRel, AccessShareLock); +} + +/* + * emit_rules + * CREATE RULE per cached rewrite rule on the relation. + */ +static void +emit_rules(TableDdlContext *ctx) +{ + if (!is_kind_included(ctx, TABLE_DDL_KIND_RULE) || + ctx->rel->rd_rules == NULL) + return; + + for (int i = 0; i < ctx->rel->rd_rules->numLocks; i++) + { + Oid ruleid = ctx->rel->rd_rules->rules[i]->ruleId; + char *ruledef_str; + + ruledef_str = pg_get_ruledef_ddl(ruleid); + resetStringInfo(&ctx->buf); + appendStringInfoString(&ctx->buf, ruledef_str); + append_stmt(ctx); + pfree(ruledef_str); + } +} + +/* + * emit_statistics + * CREATE STATISTICS per extended statistics object on the relation. + */ +static void +emit_statistics(TableDdlContext *ctx) +{ + Relation statRel; + SysScanDesc statScan; + ScanKeyData statKey; + HeapTuple statTup; + + if (!is_kind_included(ctx, TABLE_DDL_KIND_STATISTICS)) + return; + + statRel = table_open(StatisticExtRelationId, AccessShareLock); + ScanKeyInit(&statKey, + Anum_pg_statistic_ext_stxrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(ctx->relid)); + statScan = systable_beginscan(statRel, StatisticExtRelidIndexId, + true, NULL, 1, &statKey); + + while (HeapTupleIsValid(statTup = systable_getnext(statScan))) + { + Form_pg_statistic_ext stat = (Form_pg_statistic_ext) GETSTRUCT(statTup); + char *statdef = pg_get_statisticsobjdef_ddl(stat->oid); + + resetStringInfo(&ctx->buf); + appendStringInfo(&ctx->buf, "%s;", statdef); + append_stmt(ctx); + pfree(statdef); + } + systable_endscan(statScan); + table_close(statRel, AccessShareLock); +} + +/* + * emit_replica_identity + * ALTER TABLE qualname REPLICA IDENTITY ... - emitted only when the + * relreplident differs from the default ('d' = use primary key). + */ +static void +emit_replica_identity(TableDdlContext *ctx) +{ + if (!is_kind_included(ctx, TABLE_DDL_KIND_REPLICA_IDENTITY)) + return; + if (ctx->rel->rd_rel->relreplident == REPLICA_IDENTITY_DEFAULT) + return; + + resetStringInfo(&ctx->buf); + switch (ctx->rel->rd_rel->relreplident) + { + case REPLICA_IDENTITY_NOTHING: + appendStringInfo(&ctx->buf, + "ALTER TABLE %s REPLICA IDENTITY NOTHING;", + ctx->qualname); + append_stmt(ctx); + break; + case REPLICA_IDENTITY_FULL: + appendStringInfo(&ctx->buf, + "ALTER TABLE %s REPLICA IDENTITY FULL;", + ctx->qualname); + append_stmt(ctx); + break; + case REPLICA_IDENTITY_INDEX: + { + Oid replidx = RelationGetReplicaIndex(ctx->rel); + + if (OidIsValid(replidx)) + { + char *idxname = get_rel_name(replidx); + + if (idxname == NULL) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("replica identity index with OID %u does not exist", + replidx), + errdetail("It may have been concurrently dropped."))); + + appendStringInfo(&ctx->buf, + "ALTER TABLE %s REPLICA IDENTITY USING INDEX %s;", + ctx->qualname, + quote_identifier(idxname)); + append_stmt(ctx); + pfree(idxname); + } + } + break; + } +} + +/* + * emit_rls_toggles + * ALTER TABLE qualname ENABLE / FORCE ROW LEVEL SECURITY. + */ +static void +emit_rls_toggles(TableDdlContext *ctx) +{ + if (!is_kind_included(ctx, TABLE_DDL_KIND_RLS)) + return; + + if (ctx->rel->rd_rel->relrowsecurity) + { + resetStringInfo(&ctx->buf); + appendStringInfo(&ctx->buf, + "ALTER TABLE %s ENABLE ROW LEVEL SECURITY;", + ctx->qualname); + append_stmt(ctx); + } + if (ctx->rel->rd_rel->relforcerowsecurity) + { + resetStringInfo(&ctx->buf); + appendStringInfo(&ctx->buf, + "ALTER TABLE %s FORCE ROW LEVEL SECURITY;", + ctx->qualname); + append_stmt(ctx); + } +} + +/* + * emit_partition_children + * For each direct partition child of a partitioned-table parent, + * recursively call pg_get_table_ddl_internal and append the child's + * statements. Each child's own DDL handles further levels through + * the same recursion. + */ +static void +emit_partition_children(TableDdlContext *ctx) +{ + List *children; + ListCell *lc; + + if (!is_kind_included(ctx, TABLE_DDL_KIND_PARTITION) || + ctx->rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) + return; + + children = find_inheritance_children(ctx->relid, AccessShareLock); + foreach(lc, children) + { + Oid childoid = lfirst_oid(lc); + TableDdlContext childctx = {0}; + List *childstmts; + + /* + * Each recursive invocation re-derives its own rel, namespace, + * qualname, NOT NULL cache, buffer, and statement list inside + * pg_get_table_ddl_internal. The user-supplied option fields + * carry over verbatim with one exception: KIND_PARTITION is a + * "gate" kind that controls whether we recurse at all, not a + * kind that the children themselves ever emit. If we propagated + * an only-set that contained PARTITION into a child, the child + * would not emit its own CREATE TABLE (KIND_TABLE absent) nor + * any sub-objects, and the recursion would produce nothing. So + * strip PARTITION out of only_kinds when recursing; if that + * empties the set, drop the filter entirely so the child emits + * its full DDL. except_kinds passes through unchanged because + * PARTITION in the except-set already stopped us from getting + * here. + */ + childctx.relid = childoid; + childctx.pretty = ctx->pretty; + childctx.no_owner = ctx->no_owner; + childctx.no_tablespace = ctx->no_tablespace; + childctx.schema_qualified = ctx->schema_qualified; + childctx.only_kinds = ctx->only_kinds; + childctx.except_kinds = ctx->except_kinds; + + if (childctx.only_kinds != NULL && + bms_is_member((int) TABLE_DDL_KIND_PARTITION, + childctx.only_kinds)) + { + Bitmapset *child_only = bms_copy(childctx.only_kinds); + + child_only = bms_del_member(child_only, + (int) TABLE_DDL_KIND_PARTITION); + if (bms_is_empty(child_only)) + { + bms_free(child_only); + childctx.only_kinds = NULL; + } + else + childctx.only_kinds = child_only; + } + + childstmts = pg_get_table_ddl_internal(&childctx); + ctx->statements = list_concat(ctx->statements, childstmts); + } + list_free(children); +} + +/* + * pg_get_table_ddl_internal + * Generate DDL statements to recreate a regular or partitioned table. + * + * The caller initializes *ctx with the user-supplied option fields + * (relid, pretty, no_owner, no_tablespace, schema_qualified, + * only_kinds, except_kinds). This function opens the relation, + * validates access, populates the derived fields (rel, qualname, + * nn_entries, ...), runs the emission passes, and returns the + * accumulated statement list. + * + * Each emission helper consults is_kind_included() to decide whether + * it should run. The table-proper passes (CREATE TABLE / OWNER / + * ALTER COLUMN ... SET DEFAULT / ALTER COLUMN ... SET (...)) are + * grouped under KIND_TABLE so the FK-only / sub-object-only workflow + * is expressible as a single only or except list. + * + * Trigger and policy emission are scaffolded but currently disabled + * (#if 0) - they will become a single helper call once the standalone + * pg_get_trigger_ddl / pg_get_policy_ddl helpers land. + */ +static List * +pg_get_table_ddl_internal(TableDdlContext *ctx) +{ + Relation rel; + char relkind; + AclResult aclresult; + + rel = table_open(ctx->relid, AccessShareLock); + + relkind = rel->rd_rel->relkind; + + /* + * The initial cut only supports ordinary and partitioned tables. Views, + * matviews, foreign tables, sequences, indexes, composite types, and + * TOAST tables are out of scope for now. + */ + if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE) + { + char *relname = pstrdup(RelationGetRelationName(rel)); + + table_close(rel, AccessShareLock); + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not an ordinary or partitioned table", + relname))); + } + + /* Caller needs SELECT on the table to read its definition. */ + aclresult = pg_class_aclcheck(ctx->relid, GetUserId(), ACL_SELECT); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_TABLE, + RelationGetRelationName(rel)); + + /* + * Validation: if the table has REPLICA IDENTITY USING INDEX and the + * referenced index would not be emitted under the active filter, the + * emitted REPLICA IDENTITY clause would reference an index the same DDL + * never produced. Determine which kind would emit the index (one of + * primary_key / unique / exclusion for constraint-backed indexes, + * otherwise the generic "index" kind) and require it to be in scope + * whenever "replica_identity" is. The check uses is_kind_included so + * it covers both forms naturally: an "except" list that omits the + * source kind, and an "only" list that omits it. + */ + if (rel->rd_rel->relreplident == REPLICA_IDENTITY_INDEX && + is_kind_included(ctx, TABLE_DDL_KIND_REPLICA_IDENTITY)) + { + Oid replidx = RelationGetReplicaIndex(rel); + + if (OidIsValid(replidx)) + { + TableDdlKind idx_kind = TABLE_DDL_KIND_INDEX; + Oid conoid = get_index_constraint(replidx); + + if (OidIsValid(conoid)) + { + HeapTuple conTup = SearchSysCache1(CONSTROID, + ObjectIdGetDatum(conoid)); + + if (HeapTupleIsValid(conTup)) + { + Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup); + + switch (con->contype) + { + case CONSTRAINT_PRIMARY: + idx_kind = TABLE_DDL_KIND_PRIMARY_KEY; + break; + case CONSTRAINT_UNIQUE: + idx_kind = TABLE_DDL_KIND_UNIQUE; + break; + case CONSTRAINT_EXCLUSION: + idx_kind = TABLE_DDL_KIND_EXCLUSION; + break; + default: + break; + } + ReleaseSysCache(conTup); + } + } + + if (!is_kind_included(ctx, idx_kind)) + { + char *relname = pstrdup(RelationGetRelationName(rel)); + const char *idx_name = table_ddl_kind_names[(int) idx_kind].name; + + table_close(rel, AccessShareLock); + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("REPLICA IDENTITY for table \"%s\" requires kind \"%s\" to be emitted", + relname, idx_name), + errdetail("The table's REPLICA IDENTITY USING INDEX references an index produced by the \"%s\" kind, which is not in the active filter.", + idx_name), + errhint("Either add \"%s\" to the filter or remove \"replica_identity\" from it.", + idx_name))); + } + } + } + + /* + * Populate derived fields now that the relation is open and validated. + * The remaining derived fields (nn_entries, skip_notnull_oids, buf, + * statements) start zeroed via the caller's `TableDdlContext ctx = {0}` + * initializer; the NOT NULL cache is populated by collect_local_not_null, + * the buffer is initStringInfo'd, and each emit pass appends to + * statements via lappend. + */ + ctx->rel = rel; + ctx->base_namespace = RelationGetNamespace(rel); + ctx->save_nestlevel = -1; + ctx->qualname = lookup_relname_for_emit(ctx->relid, ctx->schema_qualified, + ctx->base_namespace); + + /* + * Temporarily override search_path so that the ruleutils helpers + * (pg_get_indexdef_ddl, pg_get_constraintdef_body, + * pg_get_statisticsobjdef_ddl, pg_get_ruledef_ddl, etc.) produce names + * that match the schema_qualified flag. Those helpers decide whether to + * qualify a name by calling RelationIsVisible(), which checks whether the + * object's schema appears in the active search_path. + * + * schema_qualified = false: narrow to the base schema so that same-schema + * references in DEFAULT expressions, FK targets, indexes, rules, and + * statistics come out unqualified automatically. Cross-schema references + * stay qualified, which is the correctness requirement. + * + * schema_qualified = true: narrow to pg_catalog only so that objects in + * the base schema (or any user schema the caller placed on search_path) + * are not reachable without qualification, forcing fully-qualified output + * from every helper regardless of the caller's session search_path. + * + * AtEOXact_GUC cleans up at xact end if anything throws between here and + * the explicit restore below; on the normal path we restore right before + * returning. + */ + if (!ctx->schema_qualified) + { + char *nspname = get_namespace_name(ctx->base_namespace); + + if (nspname != NULL) + { + const char *qnsp = quote_identifier(nspname); + + ctx->save_nestlevel = NewGUCNestLevel(); + (void) set_config_option("search_path", qnsp, + PGC_USERSET, PGC_S_SESSION, + GUC_ACTION_SAVE, true, 0, false); + pfree(nspname); + } + } + else + { + ctx->save_nestlevel = NewGUCNestLevel(); + (void) set_config_option("search_path", "pg_catalog", + PGC_USERSET, PGC_S_SESSION, + GUC_ACTION_SAVE, true, 0, false); + } + + /* + * Cache locally-declared NOT NULL constraint metadata so the column- emit + * helpers can produce "CONSTRAINT name NOT NULL" inline for user-named + * constraints, and so the constraint loop can avoid double-emitting them. + */ + ctx->nn_entries = collect_local_not_null(rel, &ctx->skip_notnull_oids); + + initStringInfo(&ctx->buf); + + /* + * Emission passes. Order is significant: CREATE TABLE first; OWNER and + * the per-column ALTER COLUMN passes before sub-object emission; + * sub-objects in dependency-friendly order (indexes before constraints, + * since constraint-backed indexes are emitted out-of-line by the + * constraint loop); partition children last so the parent already exists + * at replay time. Each helper gates itself on is_kind_included for the + * relevant TABLE_DDL_KIND_*; the four "table itself" passes are grouped + * here under KIND_TABLE so all of CREATE TABLE / OWNER / SET DEFAULT / + * SET (...) drop out together when the user asks for only sub-objects + * (e.g. only => 'foreign_key' for the second pass of a two-pass FK + * clone). + */ + if (is_kind_included(ctx, TABLE_DDL_KIND_TABLE)) + { + emit_create_table_stmt(ctx); + emit_owner_stmt(ctx); + emit_child_default_overrides(ctx); + emit_attoptions(ctx); + } + emit_indexes(ctx); + emit_local_constraints(ctx); + emit_rules(ctx); + emit_statistics(ctx); + emit_replica_identity(ctx); + emit_rls_toggles(ctx); + + /* + * Triggers and row-level security policies - disabled until the + * standalone pg_get_trigger_ddl() and pg_get_policy_ddl() helpers land. + * The scan + lock + filter scaffolding below is preserved (inside #if 0) + * so wiring up each emission becomes a one-liner in the loop body once + * those helpers are available. + */ +#if 0 + if (is_kind_included(ctx, TABLE_DDL_KIND_TRIGGER)) + { + Relation trigRel; + SysScanDesc trigScan; + ScanKeyData trigKey; + HeapTuple trigTup; + + trigRel = table_open(TriggerRelationId, AccessShareLock); + ScanKeyInit(&trigKey, + Anum_pg_trigger_tgrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(ctx->relid)); + trigScan = systable_beginscan(trigRel, TriggerRelidNameIndexId, + true, NULL, 1, &trigKey); + while (HeapTupleIsValid(trigTup = systable_getnext(trigScan))) + { + Form_pg_trigger trg = (Form_pg_trigger) GETSTRUCT(trigTup); + + if (trg->tgisinternal) + continue; + + /* TODO: append pg_get_trigger_ddl(trg->oid) output here. */ + } + systable_endscan(trigScan); + table_close(trigRel, AccessShareLock); + } + + if (is_kind_included(ctx, TABLE_DDL_KIND_POLICY)) + { + Relation polRel; + SysScanDesc polScan; + ScanKeyData polKey; + HeapTuple polTup; + + polRel = table_open(PolicyRelationId, AccessShareLock); + ScanKeyInit(&polKey, + Anum_pg_policy_polrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(ctx->relid)); + polScan = systable_beginscan(polRel, PolicyPolrelidPolnameIndexId, + true, NULL, 1, &polKey); + while (HeapTupleIsValid(polTup = systable_getnext(polScan))) + { + /* TODO: append pg_get_policy_ddl(relid, polname) output here. */ + } + systable_endscan(polScan); + table_close(polRel, AccessShareLock); + } +#endif + + emit_partition_children(ctx); + + { + int natts = RelationGetDescr(rel)->natts; + + for (int i = 1; i <= natts; i++) + if (ctx->nn_entries[i].name != NULL) + pfree(ctx->nn_entries[i].name); + } + pfree(ctx->nn_entries); + + table_close(rel, AccessShareLock); + pfree(ctx->buf.data); + pfree(ctx->qualname); + list_free(ctx->skip_notnull_oids); + + /* + * Pop the narrowed search_path now that all helpers have run. Errors + * thrown earlier are cleaned up by AtEOXact_GUC at xact end. + */ + if (ctx->save_nestlevel >= 0) + AtEOXact_GUC(true, ctx->save_nestlevel); + + return ctx->statements; +} + +/* + * pg_get_table_ddl + * Return DDL to recreate a table as a set of text rows. + */ +Datum +pg_get_table_ddl(PG_FUNCTION_ARGS) +{ + FuncCallContext *funcctx; + List *statements; + + if (SRF_IS_FIRSTCALL()) + { + MemoryContext oldcontext; + TableDdlContext ctx = {0}; + + funcctx = SRF_FIRSTCALL_INIT(); + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + if (PG_ARGISNULL(0)) + { + MemoryContextSwitchTo(oldcontext); + SRF_RETURN_DONE(funcctx); + } + + if (!PG_ARGISNULL(5) && !PG_ARGISNULL(6)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("\"only_kinds\" and \"except_kinds\" parameters are mutually exclusive"))); + + /* Option defaults (match proargdefaults in pg_proc.dat). */ + ctx.relid = PG_GETARG_OID(0); + ctx.pretty = false; + ctx.no_owner = false; /* owner DEFAULT true → no_owner = false */ + ctx.no_tablespace = false; /* tablespace DEFAULT true → no_tablespace = false */ + ctx.schema_qualified = true; + ctx.only_kinds = NULL; + ctx.except_kinds = NULL; + + /* Override defaults with any explicitly supplied values. */ + if (!PG_ARGISNULL(1)) + ctx.pretty = PG_GETARG_BOOL(1); + if (!PG_ARGISNULL(2)) + ctx.no_owner = !PG_GETARG_BOOL(2); + if (!PG_ARGISNULL(3)) + ctx.no_tablespace = !PG_GETARG_BOOL(3); + if (!PG_ARGISNULL(4)) + ctx.schema_qualified = PG_GETARG_BOOL(4); + if (!PG_ARGISNULL(5)) + ctx.only_kinds = parse_kind_array("only_kinds", + PG_GETARG_ARRAYTYPE_P(5)); + if (!PG_ARGISNULL(6)) + ctx.except_kinds = parse_kind_array("except_kinds", + PG_GETARG_ARRAYTYPE_P(6)); + + statements = pg_get_table_ddl_internal(&ctx); + funcctx->user_fctx = statements; + funcctx->max_calls = list_length(statements); + + MemoryContextSwitchTo(oldcontext); + } + + funcctx = SRF_PERCALL_SETUP(); + statements = (List *) funcctx->user_fctx; + + if (funcctx->call_cntr < funcctx->max_calls) + { + char *stmt; + + stmt = list_nth(statements, funcctx->call_cntr); + + SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt)); + } + else + { + list_free_deep(statements); + SRF_RETURN_DONE(funcctx); + } +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 88de5c0481c..7186cf83e1d 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -23,6 +23,7 @@ #include "access/htup_details.h" #include "access/relation.h" #include "access/table.h" +#include "catalog/namespace.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_am.h" #include "catalog/pg_authid.h" @@ -370,7 +371,7 @@ static void make_propgraphdef_elements(StringInfo buf, Oid pgrelid, char pgekind static void make_propgraphdef_labels(StringInfo buf, Oid elid, const char *elalias, Oid elrelid); static void make_propgraphdef_properties(StringInfo buf, Oid ellabelid, Oid elrelid); static char *pg_get_statisticsobj_worker(Oid statextid, bool columns_only, - bool missing_ok); + bool missing_ok, int prettyFlags); static char *pg_get_partkeydef_worker(Oid relid, int prettyFlags, bool attrsOnly, bool missing_ok); static char *pg_get_constraintdef_worker(Oid constraintId, bool fullCommand, @@ -603,6 +604,19 @@ pg_get_ruledef_ext(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(string_to_text(res)); } +/* + * pg_get_ruledef_ddl + * Like pg_get_ruledef but uses the active search_path to decide + * whether to schema-qualify the rule's target relation name. + * For use by pg_get_table_ddl which has already set search_path + * to the base schema when schema_qualified=false. + */ +char * +pg_get_ruledef_ddl(Oid ruleoid) +{ + return pg_get_ruledef_worker(ruleoid, PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA); +} + static char * pg_get_ruledef_worker(Oid ruleoid, int prettyFlags) @@ -1241,6 +1255,22 @@ pg_get_indexdef_string(Oid indexrelid) 0, false); } +/* + * pg_get_indexdef_ddl + * Like pg_get_indexdef_string but uses the active search_path to + * decide whether to schema-qualify the indexed relation name. + * For use by pg_get_table_ddl which has already set search_path + * to the base schema when schema_qualified=false. + */ +char * +pg_get_indexdef_ddl(Oid indexrelid) +{ + return pg_get_indexdef_worker(indexrelid, 0, NULL, + false, false, + true, true, + PRETTYFLAG_SCHEMA, false); +} + /* Internal version that just reports the key-column definitions */ char * pg_get_indexdef_columns(Oid indexrelid, bool pretty) @@ -1969,7 +1999,7 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS) Oid statextid = PG_GETARG_OID(0); char *res; - res = pg_get_statisticsobj_worker(statextid, false, true); + res = pg_get_statisticsobj_worker(statextid, false, true, 0); if (res == NULL) PG_RETURN_NULL(); @@ -1984,7 +2014,20 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS) char * pg_get_statisticsobjdef_string(Oid statextid) { - return pg_get_statisticsobj_worker(statextid, false, false); + return pg_get_statisticsobj_worker(statextid, false, false, 0); +} + +/* + * pg_get_statisticsobjdef_ddl + * Like pg_get_statisticsobjdef_string but uses the active search_path + * to decide whether to schema-qualify the statistics object name. + * For use by pg_get_table_ddl which has already set search_path + * to the base schema when schema_qualified=false. + */ +char * +pg_get_statisticsobjdef_ddl(Oid statextid) +{ + return pg_get_statisticsobj_worker(statextid, false, false, PRETTYFLAG_SCHEMA); } /* @@ -1997,7 +2040,7 @@ pg_get_statisticsobjdef_columns(PG_FUNCTION_ARGS) Oid statextid = PG_GETARG_OID(0); char *res; - res = pg_get_statisticsobj_worker(statextid, true, true); + res = pg_get_statisticsobj_worker(statextid, true, true, 0); if (res == NULL) PG_RETURN_NULL(); @@ -2009,7 +2052,8 @@ pg_get_statisticsobjdef_columns(PG_FUNCTION_ARGS) * Internal workhorse to decompile an extended statistics object. */ static char * -pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok) +pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok, + int prettyFlags) { Form_pg_statistic_ext statextrec; HeapTuple statexttup; @@ -2069,10 +2113,18 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok) if (!columns_only) { - nsp = get_namespace_name_or_temp(statextrec->stxnamespace); - appendStringInfo(&buf, "CREATE STATISTICS %s", - quote_qualified_identifier(nsp, - NameStr(statextrec->stxname))); + if ((prettyFlags & PRETTYFLAG_SCHEMA) && + StatisticsObjIsVisible(statextid)) + appendStringInfo(&buf, "CREATE STATISTICS %s", + quote_identifier(NameStr(statextrec->stxname))); + else + { + nsp = get_namespace_name_or_temp(statextrec->stxnamespace); + appendStringInfo(&buf, "CREATE STATISTICS %s", + quote_qualified_identifier(nsp, + NameStr(statextrec->stxname))); + pfree(nsp); + } /* * Decode the stxkind column so that we know which stats types to @@ -2163,10 +2215,9 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok) { Node *expr = (Node *) lfirst(lc); char *str; - int prettyFlags = PRETTYFLAG_PAREN; str = deparse_expression_pretty(expr, context, false, false, - prettyFlags, 0); + PRETTYFLAG_PAREN, 0); if (colno > 0) appendStringInfoString(&buf, ", "); @@ -2544,6 +2595,20 @@ pg_get_constraintdef_command(Oid constraintId) return pg_get_constraintdef_worker(constraintId, true, 0, false); } +/* + * pg_get_constraintdef_body + * Returns the constraint definition body without the + * "ALTER TABLE name ADD CONSTRAINT cname" prefix. + * FK target relation names in the body already use + * generate_relation_name (search_path-aware). For use by + * pg_get_table_ddl which builds the ALTER TABLE prefix itself. + */ +char * +pg_get_constraintdef_body(Oid constraintId) +{ + return pg_get_constraintdef_worker(constraintId, false, 0, false); +} + /* * As of 9.4, we now use an MVCC snapshot for this. */ diff --git a/src/include/catalog/pg_inherits.h b/src/include/catalog/pg_inherits.h index cc874abaabb..1e463b0106d 100644 --- a/src/include/catalog/pg_inherits.h +++ b/src/include/catalog/pg_inherits.h @@ -55,6 +55,7 @@ DECLARE_INDEX(pg_inherits_parent_index, 2187, InheritsParentIndexId, pg_inherits extern List *find_inheritance_children(Oid parentrelId, LOCKMODE lockmode); extern List *find_inheritance_children_extended(Oid parentrelId, bool omit_detached, LOCKMODE lockmode, bool *detached_exist, TransactionId *detached_xmin); +extern List *find_inheritance_parents(Oid relid, LOCKMODE lockmode); extern List *find_all_inheritors(Oid parentrelId, LOCKMODE lockmode, List **numparents); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 73bb7fbb430..8209acf4683 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8620,6 +8620,14 @@ proargtypes => 'regdatabase bool bool bool', proargnames => '{database,pretty,owner,tablespace}', proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' }, +{ oid => '8215', descr => 'get DDL to recreate a table', + proname => 'pg_get_table_ddl', prorows => '50', + proisstrict => 'f', proretset => 't', provolatile => 's', proparallel => 'r', + pronargdefaults => '6', prorettype => 'text', + proargtypes => 'regclass bool bool bool bool _text _text', + proargnames => '{relation,pretty,owner,tablespace,schema_qualified,only_kinds,except_kinds}', + proargdefaults => '{false,true,true,true,NULL,NULL}', + prosrc => 'pg_get_table_ddl' }, { oid => '2509', descr => 'deparse an encoded expression with pretty-print option', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h index c3d8518cb62..d33563fdead 100644 --- a/src/include/commands/tablecmds.h +++ b/src/include/commands/tablecmds.h @@ -92,6 +92,7 @@ extern void check_of_type(HeapTuple typetuple); extern void register_on_commit_action(Oid relid, OnCommitAction action); extern void remove_on_commit_action(Oid relid); +extern OnCommitAction get_on_commit_action(Oid relid); extern void PreCommit_on_commit_actions(void); extern void AtEOXact_on_commit_actions(bool isCommit); @@ -108,4 +109,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation, extern bool PartConstraintImpliedByRelConstraint(Relation scanrel, List *partConstraint); +extern const char *storage_name(char c); + #endif /* TABLECMDS_H */ diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h index 25c05e2f649..c3b3dc65c93 100644 --- a/src/include/utils/ruleutils.h +++ b/src/include/utils/ruleutils.h @@ -25,6 +25,7 @@ typedef struct PlannedStmt PlannedStmt; #define RULE_INDEXDEF_KEYS_ONLY 0x02 /* ignore included attributes */ extern char *pg_get_indexdef_string(Oid indexrelid); +extern char *pg_get_indexdef_ddl(Oid indexrelid); extern char *pg_get_indexdef_columns(Oid indexrelid, bool pretty); extern char *pg_get_indexdef_columns_extended(Oid indexrelid, uint16 flags); @@ -34,6 +35,7 @@ extern char *pg_get_partkeydef_columns(Oid relid, bool pretty); extern char *pg_get_partconstrdef_string(Oid partitionId, char *aliasname); extern char *pg_get_constraintdef_command(Oid constraintId); +extern char *pg_get_constraintdef_body(Oid constraintId); extern char *deparse_expression(Node *expr, List *dpcontext, bool forceprefix, bool showimplicit); extern List *deparse_context_for(const char *aliasname, Oid relid); @@ -54,5 +56,7 @@ extern char *get_range_partbound_string(List *bound_datums); extern void get_reloptions(StringInfo buf, Datum reloptions); extern char *pg_get_statisticsobjdef_string(Oid statextid); +extern char *pg_get_statisticsobjdef_ddl(Oid statextid); +extern char *pg_get_ruledef_ddl(Oid ruleoid); #endif /* RULEUTILS_H */ diff --git a/src/test/regress/expected/pg_get_table_ddl.out b/src/test/regress/expected/pg_get_table_ddl.out new file mode 100644 index 00000000000..879584f101a --- /dev/null +++ b/src/test/regress/expected/pg_get_table_ddl.out @@ -0,0 +1,1274 @@ +-- +-- pg_get_table_ddl +-- +-- All tests pass owner=>false so the ALTER TABLE OWNER TO line is not +-- emitted, keeping output stable across test runners (which may run under +-- different role names). +-- +CREATE SCHEMA pgtbl_ddl_test; +SET search_path = pgtbl_ddl_test; +-- Basic table with PRIMARY KEY, NOT NULL, DEFAULT, COLLATE. +CREATE TABLE basic ( + id int PRIMARY KEY, + name text NOT NULL DEFAULT 'anon' COLLATE "C" +); +SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false); + pg_get_table_ddl +--------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.basic (id integer NOT NULL, name text COLLATE "C" DEFAULT 'anon'::text NOT NULL); + ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id); +(2 rows) + +-- Identity columns (with default and custom sequence options). +CREATE TABLE id_cols ( + id_always int GENERATED ALWAYS AS IDENTITY, + id_default int GENERATED BY DEFAULT AS IDENTITY +); +SELECT * FROM pg_get_table_ddl('id_cols'::regclass, owner => false); + pg_get_table_ddl +-------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.id_cols (id_always integer GENERATED ALWAYS AS IDENTITY NOT NULL, id_default integer GENERATED BY DEFAULT AS IDENTITY NOT NULL); +(1 row) + +CREATE TABLE id_custom ( + v int GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME id_custom_v_seq + START WITH 100 INCREMENT BY 5 + MINVALUE 50 MAXVALUE 1000 + CACHE 10 CYCLE + ) +); +SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false); + pg_get_table_ddl +------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.id_custom (v integer GENERATED ALWAYS AS IDENTITY (START WITH 100 INCREMENT BY 5 MINVALUE 50 MAXVALUE 1000 CACHE 10 CYCLE) NOT NULL); +(1 row) + +-- Generated stored column. +CREATE TABLE gen_cols ( + cents int, + dollars numeric GENERATED ALWAYS AS (cents / 100.0) STORED +); +SELECT * FROM pg_get_table_ddl('gen_cols'::regclass, owner => false); + pg_get_table_ddl +-------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.gen_cols (cents integer, dollars numeric GENERATED ALWAYS AS (((cents)::numeric / 100.0)) STORED); +(1 row) + +-- Generated virtual column. +CREATE TABLE gen_virtual ( + base int, + derived int GENERATED ALWAYS AS (base * 2) VIRTUAL +); +SELECT * FROM pg_get_table_ddl('gen_virtual'::regclass, owner => false); + pg_get_table_ddl +------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.gen_virtual (base integer, derived integer GENERATED ALWAYS AS ((base * 2)) VIRTUAL); +(1 row) + +-- STORAGE and COMPRESSION (only emitted when non-default for the type). +CREATE TABLE storage_cols ( + a text STORAGE EXTERNAL, + b text STORAGE MAIN, + c text COMPRESSION pglz +); +SELECT * FROM pg_get_table_ddl('storage_cols'::regclass, owner => false); + pg_get_table_ddl +------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.storage_cols (a text STORAGE EXTERNAL, b text STORAGE MAIN, c text COMPRESSION pglz); +(1 row) + +-- Constraints: CHECK, UNIQUE, FOREIGN KEY (DEFERRABLE). +CREATE TABLE refd (id int PRIMARY KEY); +CREATE TABLE cons ( + a int CHECK (a > 0), + b int UNIQUE, + c int REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED +); +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false); + pg_get_table_ddl +---------------------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0))); + ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b); + ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED; +(3 rows) + +-- UNIQUE NULLS NOT DISTINCT (PG 15): null values are treated as equal for +-- uniqueness purposes, so the NULLS NOT DISTINCT clause must be emitted. +CREATE TABLE nulls_nd ( + a int, + b int, + UNIQUE NULLS NOT DISTINCT (a, b) +); +SELECT * FROM pg_get_table_ddl('nulls_nd'::regclass, owner => false); + pg_get_table_ddl +------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.nulls_nd (a integer, b integer); + ALTER TABLE pgtbl_ddl_test.nulls_nd ADD CONSTRAINT nulls_nd_a_b_key UNIQUE NULLS NOT DISTINCT (a, b); +(2 rows) + +-- INCLUDE columns on a constraint-backed UNIQUE index (PG 11): the +-- covering columns appear in an INCLUDE (...) clause on the ALTER TABLE +-- ADD CONSTRAINT statement, not in the key column list. +CREATE TABLE idx_inc ( + id int PRIMARY KEY, + name text, + extra text, + UNIQUE (name) INCLUDE (extra) +); +SELECT * FROM pg_get_table_ddl('idx_inc'::regclass, owner => false); + pg_get_table_ddl +--------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.idx_inc (id integer NOT NULL, name text, extra text); + ALTER TABLE pgtbl_ddl_test.idx_inc ADD CONSTRAINT idx_inc_name_extra_key UNIQUE (name) INCLUDE (extra); + ALTER TABLE pgtbl_ddl_test.idx_inc ADD CONSTRAINT idx_inc_pkey PRIMARY KEY (id); +(3 rows) + +-- FOREIGN KEY with ON DELETE / ON UPDATE referential actions and MATCH +-- clause. Each variant must appear verbatim in the reconstructed DDL. +CREATE TABLE fk_acts_tgt (id int PRIMARY KEY); +CREATE TABLE fk_acts ( + a int, + b int, + c int, + d int, + CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES fk_acts_tgt(id) + ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES fk_acts_tgt(id) + ON DELETE SET NULL, + CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES fk_acts_tgt(id) + ON DELETE SET DEFAULT, + CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES fk_acts_tgt(id) + MATCH FULL +); +SELECT * FROM pg_get_table_ddl('fk_acts'::regclass, owner => false); + pg_get_table_ddl +-------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.fk_acts (a integer, b integer, c integer, d integer); + ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON UPDATE RESTRICT ON DELETE CASCADE; + ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) MATCH FULL; + ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON DELETE SET DEFAULT; + ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON DELETE SET NULL; +(5 rows) + +-- NOT ENFORCED foreign key (PG 17): the constraint is recorded in the +-- catalog but not validated by the engine; NOT ENFORCED must appear in +-- the reconstructed DDL. +CREATE TABLE notenf_tgt (id int PRIMARY KEY); +CREATE TABLE notenf ( + a int, + CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES notenf_tgt(id) + NOT ENFORCED +); +SELECT * FROM pg_get_table_ddl('notenf'::regclass, owner => false); + pg_get_table_ddl +----------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.notenf (a integer); + ALTER TABLE pgtbl_ddl_test.notenf ADD CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.notenf_tgt(id) NOT ENFORCED; +(2 rows) + +-- WITHOUT OVERLAPS on a PRIMARY KEY (PG 17): the range-type period +-- column forms a temporal primary key; WITHOUT OVERLAPS must appear +-- inside the key column list in the reconstructed ALTER TABLE. +-- Both columns use range types with native GiST support so no +-- btree_gist extension is needed. +CREATE TABLE temporal_pk ( + grp int4range, + during daterange, + PRIMARY KEY (grp, during WITHOUT OVERLAPS) +); +SELECT * FROM pg_get_table_ddl('temporal_pk'::regclass, owner => false); + pg_get_table_ddl +-------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.temporal_pk (grp int4range NOT NULL, during daterange NOT NULL); + ALTER TABLE pgtbl_ddl_test.temporal_pk ADD CONSTRAINT temporal_pk_pkey PRIMARY KEY (grp, during WITHOUT OVERLAPS); +(2 rows) + +-- Indexes: functional and partial. Constraint-backing indexes are +-- suppressed (they are emitted by the constraint loop). +CREATE TABLE idxd (id int PRIMARY KEY, name text); +CREATE INDEX idxd_lower ON idxd (lower(name)); +CREATE INDEX idxd_partial ON idxd (id) WHERE id > 100; +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false); + pg_get_table_ddl +------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text); + CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name)); + CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100); + ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id); +(4 rows) + +-- Inheritance, including a child DEFAULT override on an inherited column. +CREATE TABLE par (a int DEFAULT 1, b text); +CREATE TABLE ch (c int) INHERITS (par); +ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999; +SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false); + pg_get_table_ddl +--------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.ch (c integer) INHERITS (pgtbl_ddl_test.par); + ALTER TABLE pgtbl_ddl_test.ch ALTER COLUMN a SET DEFAULT 999; +(2 rows) + +-- Per-column attoptions: emitted as ALTER COLUMN SET (...). +CREATE TABLE attopt (a int, b text); +ALTER TABLE attopt ALTER COLUMN a SET (n_distinct = 100); +SELECT * FROM pg_get_table_ddl('attopt'::regclass, owner => false); + pg_get_table_ddl +-------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.attopt (a integer, b text); + ALTER TABLE pgtbl_ddl_test.attopt ALTER COLUMN a SET (n_distinct='100'); +(2 rows) + +-- Partitioned table parents (RANGE, HASH, and LIST). +CREATE TABLE parted_range (id int, k int) PARTITION BY RANGE (id); +SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false); + pg_get_table_ddl +------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.parted_range (id integer, k integer) PARTITION BY RANGE (id); +(1 row) + +CREATE TABLE parted_hash (id int) PARTITION BY HASH (id); +SELECT * FROM pg_get_table_ddl('parted_hash'::regclass, owner => false); + pg_get_table_ddl +------------------------------------------------------------------------------ + CREATE TABLE pgtbl_ddl_test.parted_hash (id integer) PARTITION BY HASH (id); +(1 row) + +CREATE TABLE parted_list (id int, region text) PARTITION BY LIST (region); +SELECT * FROM pg_get_table_ddl('parted_list'::regclass, owner => false); + pg_get_table_ddl +----------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.parted_list (id integer, region text) PARTITION BY LIST (region); +(1 row) + +-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT. +CREATE TABLE parted_range_1 PARTITION OF parted_range + FOR VALUES FROM (0) TO (100); +ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7; +SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false); + pg_get_table_ddl +------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100); + ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7; +(2 rows) + +CREATE TABLE parted_hash_0 PARTITION OF parted_hash + FOR VALUES WITH (modulus 2, remainder 0); +SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, owner => false); + pg_get_table_ddl +----------------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0); +(1 row) + +CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT; +SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, owner => false); + pg_get_table_ddl +------------------------------------------------------------------------------------------------ + CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT; +(1 row) + +-- Rules. +CREATE TABLE rt (id int); +CREATE TABLE rt_log (id int); +CREATE RULE rt_log_insert AS ON INSERT TO rt + DO ALSO INSERT INTO rt_log VALUES (NEW.id); +SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false); + pg_get_table_ddl +------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.rt (id integer); + CREATE RULE rt_log_insert AS + + ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO pgtbl_ddl_test.rt_log (id)+ + VALUES (new.id); +(2 rows) + +-- Extended statistics. +CREATE TABLE stx (a int, b int, c int); +CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx; +SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false); + pg_get_table_ddl +--------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer); + CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM pgtbl_ddl_test.stx; +(2 rows) + +-- Row-level security toggles. +CREATE TABLE rls (id int); +ALTER TABLE rls ENABLE ROW LEVEL SECURITY; +ALTER TABLE rls FORCE ROW LEVEL SECURITY; +SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false); + pg_get_table_ddl +----------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.rls (id integer); + ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY; + ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY; +(3 rows) + +-- REPLICA IDENTITY: emitted only when not the default. +CREATE TABLE ri_full (a int); +ALTER TABLE ri_full REPLICA IDENTITY FULL; +SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false); + pg_get_table_ddl +----------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.ri_full (a integer); + ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL; +(2 rows) + +CREATE TABLE ri_idx (a int NOT NULL); +CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a); +ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a; +SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, owner => false); + pg_get_table_ddl +-------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.ri_idx (a integer NOT NULL); + CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a); + ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a; +(3 rows) + +-- UNLOGGED + reloptions. +CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70); +SELECT * FROM pg_get_table_ddl('uno'::regclass, owner => false); + pg_get_table_ddl +------------------------------------------------------------------------------- + CREATE UNLOGGED TABLE pgtbl_ddl_test.uno (id integer) WITH (fillfactor='70'); +(1 row) + +-- Typed table (CREATE TABLE OF type_name). Columns inherited from the +-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come +-- out through a single "(col WITH OPTIONS ...)" list. +CREATE TYPE typed_t AS (a int, b text); +CREATE TABLE typed_plain OF typed_t; +SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, owner => false); + pg_get_table_ddl +-------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.typed_plain OF pgtbl_ddl_test.typed_t; +(1 row) + +CREATE TABLE typed_over OF typed_t ( + a WITH OPTIONS DEFAULT 7 NOT NULL, + b WITH OPTIONS NOT NULL, + CONSTRAINT b_nonempty CHECK (length(b) > 0) +); +SELECT * FROM pg_get_table_ddl('typed_over'::regclass, owner => false); + pg_get_table_ddl +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.typed_over OF pgtbl_ddl_test.typed_t (a WITH OPTIONS DEFAULT 7 NOT NULL, b WITH OPTIONS NOT NULL, CONSTRAINT b_nonempty CHECK ((length(b) > 0))); +(1 row) + +-- Temporary tables + ON COMMIT. Temp tables must never be emitted with +-- the session-private pg_temp_NN schema prefix -- the TEMPORARY keyword +-- already places the table in pg_temp and the prefix is non-replayable. +-- ON COMMIT DROP only fires at commit, so the queries that need to see +-- the catalog entry must run in the same transaction as the CREATE. +BEGIN; +CREATE TEMP TABLE temp_default (id int); +CREATE TEMP TABLE temp_delete (id int) ON COMMIT DELETE ROWS; +CREATE TEMP TABLE temp_drop (id int) ON COMMIT DROP; +SELECT line FROM pg_get_table_ddl('temp_default'::regclass, owner => false) AS line +WHERE line LIKE 'CREATE %'; + line +--------------------------------------------------- + CREATE TEMPORARY TABLE temp_default (id integer); +(1 row) + +SELECT line FROM pg_get_table_ddl('temp_delete'::regclass, owner => false) AS line +WHERE line LIKE 'CREATE %'; + line +------------------------------------------------------------------------ + CREATE TEMPORARY TABLE temp_delete (id integer) ON COMMIT DELETE ROWS; +(1 row) + +SELECT line FROM pg_get_table_ddl('temp_drop'::regclass, owner => false) AS line +WHERE line LIKE 'CREATE %'; + line +--------------------------------------------------------------- + CREATE TEMPORARY TABLE temp_drop (id integer) ON COMMIT DROP; +(1 row) + +ROLLBACK; +-- Pretty mode. +SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, pretty => true); + pg_get_table_ddl +------------------------------------------------------------------------------ + CREATE TABLE pgtbl_ddl_test.basic ( + + id integer NOT NULL, + + name text COLLATE "C" DEFAULT 'anon'::text NOT NULL + + ); + ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id); +(2 rows) + +-- only_kinds / except_kinds gating: each emits either only the listed kinds +-- (only_kinds) or every kind except the listed ones (except_kinds). The two +-- are mutually exclusive. Kind vocabulary: table, index, +-- primary_key, unique, check, foreign_key, exclusion, rule, +-- statistics, trigger, policy, rls, replica_identity, partition. +-- NOT NULL is not in the vocabulary - always emitted to prevent +-- producing schemas that silently accept NULLs the source would +-- have rejected. +-- +-- except_kinds=index hides the CREATE INDEX statements. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['index']); + pg_get_table_ddl +---------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text); + ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id); +(2 rows) + +-- except_kinds=primary_key,unique,check,foreign_key,exclusion hides every +-- table-level constraint. Inline CHECK in the CREATE TABLE body is +-- suppressed too. +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['primary_key','unique','check','foreign_key','exclusion']); + pg_get_table_ddl +--------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer); +(1 row) + +-- except_kinds=foreign_key suppresses only FOREIGN KEY constraints; +-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the +-- first pass of the two-pass FK-clone workflow: emit tables without +-- cross-table FKs, load data, then re-run with only_kinds=foreign_key +-- to add them once all targets exist. +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['foreign_key']); + pg_get_table_ddl +-------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0))); + ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b); +(2 rows) + +-- only_kinds=foreign_key is the second pass of the two-pass FK-clone +-- workflow: emit only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN +-- KEY rows; the CREATE TABLE and every non-FK sub-object are +-- suppressed (the table already exists from the first pass). +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['foreign_key']); + pg_get_table_ddl +---------------------------------------------------------------------------------------------------------------------------------------------- + ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED; +(1 row) + +-- A table with no foreign keys produces no rows under +-- only_kinds=foreign_key. +SELECT * FROM pg_get_table_ddl('refd'::regclass, owner => false, only_kinds => ARRAY['foreign_key']); + pg_get_table_ddl +------------------ +(0 rows) + +-- only_kinds=table emits only the bare CREATE TABLE (with any inline +-- column-level NOT NULL and CHECK) - no indexes, no PRIMARY KEY ALTER, +-- no other sub-objects. Useful for capturing a table's shape without +-- pulling in everything attached to it. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table']); + pg_get_table_ddl +-------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text); +(1 row) + +-- only_kinds=check on a non-partition table. The constraint loop must emit +-- column-level and table-level CHECK constraints via ALTER TABLE here, +-- because the inline path in CREATE TABLE never runs (KIND_TABLE is +-- not in the filter). +CREATE TABLE chk_only ( + id int PRIMARY KEY, + qty int CHECK (qty > 0), + CONSTRAINT chk_only_id_pos CHECK (id > 0) +); +SELECT * FROM pg_get_table_ddl('chk_only'::regclass, owner => false, only_kinds => ARRAY['check']); + pg_get_table_ddl +------------------------------------------------------------------------------------------ + ALTER TABLE pgtbl_ddl_test.chk_only ADD CONSTRAINT chk_only_id_pos CHECK ((id > 0)); + ALTER TABLE pgtbl_ddl_test.chk_only ADD CONSTRAINT chk_only_qty_check CHECK ((qty > 0)); +(2 rows) + +DROP TABLE chk_only; +-- only_kinds=partition on a partitioned-table parent: each child's full DDL +-- is emitted (CREATE TABLE ... PARTITION OF ...). The "partition" +-- keyword is a gate at the parent level; it is stripped from the +-- propagated filter so the child does not exclude its own +-- CREATE TABLE. +CREATE TABLE p_only (id int, val text) PARTITION BY RANGE (id); +CREATE TABLE p_only_a PARTITION OF p_only FOR VALUES FROM (0) TO (100); +CREATE TABLE p_only_b PARTITION OF p_only FOR VALUES FROM (100) TO (200); +SELECT * FROM pg_get_table_ddl('p_only'::regclass, owner => false, only_kinds => ARRAY['partition']); + pg_get_table_ddl +--------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.p_only_a PARTITION OF pgtbl_ddl_test.p_only FOR VALUES FROM (0) TO (100); + CREATE TABLE pgtbl_ddl_test.p_only_b PARTITION OF pgtbl_ddl_test.p_only FOR VALUES FROM (100) TO (200); +(2 rows) + +DROP TABLE p_only; +-- only_kinds and except_kinds are mutually exclusive. +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, + only_kinds => ARRAY['foreign_key'], + except_kinds => ARRAY['foreign_key']); +ERROR: "only_kinds" and "except_kinds" parameters are mutually exclusive +-- Pub/sub schema clone: keep the table and its primary key, drop the +-- rest of the constraints so the subscriber can replicate without +-- pulling cross-table dependencies along. +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique','check','foreign_key','exclusion']); + pg_get_table_ddl +--------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer); +(1 row) + +-- except_kinds=rule hides the CREATE RULE. +SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, except_kinds => ARRAY['rule']); + pg_get_table_ddl +---------------------------------------------- + CREATE TABLE pgtbl_ddl_test.rt (id integer); +(1 row) + +-- except_kinds=statistics hides the CREATE STATISTICS. +SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, except_kinds => ARRAY['statistics']); + pg_get_table_ddl +-------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer); +(1 row) + +-- except_kinds=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles. +SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, except_kinds => ARRAY['rls']); + pg_get_table_ddl +----------------------------------------------- + CREATE TABLE pgtbl_ddl_test.rls (id integer); +(1 row) + +-- except_kinds=replica_identity hides the REPLICA IDENTITY clause. +SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, except_kinds => ARRAY['replica_identity']); + pg_get_table_ddl +-------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.ri_full (a integer); +(1 row) + +-- Partition children are emitted by default; except_kinds=partition +-- suppresses them so only the partitioned-table parent comes out. +SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false, except_kinds => ARRAY['partition']); + pg_get_table_ddl +------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.parted_range (id integer, k integer) PARTITION BY RANGE (id); +(1 row) + +-- Whitespace around kind names is allowed; case is folded. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY[' INDEX ',' RULE ']); + pg_get_table_ddl +---------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text); + ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id); +(2 rows) + +-- schema_qualified=false strips the schema from the target table itself +-- and from same-schema sibling references (inheritance/partition +-- parents, identity sequences in the same schema), while cross-schema +-- references are kept qualified for correctness. +-- +-- Bare CREATE TABLE for an ordinary table. +SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, + schema_qualified => false); + pg_get_table_ddl +------------------------------------------------------------------------------------------------ + CREATE TABLE basic (id integer NOT NULL, name text COLLATE "C" DEFAULT 'anon'::text NOT NULL); + ALTER TABLE basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id); +(2 rows) + +-- INHERITS parent in the same schema is emitted unqualified. +SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false, + schema_qualified => false); + pg_get_table_ddl +------------------------------------------------ + CREATE TABLE ch (c integer) INHERITS (par); + ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999; +(2 rows) + +-- Partition parent in the same schema is emitted unqualified. +SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false, + schema_qualified => false); + pg_get_table_ddl +------------------------------------------------------------------------------------- + CREATE TABLE parted_range_1 PARTITION OF parted_range FOR VALUES FROM (0) TO (100); + ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7; +(2 rows) + +-- Cross-schema FK target keeps qualification. The 'refd' table is in +-- pgtbl_ddl_test; place an FK target in a different schema and verify it +-- stays qualified even with schema_qualified=false. +CREATE SCHEMA pgtbl_ddl_other; +CREATE TABLE pgtbl_ddl_other.parent (id int PRIMARY KEY); +CREATE TABLE xschema_fk ( + id int PRIMARY KEY, + pid int REFERENCES pgtbl_ddl_other.parent +); +SELECT * FROM pg_get_table_ddl('xschema_fk'::regclass, owner => false, + schema_qualified => false); + pg_get_table_ddl +-------------------------------------------------------------------------------------------------------------------- + CREATE TABLE xschema_fk (id integer NOT NULL, pid integer); + ALTER TABLE xschema_fk ADD CONSTRAINT xschema_fk_pid_fkey FOREIGN KEY (pid) REFERENCES pgtbl_ddl_other.parent(id); + ALTER TABLE xschema_fk ADD CONSTRAINT xschema_fk_pkey PRIMARY KEY (id); +(3 rows) + +-- Cross-schema FK whose target schema name ends with the base schema name +-- (e.g. "xpgtbl_ddl_test" ends with "pgtbl_ddl_test"). A post-hoc text +-- strip would mangle "xpgtbl_ddl_test.reftbl" into "xreftbl"; deciding +-- qualification at generation time avoids the problem entirely. +CREATE SCHEMA xpgtbl_ddl_test; +CREATE TABLE xpgtbl_ddl_test.reftbl (id int PRIMARY KEY); +CREATE TABLE suffix_schema_fk ( + id int PRIMARY KEY, + ref int REFERENCES xpgtbl_ddl_test.reftbl +); +SELECT * FROM pg_get_table_ddl('suffix_schema_fk'::regclass, owner => false, + schema_qualified => false); + pg_get_table_ddl +-------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLE suffix_schema_fk (id integer NOT NULL, ref integer); + ALTER TABLE suffix_schema_fk ADD CONSTRAINT suffix_schema_fk_pkey PRIMARY KEY (id); + ALTER TABLE suffix_schema_fk ADD CONSTRAINT suffix_schema_fk_ref_fkey FOREIGN KEY (ref) REFERENCES xpgtbl_ddl_test.reftbl(id); +(3 rows) + +DROP TABLE suffix_schema_fk; +DROP SCHEMA xpgtbl_ddl_test CASCADE; +NOTICE: drop cascades to table xpgtbl_ddl_test.reftbl +-- Identity column with a custom sequence name in the same schema is +-- emitted unqualified. +SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false, + schema_qualified => false); + pg_get_table_ddl +---------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLE id_custom (v integer GENERATED ALWAYS AS IDENTITY (START WITH 100 INCREMENT BY 5 MINVALUE 50 MAXVALUE 1000 CACHE 10 CYCLE) NOT NULL); +(1 row) + +-- Round-trip: with schema_qualified=false, after SET search_path the +-- DDL can be replayed into a different target schema. +CREATE SCHEMA pgtbl_ddl_replay; +DO $$ +DECLARE + stmt text; +BEGIN + SET LOCAL search_path = pgtbl_ddl_replay; + FOR stmt IN + SELECT line FROM pg_get_table_ddl('pgtbl_ddl_test.basic'::regclass, + owner => false, + schema_qualified => false) AS line + LOOP + EXECUTE stmt; + END LOOP; +END $$; +SELECT relnamespace::regnamespace::text, relname +FROM pg_class WHERE oid = 'pgtbl_ddl_replay.basic'::regclass; + relnamespace | relname +------------------+--------- + pgtbl_ddl_replay | basic +(1 row) + +DROP SCHEMA pgtbl_ddl_replay CASCADE; +NOTICE: drop cascades to table pgtbl_ddl_replay.basic +DROP TABLE xschema_fk; +DROP SCHEMA pgtbl_ddl_other CASCADE; +NOTICE: drop cascades to table pgtbl_ddl_other.parent +-- Locally-declared CHECK on a partition child has no inline column +-- list to live in (PARTITION OF form), so it must come out as a +-- separate ALTER TABLE ... ADD CONSTRAINT statement. +CREATE TABLE parted_chk (id int, val int) PARTITION BY RANGE (id); +CREATE TABLE parted_chk_child PARTITION OF parted_chk + (CONSTRAINT chk_inline CHECK (val > 0)) + FOR VALUES FROM (0) TO (100); +SELECT * FROM pg_get_table_ddl('parted_chk_child'::regclass, owner => false); + pg_get_table_ddl +------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.parted_chk_child PARTITION OF pgtbl_ddl_test.parted_chk FOR VALUES FROM (0) TO (100); + ALTER TABLE pgtbl_ddl_test.parted_chk_child ADD CONSTRAINT chk_inline CHECK ((val > 0)); +(2 rows) + +DROP TABLE parted_chk; +-- Inheritance children of a parent with a generated column should not +-- emit an ALTER COLUMN ... SET DEFAULT for the inherited generated +-- column: the GENERATED expression carries down through inheritance, +-- and SET DEFAULT would fail at replay against a generated column. +CREATE TABLE par_gen ( + id int, + g int GENERATED ALWAYS AS (id * 2) STORED +); +CREATE TABLE ch_gen () INHERITS (par_gen); +SELECT * FROM pg_get_table_ddl('ch_gen'::regclass, owner => false); + pg_get_table_ddl +-------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.ch_gen () INHERITS (pgtbl_ddl_test.par_gen); +(1 row) + +DROP TABLE par_gen CASCADE; +NOTICE: drop cascades to table ch_gen +-- A user-named NOT NULL on a local column is emitted inline as +-- "CONSTRAINT name NOT NULL" so the original name is preserved. An +-- auto-named NOT NULL on a local column is emitted as a plain inline +-- "NOT NULL" - PG re-creates the auto-named constraint when CREATE +-- TABLE runs, so the constraint loop skips both forms to avoid the +-- redundant (and on table-rename or sequence-collision, broken) +-- ALTER TABLE ... ADD CONSTRAINT ... NOT NULL statement. +CREATE TABLE nn_named ( + a int CONSTRAINT my_nn NOT NULL, + b int NOT NULL +); +SELECT * FROM pg_get_table_ddl('nn_named'::regclass, owner => false); + pg_get_table_ddl +------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.nn_named (a integer CONSTRAINT my_nn NOT NULL, b integer NOT NULL); +(1 row) + +DROP TABLE nn_named; +-- Renaming the table after an IDENTITY NOT NULL column was declared +-- leaves the auto-named constraint frozen at the original name, which +-- no longer matches the post-rename "
__not_null" pattern. +-- The emitted DDL must still round-trip: the constraint loop must not +-- ALTER TABLE ... ADD CONSTRAINT a second NOT NULL, since CREATE TABLE +-- with the inline IDENTITY already creates an auto-NOT-NULL under the +-- new table name and PG only permits one NOT NULL per column. +CREATE TABLE nn_renamed ( + a int GENERATED ALWAYS AS IDENTITY NOT NULL +); +ALTER TABLE nn_renamed RENAME TO nn_renamed2; +SELECT * FROM pg_get_table_ddl('nn_renamed2'::regclass, owner => false); + pg_get_table_ddl +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.nn_renamed2 (a integer GENERATED ALWAYS AS IDENTITY (SEQUENCE NAME pgtbl_ddl_test.nn_renamed_a_seq) CONSTRAINT nn_renamed_a_not_null NOT NULL); +(1 row) + +DROP TABLE nn_renamed2; +-- Locally-declared NOT NULL on an inherited column. The column itself +-- is omitted from the child's column list (attislocal=false), so the +-- user-named NOT NULL must come out via the post-CREATE constraint +-- loop rather than inline. +CREATE TABLE nn_par (a int); +CREATE TABLE nn_ch (b int) INHERITS (nn_par); +ALTER TABLE nn_ch ADD CONSTRAINT my_nn NOT NULL a; +SELECT * FROM pg_get_table_ddl('nn_ch'::regclass, owner => false); + pg_get_table_ddl +--------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.nn_ch (b integer) INHERITS (pgtbl_ddl_test.nn_par); + ALTER TABLE pgtbl_ddl_test.nn_ch ADD CONSTRAINT my_nn NOT NULL a; +(2 rows) + +DROP TABLE nn_par CASCADE; +NOTICE: drop cascades to table nn_ch +-- schema_qualified=false must drop the schema prefix from DEFAULT +-- expressions and inline CHECK bodies that reference same-schema +-- objects (sequence via nextval, function via direct call). This +-- relies on the deparser respecting the narrowed search_path rather +-- than on substring stripping. +CREATE SCHEMA pgtbl_ddl_xref; +CREATE SEQUENCE pgtbl_ddl_xref.myseq; +CREATE FUNCTION pgtbl_ddl_xref.f(int) RETURNS int LANGUAGE sql IMMUTABLE + AS 'SELECT $1'; +CREATE TABLE pgtbl_ddl_xref.t ( + id int DEFAULT nextval('pgtbl_ddl_xref.myseq'), + val int, + CONSTRAINT chk CHECK (pgtbl_ddl_xref.f(val) > 0) +); +SELECT * FROM pg_get_table_ddl('pgtbl_ddl_xref.t'::regclass, + owner => false, + schema_qualified => false); + pg_get_table_ddl +------------------------------------------------------------------------------------------------------------------- + CREATE TABLE t (id integer DEFAULT nextval('myseq'::regclass), val integer, CONSTRAINT chk CHECK ((f(val) > 0))); +(1 row) + +DROP SCHEMA pgtbl_ddl_xref CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to sequence pgtbl_ddl_xref.myseq +drop cascades to function pgtbl_ddl_xref.f(integer) +drop cascades to table pgtbl_ddl_xref.t +-- schema_qualified=false must NOT corrupt string literals whose +-- contents happen to contain the schema name followed by a dot. String +-- literal text is output verbatim by the deparser and is unaffected by +-- search_path narrowing. +CREATE SCHEMA pgtbl_ddl_str; +CREATE TABLE pgtbl_ddl_str.p (id int, note text) PARTITION BY RANGE (id); +CREATE TABLE pgtbl_ddl_str.pc PARTITION OF pgtbl_ddl_str.p + (CONSTRAINT chk CHECK (note <> 'pgtbl_ddl_str.secret')) + FOR VALUES FROM (0) TO (100); +SELECT * FROM pg_get_table_ddl('pgtbl_ddl_str.pc'::regclass, + owner => false, + schema_qualified => false); + pg_get_table_ddl +----------------------------------------------------------------------------------- + CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100); + ALTER TABLE pc ADD CONSTRAINT chk CHECK ((note <> 'pgtbl_ddl_str.secret'::text)); +(2 rows) + +DROP SCHEMA pgtbl_ddl_str CASCADE; +NOTICE: drop cascades to table pgtbl_ddl_str.p +-- schema_qualified=false must NOT corrupt double-quoted identifiers +-- whose contents happen to contain the schema name followed by a dot +-- (for example a column literally named ".weird" inside that +-- schema). The mechanism is purely search_path narrowing: the deparser +-- outputs attribute names verbatim from pg_attribute, so the quoted +-- identifier content is never examined for schema prefixes. This holds +-- even when the schema name is a single letter. +CREATE SCHEMA pgtbl_ddl_qid; +CREATE TABLE pgtbl_ddl_qid.p (id int, "pgtbl_ddl_qid.weird" int) + PARTITION BY RANGE (id); +CREATE TABLE pgtbl_ddl_qid.pc PARTITION OF pgtbl_ddl_qid.p + (CONSTRAINT chk CHECK ("pgtbl_ddl_qid.weird" > 0)) + FOR VALUES FROM (0) TO (100); +SELECT * FROM pg_get_table_ddl('pgtbl_ddl_qid.pc'::regclass, + owner => false, + schema_qualified => false); + pg_get_table_ddl +------------------------------------------------------------------------ + CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100); + ALTER TABLE pc ADD CONSTRAINT chk CHECK (("pgtbl_ddl_qid.weird" > 0)); +(2 rows) + +DROP SCHEMA pgtbl_ddl_qid CASCADE; +NOTICE: drop cascades to table pgtbl_ddl_qid.p +-- Same with a short (single-letter) schema name to confirm there is no +-- minimum-length assumption in the stripping path. +CREATE SCHEMA pgtbl_ddl_q1; +CREATE TABLE pgtbl_ddl_q1.p (id int, "pgtbl_ddl_q1.weird" int) + PARTITION BY RANGE (id); +CREATE TABLE pgtbl_ddl_q1.pc PARTITION OF pgtbl_ddl_q1.p + (CONSTRAINT chk CHECK ("pgtbl_ddl_q1.weird" > 0)) + FOR VALUES FROM (0) TO (100); +SELECT * FROM pg_get_table_ddl('pgtbl_ddl_q1.pc'::regclass, + owner => false, + schema_qualified => false); + pg_get_table_ddl +----------------------------------------------------------------------- + CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100); + ALTER TABLE pc ADD CONSTRAINT chk CHECK (("pgtbl_ddl_q1.weird" > 0)); +(2 rows) + +DROP SCHEMA pgtbl_ddl_q1 CASCADE; +NOTICE: drop cascades to table pgtbl_ddl_q1.p +-- schema_qualified=false when the base schema name itself requires +-- quoting (its prefix starts with "). Both forms of the prefix - +-- bare-lowercase and quoted - must be stripped from outer +-- ALTER TABLE / ON / CREATE INDEX prefixes, while inner quoted +-- identifiers that happen to begin the same way must be preserved. +CREATE SCHEMA "PgTbl-Ddl-Q"; +CREATE TABLE "PgTbl-Ddl-Q"."T" (id int, val int, + CONSTRAINT chk CHECK (val > 0)); +CREATE INDEX "T_val_idx" ON "PgTbl-Ddl-Q"."T" (val); +SELECT * FROM pg_get_table_ddl('"PgTbl-Ddl-Q"."T"'::regclass, + owner => false, + schema_qualified => false); + pg_get_table_ddl +------------------------------------------------------------------------------- + CREATE TABLE "T" (id integer, val integer, CONSTRAINT chk CHECK ((val > 0))); + CREATE INDEX "T_val_idx" ON "T" USING btree (val); +(2 rows) + +DROP SCHEMA "PgTbl-Ddl-Q" CASCADE; +NOTICE: drop cascades to table "PgTbl-Ddl-Q"."T" +-- Error: not an ordinary or partitioned table. +CREATE VIEW v AS SELECT 1 AS x; +SELECT * FROM pg_get_table_ddl('v'::regclass); +ERROR: "v" is not an ordinary or partitioned table +CREATE SEQUENCE s; +SELECT * FROM pg_get_table_ddl('s'::regclass); +ERROR: "s" is not an ordinary or partitioned table +-- NULL argument returns no rows. +SELECT * FROM pg_get_table_ddl(NULL); + pg_get_table_ddl +------------------ +(0 rows) + +-- REPLICA IDENTITY USING INDEX validation: if the referenced index +-- would not be emitted (because its source kind is suppressed) but +-- replica_identity itself would be, the function raises an error so +-- the emitted DDL never dangles. Exercise all three constraint-backed +-- forms (PK, UNIQUE, EXCLUSION) and a bare CREATE UNIQUE INDEX, in +-- both the except-list and only-list shapes. +-- +-- PK-backed: except_kinds=primary_key without except_kinds=replica_identity. +CREATE TABLE ri_pk_excl (a int PRIMARY KEY); +ALTER TABLE ri_pk_excl REPLICA IDENTITY USING INDEX ri_pk_excl_pkey; +SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key']); +ERROR: REPLICA IDENTITY for table "ri_pk_excl" requires kind "primary_key" to be emitted +DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "primary_key" kind, which is not in the active filter. +HINT: Either add "primary_key" to the filter or remove "replica_identity" from it. +-- Same call but also excluding replica_identity succeeds. +SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key','replica_identity']); + pg_get_table_ddl +-------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.ri_pk_excl (a integer NOT NULL); +(1 row) + +DROP TABLE ri_pk_excl; +-- UNIQUE-constraint-backed replica identity index. +CREATE TABLE ri_uniq_excl (a int NOT NULL UNIQUE, b int); +ALTER TABLE ri_uniq_excl REPLICA IDENTITY USING INDEX ri_uniq_excl_a_key; +SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique']); +ERROR: REPLICA IDENTITY for table "ri_uniq_excl" requires kind "unique" to be emitted +DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "unique" kind, which is not in the active filter. +HINT: Either add "unique" to the filter or remove "replica_identity" from it. +SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique','replica_identity']); + pg_get_table_ddl +--------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.ri_uniq_excl (a integer NOT NULL, b integer); +(1 row) + +DROP TABLE ri_uniq_excl; +-- Bare CREATE UNIQUE INDEX (no backing constraint) used as REPLICA +-- IDENTITY: the index is emitted by the "index" kind, so excluding +-- "index" without also excluding "replica_identity" must error. +CREATE TABLE ri_idx_excl (a int NOT NULL, b int); +CREATE UNIQUE INDEX ri_idx_excl_a_uniq ON ri_idx_excl (a); +ALTER TABLE ri_idx_excl REPLICA IDENTITY USING INDEX ri_idx_excl_a_uniq; +SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index']); +ERROR: REPLICA IDENTITY for table "ri_idx_excl" requires kind "index" to be emitted +DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "index" kind, which is not in the active filter. +HINT: Either add "index" to the filter or remove "replica_identity" from it. +SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index','replica_identity']); + pg_get_table_ddl +-------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.ri_idx_excl (a integer NOT NULL, b integer); +(1 row) + +DROP TABLE ri_idx_excl; +-- Include-side analog: only_kinds=replica_identity must include the kind +-- that emits the index, or the function errors. +CREATE TABLE ri_only (a int NOT NULL UNIQUE, b int); +ALTER TABLE ri_only REPLICA IDENTITY USING INDEX ri_only_a_key; +SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['replica_identity']); +ERROR: REPLICA IDENTITY for table "ri_only" requires kind "unique" to be emitted +DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "unique" kind, which is not in the active filter. +HINT: Either add "unique" to the filter or remove "replica_identity" from it. +-- Including both kinds succeeds. +SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['unique','replica_identity']); + pg_get_table_ddl +-------------------------------------------------------------------------------- + ALTER TABLE pgtbl_ddl_test.ri_only ADD CONSTRAINT ri_only_a_key UNIQUE (a); + ALTER TABLE pgtbl_ddl_test.ri_only REPLICA IDENTITY USING INDEX ri_only_a_key; +(2 rows) + +DROP TABLE ri_only; +-- Per-kind only_kinds / except_kinds coverage. For each kind in the vocabulary, +-- verify that only_kinds=K emits just that kind's output and that except_kinds=K +-- omits just that kind's output. Kinds with no fixture above (trigger, +-- policy) are still recognized but currently emit nothing; only_kinds=K +-- returns zero rows for them. Fixtures used: +-- idxd: PK + two secondary CREATE INDEX +-- cons: PK + UNIQUE + CHECK + FK (constraint-backed) +-- rt: CREATE RULE +-- stx: CREATE STATISTICS +-- rls: ENABLE/FORCE ROW LEVEL SECURITY +-- ri_full: REPLICA IDENTITY FULL (no source-kind dependency) +CREATE TABLE excl_fix (rng int4range, + EXCLUDE USING gist (rng WITH &&)); +-- only_kinds=K: emit only kind K's statements. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index']); + pg_get_table_ddl +------------------------------------------------------------------------------------- + CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name)); + CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100); +(2 rows) + +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['primary_key']); + pg_get_table_ddl +---------------------------------------------------------------------------- + ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id); +(1 row) + +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['unique']); + pg_get_table_ddl +----------------------------------------------------------------------- + ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b); +(1 row) + +SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, only_kinds => ARRAY['exclusion']); + pg_get_table_ddl +-------------------------------------------------------------------------------------------------------- + ALTER TABLE pgtbl_ddl_test.excl_fix ADD CONSTRAINT excl_fix_rng_excl EXCLUDE USING gist (rng WITH &&); +(1 row) + +SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, only_kinds => ARRAY['rule']); + pg_get_table_ddl +------------------------------------------------------------------------------- + CREATE RULE rt_log_insert AS + + ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO pgtbl_ddl_test.rt_log (id)+ + VALUES (new.id); +(1 row) + +SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, only_kinds => ARRAY['statistics']); + pg_get_table_ddl +--------------------------------------------------------------------------------------- + CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM pgtbl_ddl_test.stx; +(1 row) + +SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, only_kinds => ARRAY['rls']); + pg_get_table_ddl +----------------------------------------------------------- + ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY; + ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY; +(2 rows) + +SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, only_kinds => ARRAY['replica_identity']); + pg_get_table_ddl +----------------------------------------------------------- + ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL; +(1 row) + +-- trigger / policy are not yet implemented; uncomment when +-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land. +-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['trigger']); +-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['policy']); +-- except_kinds=K: emit everything except kind K's statements. PK/UNIQUE/ +-- INDEX are exercised on tables without REPLICA IDENTITY USING INDEX +-- so the validation does not trip. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['primary_key']); + pg_get_table_ddl +------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text); + CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name)); + CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100); +(3 rows) + +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique']); + pg_get_table_ddl +---------------------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0))); + ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED; +(2 rows) + +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['check']); + pg_get_table_ddl +---------------------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer); + ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b); + ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED; +(3 rows) + +SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, except_kinds => ARRAY['exclusion']); + pg_get_table_ddl +------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.excl_fix (rng int4range); +(1 row) + +-- trigger / policy are not yet implemented; uncomment when +-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land. +-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['trigger']); +-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['policy']); +-- except_kinds=table suppresses the CREATE TABLE + OWNER + child-default +-- SET DEFAULT + attoptions passes, leaving only the sub-object passes. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['table']); + pg_get_table_ddl +------------------------------------------------------------------------------------- + CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name)); + CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100); + ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id); +(3 rows) + +-- Multi-kind compositions. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table','index']); + pg_get_table_ddl +------------------------------------------------------------------------------------- + CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text); + CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name)); + CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100); +(3 rows) + +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['primary_key','foreign_key']); + pg_get_table_ddl +---------------------------------------------------------------------------------------------------------------------------------------------- + ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED; +(1 row) + +-- Duplicate entries in the list are silently de-duplicated by the +-- Bitmapset. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index','index']); + pg_get_table_ddl +------------------------------------------------------------------------------------- + CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name)); + CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100); +(2 rows) + +DROP TABLE excl_fix; +-- only_kinds=K on a table that does not have any K of that kind is a valid +-- filter and returns zero rows. Tested previously with foreign_key +-- on 'refd'; here also rule, statistics, rls, replica_identity, and +-- exclusion on 'basic' (which has only PK + columns). +SELECT count(*) AS rule_rows + FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rule']); + rule_rows +----------- + 0 +(1 row) + +SELECT count(*) AS stats_rows + FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['statistics']); + stats_rows +------------ + 0 +(1 row) + +SELECT count(*) AS rls_rows + FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rls']); + rls_rows +---------- + 0 +(1 row) + +SELECT count(*) AS ri_rows + FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['replica_identity']); + ri_rows +--------- + 0 +(1 row) + +SELECT count(*) AS excl_rows + FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['exclusion']); + excl_rows +----------- + 0 +(1 row) + +-- Input-validation edge cases for the kind array. All +-- of these raise an error; the function never reaches relation open. +-- Empty array: +SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[]::text[]); +ERROR: parameter "only_kinds" must specify at least one kind +-- NULL element: +SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[NULL::text]); +ERROR: parameter "only_kinds" must not contain NULL elements +-- Unrecognized kind: +SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY['no_such_kind']); +ERROR: unrecognized kind "no_such_kind" in parameter "only_kinds" +-- Same kind specified through both parameters is still mutex-rejected. +SELECT * FROM pg_get_table_ddl('basic'::regclass, + only_kinds => ARRAY['index'], + except_kinds => ARRAY['rule']); +ERROR: "only_kinds" and "except_kinds" parameters are mutually exclusive +-- Round-trip verification. For every test table, capture the DDL the +-- function emits, drop the schema, replay each table's DDL in +-- dependency order, and confirm that pg_get_table_ddl on the recreated +-- relation matches the original line-for-line. The final SELECT must +-- return zero rows. +CREATE TEMP TABLE pgtbl_ddl_rt_orig (name text, ord int, line text); +INSERT INTO pgtbl_ddl_rt_orig +SELECT t.name, o.ord, o.line +FROM (VALUES + ('basic'), ('id_cols'), ('id_custom'), ('gen_cols'), ('gen_virtual'), ('storage_cols'), + ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'), + ('fk_acts_tgt'), ('fk_acts'), ('notenf_tgt'), ('notenf'), ('temporal_pk'), + ('idxd'), + ('par'), ('ch'), ('attopt'), + ('parted_range'), ('parted_range_1'), ('parted_range_def'), + ('parted_hash'), ('parted_hash_0'), + ('parted_list'), + ('rt_log'), ('rt'), + ('stx'), ('rls'), ('ri_full'), ('ri_idx'), ('uno') + ) AS t(name), + LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass, + owner => false) WITH ORDINALITY o(line, ord); +DO $$ +DECLARE + tables CONSTANT text[] := ARRAY[ + 'basic', 'id_cols', 'id_custom', 'gen_cols', 'gen_virtual', 'storage_cols', + 'refd', 'cons', 'nulls_nd', 'idx_inc', + 'fk_acts_tgt', 'fk_acts', 'notenf_tgt', 'notenf', 'temporal_pk', + 'idxd', + 'par', 'ch', 'attopt', + 'parted_range', 'parted_range_1', 'parted_range_def', + 'parted_hash', 'parted_hash_0', + 'parted_list', + 'rt_log', 'rt', + 'stx', 'rls', 'ri_full', 'ri_idx', 'uno' + ]; + t text; + stmt text; +BEGIN + DROP SCHEMA pgtbl_ddl_test CASCADE; + CREATE SCHEMA pgtbl_ddl_test; + FOREACH t IN ARRAY tables LOOP + FOR stmt IN + SELECT line FROM pgtbl_ddl_rt_orig WHERE name = t ORDER BY ord + LOOP + BEGIN + EXECUTE stmt; + EXCEPTION WHEN duplicate_table THEN + NULL; -- partition child already created by its parent's DDL + END; + END LOOP; + END LOOP; +END $$; +NOTICE: drop cascades to 34 other objects +DETAIL: drop cascades to table basic +drop cascades to table id_cols +drop cascades to table id_custom +drop cascades to table gen_cols +drop cascades to table gen_virtual +drop cascades to table storage_cols +drop cascades to table refd +drop cascades to table cons +drop cascades to table nulls_nd +drop cascades to table idx_inc +drop cascades to table fk_acts_tgt +drop cascades to table fk_acts +drop cascades to table notenf_tgt +drop cascades to table notenf +drop cascades to table temporal_pk +drop cascades to table idxd +drop cascades to table par +drop cascades to table ch +drop cascades to table attopt +drop cascades to table parted_range +drop cascades to table parted_hash +drop cascades to table parted_list +drop cascades to table rt +drop cascades to table rt_log +drop cascades to table stx +drop cascades to table rls +drop cascades to table ri_full +drop cascades to table ri_idx +drop cascades to table uno +drop cascades to type typed_t +drop cascades to table typed_plain +drop cascades to table typed_over +drop cascades to view v +drop cascades to sequence s +WITH after_ddl AS ( + SELECT t.name, o.ord, o.line + FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t, + LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass, + owner => false) WITH ORDINALITY o(line, ord) +) +(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig + EXCEPT + SELECT 'missing-in-copy', name, ord, line FROM after_ddl) +UNION ALL +(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl + EXCEPT + SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig) +ORDER BY kind, name, ord; + kind | name | ord | line +------+------+-----+------ +(0 rows) + +-- Cleanup. +DROP SCHEMA pgtbl_ddl_test CASCADE; +NOTICE: drop cascades to 29 other objects +DETAIL: drop cascades to table basic +drop cascades to table id_cols +drop cascades to table id_custom +drop cascades to table gen_cols +drop cascades to table gen_virtual +drop cascades to table storage_cols +drop cascades to table refd +drop cascades to table cons +drop cascades to table nulls_nd +drop cascades to table idx_inc +drop cascades to table fk_acts_tgt +drop cascades to table fk_acts +drop cascades to table notenf_tgt +drop cascades to table notenf +drop cascades to table temporal_pk +drop cascades to table idxd +drop cascades to table par +drop cascades to table ch +drop cascades to table attopt +drop cascades to table parted_range +drop cascades to table parted_hash +drop cascades to table parted_list +drop cascades to table rt_log +drop cascades to table rt +drop cascades to table stx +drop cascades to table rls +drop cascades to table ri_full +drop cascades to table ri_idx +drop cascades to table uno diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 8fa0a6c47fb..9eca64cca3b 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis # collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other # psql depends on create_am # amutils depends on geometry, create_index_spgist, hash_index, brin -test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 +test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl # ---------- # Run these alone so they don't run out of parallel workers diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql new file mode 100644 index 00000000000..f7bf58ab213 --- /dev/null +++ b/src/test/regress/sql/pg_get_table_ddl.sql @@ -0,0 +1,740 @@ +-- +-- pg_get_table_ddl +-- +-- All tests pass owner=>false so the ALTER TABLE OWNER TO line is not +-- emitted, keeping output stable across test runners (which may run under +-- different role names). +-- +CREATE SCHEMA pgtbl_ddl_test; +SET search_path = pgtbl_ddl_test; + +-- Basic table with PRIMARY KEY, NOT NULL, DEFAULT, COLLATE. +CREATE TABLE basic ( + id int PRIMARY KEY, + name text NOT NULL DEFAULT 'anon' COLLATE "C" +); +SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false); + +-- Identity columns (with default and custom sequence options). +CREATE TABLE id_cols ( + id_always int GENERATED ALWAYS AS IDENTITY, + id_default int GENERATED BY DEFAULT AS IDENTITY +); +SELECT * FROM pg_get_table_ddl('id_cols'::regclass, owner => false); + +CREATE TABLE id_custom ( + v int GENERATED ALWAYS AS IDENTITY ( + SEQUENCE NAME id_custom_v_seq + START WITH 100 INCREMENT BY 5 + MINVALUE 50 MAXVALUE 1000 + CACHE 10 CYCLE + ) +); +SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false); + +-- Generated stored column. +CREATE TABLE gen_cols ( + cents int, + dollars numeric GENERATED ALWAYS AS (cents / 100.0) STORED +); +SELECT * FROM pg_get_table_ddl('gen_cols'::regclass, owner => false); + +-- Generated virtual column. +CREATE TABLE gen_virtual ( + base int, + derived int GENERATED ALWAYS AS (base * 2) VIRTUAL +); +SELECT * FROM pg_get_table_ddl('gen_virtual'::regclass, owner => false); + +-- STORAGE and COMPRESSION (only emitted when non-default for the type). +CREATE TABLE storage_cols ( + a text STORAGE EXTERNAL, + b text STORAGE MAIN, + c text COMPRESSION pglz +); +SELECT * FROM pg_get_table_ddl('storage_cols'::regclass, owner => false); + +-- Constraints: CHECK, UNIQUE, FOREIGN KEY (DEFERRABLE). +CREATE TABLE refd (id int PRIMARY KEY); +CREATE TABLE cons ( + a int CHECK (a > 0), + b int UNIQUE, + c int REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED +); +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false); + +-- UNIQUE NULLS NOT DISTINCT (PG 15): null values are treated as equal for +-- uniqueness purposes, so the NULLS NOT DISTINCT clause must be emitted. +CREATE TABLE nulls_nd ( + a int, + b int, + UNIQUE NULLS NOT DISTINCT (a, b) +); +SELECT * FROM pg_get_table_ddl('nulls_nd'::regclass, owner => false); + +-- INCLUDE columns on a constraint-backed UNIQUE index (PG 11): the +-- covering columns appear in an INCLUDE (...) clause on the ALTER TABLE +-- ADD CONSTRAINT statement, not in the key column list. +CREATE TABLE idx_inc ( + id int PRIMARY KEY, + name text, + extra text, + UNIQUE (name) INCLUDE (extra) +); +SELECT * FROM pg_get_table_ddl('idx_inc'::regclass, owner => false); + +-- FOREIGN KEY with ON DELETE / ON UPDATE referential actions and MATCH +-- clause. Each variant must appear verbatim in the reconstructed DDL. +CREATE TABLE fk_acts_tgt (id int PRIMARY KEY); +CREATE TABLE fk_acts ( + a int, + b int, + c int, + d int, + CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES fk_acts_tgt(id) + ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES fk_acts_tgt(id) + ON DELETE SET NULL, + CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES fk_acts_tgt(id) + ON DELETE SET DEFAULT, + CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES fk_acts_tgt(id) + MATCH FULL +); +SELECT * FROM pg_get_table_ddl('fk_acts'::regclass, owner => false); + +-- NOT ENFORCED foreign key (PG 17): the constraint is recorded in the +-- catalog but not validated by the engine; NOT ENFORCED must appear in +-- the reconstructed DDL. +CREATE TABLE notenf_tgt (id int PRIMARY KEY); +CREATE TABLE notenf ( + a int, + CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES notenf_tgt(id) + NOT ENFORCED +); +SELECT * FROM pg_get_table_ddl('notenf'::regclass, owner => false); + +-- WITHOUT OVERLAPS on a PRIMARY KEY (PG 17): the range-type period +-- column forms a temporal primary key; WITHOUT OVERLAPS must appear +-- inside the key column list in the reconstructed ALTER TABLE. +-- Both columns use range types with native GiST support so no +-- btree_gist extension is needed. +CREATE TABLE temporal_pk ( + grp int4range, + during daterange, + PRIMARY KEY (grp, during WITHOUT OVERLAPS) +); +SELECT * FROM pg_get_table_ddl('temporal_pk'::regclass, owner => false); + +-- Indexes: functional and partial. Constraint-backing indexes are +-- suppressed (they are emitted by the constraint loop). +CREATE TABLE idxd (id int PRIMARY KEY, name text); +CREATE INDEX idxd_lower ON idxd (lower(name)); +CREATE INDEX idxd_partial ON idxd (id) WHERE id > 100; +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false); + +-- Inheritance, including a child DEFAULT override on an inherited column. +CREATE TABLE par (a int DEFAULT 1, b text); +CREATE TABLE ch (c int) INHERITS (par); +ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999; +SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false); + +-- Per-column attoptions: emitted as ALTER COLUMN SET (...). +CREATE TABLE attopt (a int, b text); +ALTER TABLE attopt ALTER COLUMN a SET (n_distinct = 100); +SELECT * FROM pg_get_table_ddl('attopt'::regclass, owner => false); + +-- Partitioned table parents (RANGE, HASH, and LIST). +CREATE TABLE parted_range (id int, k int) PARTITION BY RANGE (id); +SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false); + +CREATE TABLE parted_hash (id int) PARTITION BY HASH (id); +SELECT * FROM pg_get_table_ddl('parted_hash'::regclass, owner => false); + +CREATE TABLE parted_list (id int, region text) PARTITION BY LIST (region); +SELECT * FROM pg_get_table_ddl('parted_list'::regclass, owner => false); + +-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT. +CREATE TABLE parted_range_1 PARTITION OF parted_range + FOR VALUES FROM (0) TO (100); +ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7; +SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false); + +CREATE TABLE parted_hash_0 PARTITION OF parted_hash + FOR VALUES WITH (modulus 2, remainder 0); +SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, owner => false); + +CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT; +SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, owner => false); + +-- Rules. +CREATE TABLE rt (id int); +CREATE TABLE rt_log (id int); +CREATE RULE rt_log_insert AS ON INSERT TO rt + DO ALSO INSERT INTO rt_log VALUES (NEW.id); +SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false); + +-- Extended statistics. +CREATE TABLE stx (a int, b int, c int); +CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx; +SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false); + +-- Row-level security toggles. +CREATE TABLE rls (id int); +ALTER TABLE rls ENABLE ROW LEVEL SECURITY; +ALTER TABLE rls FORCE ROW LEVEL SECURITY; +SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false); + +-- REPLICA IDENTITY: emitted only when not the default. +CREATE TABLE ri_full (a int); +ALTER TABLE ri_full REPLICA IDENTITY FULL; +SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false); + +CREATE TABLE ri_idx (a int NOT NULL); +CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a); +ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a; +SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, owner => false); + +-- UNLOGGED + reloptions. +CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70); +SELECT * FROM pg_get_table_ddl('uno'::regclass, owner => false); + +-- Typed table (CREATE TABLE OF type_name). Columns inherited from the +-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come +-- out through a single "(col WITH OPTIONS ...)" list. +CREATE TYPE typed_t AS (a int, b text); +CREATE TABLE typed_plain OF typed_t; +SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, owner => false); + +CREATE TABLE typed_over OF typed_t ( + a WITH OPTIONS DEFAULT 7 NOT NULL, + b WITH OPTIONS NOT NULL, + CONSTRAINT b_nonempty CHECK (length(b) > 0) +); +SELECT * FROM pg_get_table_ddl('typed_over'::regclass, owner => false); + +-- Temporary tables + ON COMMIT. Temp tables must never be emitted with +-- the session-private pg_temp_NN schema prefix -- the TEMPORARY keyword +-- already places the table in pg_temp and the prefix is non-replayable. +-- ON COMMIT DROP only fires at commit, so the queries that need to see +-- the catalog entry must run in the same transaction as the CREATE. +BEGIN; +CREATE TEMP TABLE temp_default (id int); +CREATE TEMP TABLE temp_delete (id int) ON COMMIT DELETE ROWS; +CREATE TEMP TABLE temp_drop (id int) ON COMMIT DROP; + +SELECT line FROM pg_get_table_ddl('temp_default'::regclass, owner => false) AS line +WHERE line LIKE 'CREATE %'; + +SELECT line FROM pg_get_table_ddl('temp_delete'::regclass, owner => false) AS line +WHERE line LIKE 'CREATE %'; + +SELECT line FROM pg_get_table_ddl('temp_drop'::regclass, owner => false) AS line +WHERE line LIKE 'CREATE %'; +ROLLBACK; + +-- Pretty mode. +SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, pretty => true); + +-- only_kinds / except_kinds gating: each emits either only the listed kinds +-- (only_kinds) or every kind except the listed ones (except_kinds). The two +-- are mutually exclusive. Kind vocabulary: table, index, +-- primary_key, unique, check, foreign_key, exclusion, rule, +-- statistics, trigger, policy, rls, replica_identity, partition. +-- NOT NULL is not in the vocabulary - always emitted to prevent +-- producing schemas that silently accept NULLs the source would +-- have rejected. +-- +-- except_kinds=index hides the CREATE INDEX statements. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['index']); + +-- except_kinds=primary_key,unique,check,foreign_key,exclusion hides every +-- table-level constraint. Inline CHECK in the CREATE TABLE body is +-- suppressed too. +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['primary_key','unique','check','foreign_key','exclusion']); + +-- except_kinds=foreign_key suppresses only FOREIGN KEY constraints; +-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the +-- first pass of the two-pass FK-clone workflow: emit tables without +-- cross-table FKs, load data, then re-run with only_kinds=foreign_key +-- to add them once all targets exist. +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['foreign_key']); + +-- only_kinds=foreign_key is the second pass of the two-pass FK-clone +-- workflow: emit only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN +-- KEY rows; the CREATE TABLE and every non-FK sub-object are +-- suppressed (the table already exists from the first pass). +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['foreign_key']); + +-- A table with no foreign keys produces no rows under +-- only_kinds=foreign_key. +SELECT * FROM pg_get_table_ddl('refd'::regclass, owner => false, only_kinds => ARRAY['foreign_key']); + +-- only_kinds=table emits only the bare CREATE TABLE (with any inline +-- column-level NOT NULL and CHECK) - no indexes, no PRIMARY KEY ALTER, +-- no other sub-objects. Useful for capturing a table's shape without +-- pulling in everything attached to it. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table']); + +-- only_kinds=check on a non-partition table. The constraint loop must emit +-- column-level and table-level CHECK constraints via ALTER TABLE here, +-- because the inline path in CREATE TABLE never runs (KIND_TABLE is +-- not in the filter). +CREATE TABLE chk_only ( + id int PRIMARY KEY, + qty int CHECK (qty > 0), + CONSTRAINT chk_only_id_pos CHECK (id > 0) +); +SELECT * FROM pg_get_table_ddl('chk_only'::regclass, owner => false, only_kinds => ARRAY['check']); +DROP TABLE chk_only; + +-- only_kinds=partition on a partitioned-table parent: each child's full DDL +-- is emitted (CREATE TABLE ... PARTITION OF ...). The "partition" +-- keyword is a gate at the parent level; it is stripped from the +-- propagated filter so the child does not exclude its own +-- CREATE TABLE. +CREATE TABLE p_only (id int, val text) PARTITION BY RANGE (id); +CREATE TABLE p_only_a PARTITION OF p_only FOR VALUES FROM (0) TO (100); +CREATE TABLE p_only_b PARTITION OF p_only FOR VALUES FROM (100) TO (200); +SELECT * FROM pg_get_table_ddl('p_only'::regclass, owner => false, only_kinds => ARRAY['partition']); +DROP TABLE p_only; + +-- only_kinds and except_kinds are mutually exclusive. +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, + only_kinds => ARRAY['foreign_key'], + except_kinds => ARRAY['foreign_key']); + +-- Pub/sub schema clone: keep the table and its primary key, drop the +-- rest of the constraints so the subscriber can replicate without +-- pulling cross-table dependencies along. +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique','check','foreign_key','exclusion']); + +-- except_kinds=rule hides the CREATE RULE. +SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, except_kinds => ARRAY['rule']); + +-- except_kinds=statistics hides the CREATE STATISTICS. +SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, except_kinds => ARRAY['statistics']); + +-- except_kinds=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles. +SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, except_kinds => ARRAY['rls']); + +-- except_kinds=replica_identity hides the REPLICA IDENTITY clause. +SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, except_kinds => ARRAY['replica_identity']); + +-- Partition children are emitted by default; except_kinds=partition +-- suppresses them so only the partitioned-table parent comes out. +SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false, except_kinds => ARRAY['partition']); + +-- Whitespace around kind names is allowed; case is folded. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY[' INDEX ',' RULE ']); + +-- schema_qualified=false strips the schema from the target table itself +-- and from same-schema sibling references (inheritance/partition +-- parents, identity sequences in the same schema), while cross-schema +-- references are kept qualified for correctness. +-- +-- Bare CREATE TABLE for an ordinary table. +SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, + schema_qualified => false); + +-- INHERITS parent in the same schema is emitted unqualified. +SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false, + schema_qualified => false); + +-- Partition parent in the same schema is emitted unqualified. +SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false, + schema_qualified => false); + +-- Cross-schema FK target keeps qualification. The 'refd' table is in +-- pgtbl_ddl_test; place an FK target in a different schema and verify it +-- stays qualified even with schema_qualified=false. +CREATE SCHEMA pgtbl_ddl_other; +CREATE TABLE pgtbl_ddl_other.parent (id int PRIMARY KEY); +CREATE TABLE xschema_fk ( + id int PRIMARY KEY, + pid int REFERENCES pgtbl_ddl_other.parent +); +SELECT * FROM pg_get_table_ddl('xschema_fk'::regclass, owner => false, + schema_qualified => false); + +-- Cross-schema FK whose target schema name ends with the base schema name +-- (e.g. "xpgtbl_ddl_test" ends with "pgtbl_ddl_test"). A post-hoc text +-- strip would mangle "xpgtbl_ddl_test.reftbl" into "xreftbl"; deciding +-- qualification at generation time avoids the problem entirely. +CREATE SCHEMA xpgtbl_ddl_test; +CREATE TABLE xpgtbl_ddl_test.reftbl (id int PRIMARY KEY); +CREATE TABLE suffix_schema_fk ( + id int PRIMARY KEY, + ref int REFERENCES xpgtbl_ddl_test.reftbl +); +SELECT * FROM pg_get_table_ddl('suffix_schema_fk'::regclass, owner => false, + schema_qualified => false); +DROP TABLE suffix_schema_fk; +DROP SCHEMA xpgtbl_ddl_test CASCADE; + +-- Identity column with a custom sequence name in the same schema is +-- emitted unqualified. +SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false, + schema_qualified => false); + +-- Round-trip: with schema_qualified=false, after SET search_path the +-- DDL can be replayed into a different target schema. +CREATE SCHEMA pgtbl_ddl_replay; +DO $$ +DECLARE + stmt text; +BEGIN + SET LOCAL search_path = pgtbl_ddl_replay; + FOR stmt IN + SELECT line FROM pg_get_table_ddl('pgtbl_ddl_test.basic'::regclass, + owner => false, + schema_qualified => false) AS line + LOOP + EXECUTE stmt; + END LOOP; +END $$; +SELECT relnamespace::regnamespace::text, relname +FROM pg_class WHERE oid = 'pgtbl_ddl_replay.basic'::regclass; +DROP SCHEMA pgtbl_ddl_replay CASCADE; +DROP TABLE xschema_fk; +DROP SCHEMA pgtbl_ddl_other CASCADE; + +-- Locally-declared CHECK on a partition child has no inline column +-- list to live in (PARTITION OF form), so it must come out as a +-- separate ALTER TABLE ... ADD CONSTRAINT statement. +CREATE TABLE parted_chk (id int, val int) PARTITION BY RANGE (id); +CREATE TABLE parted_chk_child PARTITION OF parted_chk + (CONSTRAINT chk_inline CHECK (val > 0)) + FOR VALUES FROM (0) TO (100); +SELECT * FROM pg_get_table_ddl('parted_chk_child'::regclass, owner => false); +DROP TABLE parted_chk; + +-- Inheritance children of a parent with a generated column should not +-- emit an ALTER COLUMN ... SET DEFAULT for the inherited generated +-- column: the GENERATED expression carries down through inheritance, +-- and SET DEFAULT would fail at replay against a generated column. +CREATE TABLE par_gen ( + id int, + g int GENERATED ALWAYS AS (id * 2) STORED +); +CREATE TABLE ch_gen () INHERITS (par_gen); +SELECT * FROM pg_get_table_ddl('ch_gen'::regclass, owner => false); +DROP TABLE par_gen CASCADE; + +-- A user-named NOT NULL on a local column is emitted inline as +-- "CONSTRAINT name NOT NULL" so the original name is preserved. An +-- auto-named NOT NULL on a local column is emitted as a plain inline +-- "NOT NULL" - PG re-creates the auto-named constraint when CREATE +-- TABLE runs, so the constraint loop skips both forms to avoid the +-- redundant (and on table-rename or sequence-collision, broken) +-- ALTER TABLE ... ADD CONSTRAINT ... NOT NULL statement. +CREATE TABLE nn_named ( + a int CONSTRAINT my_nn NOT NULL, + b int NOT NULL +); +SELECT * FROM pg_get_table_ddl('nn_named'::regclass, owner => false); +DROP TABLE nn_named; + +-- Renaming the table after an IDENTITY NOT NULL column was declared +-- leaves the auto-named constraint frozen at the original name, which +-- no longer matches the post-rename "
__not_null" pattern. +-- The emitted DDL must still round-trip: the constraint loop must not +-- ALTER TABLE ... ADD CONSTRAINT a second NOT NULL, since CREATE TABLE +-- with the inline IDENTITY already creates an auto-NOT-NULL under the +-- new table name and PG only permits one NOT NULL per column. +CREATE TABLE nn_renamed ( + a int GENERATED ALWAYS AS IDENTITY NOT NULL +); +ALTER TABLE nn_renamed RENAME TO nn_renamed2; +SELECT * FROM pg_get_table_ddl('nn_renamed2'::regclass, owner => false); +DROP TABLE nn_renamed2; + +-- Locally-declared NOT NULL on an inherited column. The column itself +-- is omitted from the child's column list (attislocal=false), so the +-- user-named NOT NULL must come out via the post-CREATE constraint +-- loop rather than inline. +CREATE TABLE nn_par (a int); +CREATE TABLE nn_ch (b int) INHERITS (nn_par); +ALTER TABLE nn_ch ADD CONSTRAINT my_nn NOT NULL a; +SELECT * FROM pg_get_table_ddl('nn_ch'::regclass, owner => false); +DROP TABLE nn_par CASCADE; + +-- schema_qualified=false must drop the schema prefix from DEFAULT +-- expressions and inline CHECK bodies that reference same-schema +-- objects (sequence via nextval, function via direct call). This +-- relies on the deparser respecting the narrowed search_path rather +-- than on substring stripping. +CREATE SCHEMA pgtbl_ddl_xref; +CREATE SEQUENCE pgtbl_ddl_xref.myseq; +CREATE FUNCTION pgtbl_ddl_xref.f(int) RETURNS int LANGUAGE sql IMMUTABLE + AS 'SELECT $1'; +CREATE TABLE pgtbl_ddl_xref.t ( + id int DEFAULT nextval('pgtbl_ddl_xref.myseq'), + val int, + CONSTRAINT chk CHECK (pgtbl_ddl_xref.f(val) > 0) +); +SELECT * FROM pg_get_table_ddl('pgtbl_ddl_xref.t'::regclass, + owner => false, + schema_qualified => false); +DROP SCHEMA pgtbl_ddl_xref CASCADE; + +-- schema_qualified=false must NOT corrupt string literals whose +-- contents happen to contain the schema name followed by a dot. String +-- literal text is output verbatim by the deparser and is unaffected by +-- search_path narrowing. +CREATE SCHEMA pgtbl_ddl_str; +CREATE TABLE pgtbl_ddl_str.p (id int, note text) PARTITION BY RANGE (id); +CREATE TABLE pgtbl_ddl_str.pc PARTITION OF pgtbl_ddl_str.p + (CONSTRAINT chk CHECK (note <> 'pgtbl_ddl_str.secret')) + FOR VALUES FROM (0) TO (100); +SELECT * FROM pg_get_table_ddl('pgtbl_ddl_str.pc'::regclass, + owner => false, + schema_qualified => false); +DROP SCHEMA pgtbl_ddl_str CASCADE; + +-- schema_qualified=false must NOT corrupt double-quoted identifiers +-- whose contents happen to contain the schema name followed by a dot +-- (for example a column literally named ".weird" inside that +-- schema). The mechanism is purely search_path narrowing: the deparser +-- outputs attribute names verbatim from pg_attribute, so the quoted +-- identifier content is never examined for schema prefixes. This holds +-- even when the schema name is a single letter. +CREATE SCHEMA pgtbl_ddl_qid; +CREATE TABLE pgtbl_ddl_qid.p (id int, "pgtbl_ddl_qid.weird" int) + PARTITION BY RANGE (id); +CREATE TABLE pgtbl_ddl_qid.pc PARTITION OF pgtbl_ddl_qid.p + (CONSTRAINT chk CHECK ("pgtbl_ddl_qid.weird" > 0)) + FOR VALUES FROM (0) TO (100); +SELECT * FROM pg_get_table_ddl('pgtbl_ddl_qid.pc'::regclass, + owner => false, + schema_qualified => false); +DROP SCHEMA pgtbl_ddl_qid CASCADE; +-- Same with a short (single-letter) schema name to confirm there is no +-- minimum-length assumption in the stripping path. +CREATE SCHEMA pgtbl_ddl_q1; +CREATE TABLE pgtbl_ddl_q1.p (id int, "pgtbl_ddl_q1.weird" int) + PARTITION BY RANGE (id); +CREATE TABLE pgtbl_ddl_q1.pc PARTITION OF pgtbl_ddl_q1.p + (CONSTRAINT chk CHECK ("pgtbl_ddl_q1.weird" > 0)) + FOR VALUES FROM (0) TO (100); +SELECT * FROM pg_get_table_ddl('pgtbl_ddl_q1.pc'::regclass, + owner => false, + schema_qualified => false); +DROP SCHEMA pgtbl_ddl_q1 CASCADE; + +-- schema_qualified=false when the base schema name itself requires +-- quoting (its prefix starts with "). Both forms of the prefix - +-- bare-lowercase and quoted - must be stripped from outer +-- ALTER TABLE / ON / CREATE INDEX prefixes, while inner quoted +-- identifiers that happen to begin the same way must be preserved. +CREATE SCHEMA "PgTbl-Ddl-Q"; +CREATE TABLE "PgTbl-Ddl-Q"."T" (id int, val int, + CONSTRAINT chk CHECK (val > 0)); +CREATE INDEX "T_val_idx" ON "PgTbl-Ddl-Q"."T" (val); +SELECT * FROM pg_get_table_ddl('"PgTbl-Ddl-Q"."T"'::regclass, + owner => false, + schema_qualified => false); +DROP SCHEMA "PgTbl-Ddl-Q" CASCADE; + +-- Error: not an ordinary or partitioned table. +CREATE VIEW v AS SELECT 1 AS x; +SELECT * FROM pg_get_table_ddl('v'::regclass); + +CREATE SEQUENCE s; +SELECT * FROM pg_get_table_ddl('s'::regclass); + +-- NULL argument returns no rows. +SELECT * FROM pg_get_table_ddl(NULL); + +-- REPLICA IDENTITY USING INDEX validation: if the referenced index +-- would not be emitted (because its source kind is suppressed) but +-- replica_identity itself would be, the function raises an error so +-- the emitted DDL never dangles. Exercise all three constraint-backed +-- forms (PK, UNIQUE, EXCLUSION) and a bare CREATE UNIQUE INDEX, in +-- both the except-list and only-list shapes. +-- +-- PK-backed: except_kinds=primary_key without except_kinds=replica_identity. +CREATE TABLE ri_pk_excl (a int PRIMARY KEY); +ALTER TABLE ri_pk_excl REPLICA IDENTITY USING INDEX ri_pk_excl_pkey; +SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key']); +-- Same call but also excluding replica_identity succeeds. +SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key','replica_identity']); +DROP TABLE ri_pk_excl; + +-- UNIQUE-constraint-backed replica identity index. +CREATE TABLE ri_uniq_excl (a int NOT NULL UNIQUE, b int); +ALTER TABLE ri_uniq_excl REPLICA IDENTITY USING INDEX ri_uniq_excl_a_key; +SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique']); +SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique','replica_identity']); +DROP TABLE ri_uniq_excl; + +-- Bare CREATE UNIQUE INDEX (no backing constraint) used as REPLICA +-- IDENTITY: the index is emitted by the "index" kind, so excluding +-- "index" without also excluding "replica_identity" must error. +CREATE TABLE ri_idx_excl (a int NOT NULL, b int); +CREATE UNIQUE INDEX ri_idx_excl_a_uniq ON ri_idx_excl (a); +ALTER TABLE ri_idx_excl REPLICA IDENTITY USING INDEX ri_idx_excl_a_uniq; +SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index']); +SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index','replica_identity']); +DROP TABLE ri_idx_excl; + +-- Include-side analog: only_kinds=replica_identity must include the kind +-- that emits the index, or the function errors. +CREATE TABLE ri_only (a int NOT NULL UNIQUE, b int); +ALTER TABLE ri_only REPLICA IDENTITY USING INDEX ri_only_a_key; +SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['replica_identity']); +-- Including both kinds succeeds. +SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['unique','replica_identity']); +DROP TABLE ri_only; + +-- Per-kind only_kinds / except_kinds coverage. For each kind in the vocabulary, +-- verify that only_kinds=K emits just that kind's output and that except_kinds=K +-- omits just that kind's output. Kinds with no fixture above (trigger, +-- policy) are still recognized but currently emit nothing; only_kinds=K +-- returns zero rows for them. Fixtures used: +-- idxd: PK + two secondary CREATE INDEX +-- cons: PK + UNIQUE + CHECK + FK (constraint-backed) +-- rt: CREATE RULE +-- stx: CREATE STATISTICS +-- rls: ENABLE/FORCE ROW LEVEL SECURITY +-- ri_full: REPLICA IDENTITY FULL (no source-kind dependency) +CREATE TABLE excl_fix (rng int4range, + EXCLUDE USING gist (rng WITH &&)); + +-- only_kinds=K: emit only kind K's statements. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index']); +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['primary_key']); +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['unique']); +SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, only_kinds => ARRAY['exclusion']); +SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, only_kinds => ARRAY['rule']); +SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, only_kinds => ARRAY['statistics']); +SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, only_kinds => ARRAY['rls']); +SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, only_kinds => ARRAY['replica_identity']); +-- trigger / policy are not yet implemented; uncomment when +-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land. +-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['trigger']); +-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['policy']); + +-- except_kinds=K: emit everything except kind K's statements. PK/UNIQUE/ +-- INDEX are exercised on tables without REPLICA IDENTITY USING INDEX +-- so the validation does not trip. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['primary_key']); +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique']); +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['check']); +SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, except_kinds => ARRAY['exclusion']); +-- trigger / policy are not yet implemented; uncomment when +-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land. +-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['trigger']); +-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['policy']); +-- except_kinds=table suppresses the CREATE TABLE + OWNER + child-default +-- SET DEFAULT + attoptions passes, leaving only the sub-object passes. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['table']); + +-- Multi-kind compositions. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table','index']); +SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['primary_key','foreign_key']); +-- Duplicate entries in the list are silently de-duplicated by the +-- Bitmapset. +SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index','index']); + +DROP TABLE excl_fix; + +-- only_kinds=K on a table that does not have any K of that kind is a valid +-- filter and returns zero rows. Tested previously with foreign_key +-- on 'refd'; here also rule, statistics, rls, replica_identity, and +-- exclusion on 'basic' (which has only PK + columns). +SELECT count(*) AS rule_rows + FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rule']); +SELECT count(*) AS stats_rows + FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['statistics']); +SELECT count(*) AS rls_rows + FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rls']); +SELECT count(*) AS ri_rows + FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['replica_identity']); +SELECT count(*) AS excl_rows + FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['exclusion']); + +-- Input-validation edge cases for the kind array. All +-- of these raise an error; the function never reaches relation open. +-- Empty array: +SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[]::text[]); +-- NULL element: +SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[NULL::text]); +-- Unrecognized kind: +SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY['no_such_kind']); +-- Same kind specified through both parameters is still mutex-rejected. +SELECT * FROM pg_get_table_ddl('basic'::regclass, + only_kinds => ARRAY['index'], + except_kinds => ARRAY['rule']); + +-- Round-trip verification. For every test table, capture the DDL the +-- function emits, drop the schema, replay each table's DDL in +-- dependency order, and confirm that pg_get_table_ddl on the recreated +-- relation matches the original line-for-line. The final SELECT must +-- return zero rows. +CREATE TEMP TABLE pgtbl_ddl_rt_orig (name text, ord int, line text); +INSERT INTO pgtbl_ddl_rt_orig +SELECT t.name, o.ord, o.line +FROM (VALUES + ('basic'), ('id_cols'), ('id_custom'), ('gen_cols'), ('gen_virtual'), ('storage_cols'), + ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'), + ('fk_acts_tgt'), ('fk_acts'), ('notenf_tgt'), ('notenf'), ('temporal_pk'), + ('idxd'), + ('par'), ('ch'), ('attopt'), + ('parted_range'), ('parted_range_1'), ('parted_range_def'), + ('parted_hash'), ('parted_hash_0'), + ('parted_list'), + ('rt_log'), ('rt'), + ('stx'), ('rls'), ('ri_full'), ('ri_idx'), ('uno') + ) AS t(name), + LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass, + owner => false) WITH ORDINALITY o(line, ord); + +DO $$ +DECLARE + tables CONSTANT text[] := ARRAY[ + 'basic', 'id_cols', 'id_custom', 'gen_cols', 'gen_virtual', 'storage_cols', + 'refd', 'cons', 'nulls_nd', 'idx_inc', + 'fk_acts_tgt', 'fk_acts', 'notenf_tgt', 'notenf', 'temporal_pk', + 'idxd', + 'par', 'ch', 'attopt', + 'parted_range', 'parted_range_1', 'parted_range_def', + 'parted_hash', 'parted_hash_0', + 'parted_list', + 'rt_log', 'rt', + 'stx', 'rls', 'ri_full', 'ri_idx', 'uno' + ]; + t text; + stmt text; +BEGIN + DROP SCHEMA pgtbl_ddl_test CASCADE; + CREATE SCHEMA pgtbl_ddl_test; + FOREACH t IN ARRAY tables LOOP + FOR stmt IN + SELECT line FROM pgtbl_ddl_rt_orig WHERE name = t ORDER BY ord + LOOP + BEGIN + EXECUTE stmt; + EXCEPTION WHEN duplicate_table THEN + NULL; -- partition child already created by its parent's DDL + END; + END LOOP; + END LOOP; +END $$; + +WITH after_ddl AS ( + SELECT t.name, o.ord, o.line + FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t, + LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass, + owner => false) WITH ORDINALITY o(line, ord) +) +(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig + EXCEPT + SELECT 'missing-in-copy', name, ord, line FROM after_ddl) +UNION ALL +(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl + EXCEPT + SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig) +ORDER BY kind, name, ord; + +-- Cleanup. +DROP SCHEMA pgtbl_ddl_test CASCADE; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3a2720fb5f9..49a7811050c 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1645,6 +1645,7 @@ ListenerEntry LoInfo LoadStmt LocalBufferLookupEnt +LocalNotNullEntry LocalPgBackendStatus LocalTransactionId Location @@ -3149,6 +3150,7 @@ TableFuncScan TableFuncScanState TableFuncType TableInfo +TableDdlContext TableLikeClause TableSampleClause TableScanDesc -- 2.51.0