v12-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch
application/octet-stream
Filename: v12-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch
Type: application/octet-stream
Part: 0
Patch
Format: format-patch
Series: patch v12-0001
Subject: Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
| File | + | − |
|---|---|---|
| doc/src/sgml/func/func-info.sgml | 124 | 0 |
| src/backend/catalog/pg_inherits.c | 93 | 0 |
| src/backend/commands/tablecmds.c | 28 | 2 |
| src/backend/utils/adt/ddlutils.c | 2152 | 0 |
| src/backend/utils/adt/ruleutils.c | 75 | 9 |
| src/include/catalog/pg_inherits.h | 1 | 0 |
| src/include/catalog/pg_proc.dat | 7 | 0 |
| src/include/commands/tablecmds.h | 3 | 0 |
| src/include/utils/ruleutils.h | 4 | 0 |
| src/test/regress/expected/pg_get_table_ddl.out | 1265 | 0 |
| src/test/regress/parallel_schedule | 1 | 1 |
| src/test/regress/sql/pg_get_table_ddl.sql | 741 | 0 |
| src/tools/pgindent/typedefs.list | 2 | 0 |
From c1f03685fe93e3c033c0935eea9989640a9f8afa Mon Sep 17 00:00:00 2001
From: Akshay Joshi <akshay.joshi@enterprisedb.com>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v12] 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, 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 via pg_get_indexdef_ddl, constraints
(PRIMARY KEY, UNIQUE, FOREIGN KEY, 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.
Options are passed as variadic name/value pairs. Formatting options
are pretty (boolean), owner (boolean, controls the
ALTER TABLE ... OWNER TO line), tablespace (boolean, controls the
TABLESPACE clause), and schema_qualified (boolean, controls whether
the target table and same-schema sibling references are emitted with
their schema prefix). Temporary tables are never schema-qualified
regardless of schema_qualified: the TEMPORARY keyword already places
them in pg_temp, and emitting pg_temp_NN.relname would be
non-replayable. Object-class filtering is expressed through two
mutually-exclusive options, only and except, each taking a
comma-separated list drawn from the kind vocabulary
{table, index, primary_key, unique, check, foreign_key, exclusion,
rule, statistics, trigger, policy, rls, replica_identity, partition}.
When only is set, only the listed kinds are emitted; when except is
set, every kind except the listed ones is emitted; when neither is
set, every kind is emitted. Unknown kind names raise an error at
parse time. NOT NULL is deliberately not part of the vocabulary - it
is always emitted to prevent producing schemas that silently accept
NULLs the source would have rejected. Excluding primary_key on a
table whose REPLICA IDENTITY is USING INDEX of the primary key is
rejected unless replica_identity is also excluded, so the emitted DDL
never references an index it just dropped.
Default omission convention: every optional clause is dropped when its
value equals what the system 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 <akshay.joshi@enterprisedb.com>
Reviewed-by: Marcos Pegoraro <marcos@f10.com.br>
Reviewed-by: Zsolt Parragi <zsolt.parragi@percona.com>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: Rui Zhao <zhaorui126@gmail.com>
---
doc/src/sgml/func/func-info.sgml | 124 +
src/backend/catalog/pg_inherits.c | 93 +
src/backend/commands/tablecmds.c | 30 +-
src/backend/utils/adt/ddlutils.c | 2152 +++++++++++++++++
src/backend/utils/adt/ruleutils.c | 84 +-
src/include/catalog/pg_inherits.h | 1 +
src/include/catalog/pg_proc.dat | 7 +
src/include/commands/tablecmds.h | 3 +
src/include/utils/ruleutils.h | 4 +
.../regress/expected/pg_get_table_ddl.out | 1265 ++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 741 ++++++
src/tools/pgindent/typedefs.list | 2 +
13 files changed, 4496 insertions(+), 12 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..3a6ad63c634 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3960,6 +3960,130 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
is false, the <literal>OWNER</literal> clause is omitted.
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_table_ddl</primary>
+ </indexterm>
+ <function>pg_get_table_ddl</function>
+ ( <parameter>table</parameter> <type>regclass</type>
+ <optional>, <literal>VARIADIC</literal> <parameter>options</parameter>
+ <type>text[]</type> </optional> )
+ <returnvalue>setof text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the <command>CREATE TABLE</command> statement for the
+ specified ordinary or partitioned table, followed by the
+ <command>ALTER TABLE</command>, <command>CREATE INDEX</command>,
+ <command>CREATE RULE</command>, and <command>CREATE STATISTICS</command>
+ 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.
+ The following options are supported:
+ <literal>pretty</literal> (boolean) for formatted output,
+ <literal>owner</literal> (boolean) to include the
+ <command>ALTER TABLE ... OWNER TO</command> statement,
+ <literal>tablespace</literal> (boolean) to include the
+ <literal>TABLESPACE</literal> clause on the
+ <command>CREATE TABLE</command> statement, and the
+ <literal>only</literal> and <literal>except</literal>
+ options described below for filtering which object classes are
+ emitted.
+ </para>
+ <para>
+ The <literal>only</literal> and <literal>except</literal>
+ options each take a comma-separated list of object-class kind
+ names and are mutually exclusive (specifying both raises an
+ error). When <literal>only</literal> is set, only the
+ listed kinds are emitted; when <literal>except</literal> is
+ set, every kind <emphasis>except</emphasis> the listed ones is
+ emitted. When neither is set (the default), every kind is
+ emitted. Whitespace around each entry is ignored and matching
+ is case-insensitive; an unrecognized kind name raises an error.
+ The kind vocabulary is
+ <literal>table</literal>,
+ <literal>index</literal>,
+ <literal>primary_key</literal>,
+ <literal>unique</literal>,
+ <literal>check</literal>,
+ <literal>foreign_key</literal>,
+ <literal>exclusion</literal>,
+ <literal>rule</literal>,
+ <literal>statistics</literal>,
+ <literal>trigger</literal>,
+ <literal>policy</literal>,
+ <literal>rls</literal> (the
+ <command>ENABLE</command>/<command>FORCE ROW LEVEL SECURITY</command>
+ toggles),
+ <literal>replica_identity</literal>, and
+ <literal>partition</literal> (the DDL for each direct
+ partition child of a partitioned-table parent). The
+ <literal>table</literal> kind groups the
+ <command>CREATE TABLE</command> statement together with the
+ related per-table <command>ALTER TABLE</command> passes:
+ <command>OWNER TO</command>, child-default
+ <command>SET DEFAULT</command>, and per-column
+ <command>SET (<replaceable>attoptions</replaceable>)</command>.
+ <literal>NOT NULL</literal> constraints are not part of the
+ vocabulary; they are always emitted as part of the table to
+ avoid producing schemas that silently accept
+ <literal>NULL</literal> 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
+ <literal>only => 'foreign_key'</literal>, and the
+ pub/sub-style clone that keeps a primary key but drops every
+ other constraint can be written as
+ <literal>except => 'unique,check,foreign_key,exclusion'</literal>.
+ </para>
+ <para>
+ When the table has
+ <literal>REPLICA IDENTITY USING INDEX</literal> and the
+ <literal>replica_identity</literal> kind is in the active
+ filter, the kind that emits the referenced index
+ (<literal>primary_key</literal>, <literal>unique</literal>,
+ <literal>exclusion</literal>, or <literal>index</literal>) must
+ also be in the filter. Otherwise the emitted
+ <command>ALTER TABLE ... REPLICA IDENTITY USING INDEX</command>
+ would reference an index the same DDL never produced, and the
+ function reports an error before emitting any statements.
+ </para>
+ <para>
+ The <literal>schema_qualified</literal> option (boolean, default
+ <literal>true</literal>) controls whether the target table's own
+ schema is included in the generated DDL. When set to
+ <literal>false</literal>, the table name is emitted unqualified
+ in the <command>CREATE TABLE</command> and every subsequent
+ <command>ALTER TABLE</command>, <command>CREATE INDEX</command>,
+ <command>CREATE RULE</command>, and
+ <command>CREATE STATISTICS</command> 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 <varname>search_path</varname> to recreate the table
+ in another schema. Cross-schema references (for example a
+ foreign key target in a different schema) remain qualified for
+ correctness.
+ </para>
+ <para>
+ All three forms of <command>CREATE TABLE</command> are supported:
+ the ordinary column-list form, the typed-table form
+ (<literal>OF <replaceable>type_name</replaceable></literal>, with
+ per-column <literal>WITH OPTIONS</literal> overrides for local
+ defaults, <literal>NOT NULL</literal>, and <literal>CHECK</literal>
+ constraints), and the <literal>PARTITION OF</literal> form.
+ <literal>TEMPORARY</literal> and <literal>UNLOGGED</literal>
+ persistence modes are emitted from
+ <structfield>relpersistence</structfield>. For temporary tables
+ registered in the current session, the
+ <literal>ON COMMIT DELETE ROWS</literal> and
+ <literal>ON COMMIT DROP</literal> clauses are emitted; the default
+ <literal>ON COMMIT PRESERVE ROWS</literal> is omitted.
+ </para></entry>
+ </row>
</tbody>
</tgroup>
</table>
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..b2e15384b2a 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -21,17 +21,31 @@
#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 "nodes/nodes.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/datetime.h"
@@ -45,6 +59,82 @@
#include "utils/timestamp.h"
#include "utils/varlena.h"
+/* Option value types for DDL option parsing */
+typedef enum
+{
+ DDL_OPT_BOOL,
+ DDL_OPT_TEXT,
+ DDL_OPT_INT,
+} DdlOptType;
+
+/*
+ * A single DDL option descriptor: caller fills in name and type,
+ * parse_ddl_options fills in isset + the appropriate value field.
+ */
+typedef struct DdlOption
+{
+ const char *name; /* option name (case-insensitive match) */
+ DdlOptType type; /* expected value type */
+ bool isset; /* true if caller supplied this option */
+ /* fields for specific option types */
+ union
+ {
+ bool boolval; /* filled in for DDL_OPT_BOOL */
+ char *textval; /* filled in for DDL_OPT_TEXT (palloc'd) */
+ int intval; /* filled in for DDL_OPT_INT */
+ };
+} DdlOption;
+
+
+/*
+ * 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 void parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start,
+ DdlOption *opts, int nopts);
+static Bitmapset *parse_kind_list(const char *optname, const char *list);
static void append_ddl_option(StringInfo buf, bool pretty, int indent,
const char *fmt, ...)
pg_attribute_printf(4, 5);
@@ -57,6 +147,341 @@ 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 "<table>_<col>_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_ddl_options
+ * Parse variadic name/value option pairs
+ *
+ * Options are passed as alternating key/value text pairs. The caller
+ * provides an array of DdlOption descriptors specifying the accepted
+ * option names and their types; this function matches each supplied
+ * pair against the array, validates the value, and fills in the
+ * result fields.
+ */
+static void
+parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start,
+ DdlOption *opts, int nopts)
+{
+ Datum *args;
+ bool *nulls;
+ Oid *types;
+ int nargs;
+
+ /* Clear all output fields */
+ for (int i = 0; i < nopts; i++)
+ {
+ opts[i].isset = false;
+ switch (opts[i].type)
+ {
+ case DDL_OPT_BOOL:
+ opts[i].boolval = false;
+ break;
+ case DDL_OPT_TEXT:
+ opts[i].textval = NULL;
+ break;
+ case DDL_OPT_INT:
+ opts[i].intval = 0;
+ break;
+ }
+ }
+
+ nargs = extract_variadic_args(fcinfo, variadic_start, true,
+ &args, &types, &nulls);
+
+ if (nargs <= 0)
+ return;
+
+ /* Handle DEFAULT NULL case */
+ if (nargs == 1 && nulls[0])
+ return;
+
+ if (nargs % 2 != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("variadic arguments must be name/value pairs"),
+ errhint("Provide an even number of variadic arguments that can be divided into pairs.")));
+
+ /*
+ * For each option name/value pair, find corresponding positional option
+ * for the option name, and assign the option value.
+ */
+ for (int i = 0; i < nargs; i += 2)
+ {
+ char *name;
+ char *valstr;
+ DdlOption *opt = NULL;
+
+ if (nulls[i])
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("option name at variadic position %d is null", i + 1)));
+
+ name = TextDatumGetCString(args[i]);
+
+ if (nulls[i + 1])
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("value for option \"%s\" must not be null", name)));
+
+ /* Find matching option descriptor */
+ for (int j = 0; j < nopts; j++)
+ {
+ if (pg_strcasecmp(name, opts[j].name) == 0)
+ {
+ opt = &opts[j];
+ break;
+ }
+ }
+
+ if (opt == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("unrecognized option: \"%s\"", name)));
+
+ if (opt->isset)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("option \"%s\" is specified more than once",
+ name)));
+
+ valstr = TextDatumGetCString(args[i + 1]);
+
+ switch (opt->type)
+ {
+ case DDL_OPT_BOOL:
+ if (!parse_bool(valstr, &opt->boolval))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid value for boolean option \"%s\": %s",
+ name, valstr)));
+ break;
+
+ case DDL_OPT_TEXT:
+ opt->textval = valstr;
+ valstr = NULL; /* don't pfree below */
+ break;
+
+ case DDL_OPT_INT:
+ {
+ char *endp;
+ long val;
+
+ errno = 0;
+ val = strtol(valstr, &endp, 10);
+ if (*endp != '\0' || errno == ERANGE ||
+ val < PG_INT32_MIN || val > PG_INT32_MAX)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid value for integer option \"%s\": %s",
+ name, valstr)));
+ opt->intval = (int) val;
+ }
+ break;
+ }
+
+ opt->isset = true;
+
+ if (valstr)
+ pfree(valstr);
+ pfree(name);
+ }
+}
+
+/*
+ * parse_kind_list
+ * Parse a comma-separated list of object-class kind names into a
+ * Bitmapset of TableDdlKind values.
+ *
+ * Whitespace around each entry is trimmed. Empty entries (e.g. "a,,b" or a
+ * trailing comma) are rejected. Unknown kind names raise an error citing
+ * the supplied option name so the message points at the offending option.
+ * Returns NULL only when the list is structurally empty; callers should
+ * treat an all-whitespace list as an error rather than equivalent to NULL.
+ */
+static Bitmapset *
+parse_kind_list(const char *optname, const char *list)
+{
+ Bitmapset *result = NULL;
+ const char *p = list;
+
+ while (*p)
+ {
+ const char *start;
+ const char *end;
+ char *token;
+ bool found = false;
+
+ /* Skip leading whitespace. */
+ while (*p == ' ' || *p == '\t')
+ p++;
+
+ if (*p == ',' || *p == '\0')
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("empty entry in option \"%s\"", optname)));
+
+ start = p;
+ while (*p && *p != ',' && *p != ' ' && *p != '\t')
+ p++;
+ end = p;
+
+ /* Skip trailing whitespace. */
+ while (*p == ' ' || *p == '\t')
+ p++;
+
+ if (*p != '\0' && *p != ',')
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid character in option \"%s\"", optname)));
+
+ if (*p == ',')
+ {
+ p++;
+ /*
+ * A comma must be followed by another entry. Without this
+ * check a trailing comma like "index," would be silently
+ * accepted as "index", while ",index" and "index,,rule" are
+ * already rejected as "empty entry" - this guard makes the
+ * three forms consistent.
+ */
+ if (*p == '\0')
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("empty entry in option \"%s\"", optname)));
+ }
+
+ token = pnstrdup(start, end - start);
+
+ for (size_t i = 0; i < lengthof(table_ddl_kind_names); i++)
+ {
+ if (pg_strcasecmp(token, table_ddl_kind_names[i].name) == 0)
+ {
+ result = bms_add_member(result,
+ (int) table_ddl_kind_names[i].kind);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("unrecognized kind \"%s\" in option \"%s\"",
+ token, optname)));
+
+ pfree(token);
+ }
+
+ if (result == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("option \"%s\" must specify at least one kind",
+ optname)));
+
+ 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 +1400,1730 @@ 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 "<tablename>_<columnname>_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 <pg_get_constraintdef>", 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
+ * "<tablename>_<columnname>_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);
+
+ /*
+ * When schema_qualified is false, narrow the active search_path to the
+ * target table's own namespace for the duration of DDL emission. The
+ * deparse code in ruleutils (and the regclass/regproc output functions we
+ * transitively call) decides whether to schema-qualify a name based on
+ * whether the referenced object is reachable through search_path, so this
+ * makes same-schema references in DEFAULT and CHECK expressions, regclass
+ * literals, FK targets, indexes, constraints, rules, and statistics come
+ * out unqualified automatically. Cross-schema references stay qualified,
+ * which is the correctness requirement.
+ * 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);
+ }
+ }
+
+ /*
+ * 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};
+ DdlOption opts[] = {
+ {"pretty", DDL_OPT_BOOL},
+ {"owner", DDL_OPT_BOOL},
+ {"tablespace", DDL_OPT_BOOL},
+ {"schema_qualified", DDL_OPT_BOOL},
+ {"only", DDL_OPT_TEXT},
+ {"except", DDL_OPT_TEXT},
+ };
+ enum
+ {
+ OPT_PRETTY,
+ OPT_OWNER,
+ OPT_TABLESPACE,
+ OPT_SCHEMA_QUALIFIED,
+ OPT_ONLY,
+ OPT_EXCEPT,
+ };
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ parse_ddl_options(fcinfo, 1, opts, lengthof(opts));
+
+ if (opts[OPT_ONLY].isset && opts[OPT_EXCEPT].isset)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"only\" and \"except\" options are mutually exclusive")));
+
+ /*
+ * Initialize the option fields of the context to their defaults
+ * first, then override only those that the caller explicitly
+ * specified. Keeping the default values literal here (rather than
+ * folded into each opts[] lookup) makes the defaults obvious in one
+ * place and avoids the ternary-soup that made the previous
+ * positional-bool call site error-prone.
+ */
+ ctx.relid = PG_GETARG_OID(0);
+ ctx.pretty = false;
+ ctx.no_owner = false;
+ ctx.no_tablespace = false;
+ ctx.schema_qualified = true;
+ ctx.only_kinds = NULL;
+ ctx.except_kinds = NULL;
+
+ if (opts[OPT_PRETTY].isset)
+ ctx.pretty = opts[OPT_PRETTY].boolval;
+ if (opts[OPT_OWNER].isset)
+ ctx.no_owner = !opts[OPT_OWNER].boolval;
+ if (opts[OPT_TABLESPACE].isset)
+ ctx.no_tablespace = !opts[OPT_TABLESPACE].boolval;
+ if (opts[OPT_SCHEMA_QUALIFIED].isset)
+ ctx.schema_qualified = opts[OPT_SCHEMA_QUALIFIED].boolval;
+ if (opts[OPT_ONLY].isset)
+ ctx.only_kinds = parse_kind_list("only",
+ opts[OPT_ONLY].textval);
+ if (opts[OPT_EXCEPT].isset)
+ ctx.except_kinds = parse_kind_list("except",
+ opts[OPT_EXCEPT].textval);
+
+ 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..9537669f64a 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
@@ -2544,6 +2596,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 402d869710b..b01454d06df 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8614,6 +8614,13 @@
prorettype => 'text', 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', provariadic => 'text',
+ proisstrict => 'f', proretset => 't', provolatile => 's', proparallel => 'r',
+ pronargdefaults => '1', prorettype => 'text',
+ proargtypes => 'regclass _text', proallargtypes => '{regclass,_text}',
+ proargmodes => '{i,v}', proargdefaults => '{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..4ced649e9a3
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,1265 @@
+--
+-- 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 refd(id) DEFERRABLE INITIALLY DEFERRED;
+(3 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 idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON 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 rt DO INSERT INTO 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 stx_ndv (ndistinct) ON a, b FROM 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 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)
+
+-- VARIADIC ARRAY[...] call form: option pairs can be passed as a single
+-- text[] tagged with VARIADIC, in addition to the positional spelling
+-- exercised above. This requires proargtypes to declare the variadic
+-- argument as the array type (_text).
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ VARIADIC ARRAY['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)
+
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ VARIADIC ARRAY['owner', 'false',
+ 'pretty', 'true']::text[]);
+ 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)
+
+-- VARIADIC NULL::text[] is equivalent to omitting the variadic arguments;
+-- check the row count rather than the rows themselves, because without
+-- 'owner', 'false' the output contains an ALTER TABLE ... OWNER TO line
+-- that varies across test runners.
+SELECT count(*) > 0 AS ok
+ FROM pg_get_table_ddl('basic'::regclass, VARIADIC NULL::text[]);
+ ok
+----
+ t
+(1 row)
+
+-- only / except gating: each emits either only the listed kinds
+-- (only) or every kind except the listed ones (except). 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=index hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'except', '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=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',
+ '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=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=foreign_key
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'except', '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=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', 'foreign_key');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED;
+(1 row)
+
+-- A table with no foreign keys produces no rows under
+-- only=foreign_key.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, 'owner', 'false',
+ 'only', 'foreign_key');
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- only=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', 'table');
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+(1 row)
+
+-- only=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', '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=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', '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 and except are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'only', 'foreign_key',
+ 'except', 'foreign_key');
+ERROR: "only" and "except" options 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',
+ 'unique,check,foreign_key,exclusion');
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- except=rule hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'except', 'rule');
+ pg_get_table_ddl
+----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+(1 row)
+
+-- except=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'except', 'statistics');
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+(1 row)
+
+-- except=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'except', 'rls');
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+(1 row)
+
+-- except=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'except', 'replica_identity');
+ pg_get_table_ddl
+--------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+(1 row)
+
+-- Partition children are emitted by default; except=partition
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'except', 'partition');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range (id integer, k integer) PARTITION BY RANGE (id);
+(1 row)
+
+-- Unknown kind name is rejected at option-parse time.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'except', 'no_such_kind');
+ERROR: unrecognized kind "no_such_kind" in option "except"
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'except', ' 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 "<table>_<col>_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. The
+-- in-string state of the prefix stripper preserves them verbatim.
+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 "<schema>.weird" inside that
+-- schema). The in-ident state of the prefix stripper preserves them
+-- verbatim.
+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
+-- 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)
+
+-- Unrecognized option.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'bogus', 'true');
+ERROR: unrecognized option: "bogus"
+-- Odd number of variadic args.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'pretty');
+ERROR: variadic arguments must be name/value pairs
+HINT: Provide an even number of variadic arguments that can be divided into pairs.
+-- 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=primary_key without except=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', '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', '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', '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', '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', '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', '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=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', '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', '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 / except coverage. For each kind in the vocabulary,
+-- verify that only=K emits just that kind's output and that except=K
+-- omits just that kind's output. Kinds with no fixture above (trigger,
+-- policy) are still recognized but currently emit nothing; only=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=K: emit only kind K's statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'only', 'index');
+ pg_get_table_ddl
+----------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'only', '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', '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', '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', 'rule');
+ pg_get_table_ddl
+-------------------------------------------------
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO rt DO INSERT INTO rt_log (id)+
+ VALUES (new.id);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'only', 'statistics');
+ pg_get_table_ddl
+---------------------------------------------------------
+ CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'only', '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', 'replica_identity');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(1 row)
+
+-- trigger / policy are scaffolded but not yet emitting; only=K is a
+-- valid filter that produces zero rows. When the standalone
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land, these queries
+-- start returning rows without an API change.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'only', 'trigger');
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'only', 'policy');
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- except=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', 'primary_key');
+ pg_get_table_ddl
+----------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'except', '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 refd(id) DEFERRABLE INITIALLY DEFERRED;
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'except', '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 refd(id) DEFERRABLE INITIALLY DEFERRED;
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, 'owner', 'false',
+ 'except', 'exclusion');
+ pg_get_table_ddl
+-------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.excl_fix (rng int4range);
+(1 row)
+
+-- except=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', 'table');
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON 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', 'table,index');
+ pg_get_table_ddl
+----------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'only', 'primary_key,foreign_key');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES 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', 'index,index');
+ pg_get_table_ddl
+----------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+DROP TABLE excl_fix;
+-- only=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', 'rule');
+ rule_rows
+-----------
+ 0
+(1 row)
+
+SELECT count(*) AS stats_rows
+ FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'only', 'statistics');
+ stats_rows
+------------
+ 0
+(1 row)
+
+SELECT count(*) AS rls_rows
+ FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'only', 'rls');
+ rls_rows
+----------
+ 0
+(1 row)
+
+SELECT count(*) AS ri_rows
+ FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'only', 'replica_identity');
+ ri_rows
+---------
+ 0
+(1 row)
+
+SELECT count(*) AS excl_rows
+ FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'only', 'exclusion');
+ excl_rows
+-----------
+ 0
+(1 row)
+
+-- Input-validation edge cases for the comma-separated kind list. All
+-- of these raise an error at the variadic-parse step; the function
+-- never reaches relation open.
+-- Empty list:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'only', '');
+ERROR: option "only" must specify at least one kind
+-- Whitespace-only list:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'only', ' ');
+ERROR: empty entry in option "only"
+-- Trailing comma:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'only', 'index,');
+ERROR: empty entry in option "only"
+-- Leading comma:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'only', ',index');
+ERROR: empty entry in option "only"
+-- Consecutive commas:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'only', 'index,,rule');
+ERROR: empty entry in option "only"
+-- Token containing inner whitespace:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'only', 'foreign key');
+ERROR: invalid character in option "only"
+-- NULL value for a non-NULL option:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'only', NULL);
+ERROR: value for option "only" must not be null
+-- Option specified twice as separate name/value pairs:
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ 'only', 'index',
+ 'only', 'rule');
+ERROR: option "only" is specified more than once
+-- Same kind specified through both options is still mutex-rejected.
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ 'only', 'index',
+ 'except', 'rule');
+ERROR: "only" and "except" options 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'), ('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', '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
+ EXECUTE stmt;
+ END LOOP;
+ END LOOP;
+END $$;
+NOTICE: drop cascades to 27 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 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
+ERROR: relation "parted_range_1" already exists
+CONTEXT: SQL statement "CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);"
+PL/pgSQL function inline_code_block line 22 at EXECUTE
+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 27 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 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
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..95a4dd0b568
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,741 @@
+--
+-- 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');
+
+-- 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');
+
+-- VARIADIC ARRAY[...] call form: option pairs can be passed as a single
+-- text[] tagged with VARIADIC, in addition to the positional spelling
+-- exercised above. This requires proargtypes to declare the variadic
+-- argument as the array type (_text).
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ VARIADIC ARRAY['owner', 'false']);
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ VARIADIC ARRAY['owner', 'false',
+ 'pretty', 'true']::text[]);
+
+-- VARIADIC NULL::text[] is equivalent to omitting the variadic arguments;
+-- check the row count rather than the rows themselves, because without
+-- 'owner', 'false' the output contains an ALTER TABLE ... OWNER TO line
+-- that varies across test runners.
+SELECT count(*) > 0 AS ok
+ FROM pg_get_table_ddl('basic'::regclass, VARIADIC NULL::text[]);
+
+-- only / except gating: each emits either only the listed kinds
+-- (only) or every kind except the listed ones (except). 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=index hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'except', 'index');
+
+-- except=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',
+ 'primary_key,unique,check,foreign_key,exclusion');
+
+-- except=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=foreign_key
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'except', 'foreign_key');
+
+-- only=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', 'foreign_key');
+
+-- A table with no foreign keys produces no rows under
+-- only=foreign_key.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, 'owner', 'false',
+ 'only', 'foreign_key');
+
+-- only=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', 'table');
+
+-- only=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', 'check');
+DROP TABLE chk_only;
+
+-- only=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', 'partition');
+DROP TABLE p_only;
+
+-- only and except are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'only', 'foreign_key',
+ 'except', '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',
+ 'unique,check,foreign_key,exclusion');
+
+-- except=rule hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'except', 'rule');
+
+-- except=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'except', 'statistics');
+
+-- except=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'except', 'rls');
+
+-- except=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'except', 'replica_identity');
+
+-- Partition children are emitted by default; except=partition
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'except', 'partition');
+
+-- Unknown kind name is rejected at option-parse time.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'except', 'no_such_kind');
+
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'except', ' 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 "<table>_<col>_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. The
+-- in-string state of the prefix stripper preserves them verbatim.
+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 "<schema>.weird" inside that
+-- schema). The in-ident state of the prefix stripper preserves them
+-- verbatim.
+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;
+
+-- 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);
+
+-- Unrecognized option.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'bogus', 'true');
+
+-- Odd number of variadic args.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'pretty');
+
+-- 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=primary_key without except=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', 'primary_key');
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, 'owner', 'false',
+ 'except', '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', 'unique');
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, 'owner', 'false',
+ 'except', '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', 'index');
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, 'owner', 'false',
+ 'except', 'index,replica_identity');
+DROP TABLE ri_idx_excl;
+
+-- Include-side analog: only=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', 'replica_identity');
+-- Including both kinds succeeds.
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, 'owner', 'false',
+ 'only', 'unique,replica_identity');
+DROP TABLE ri_only;
+
+-- Per-kind only / except coverage. For each kind in the vocabulary,
+-- verify that only=K emits just that kind's output and that except=K
+-- omits just that kind's output. Kinds with no fixture above (trigger,
+-- policy) are still recognized but currently emit nothing; only=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=K: emit only kind K's statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'only', 'index');
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'only', 'primary_key');
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'only', 'unique');
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, 'owner', 'false',
+ 'only', 'exclusion');
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'only', 'rule');
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'only', 'statistics');
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'only', 'rls');
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'only', 'replica_identity');
+-- trigger / policy are scaffolded but not yet emitting; only=K is a
+-- valid filter that produces zero rows. When the standalone
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land, these queries
+-- start returning rows without an API change.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'only', 'trigger');
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'only', 'policy');
+
+-- except=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', 'primary_key');
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'except', 'unique');
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'except', 'check');
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, 'owner', 'false',
+ 'except', 'exclusion');
+-- except=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', 'table');
+
+-- Multi-kind compositions.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'only', 'table,index');
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'only', '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', 'index,index');
+
+DROP TABLE excl_fix;
+
+-- only=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', 'rule');
+SELECT count(*) AS stats_rows
+ FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'only', 'statistics');
+SELECT count(*) AS rls_rows
+ FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'only', 'rls');
+SELECT count(*) AS ri_rows
+ FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'only', 'replica_identity');
+SELECT count(*) AS excl_rows
+ FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'only', 'exclusion');
+
+-- Input-validation edge cases for the comma-separated kind list. All
+-- of these raise an error at the variadic-parse step; the function
+-- never reaches relation open.
+-- Empty list:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'only', '');
+-- Whitespace-only list:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'only', ' ');
+-- Trailing comma:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'only', 'index,');
+-- Leading comma:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'only', ',index');
+-- Consecutive commas:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'only', 'index,,rule');
+-- Token containing inner whitespace:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'only', 'foreign key');
+-- NULL value for a non-NULL option:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'only', NULL);
+-- Option specified twice as separate name/value pairs:
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ 'only', 'index',
+ 'only', 'rule');
+-- Same kind specified through both options is still mutex-rejected.
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ 'only', 'index',
+ 'except', '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'), ('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', '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
+ EXECUTE stmt;
+ 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 c5db6ca6705..3dc93f5468c 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
@@ -3150,6 +3151,7 @@ TableFuncScan
TableFuncScanState
TableFuncType
TableInfo
+TableDdlContext
TableLikeClause
TableSampleClause
TableScanDesc
--
2.51.0