public inbox for [email protected]
help / color / mirror / Atom feed[PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
54+ messages / 6 participants
[nested] [flat]
* [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-03 12:58 Akshay Joshi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-06-03 12:58 UTC (permalink / raw)
To: pgsql-hackers
Hackers,
Attached is a patch adding pg_get_table_ddl(regclass, VARIADIC text[]),
extending the existing pg_get_database/role/tablespace_ddl family.
relations. It returns the CREATE TABLE statement plus the follow-up ALTER
TABLE, CREATE INDEX, CREATE RULE, and CREATE STATISTICS statements needed
for a full reconstruction, one per row.
*Coverage*
--------
*Per-column:* typmod, COLLATE, STORAGE, COMPRESSION (pglz/lz4), GENERATED
STORED/VIRTUAL, IDENTITY ALWAYS/BY DEFAULT (with non-default sequence
options), DEFAULT, NOT NULL, attoptions.
*Table-level:* UNLOGGED, INHERITS, PARTITION BY (RANGE/LIST/HASH),
PARTITION OF ... FOR VALUES, USING access method, WITH (reloptions),
TABLESPACE, inline CHECK.
*Sub-objects:* Indexes, Constraints (PK/UNIQUE/FK/EXCLUDE/named NOT NULL),
Rules, extended statistics, REPLICA IDENTITY, ENABLE/FORCE RLS, and
child-local DEFAULT overrides.
*Triggers and policies are TODOs pending pg_get_trigger_ddl (Phil's
re-roll) and pg_get_policy_ddl (Waiting for review/commit)*.
Options (pretty, owner, tablespace, and a family of *includes_** toggles
for each sub-object class) let callers fine-tune the output. Every clause
is omitted when its value equals what the system would reapply on
round-trip same default-omission convention as the existing _ddl functions.
I deliberately did *not* add pg_get_index_ddl / _constraint_ddl / _rule_ddl
/ _stat_ddl wrappers. The existing C helpers in ruleutils.c
(pg_get_indexdef_string, pg_get_constraintdef_command, etc.) already emit
reproducible statements, so pg_get_table_ddl_internal calls them directly.
Happy to revisit if reviewers prefer separate SQL-level surfaces.
*Testing: *
pg_regress test at src/test/regress/sql/pg_get_table_ddl.sql covering the
matrix above plus error paths. I kept it as pg_regress rather than TAP
(where the sibling _ddl tests live) since exact-.out diffs are the more
rigorous check for a deparse function, and the pg_regress restriction that
drove the TAP choice for CREATE DATABASE/TABLESPACE doesn't apply to
tables. A manual round-trip across IDENTITY/GENERATED/
STORAGE/COMPRESSION/PK produces an empty EXCEPT diff against the original.
*Out of scope:* COMMENT ON, GRANT/REVOKE (matching the existing _ddl
family), and typed tables (CREATE TABLE name OF type).
Feedback is welcome, particularly on the option naming and the decision to
reuse the C helpers directly rather than add new SQL wrappers.
-----
Regards,
Akshay Joshi
Principal Engineer | Engineering Manager | pgAdmin Hacker
EDB (EnterpriseDB)
Attachments:
[application/octet-stream] v1-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (86.2K, ../../CANxoLDfjQnhM=E6JSyYo9s9OdjqoN8s_3wE5yL=kaDu_X8j-dA@mail.gmail.com/3-v1-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From 8c495d04657841352ce982b28ca9203b2879723e Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v1] 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_string, constraints
(PRIMARY KEY, UNIQUE, FOREIGN KEY, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_command, rules via pg_get_ruledef, extended
statistics via pg_get_statisticsobjdef_string, REPLICA IDENTITY
NOTHING/FULL/USING INDEX, ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY,
and child-local DEFAULT overrides on inheritance/partition children.
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.
A regression test under src/test/regress covers ordinary tables,
identity (default and custom sequence options), generated columns,
STORAGE/COMPRESSION, constraints (CHECK/UNIQUE/FK with deferrable),
functional and partial indexes, inheritance and partitioning,
partition children with FOR VALUES FROM/TO, WITH modulus/remainder, and
DEFAULT, rules, extended statistics, RLS toggles, REPLICA IDENTITY,
UNLOGGED with reloptions, per-column attoptions, child DEFAULT
overrides, pretty mode, owner=false, and the error paths for views,
sequences, NULL, unknown options, and odd-variadic argument counts.
Author: Akshay Joshi <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 61 +
src/backend/commands/tablecmds.c | 27 +
src/backend/utils/adt/ddlutils.c | 1325 +++++++++++++++++
src/include/catalog/pg_proc.dat | 7 +
src/include/commands/tablecmds.h | 1 +
.../regress/expected/pg_get_table_ddl.out | 498 +++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 278 ++++
8 files changed, 2198 insertions(+), 1 deletion(-)
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 00f64f50ceb..48e987208d5 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3961,6 +3961,67 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
<literal>TABLESPACE</literal>.
</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 a family of
+ <literal>includes_<replaceable>category</replaceable></literal>
+ booleans that gate emission of optional sub-objects:
+ <literal>includes_indexes</literal>,
+ <literal>includes_constraints</literal>,
+ <literal>includes_rules</literal>,
+ <literal>includes_statistics</literal>,
+ <literal>includes_triggers</literal>,
+ <literal>includes_policies</literal>,
+ <literal>includes_rls</literal> (the
+ <command>ENABLE</command>/<command>FORCE ROW LEVEL SECURITY</command>
+ toggles), and
+ <literal>includes_replica_identity</literal>. Each of these
+ defaults to <literal>true</literal>. The
+ <literal>includes_partition</literal> option, which defaults to
+ <literal>false</literal>, controls whether the DDL for partition
+ children is appended after a partitioned-table parent.
+ </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/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a1845240a98..b02dfb451f8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19583,6 +19583,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 f32fcd453ef..3761f65e153 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -21,12 +21,27 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.h"
#include "catalog/pg_auth_members.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_class.h"
+#include "catalog/dependency.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_am.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 "catalog/partition.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
+#include "rewrite/prs2lock.h"
+#include "nodes/nodes.h"
#include "commands/tablespace.h"
#include "common/relpath.h"
#include "funcapi.h"
@@ -86,6 +101,27 @@ static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner
static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool isnull);
static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
bool no_owner, bool no_tablespace);
+static List *pg_get_table_ddl_internal(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition);
+static void append_column_defs(StringInfo buf, Relation rel, bool pretty,
+ bool include_constraints);
+static void append_typed_column_overrides(StringInfo buf, Relation rel,
+ bool pretty, bool include_constraints);
+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_qualified_relname(Oid relid);
+static List *get_inheritance_parents(Oid relid);
/*
@@ -1185,3 +1221,1292 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
}
+
+/*
+ * get_inheritance_parents
+ * Return a List of parent OIDs for relid, ordered by inhseqno.
+ *
+ * find_inheritance_children() walks the opposite direction (parent->children),
+ * so we scan pg_inherits directly here using the (inhrelid, inhseqno) index,
+ * which yields rows in the order they need to appear in the INHERITS clause.
+ * Partition children also have a pg_inherits entry, so callers must skip the
+ * INHERITS clause when relispartition is true.
+ */
+static List *
+get_inheritance_parents(Oid relid)
+{
+ Relation inheritsRel;
+ SysScanDesc scan;
+ ScanKeyData key;
+ HeapTuple tup;
+ List *parents = NIL;
+
+ inheritsRel = table_open(InheritsRelationId, AccessShareLock);
+ ScanKeyInit(&key,
+ Anum_pg_inherits_inhrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(relid));
+ scan = systable_beginscan(inheritsRel, InheritsRelidSeqnoIndexId,
+ true, NULL, 1, &key);
+ while (HeapTupleIsValid(tup = systable_getnext(scan)))
+ {
+ Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(tup);
+
+ parents = lappend_oid(parents, inh->inhparent);
+ }
+ systable_endscan(scan);
+ table_close(inheritsRel, AccessShareLock);
+
+ return parents;
+}
+
+/*
+ * lookup_qualified_relname
+ * Return the schema-qualified, identifier-quoted name of a relation,
+ * or raise ERRCODE_UNDEFINED_OBJECT if the relation has disappeared.
+ *
+ * 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_qualified_relname(Oid relid)
+{
+ 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);
+ 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;
+}
+
+/*
+ * 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, ',');
+ *first = false;
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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_constraints)
+{
+ TupleDesc tupdesc = RelationGetDescr(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 emitted by the INHERITS clause
+ * (once implemented), not the column list, unless the child
+ * redeclared them locally (attislocal=true).
+ */
+ if (!att->attislocal)
+ continue;
+
+ if (!first)
+ appendStringInfoChar(buf, ',');
+ first = false;
+
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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))
+ {
+ const char *storage = NULL;
+
+ switch (att->attstorage)
+ {
+ case TYPSTORAGE_PLAIN:
+ storage = "PLAIN";
+ break;
+ case TYPSTORAGE_EXTERNAL:
+ storage = "EXTERNAL";
+ break;
+ case TYPSTORAGE_MAIN:
+ storage = "MAIN";
+ break;
+ case TYPSTORAGE_EXTENDED:
+ storage = "EXTENDED";
+ break;
+ }
+ if (storage)
+ appendStringInfo(buf, " STORAGE %s", storage);
+ }
+
+ /* COMPRESSION clause, only if explicitly set on the column. */
+ if (CompressionMethodIsValid(att->attcompression))
+ {
+ const char *cm = NULL;
+
+ switch (att->attcompression)
+ {
+ case TOAST_PGLZ_COMPRESSION:
+ cm = "pglz";
+ break;
+ case TOAST_LZ4_COMPRESSION:
+ cm = "lz4";
+ break;
+ }
+ if (cm)
+ appendStringInfo(buf, " COMPRESSION %s", cm);
+ }
+
+ /*
+ * 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_qualified_relname(seqid);
+
+ 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)
+ appendStringInfoString(buf, " NOT NULL");
+ }
+
+ /*
+ * 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_constraints)
+ 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_constraints)
+{
+ 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, ',');
+ first = false;
+ if (pretty)
+ appendStringInfoString(&inner, "\n ");
+ else
+ appendStringInfoChar(&inner, ' ');
+
+ appendStringInfo(&inner, "%s WITH OPTIONS",
+ quote_identifier(NameStr(att->attname)));
+ if (has_default)
+ appendStringInfo(&inner, " DEFAULT %s", defexpr);
+ if (has_notnull)
+ appendStringInfoString(&inner, " NOT NULL");
+
+ 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_constraints)
+ 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);
+}
+
+/*
+ * pg_get_table_ddl_internal
+ * Generate DDL statements to recreate a regular or partitioned table.
+ *
+ * The first list element is the CREATE TABLE statement. Subsequent
+ * elements are the ALTER TABLE / CREATE INDEX / CREATE RULE /
+ * CREATE STATISTICS statements needed to restore the table's full
+ * definition.
+ *
+ * Trigger and policy emission are scaffolded but not yet wired up — they
+ * are gated on standalone pg_get_trigger_ddl / pg_get_policy_ddl helpers
+ * landing.
+ */
+static List *
+pg_get_table_ddl_internal(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition)
+{
+ Relation rel;
+ StringInfoData buf;
+ List *statements = NIL;
+ char *qualname;
+ char relkind;
+ char relpersistence;
+ bool is_typed;
+ AclResult aclresult;
+
+ rel = table_open(relid, AccessShareLock);
+
+ relkind = rel->rd_rel->relkind;
+ relpersistence = rel->rd_rel->relpersistence;
+ is_typed = OidIsValid(rel->rd_rel->reloftype);
+
+ /*
+ * 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(relid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLE,
+ RelationGetRelationName(rel));
+
+ qualname = lookup_qualified_relname(relid);
+
+ initStringInfo(&buf);
+
+ /* pg_class tuple — for relpartbound and reloptions */
+ {
+ HeapTuple classtup;
+ Datum reloptDatum;
+ bool reloptIsnull;
+
+ classtup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(classtup))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ reloptDatum = SysCacheGetAttr(RELOID, classtup,
+ Anum_pg_class_reloptions, &reloptIsnull);
+
+ /*
+ * CREATE [TEMPORARY | UNLOGGED] TABLE qualname ...
+ *
+ * Persistence applies uniformly to all three CREATE TABLE forms
+ * (regular column list, OF type_name, and PARTITION OF). Only one
+ * of TEMPORARY / UNLOGGED can be set; the relpersistence catalog
+ * field is the single source of truth.
+ */
+ appendStringInfoString(&buf, "CREATE ");
+ if (relpersistence == RELPERSISTENCE_TEMP)
+ appendStringInfoString(&buf, "TEMPORARY ");
+ else if (relpersistence == RELPERSISTENCE_UNLOGGED)
+ appendStringInfoString(&buf, "UNLOGGED ");
+ appendStringInfo(&buf, "TABLE %s", qualname);
+
+ if (rel->rd_rel->relispartition)
+ {
+ /* PARTITION OF parent FOR VALUES ... */
+ Oid parentOid = get_partition_parent(relid, true);
+ char *parentQual = lookup_qualified_relname(parentOid);
+ 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(&buf, " PARTITION OF %s %s",
+ parentQual, forValues ? forValues : "DEFAULT");
+ if (forValues)
+ pfree(forValues);
+ if (boundStr)
+ pfree(boundStr);
+ pfree(parentQual);
+ pfree(parentRelname);
+
+ /*
+ * Column-level overrides redeclared on the child are emitted
+ * out-of-line: NOT NULL/CHECK come through the constraint loop
+ * (conislocal=true), and DEFAULT comes through the dedicated
+ * ALTER COLUMN SET DEFAULT pass below.
+ */
+ }
+ else if (is_typed)
+ {
+ /*
+ * Typed table: CREATE TABLE name OF type_name [(col WITH
+ * OPTIONS ...)]. The column list (when present) carries only
+ * locally-applied overrides — defaults, NOT NULL toggles, and
+ * locally-declared CHECK constraints — for columns whose type
+ * is otherwise dictated by reloftype.
+ */
+ char *typname = format_type_be_qualified(rel->rd_rel->reloftype);
+
+ appendStringInfo(&buf, " OF %s", typname);
+ pfree(typname);
+
+ append_typed_column_overrides(&buf, rel, pretty, include_constraints);
+ }
+ else
+ {
+ List *parents;
+ ListCell *lc;
+ bool first;
+
+ appendStringInfoString(&buf, " (");
+
+ append_column_defs(&buf, rel, pretty, include_constraints);
+
+ if (pretty)
+ appendStringInfoString(&buf, "\n)");
+ else
+ appendStringInfoChar(&buf, ')');
+
+ /* INHERITS (parent1, parent2, ...) — non-partition inheritance only */
+ parents = get_inheritance_parents(relid);
+ if (parents != NIL)
+ {
+ appendStringInfoString(&buf, " INHERITS (");
+ first = true;
+ foreach(lc, parents)
+ {
+ Oid poid = lfirst_oid(lc);
+ char *pname = lookup_qualified_relname(poid);
+
+ if (!first)
+ appendStringInfoString(&buf, ", ");
+ first = false;
+ appendStringInfoString(&buf, pname);
+ pfree(pname);
+ }
+ appendStringInfoChar(&buf, ')');
+ list_free(parents);
+ }
+ }
+
+ /* PARTITION BY — applies whenever this relation is a partitioned table */
+ if (relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Datum partkeyDatum;
+ char *partkey;
+
+ partkeyDatum = OidFunctionCall1(F_PG_GET_PARTKEYDEF,
+ ObjectIdGetDatum(relid));
+ partkey = TextDatumGetCString(partkeyDatum);
+ appendStringInfo(&buf, " PARTITION BY %s", partkey);
+ pfree(partkey);
+ }
+
+ /*
+ * USING method — emit only when the table access method differs
+ * from heap (the cluster default). Pluggable table AMs have been
+ * supported since PostgreSQL 12.
+ */
+ 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(&buf, " USING %s", quote_identifier(amname));
+ pfree(amname);
+ }
+ }
+
+ /* WITH (reloptions) */
+ if (!reloptIsnull)
+ {
+ appendStringInfoString(&buf, " WITH (");
+ get_reloptions(&buf, reloptDatum);
+ appendStringInfoChar(&buf, ')');
+ }
+
+ ReleaseSysCache(classtup);
+ }
+
+ /* TABLESPACE */
+ if (!no_tablespace && OidIsValid(rel->rd_rel->reltablespace))
+ {
+ char *tsname = get_tablespace_name(rel->rd_rel->reltablespace);
+
+ if (tsname != NULL)
+ {
+ appendStringInfo(&buf, " TABLESPACE %s", quote_identifier(tsname));
+ pfree(tsname);
+ }
+ }
+
+ /*
+ * ON COMMIT applies only to temporary tables. The action lives in a
+ * backend-local list (not the catalog) since it's a session-scoped
+ * property, so this is best-effort: we can only see entries registered
+ * in the current backend. PRESERVE ROWS is the default and is not
+ * emitted; NOOP indicates no entry was found.
+ */
+ if (relpersistence == RELPERSISTENCE_TEMP)
+ {
+ OnCommitAction oc = get_on_commit_action(relid);
+
+ if (oc == ONCOMMIT_DELETE_ROWS)
+ appendStringInfoString(&buf, " ON COMMIT DELETE ROWS");
+ else if (oc == ONCOMMIT_DROP)
+ appendStringInfoString(&buf, " ON COMMIT DROP");
+ }
+
+ appendStringInfoChar(&buf, ';');
+ statements = lappend(statements, pstrdup(buf.data));
+
+ /* OWNER */
+ if (!no_owner)
+ {
+ char *owner = GetUserNameFromId(rel->rd_rel->relowner, false);
+
+ resetStringInfo(&buf);
+ appendStringInfo(&buf, "ALTER TABLE %s OWNER TO %s;",
+ qualname, quote_identifier(owner));
+ statements = lappend(statements, pstrdup(buf.data));
+ pfree(owner);
+ }
+
+ /*
+ * Per-column DEFAULT overrides on inherited/partition children. The
+ * column list inside CREATE TABLE only emits locally-declared columns
+ * (attislocal=true), so any default set locally on a column that came
+ * from a parent table needs to be re-applied with ALTER COLUMN SET
+ * DEFAULT. Note: NOT NULL overrides come out through the constraint
+ * loop (PG 18 stores them as named pg_constraint entries with
+ * conislocal=true), and CHECK overrides do too.
+ */
+ {
+ TupleDesc tupdesc = RelationGetDescr(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;
+
+ defstr = find_attrdef_text(rel, att->attnum, &dpcontext);
+ if (defstr == NULL)
+ continue;
+
+ resetStringInfo(&buf);
+ appendStringInfo(&buf,
+ "ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s;",
+ qualname,
+ quote_identifier(NameStr(att->attname)),
+ defstr);
+ statements = lappend(statements, pstrdup(buf.data));
+ pfree(defstr);
+ }
+ }
+
+ /*
+ * Per-column attoptions — these can't be set inline in CREATE TABLE,
+ * so they come out as ALTER TABLE ... ALTER COLUMN col SET (...) after
+ * the table is created. Typical use: n_distinct overrides for the
+ * planner.
+ */
+ {
+ TupleDesc tupdesc = RelationGetDescr(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(relid),
+ Int16GetDatum(att->attnum));
+ if (!HeapTupleIsValid(attTup))
+ continue;
+
+ optDatum = SysCacheGetAttr(ATTNUM, attTup,
+ Anum_pg_attribute_attoptions, &optIsnull);
+ if (!optIsnull)
+ {
+ resetStringInfo(&buf);
+ appendStringInfo(&buf, "ALTER TABLE %s ALTER COLUMN %s SET (",
+ qualname,
+ quote_identifier(NameStr(att->attname)));
+ get_reloptions(&buf, optDatum);
+ appendStringInfoString(&buf, ");");
+ statements = lappend(statements, pstrdup(buf.data));
+ }
+ ReleaseSysCache(attTup);
+ }
+ }
+
+ /*
+ * Indexes — emit a CREATE INDEX for each non-constraint-backed index on
+ * the table. Indexes that back PK/UNIQUE/EXCLUDE constraints are
+ * emitted by the constraint loop below as part of the ALTER TABLE ...
+ * ADD CONSTRAINT statement, which creates the index implicitly.
+ */
+ if (include_indexes)
+ {
+ List *indexoids = RelationGetIndexList(rel);
+ ListCell *lc;
+
+ foreach(lc, indexoids)
+ {
+ Oid idxoid = lfirst_oid(lc);
+ char *idxdef;
+
+ if (OidIsValid(get_index_constraint(idxoid)))
+ continue;
+
+ idxdef = pg_get_indexdef_string(idxoid);
+ resetStringInfo(&buf);
+ appendStringInfo(&buf, "%s;", idxdef);
+ statements = lappend(statements, pstrdup(buf.data));
+ pfree(idxdef);
+ }
+ list_free(indexoids);
+ }
+
+ /*
+ * Constraints — emit an ALTER TABLE ... ADD CONSTRAINT for each
+ * locally-defined constraint on the table. Inherited constraints
+ * (conislocal=false) are produced by the parent's DDL and propagated
+ * automatically by INHERITS / PARTITION OF, so we skip them here.
+ */
+ if (include_constraints)
+ {
+ Relation conRel;
+ SysScanDesc conScan;
+ ScanKeyData conKey;
+ HeapTuple conTup;
+
+ conRel = table_open(ConstraintRelationId, AccessShareLock);
+ ScanKeyInit(&conKey,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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);
+ char *condef;
+
+ if (!con->conislocal)
+ continue;
+ /* CHECK constraints are emitted inline in the column list. */
+ if (con->contype == CONSTRAINT_CHECK)
+ continue;
+
+ condef = pg_get_constraintdef_command(con->oid);
+ resetStringInfo(&buf);
+ appendStringInfo(&buf, "%s;", condef);
+ statements = lappend(statements, pstrdup(buf.data));
+ pfree(condef);
+ }
+ systable_endscan(conScan);
+ table_close(conRel, AccessShareLock);
+ }
+
+ /*
+ * Rules — emit a CREATE RULE for each cached rewrite rule on the
+ * relation. Internal rules (such as the _RETURN rule on views) live on
+ * views/matviews and don't appear here because we already restricted
+ * relkind above.
+ */
+ if (include_rules && rel->rd_rules != NULL)
+ {
+ for (int i = 0; i < rel->rd_rules->numLocks; i++)
+ {
+ Oid ruleid = rel->rd_rules->rules[i]->ruleId;
+ Datum ruledef;
+ char *ruledef_str;
+
+ ruledef = OidFunctionCall1(F_PG_GET_RULEDEF_OID,
+ ObjectIdGetDatum(ruleid));
+ ruledef_str = TextDatumGetCString(ruledef);
+ resetStringInfo(&buf);
+ appendStringInfoString(&buf, ruledef_str);
+ statements = lappend(statements, pstrdup(buf.data));
+ pfree(ruledef_str);
+ }
+ }
+
+ /*
+ * Extended statistics — iterate pg_statistic_ext by stxrelid and emit
+ * pg_get_statisticsobjdef_string() for each.
+ */
+ if (include_statistics)
+ {
+ Relation statRel;
+ SysScanDesc statScan;
+ ScanKeyData statKey;
+ HeapTuple statTup;
+
+ statRel = table_open(StatisticExtRelationId, AccessShareLock);
+ ScanKeyInit(&statKey,
+ Anum_pg_statistic_ext_stxrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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_string(stat->oid);
+
+ resetStringInfo(&buf);
+ appendStringInfo(&buf, "%s;", statdef);
+ statements = lappend(statements, pstrdup(buf.data));
+ pfree(statdef);
+ }
+ systable_endscan(statScan);
+ table_close(statRel, AccessShareLock);
+ }
+
+ /*
+ * REPLICA IDENTITY — emit only when it differs from the default
+ * ('d' = use primary key). This affects logical replication
+ * behavior, so round-trip fidelity matters.
+ */
+ if (include_replica_identity &&
+ rel->rd_rel->relreplident != REPLICA_IDENTITY_DEFAULT)
+ {
+ resetStringInfo(&buf);
+ switch (rel->rd_rel->relreplident)
+ {
+ case REPLICA_IDENTITY_NOTHING:
+ appendStringInfo(&buf, "ALTER TABLE %s REPLICA IDENTITY NOTHING;",
+ qualname);
+ statements = lappend(statements, pstrdup(buf.data));
+ break;
+ case REPLICA_IDENTITY_FULL:
+ appendStringInfo(&buf, "ALTER TABLE %s REPLICA IDENTITY FULL;",
+ qualname);
+ statements = lappend(statements, pstrdup(buf.data));
+ break;
+ case REPLICA_IDENTITY_INDEX:
+ {
+ Oid replidx = RelationGetReplicaIndex(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(&buf,
+ "ALTER TABLE %s REPLICA IDENTITY USING INDEX %s;",
+ qualname,
+ quote_identifier(idxname));
+ statements = lappend(statements, pstrdup(buf.data));
+ pfree(idxname);
+ }
+ }
+ break;
+ }
+ }
+
+ /* ENABLE / FORCE ROW LEVEL SECURITY */
+ if (include_rls && rel->rd_rel->relrowsecurity)
+ {
+ resetStringInfo(&buf);
+ appendStringInfo(&buf, "ALTER TABLE %s ENABLE ROW LEVEL SECURITY;",
+ qualname);
+ statements = lappend(statements, pstrdup(buf.data));
+ }
+ if (include_rls && rel->rd_rel->relforcerowsecurity)
+ {
+ resetStringInfo(&buf);
+ appendStringInfo(&buf, "ALTER TABLE %s FORCE ROW LEVEL SECURITY;",
+ qualname);
+ statements = lappend(statements, pstrdup(buf.data));
+ }
+
+ /*
+ * Triggers — scaffolding only. The standalone pg_get_trigger_ddl()
+ * function (Phil's re-roll) is the intended emission path; once it
+ * lands the body of this loop becomes a single call into it. The
+ * scan structure, lock acquisition, and tgisinternal filter (which
+ * skips FK-backing and other system-generated triggers) are settled
+ * here so that change stays minimal.
+ */
+ if (include_triggers)
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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. */
+ (void) trg;
+ }
+ systable_endscan(trigScan);
+ table_close(trigRel, AccessShareLock);
+ }
+
+ /*
+ * Row-level security policies — scaffolding only. Once
+ * pg_get_policy_ddl() (already-submitted patch) lands, the body of
+ * this loop becomes a per-policy call into it. The ENABLE/FORCE
+ * ROW LEVEL SECURITY toggles above are the companion catalog flags
+ * and are already emitted independently of policies.
+ */
+ if (include_policies)
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(relid));
+ polScan = systable_beginscan(polRel, PolicyPolrelidPolnameIndexId,
+ true, NULL, 1, &polKey);
+ while (HeapTupleIsValid(polTup = systable_getnext(polScan)))
+ {
+ Form_pg_policy pol = (Form_pg_policy) GETSTRUCT(polTup);
+
+ /* TODO: append pg_get_policy_ddl(relid, polname) output here. */
+ (void) pol;
+ }
+ systable_endscan(polScan);
+ table_close(polRel, AccessShareLock);
+ }
+
+ /*
+ * Partition children — when include_partition is true and this relation
+ * is a partitioned-table parent, recursively emit the DDL for each
+ * direct partition child. Each child's own DDL handles further levels
+ * of sub-partitioning through the same recursion.
+ */
+ if (include_partition && relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *children = find_inheritance_children(relid, AccessShareLock);
+ ListCell *lc;
+
+ foreach(lc, children)
+ {
+ Oid childoid = lfirst_oid(lc);
+ List *childstmts;
+
+ childstmts = pg_get_table_ddl_internal(childoid, pretty,
+ no_owner, no_tablespace,
+ include_indexes,
+ include_constraints,
+ include_rules,
+ include_statistics,
+ include_triggers,
+ include_policies,
+ include_rls,
+ include_replica_identity,
+ include_partition);
+ statements = list_concat(statements, childstmts);
+ }
+ list_free(children);
+ }
+
+ table_close(rel, AccessShareLock);
+ pfree(buf.data);
+ pfree(qualname);
+
+ return 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;
+ Oid relid;
+ DdlOption opts[] = {
+ {"pretty", DDL_OPT_BOOL},
+ {"owner", DDL_OPT_BOOL},
+ {"tablespace", DDL_OPT_BOOL},
+ {"includes_indexes", DDL_OPT_BOOL},
+ {"includes_constraints", DDL_OPT_BOOL},
+ {"includes_rules", DDL_OPT_BOOL},
+ {"includes_statistics", DDL_OPT_BOOL},
+ {"includes_triggers", DDL_OPT_BOOL},
+ {"includes_policies", DDL_OPT_BOOL},
+ {"includes_rls", DDL_OPT_BOOL},
+ {"includes_replica_identity", DDL_OPT_BOOL},
+ {"includes_partition", DDL_OPT_BOOL},
+ };
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ relid = PG_GETARG_OID(0);
+ parse_ddl_options(fcinfo, 1, opts, lengthof(opts));
+
+ statements = pg_get_table_ddl_internal(relid,
+ opts[0].isset && opts[0].boolval,
+ opts[1].isset && !opts[1].boolval,
+ opts[2].isset && !opts[2].boolval,
+ !opts[3].isset || opts[3].boolval,
+ !opts[4].isset || opts[4].boolval,
+ !opts[5].isset || opts[5].boolval,
+ !opts[6].isset || opts[6].boolval,
+ !opts[7].isset || opts[7].boolval,
+ !opts[8].isset || opts[8].boolval,
+ !opts[9].isset || opts[9].boolval,
+ !opts[10].isset || opts[10].boolval,
+ opts[11].isset && opts[11].boolval);
+ 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/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..0d57081a1f3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,13 @@
proargtypes => 'regdatabase text', proallargtypes => '{regdatabase,text}',
proargmodes => '{i,v}', proargdefaults => '{NULL}',
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',
+ 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..2085027c9b1 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);
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..a5fc8add515
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,498 @@
+--
+-- 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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);
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_always_not_null NOT NULL id_always;
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_default_not_null NOT NULL id_default;
+(3 rows)
+
+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);
+ ALTER TABLE pgtbl_ddl_test.id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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)
+
+-- 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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(5 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 parent (RANGE and HASH).
+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)
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0);
+(1 row)
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(1 row)
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx ( a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx ADD CONSTRAINT ri_idx_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(4 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)));
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_b_not_null NOT NULL b;
+(3 rows)
+
+-- Temporary tables + ON COMMIT. Temp tables live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_default ( id integer);
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_delete ( id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_drop'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+ 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+(1 row)
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+ pg_get_table_ddl
+------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+(1 row)
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+ pg_get_table_ddl
+---------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+(1 row)
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range ( id integer, k integer) PARTITION BY RANGE (id);
+ 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;
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(4 rows)
+
+-- 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.
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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 25 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 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 rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ 'owner', 'false') WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 20 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 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 rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..c7f72bb824c
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,278 @@
+--
+-- 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');
+
+-- 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 parent (RANGE and HASH).
+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');
+
+-- 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 live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS 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');
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+
+-- 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');
+
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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;
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-08 11:30 Akshay Joshi <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 2 replies; 54+ messages in thread
From: Akshay Joshi @ 2026-06-08 11:30 UTC (permalink / raw)
To: pgsql-hackers
Hi Hackers,
Attached is the v2 patch, which fixes the Meson build failure, and the
rebased patch.
On Wed, Jun 3, 2026 at 6:28 PM Akshay Joshi <[email protected]>
wrote:
> Hackers,
>
> Attached is a patch adding pg_get_table_ddl(regclass, VARIADIC text[]),
> extending the existing pg_get_database/role/tablespace_ddl family.
> relations. It returns the CREATE TABLE statement plus the follow-up ALTER
> TABLE, CREATE INDEX, CREATE RULE, and CREATE STATISTICS statements needed
> for a full reconstruction, one per row.
>
> *Coverage*
> --------
> *Per-column:* typmod, COLLATE, STORAGE, COMPRESSION (pglz/lz4), GENERATED
> STORED/VIRTUAL, IDENTITY ALWAYS/BY DEFAULT (with non-default sequence
> options), DEFAULT, NOT NULL, attoptions.
>
> *Table-level:* UNLOGGED, INHERITS, PARTITION BY (RANGE/LIST/HASH),
> PARTITION OF ... FOR VALUES, USING access method, WITH (reloptions),
> TABLESPACE, inline CHECK.
>
> *Sub-objects:* Indexes, Constraints (PK/UNIQUE/FK/EXCLUDE/named NOT
> NULL), Rules, extended statistics, REPLICA IDENTITY, ENABLE/FORCE RLS, and
> child-local DEFAULT overrides.
> *Triggers and policies are TODOs pending pg_get_trigger_ddl (Phil's
> re-roll) and pg_get_policy_ddl (Waiting for review/commit)*.
>
> Options (pretty, owner, tablespace, and a family of *includes_** toggles
> for each sub-object class) let callers fine-tune the output. Every clause
> is omitted when its value equals what the system would reapply on
> round-trip same default-omission convention as the existing _ddl functions.
>
> I deliberately did *not* add pg_get_index_ddl / _constraint_ddl /
> _rule_ddl / _stat_ddl wrappers. The existing C helpers in ruleutils.c
> (pg_get_indexdef_string, pg_get_constraintdef_command, etc.) already emit
> reproducible statements, so pg_get_table_ddl_internal calls them directly.
> Happy to revisit if reviewers prefer separate SQL-level surfaces.
>
> *Testing: *
> pg_regress test at src/test/regress/sql/pg_get_table_ddl.sql covering the
> matrix above plus error paths. I kept it as pg_regress rather than TAP
> (where the sibling _ddl tests live) since exact-.out diffs are the more
> rigorous check for a deparse function, and the pg_regress restriction that
> drove the TAP choice for CREATE DATABASE/TABLESPACE doesn't apply to
> tables. A manual round-trip across IDENTITY/GENERATED/
> STORAGE/COMPRESSION/PK produces an empty EXCEPT diff against the original.
>
> *Out of scope:* COMMENT ON, GRANT/REVOKE (matching the existing _ddl
> family), and typed tables (CREATE TABLE name OF type).
>
> Feedback is welcome, particularly on the option naming and the decision to
> reuse the C helpers directly rather than add new SQL wrappers.
>
> -----
> Regards,
> Akshay Joshi
> Principal Engineer | Engineering Manager | pgAdmin Hacker
> EDB (EnterpriseDB)
>
>
Attachments:
[application/octet-stream] v2-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (86.3K, ../../CANxoLDcxmXNHNyPt5v+LaBJVJmyt=j95D7TDE9SepoM7y15t-w@mail.gmail.com/3-v2-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From 6fe8fc4f764cac718b30e0aeb89c90294ebcc3c1 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v2] 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_string, constraints
(PRIMARY KEY, UNIQUE, FOREIGN KEY, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_command, rules via pg_get_ruledef, extended
statistics via pg_get_statisticsobjdef_string, REPLICA IDENTITY
NOTHING/FULL/USING INDEX, ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY,
and child-local DEFAULT overrides on inheritance/partition children.
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.
A regression test under src/test/regress covers ordinary tables,
identity (default and custom sequence options), generated columns,
STORAGE/COMPRESSION, constraints (CHECK/UNIQUE/FK with deferrable),
functional and partial indexes, inheritance and partitioning,
partition children with FOR VALUES FROM/TO, WITH modulus/remainder, and
DEFAULT, rules, extended statistics, RLS toggles, REPLICA IDENTITY,
UNLOGGED with reloptions, per-column attoptions, child DEFAULT
overrides, pretty mode, owner=false, and the error paths for views,
sequences, NULL, unknown options, and odd-variadic argument counts.
Author: Akshay Joshi <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 61 +
src/backend/commands/tablecmds.c | 27 +
src/backend/utils/adt/ddlutils.c | 1325 +++++++++++++++++
src/include/catalog/pg_proc.dat | 7 +
src/include/commands/tablecmds.h | 1 +
.../regress/expected/pg_get_table_ddl.out | 498 +++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 278 ++++
8 files changed, 2198 insertions(+), 1 deletion(-)
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 00f64f50ceb..48e987208d5 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3961,6 +3961,67 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
<literal>TABLESPACE</literal>.
</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 a family of
+ <literal>includes_<replaceable>category</replaceable></literal>
+ booleans that gate emission of optional sub-objects:
+ <literal>includes_indexes</literal>,
+ <literal>includes_constraints</literal>,
+ <literal>includes_rules</literal>,
+ <literal>includes_statistics</literal>,
+ <literal>includes_triggers</literal>,
+ <literal>includes_policies</literal>,
+ <literal>includes_rls</literal> (the
+ <command>ENABLE</command>/<command>FORCE ROW LEVEL SECURITY</command>
+ toggles), and
+ <literal>includes_replica_identity</literal>. Each of these
+ defaults to <literal>true</literal>. The
+ <literal>includes_partition</literal> option, which defaults to
+ <literal>false</literal>, controls whether the DDL for partition
+ children is appended after a partitioned-table parent.
+ </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/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a1845240a98..b02dfb451f8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19583,6 +19583,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 f32fcd453ef..3761f65e153 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -21,12 +21,27 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.h"
#include "catalog/pg_auth_members.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_class.h"
+#include "catalog/dependency.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_am.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 "catalog/partition.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
+#include "rewrite/prs2lock.h"
+#include "nodes/nodes.h"
#include "commands/tablespace.h"
#include "common/relpath.h"
#include "funcapi.h"
@@ -86,6 +101,27 @@ static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner
static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool isnull);
static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
bool no_owner, bool no_tablespace);
+static List *pg_get_table_ddl_internal(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition);
+static void append_column_defs(StringInfo buf, Relation rel, bool pretty,
+ bool include_constraints);
+static void append_typed_column_overrides(StringInfo buf, Relation rel,
+ bool pretty, bool include_constraints);
+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_qualified_relname(Oid relid);
+static List *get_inheritance_parents(Oid relid);
/*
@@ -1185,3 +1221,1292 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
}
+
+/*
+ * get_inheritance_parents
+ * Return a List of parent OIDs for relid, ordered by inhseqno.
+ *
+ * find_inheritance_children() walks the opposite direction (parent->children),
+ * so we scan pg_inherits directly here using the (inhrelid, inhseqno) index,
+ * which yields rows in the order they need to appear in the INHERITS clause.
+ * Partition children also have a pg_inherits entry, so callers must skip the
+ * INHERITS clause when relispartition is true.
+ */
+static List *
+get_inheritance_parents(Oid relid)
+{
+ Relation inheritsRel;
+ SysScanDesc scan;
+ ScanKeyData key;
+ HeapTuple tup;
+ List *parents = NIL;
+
+ inheritsRel = table_open(InheritsRelationId, AccessShareLock);
+ ScanKeyInit(&key,
+ Anum_pg_inherits_inhrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(relid));
+ scan = systable_beginscan(inheritsRel, InheritsRelidSeqnoIndexId,
+ true, NULL, 1, &key);
+ while (HeapTupleIsValid(tup = systable_getnext(scan)))
+ {
+ Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(tup);
+
+ parents = lappend_oid(parents, inh->inhparent);
+ }
+ systable_endscan(scan);
+ table_close(inheritsRel, AccessShareLock);
+
+ return parents;
+}
+
+/*
+ * lookup_qualified_relname
+ * Return the schema-qualified, identifier-quoted name of a relation,
+ * or raise ERRCODE_UNDEFINED_OBJECT if the relation has disappeared.
+ *
+ * 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_qualified_relname(Oid relid)
+{
+ 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);
+ 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;
+}
+
+/*
+ * 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, ',');
+ *first = false;
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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_constraints)
+{
+ TupleDesc tupdesc = RelationGetDescr(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 emitted by the INHERITS clause
+ * (once implemented), not the column list, unless the child
+ * redeclared them locally (attislocal=true).
+ */
+ if (!att->attislocal)
+ continue;
+
+ if (!first)
+ appendStringInfoChar(buf, ',');
+ first = false;
+
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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))
+ {
+ const char *storage = NULL;
+
+ switch (att->attstorage)
+ {
+ case TYPSTORAGE_PLAIN:
+ storage = "PLAIN";
+ break;
+ case TYPSTORAGE_EXTERNAL:
+ storage = "EXTERNAL";
+ break;
+ case TYPSTORAGE_MAIN:
+ storage = "MAIN";
+ break;
+ case TYPSTORAGE_EXTENDED:
+ storage = "EXTENDED";
+ break;
+ }
+ if (storage)
+ appendStringInfo(buf, " STORAGE %s", storage);
+ }
+
+ /* COMPRESSION clause, only if explicitly set on the column. */
+ if (CompressionMethodIsValid(att->attcompression))
+ {
+ const char *cm = NULL;
+
+ switch (att->attcompression)
+ {
+ case TOAST_PGLZ_COMPRESSION:
+ cm = "pglz";
+ break;
+ case TOAST_LZ4_COMPRESSION:
+ cm = "lz4";
+ break;
+ }
+ if (cm)
+ appendStringInfo(buf, " COMPRESSION %s", cm);
+ }
+
+ /*
+ * 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_qualified_relname(seqid);
+
+ 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)
+ appendStringInfoString(buf, " NOT NULL");
+ }
+
+ /*
+ * 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_constraints)
+ 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_constraints)
+{
+ 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, ',');
+ first = false;
+ if (pretty)
+ appendStringInfoString(&inner, "\n ");
+ else
+ appendStringInfoChar(&inner, ' ');
+
+ appendStringInfo(&inner, "%s WITH OPTIONS",
+ quote_identifier(NameStr(att->attname)));
+ if (has_default)
+ appendStringInfo(&inner, " DEFAULT %s", defexpr);
+ if (has_notnull)
+ appendStringInfoString(&inner, " NOT NULL");
+
+ 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_constraints)
+ 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);
+}
+
+/*
+ * pg_get_table_ddl_internal
+ * Generate DDL statements to recreate a regular or partitioned table.
+ *
+ * The first list element is the CREATE TABLE statement. Subsequent
+ * elements are the ALTER TABLE / CREATE INDEX / CREATE RULE /
+ * CREATE STATISTICS statements needed to restore the table's full
+ * definition.
+ *
+ * Trigger and policy emission are scaffolded but not yet wired up — they
+ * are gated on standalone pg_get_trigger_ddl / pg_get_policy_ddl helpers
+ * landing.
+ */
+static List *
+pg_get_table_ddl_internal(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition)
+{
+ Relation rel;
+ StringInfoData buf;
+ List *statements = NIL;
+ char *qualname;
+ char relkind;
+ char relpersistence;
+ bool is_typed;
+ AclResult aclresult;
+
+ rel = table_open(relid, AccessShareLock);
+
+ relkind = rel->rd_rel->relkind;
+ relpersistence = rel->rd_rel->relpersistence;
+ is_typed = OidIsValid(rel->rd_rel->reloftype);
+
+ /*
+ * 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(relid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLE,
+ RelationGetRelationName(rel));
+
+ qualname = lookup_qualified_relname(relid);
+
+ initStringInfo(&buf);
+
+ /* pg_class tuple — for relpartbound and reloptions */
+ {
+ HeapTuple classtup;
+ Datum reloptDatum;
+ bool reloptIsnull;
+
+ classtup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(classtup))
+ elog(ERROR, "cache lookup failed for relation %u", relid);
+
+ reloptDatum = SysCacheGetAttr(RELOID, classtup,
+ Anum_pg_class_reloptions, &reloptIsnull);
+
+ /*
+ * CREATE [TEMPORARY | UNLOGGED] TABLE qualname ...
+ *
+ * Persistence applies uniformly to all three CREATE TABLE forms
+ * (regular column list, OF type_name, and PARTITION OF). Only one
+ * of TEMPORARY / UNLOGGED can be set; the relpersistence catalog
+ * field is the single source of truth.
+ */
+ appendStringInfoString(&buf, "CREATE ");
+ if (relpersistence == RELPERSISTENCE_TEMP)
+ appendStringInfoString(&buf, "TEMPORARY ");
+ else if (relpersistence == RELPERSISTENCE_UNLOGGED)
+ appendStringInfoString(&buf, "UNLOGGED ");
+ appendStringInfo(&buf, "TABLE %s", qualname);
+
+ if (rel->rd_rel->relispartition)
+ {
+ /* PARTITION OF parent FOR VALUES ... */
+ Oid parentOid = get_partition_parent(relid, true);
+ char *parentQual = lookup_qualified_relname(parentOid);
+ 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(&buf, " PARTITION OF %s %s",
+ parentQual, forValues ? forValues : "DEFAULT");
+ if (forValues)
+ pfree(forValues);
+ if (boundStr)
+ pfree(boundStr);
+ pfree(parentQual);
+ pfree(parentRelname);
+
+ /*
+ * Column-level overrides redeclared on the child are emitted
+ * out-of-line: NOT NULL/CHECK come through the constraint loop
+ * (conislocal=true), and DEFAULT comes through the dedicated
+ * ALTER COLUMN SET DEFAULT pass below.
+ */
+ }
+ else if (is_typed)
+ {
+ /*
+ * Typed table: CREATE TABLE name OF type_name [(col WITH
+ * OPTIONS ...)]. The column list (when present) carries only
+ * locally-applied overrides — defaults, NOT NULL toggles, and
+ * locally-declared CHECK constraints — for columns whose type
+ * is otherwise dictated by reloftype.
+ */
+ char *typname = format_type_be_qualified(rel->rd_rel->reloftype);
+
+ appendStringInfo(&buf, " OF %s", typname);
+ pfree(typname);
+
+ append_typed_column_overrides(&buf, rel, pretty, include_constraints);
+ }
+ else
+ {
+ List *parents;
+ ListCell *lc;
+ bool first;
+
+ appendStringInfoString(&buf, " (");
+
+ append_column_defs(&buf, rel, pretty, include_constraints);
+
+ if (pretty)
+ appendStringInfoString(&buf, "\n)");
+ else
+ appendStringInfoChar(&buf, ')');
+
+ /* INHERITS (parent1, parent2, ...) — non-partition inheritance only */
+ parents = get_inheritance_parents(relid);
+ if (parents != NIL)
+ {
+ appendStringInfoString(&buf, " INHERITS (");
+ first = true;
+ foreach(lc, parents)
+ {
+ Oid poid = lfirst_oid(lc);
+ char *pname = lookup_qualified_relname(poid);
+
+ if (!first)
+ appendStringInfoString(&buf, ", ");
+ first = false;
+ appendStringInfoString(&buf, pname);
+ pfree(pname);
+ }
+ appendStringInfoChar(&buf, ')');
+ list_free(parents);
+ }
+ }
+
+ /* PARTITION BY — applies whenever this relation is a partitioned table */
+ if (relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Datum partkeyDatum;
+ char *partkey;
+
+ partkeyDatum = OidFunctionCall1(F_PG_GET_PARTKEYDEF,
+ ObjectIdGetDatum(relid));
+ partkey = TextDatumGetCString(partkeyDatum);
+ appendStringInfo(&buf, " PARTITION BY %s", partkey);
+ pfree(partkey);
+ }
+
+ /*
+ * USING method — emit only when the table access method differs
+ * from heap (the cluster default). Pluggable table AMs have been
+ * supported since PostgreSQL 12.
+ */
+ 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(&buf, " USING %s", quote_identifier(amname));
+ pfree(amname);
+ }
+ }
+
+ /* WITH (reloptions) */
+ if (!reloptIsnull)
+ {
+ appendStringInfoString(&buf, " WITH (");
+ get_reloptions(&buf, reloptDatum);
+ appendStringInfoChar(&buf, ')');
+ }
+
+ ReleaseSysCache(classtup);
+ }
+
+ /* TABLESPACE */
+ if (!no_tablespace && OidIsValid(rel->rd_rel->reltablespace))
+ {
+ char *tsname = get_tablespace_name(rel->rd_rel->reltablespace);
+
+ if (tsname != NULL)
+ {
+ appendStringInfo(&buf, " TABLESPACE %s", quote_identifier(tsname));
+ pfree(tsname);
+ }
+ }
+
+ /*
+ * ON COMMIT applies only to temporary tables. The action lives in a
+ * backend-local list (not the catalog) since it's a session-scoped
+ * property, so this is best-effort: we can only see entries registered
+ * in the current backend. PRESERVE ROWS is the default and is not
+ * emitted; NOOP indicates no entry was found.
+ */
+ if (relpersistence == RELPERSISTENCE_TEMP)
+ {
+ OnCommitAction oc = get_on_commit_action(relid);
+
+ if (oc == ONCOMMIT_DELETE_ROWS)
+ appendStringInfoString(&buf, " ON COMMIT DELETE ROWS");
+ else if (oc == ONCOMMIT_DROP)
+ appendStringInfoString(&buf, " ON COMMIT DROP");
+ }
+
+ appendStringInfoChar(&buf, ';');
+ statements = lappend(statements, pstrdup(buf.data));
+
+ /* OWNER */
+ if (!no_owner)
+ {
+ char *owner = GetUserNameFromId(rel->rd_rel->relowner, false);
+
+ resetStringInfo(&buf);
+ appendStringInfo(&buf, "ALTER TABLE %s OWNER TO %s;",
+ qualname, quote_identifier(owner));
+ statements = lappend(statements, pstrdup(buf.data));
+ pfree(owner);
+ }
+
+ /*
+ * Per-column DEFAULT overrides on inherited/partition children. The
+ * column list inside CREATE TABLE only emits locally-declared columns
+ * (attislocal=true), so any default set locally on a column that came
+ * from a parent table needs to be re-applied with ALTER COLUMN SET
+ * DEFAULT. Note: NOT NULL overrides come out through the constraint
+ * loop (PG 18 stores them as named pg_constraint entries with
+ * conislocal=true), and CHECK overrides do too.
+ */
+ {
+ TupleDesc tupdesc = RelationGetDescr(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;
+
+ defstr = find_attrdef_text(rel, att->attnum, &dpcontext);
+ if (defstr == NULL)
+ continue;
+
+ resetStringInfo(&buf);
+ appendStringInfo(&buf,
+ "ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s;",
+ qualname,
+ quote_identifier(NameStr(att->attname)),
+ defstr);
+ statements = lappend(statements, pstrdup(buf.data));
+ pfree(defstr);
+ }
+ }
+
+ /*
+ * Per-column attoptions — these can't be set inline in CREATE TABLE,
+ * so they come out as ALTER TABLE ... ALTER COLUMN col SET (...) after
+ * the table is created. Typical use: n_distinct overrides for the
+ * planner.
+ */
+ {
+ TupleDesc tupdesc = RelationGetDescr(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(relid),
+ Int16GetDatum(att->attnum));
+ if (!HeapTupleIsValid(attTup))
+ continue;
+
+ optDatum = SysCacheGetAttr(ATTNUM, attTup,
+ Anum_pg_attribute_attoptions, &optIsnull);
+ if (!optIsnull)
+ {
+ resetStringInfo(&buf);
+ appendStringInfo(&buf, "ALTER TABLE %s ALTER COLUMN %s SET (",
+ qualname,
+ quote_identifier(NameStr(att->attname)));
+ get_reloptions(&buf, optDatum);
+ appendStringInfoString(&buf, ");");
+ statements = lappend(statements, pstrdup(buf.data));
+ }
+ ReleaseSysCache(attTup);
+ }
+ }
+
+ /*
+ * Indexes — emit a CREATE INDEX for each non-constraint-backed index on
+ * the table. Indexes that back PK/UNIQUE/EXCLUDE constraints are
+ * emitted by the constraint loop below as part of the ALTER TABLE ...
+ * ADD CONSTRAINT statement, which creates the index implicitly.
+ */
+ if (include_indexes)
+ {
+ List *indexoids = RelationGetIndexList(rel);
+ ListCell *lc;
+
+ foreach(lc, indexoids)
+ {
+ Oid idxoid = lfirst_oid(lc);
+ char *idxdef;
+
+ if (OidIsValid(get_index_constraint(idxoid)))
+ continue;
+
+ idxdef = pg_get_indexdef_string(idxoid);
+ resetStringInfo(&buf);
+ appendStringInfo(&buf, "%s;", idxdef);
+ statements = lappend(statements, pstrdup(buf.data));
+ pfree(idxdef);
+ }
+ list_free(indexoids);
+ }
+
+ /*
+ * Constraints — emit an ALTER TABLE ... ADD CONSTRAINT for each
+ * locally-defined constraint on the table. Inherited constraints
+ * (conislocal=false) are produced by the parent's DDL and propagated
+ * automatically by INHERITS / PARTITION OF, so we skip them here.
+ */
+ if (include_constraints)
+ {
+ Relation conRel;
+ SysScanDesc conScan;
+ ScanKeyData conKey;
+ HeapTuple conTup;
+
+ conRel = table_open(ConstraintRelationId, AccessShareLock);
+ ScanKeyInit(&conKey,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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);
+ char *condef;
+
+ if (!con->conislocal)
+ continue;
+ /* CHECK constraints are emitted inline in the column list. */
+ if (con->contype == CONSTRAINT_CHECK)
+ continue;
+
+ condef = pg_get_constraintdef_command(con->oid);
+ resetStringInfo(&buf);
+ appendStringInfo(&buf, "%s;", condef);
+ statements = lappend(statements, pstrdup(buf.data));
+ pfree(condef);
+ }
+ systable_endscan(conScan);
+ table_close(conRel, AccessShareLock);
+ }
+
+ /*
+ * Rules — emit a CREATE RULE for each cached rewrite rule on the
+ * relation. Internal rules (such as the _RETURN rule on views) live on
+ * views/matviews and don't appear here because we already restricted
+ * relkind above.
+ */
+ if (include_rules && rel->rd_rules != NULL)
+ {
+ for (int i = 0; i < rel->rd_rules->numLocks; i++)
+ {
+ Oid ruleid = rel->rd_rules->rules[i]->ruleId;
+ Datum ruledef;
+ char *ruledef_str;
+
+ ruledef = OidFunctionCall1(F_PG_GET_RULEDEF_OID,
+ ObjectIdGetDatum(ruleid));
+ ruledef_str = TextDatumGetCString(ruledef);
+ resetStringInfo(&buf);
+ appendStringInfoString(&buf, ruledef_str);
+ statements = lappend(statements, pstrdup(buf.data));
+ pfree(ruledef_str);
+ }
+ }
+
+ /*
+ * Extended statistics — iterate pg_statistic_ext by stxrelid and emit
+ * pg_get_statisticsobjdef_string() for each.
+ */
+ if (include_statistics)
+ {
+ Relation statRel;
+ SysScanDesc statScan;
+ ScanKeyData statKey;
+ HeapTuple statTup;
+
+ statRel = table_open(StatisticExtRelationId, AccessShareLock);
+ ScanKeyInit(&statKey,
+ Anum_pg_statistic_ext_stxrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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_string(stat->oid);
+
+ resetStringInfo(&buf);
+ appendStringInfo(&buf, "%s;", statdef);
+ statements = lappend(statements, pstrdup(buf.data));
+ pfree(statdef);
+ }
+ systable_endscan(statScan);
+ table_close(statRel, AccessShareLock);
+ }
+
+ /*
+ * REPLICA IDENTITY — emit only when it differs from the default
+ * ('d' = use primary key). This affects logical replication
+ * behavior, so round-trip fidelity matters.
+ */
+ if (include_replica_identity &&
+ rel->rd_rel->relreplident != REPLICA_IDENTITY_DEFAULT)
+ {
+ resetStringInfo(&buf);
+ switch (rel->rd_rel->relreplident)
+ {
+ case REPLICA_IDENTITY_NOTHING:
+ appendStringInfo(&buf, "ALTER TABLE %s REPLICA IDENTITY NOTHING;",
+ qualname);
+ statements = lappend(statements, pstrdup(buf.data));
+ break;
+ case REPLICA_IDENTITY_FULL:
+ appendStringInfo(&buf, "ALTER TABLE %s REPLICA IDENTITY FULL;",
+ qualname);
+ statements = lappend(statements, pstrdup(buf.data));
+ break;
+ case REPLICA_IDENTITY_INDEX:
+ {
+ Oid replidx = RelationGetReplicaIndex(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(&buf,
+ "ALTER TABLE %s REPLICA IDENTITY USING INDEX %s;",
+ qualname,
+ quote_identifier(idxname));
+ statements = lappend(statements, pstrdup(buf.data));
+ pfree(idxname);
+ }
+ }
+ break;
+ }
+ }
+
+ /* ENABLE / FORCE ROW LEVEL SECURITY */
+ if (include_rls && rel->rd_rel->relrowsecurity)
+ {
+ resetStringInfo(&buf);
+ appendStringInfo(&buf, "ALTER TABLE %s ENABLE ROW LEVEL SECURITY;",
+ qualname);
+ statements = lappend(statements, pstrdup(buf.data));
+ }
+ if (include_rls && rel->rd_rel->relforcerowsecurity)
+ {
+ resetStringInfo(&buf);
+ appendStringInfo(&buf, "ALTER TABLE %s FORCE ROW LEVEL SECURITY;",
+ qualname);
+ statements = lappend(statements, pstrdup(buf.data));
+ }
+
+ /*
+ * Triggers — scaffolding only. The standalone pg_get_trigger_ddl()
+ * function (Phil's re-roll) is the intended emission path; once it
+ * lands the body of this loop becomes a single call into it. The
+ * scan structure, lock acquisition, and tgisinternal filter (which
+ * skips FK-backing and other system-generated triggers) are settled
+ * here so that change stays minimal.
+ */
+ if (include_triggers)
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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. */
+ (void) trg;
+ }
+ systable_endscan(trigScan);
+ table_close(trigRel, AccessShareLock);
+ }
+
+ /*
+ * Row-level security policies — scaffolding only. Once
+ * pg_get_policy_ddl() (already-submitted patch) lands, the body of
+ * this loop becomes a per-policy call into it. The ENABLE/FORCE
+ * ROW LEVEL SECURITY toggles above are the companion catalog flags
+ * and are already emitted independently of policies.
+ */
+ if (include_policies)
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(relid));
+ polScan = systable_beginscan(polRel, PolicyPolrelidPolnameIndexId,
+ true, NULL, 1, &polKey);
+ while (HeapTupleIsValid(polTup = systable_getnext(polScan)))
+ {
+ Form_pg_policy pol = (Form_pg_policy) GETSTRUCT(polTup);
+
+ /* TODO: append pg_get_policy_ddl(relid, polname) output here. */
+ (void) pol;
+ }
+ systable_endscan(polScan);
+ table_close(polRel, AccessShareLock);
+ }
+
+ /*
+ * Partition children — when include_partition is true and this relation
+ * is a partitioned-table parent, recursively emit the DDL for each
+ * direct partition child. Each child's own DDL handles further levels
+ * of sub-partitioning through the same recursion.
+ */
+ if (include_partition && relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *children = find_inheritance_children(relid, AccessShareLock);
+ ListCell *lc;
+
+ foreach(lc, children)
+ {
+ Oid childoid = lfirst_oid(lc);
+ List *childstmts;
+
+ childstmts = pg_get_table_ddl_internal(childoid, pretty,
+ no_owner, no_tablespace,
+ include_indexes,
+ include_constraints,
+ include_rules,
+ include_statistics,
+ include_triggers,
+ include_policies,
+ include_rls,
+ include_replica_identity,
+ include_partition);
+ statements = list_concat(statements, childstmts);
+ }
+ list_free(children);
+ }
+
+ table_close(rel, AccessShareLock);
+ pfree(buf.data);
+ pfree(qualname);
+
+ return 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;
+ Oid relid;
+ DdlOption opts[] = {
+ {"pretty", DDL_OPT_BOOL},
+ {"owner", DDL_OPT_BOOL},
+ {"tablespace", DDL_OPT_BOOL},
+ {"includes_indexes", DDL_OPT_BOOL},
+ {"includes_constraints", DDL_OPT_BOOL},
+ {"includes_rules", DDL_OPT_BOOL},
+ {"includes_statistics", DDL_OPT_BOOL},
+ {"includes_triggers", DDL_OPT_BOOL},
+ {"includes_policies", DDL_OPT_BOOL},
+ {"includes_rls", DDL_OPT_BOOL},
+ {"includes_replica_identity", DDL_OPT_BOOL},
+ {"includes_partition", DDL_OPT_BOOL},
+ };
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ relid = PG_GETARG_OID(0);
+ parse_ddl_options(fcinfo, 1, opts, lengthof(opts));
+
+ statements = pg_get_table_ddl_internal(relid,
+ opts[0].isset && opts[0].boolval,
+ opts[1].isset && !opts[1].boolval,
+ opts[2].isset && !opts[2].boolval,
+ !opts[3].isset || opts[3].boolval,
+ !opts[4].isset || opts[4].boolval,
+ !opts[5].isset || opts[5].boolval,
+ !opts[6].isset || opts[6].boolval,
+ !opts[7].isset || opts[7].boolval,
+ !opts[8].isset || opts[8].boolval,
+ !opts[9].isset || opts[9].boolval,
+ !opts[10].isset || opts[10].boolval,
+ opts[11].isset && opts[11].boolval);
+ 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/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..e456c226f53 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,13 @@
proargtypes => 'regdatabase text', proallargtypes => '{regdatabase,text}',
proargmodes => '{i,v}', proargdefaults => '{NULL}',
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..2085027c9b1 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);
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..a5fc8add515
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,498 @@
+--
+-- 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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);
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_always_not_null NOT NULL id_always;
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_default_not_null NOT NULL id_default;
+(3 rows)
+
+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);
+ ALTER TABLE pgtbl_ddl_test.id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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)
+
+-- 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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(5 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 parent (RANGE and HASH).
+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)
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0);
+(1 row)
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(1 row)
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx ( a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx ADD CONSTRAINT ri_idx_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(4 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)));
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_b_not_null NOT NULL b;
+(3 rows)
+
+-- Temporary tables + ON COMMIT. Temp tables live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_default ( id integer);
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_delete ( id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_drop'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+ 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+(1 row)
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+ pg_get_table_ddl
+------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+(1 row)
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+ pg_get_table_ddl
+---------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+(1 row)
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range ( id integer, k integer) PARTITION BY RANGE (id);
+ 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;
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(4 rows)
+
+-- 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.
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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 25 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 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 rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ 'owner', 'false') WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 20 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 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 rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..c7f72bb824c
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,278 @@
+--
+-- 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');
+
+-- 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 parent (RANGE and HASH).
+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');
+
+-- 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 live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS 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');
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+
+-- 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');
+
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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;
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-08 19:11 Marcos Pegoraro <[email protected]>
parent: Akshay Joshi <[email protected]>
1 sibling, 0 replies; 54+ messages in thread
From: Marcos Pegoraro @ 2026-06-08 19:11 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: pgsql-hackers
Em seg., 8 de jun. de 2026 às 08:30, Akshay Joshi <
[email protected]> escreveu:
> Hi Hackers,
>
> Attached is the v2 patch, which fixes the Meson build failure, and the
> rebased patch.
>
Would be good to have an option "schema-qualified" boolean, because
sometimes I don't want a schema-qualified result.
Suppose you are duplicating a schema, the way your did you cannot do
something like this to have a complete script to generate a new schema
select 'create schema new_schema;' union all
select 'set search_path to new_schema, public;' union all
select string_agg(pg_get_table_ddl(oid),',') from pg_class where
relnamespace::regnamespace::text = 'old_schema' and relkind = 'r';
regards
Marcos
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-08 21:12 Zsolt Parragi <[email protected]>
parent: Akshay Joshi <[email protected]>
1 sibling, 1 reply; 54+ messages in thread
From: Zsolt Parragi @ 2026-06-08 21:12 UTC (permalink / raw)
To: [email protected]
Hello!
I did some basic testing with the new functions, and found a few bugs:
1. Seems like check constraints on partitions are ignored:
CREATE TABLE p (id int, val int) PARTITION BY RANGE (id);
CREATE TABLE p_child PARTITION OF p (CONSTRAINT chk_inline CHECK (val > 0))
FOR VALUES FROM (0) TO (100);
SELECT * FROM pg_get_table_ddl('p_child', 'owner','false');
2. inherited stored generated columns can't be replayed:
CREATE TABLE par_s (
id int,
g int GENERATED ALWAYS AS (id * 2) STORED
);
CREATE TABLE ch_s () INHERITS (par_s);
SELECT * FROM pg_get_table_ddl('ch_s', 'owner','false');
-- CREATE TABLE public.ch_s () INHERITS (public.par_s);
-- ALTER TABLE public.ch_s ALTER COLUMN g SET DEFAULT (id * 2);
Dropping ch_s, executing the returned statements:
ERROR: column "g" of relation "ch_s" is a generated column
HINT: Use ALTER TABLE ... ALTER COLUMN ... SET EXPRESSION instead.
3. named not null constraints can't be replayed:
CREATE TABLE t (a int CONSTRAINT my_nn NOT NULL);
SELECT * FROM pg_get_table_ddl('t'::regclass,'owner','false');
-- CREATE TABLE public.t ( a integer NOT NULL);
-- ALTER TABLE public.t ADD CONSTRAINT my_nn NOT NULL a;
Dropping t, executing the statements:
ERROR: cannot create not-null constraint "my_nn" on column "a" of table "t"
DETAIL: A not-null constraint named "t_a_not_null" already exists for
this column.
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-09 08:58 Akshay Joshi <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-06-09 08:58 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; Marcos Pegoraro <[email protected]>; +Cc: [email protected]
Thanks to Zsolt and Marcos for the review.
Added *schema_qualified (boolean, default true)*. When false, the target
table is emitted unqualified everywhere (e.g., CREATE TABLE, ALTER TABLE,
INHERITS, PARTITION OF, identity SEQUENCE NAME, etc.); same-schema sibling
references follow suit; cross-schema references (e.g. FK targets in a
different schema) remain qualified for correctness. Output from the
always-qualified ruleutils helpers (pg_get_indexdef_string,
pg_get_constraintdef_command, pg_get_ruledef,
pg_get_statisticsobjdef_string) is post-processed to strip the base-schema
prefix.
1) CHECK constraints on partition children are now emitted as ALTER TABLE …
ADD CONSTRAINT … CHECK (…). They had been silently dropped because the
PARTITION OF form has no column list to inline them into.
2) Inherited generated columns no longer emit a spurious ALTER COLUMN … SET
DEFAULT, which would fail at replay.
3) User-named NOT NULL constraints are now emitted inline as CONSTRAINT
<name> NOT NULL. Auto-named NOT NULLs keep the existing inline-NOT NULL +
ALTER TABLE dedup behaviour so the common-case output is unchanged.
4) Added regression coverage for the three bug fixes plus the
schema_qualified=false paths (same-schema vs cross-schema,
INHERITS/PARTITION OF parents, custom identity sequence name, replay into a
different target schema)
Attached is the v3 patch, ready for review.
On Tue, Jun 9, 2026 at 2:42 AM Zsolt Parragi <[email protected]>
wrote:
> Hello!
>
> I did some basic testing with the new functions, and found a few bugs:
>
> 1. Seems like check constraints on partitions are ignored:
>
> CREATE TABLE p (id int, val int) PARTITION BY RANGE (id);
> CREATE TABLE p_child PARTITION OF p (CONSTRAINT chk_inline CHECK (val > 0))
> FOR VALUES FROM (0) TO (100);
> SELECT * FROM pg_get_table_ddl('p_child', 'owner','false');
>
> 2. inherited stored generated columns can't be replayed:
>
> CREATE TABLE par_s (
> id int,
> g int GENERATED ALWAYS AS (id * 2) STORED
> );
> CREATE TABLE ch_s () INHERITS (par_s);
> SELECT * FROM pg_get_table_ddl('ch_s', 'owner','false');
> -- CREATE TABLE public.ch_s () INHERITS (public.par_s);
> -- ALTER TABLE public.ch_s ALTER COLUMN g SET DEFAULT (id * 2);
>
> Dropping ch_s, executing the returned statements:
>
> ERROR: column "g" of relation "ch_s" is a generated column
> HINT: Use ALTER TABLE ... ALTER COLUMN ... SET EXPRESSION instead.
>
> 3. named not null constraints can't be replayed:
>
> CREATE TABLE t (a int CONSTRAINT my_nn NOT NULL);
> SELECT * FROM pg_get_table_ddl('t'::regclass,'owner','false');
> -- CREATE TABLE public.t ( a integer NOT NULL);
> -- ALTER TABLE public.t ADD CONSTRAINT my_nn NOT NULL a;
>
> Dropping t, executing the statements:
>
> ERROR: cannot create not-null constraint "my_nn" on column "a" of table
> "t"
> DETAIL: A not-null constraint named "t_a_not_null" already exists for
> this column.
>
>
>
Attachments:
[application/octet-stream] v3-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (110.5K, ../../CANxoLDfL1AxL=k9SsRNReKQ-sJUSy6RvCqeog5HWckFxff=0Pg@mail.gmail.com/3-v3-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From 4d1e585c5990ebc7a011371cd7fba56534ac11ea Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v3] 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_string, constraints
(PRIMARY KEY, UNIQUE, FOREIGN KEY, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_command, rules via pg_get_ruledef, extended
statistics via pg_get_statisticsobjdef_string, REPLICA IDENTITY
NOTHING/FULL/USING INDEX, ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY,
and child-local DEFAULT overrides on inheritance/partition children.
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.
A regression test under src/test/regress covers ordinary tables,
identity (default and custom sequence options), generated columns,
STORAGE/COMPRESSION, constraints (CHECK/UNIQUE/FK with deferrable),
functional and partial indexes, inheritance and partitioning,
partition children with FOR VALUES FROM/TO, WITH modulus/remainder, and
DEFAULT, rules, extended statistics, RLS toggles, REPLICA IDENTITY,
UNLOGGED with reloptions, per-column attoptions, child DEFAULT
overrides, pretty mode, owner=false, and the error paths for views,
sequences, NULL, unknown options, and odd-variadic argument counts.
Author: Akshay Joshi <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 79 +
src/backend/commands/tablecmds.c | 27 +
src/backend/utils/adt/ddlutils.c | 1744 +++++++++++++++++
src/include/catalog/pg_proc.dat | 7 +
src/include/commands/tablecmds.h | 1 +
.../regress/expected/pg_get_table_ddl.out | 638 ++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 368 ++++
8 files changed, 2865 insertions(+), 1 deletion(-)
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 00f64f50ceb..6a34ded9628 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3961,6 +3961,85 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
<literal>TABLESPACE</literal>.
</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 a family of
+ <literal>includes_<replaceable>category</replaceable></literal>
+ booleans that gate emission of optional sub-objects:
+ <literal>includes_indexes</literal>,
+ <literal>includes_constraints</literal>,
+ <literal>includes_rules</literal>,
+ <literal>includes_statistics</literal>,
+ <literal>includes_triggers</literal>,
+ <literal>includes_policies</literal>,
+ <literal>includes_rls</literal> (the
+ <command>ENABLE</command>/<command>FORCE ROW LEVEL SECURITY</command>
+ toggles), and
+ <literal>includes_replica_identity</literal>. Each of these
+ defaults to <literal>true</literal>. The
+ <literal>includes_partition</literal> option, which defaults to
+ <literal>false</literal>, controls whether the DDL for partition
+ children is appended after a partitioned-table parent.
+ </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/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a33e22e8e61..6aa401d943c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19558,6 +19558,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 f32fcd453ef..3d68ce01e15 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -21,12 +21,27 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.h"
#include "catalog/pg_auth_members.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_class.h"
+#include "catalog/dependency.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_am.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 "catalog/partition.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
+#include "rewrite/prs2lock.h"
+#include "nodes/nodes.h"
#include "commands/tablespace.h"
#include "common/relpath.h"
#include "funcapi.h"
@@ -86,6 +101,103 @@ static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner
static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool isnull);
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, schema_prefix, 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 include_indexes;
+ bool include_constraints;
+ bool include_rules;
+ bool include_statistics;
+ bool include_triggers;
+ bool include_policies;
+ bool include_rls;
+ bool include_replica_identity;
+ bool include_partition;
+ bool schema_qualified;
+
+ /* Derived during setup */
+ Oid base_namespace;
+ char *qualname;
+ char *schema_prefix; /* NULL when schema_qualified */
+ LocalNotNullEntry *nn_entries;
+ List *skip_notnull_oids;
+
+ /* Mutable working state */
+ StringInfoData buf;
+ List *statements;
+} TableDdlContext;
+
+static List *pg_get_table_ddl_internal(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition,
+ bool schema_qualified);
+static void append_column_defs(StringInfo buf, Relation rel, bool pretty,
+ bool include_constraints,
+ bool schema_qualified,
+ LocalNotNullEntry *nn_entries);
+static void append_typed_column_overrides(StringInfo buf, Relation rel,
+ bool pretty, bool include_constraints,
+ 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_qualified_relname(Oid relid);
+static char *lookup_relname_for_emit(Oid relid, bool schema_qualified,
+ Oid base_namespace);
+static char *strip_schema_prefix(const char *stmt, const char *prefix);
+static LocalNotNullEntry *collect_local_not_null(Relation rel,
+ List **skip_oids);
+static List *get_inheritance_parents(Oid relid);
+
+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);
/*
@@ -1185,3 +1297,1635 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
}
+
+/*
+ * get_inheritance_parents
+ * Return a List of parent OIDs for relid, ordered by inhseqno.
+ *
+ * find_inheritance_children() walks the opposite direction (parent->children),
+ * so we scan pg_inherits directly here using the (inhrelid, inhseqno) index,
+ * which yields rows in the order they need to appear in the INHERITS clause.
+ * Partition children also have a pg_inherits entry, so callers must skip the
+ * INHERITS clause when relispartition is true.
+ */
+static List *
+get_inheritance_parents(Oid relid)
+{
+ Relation inheritsRel;
+ SysScanDesc scan;
+ ScanKeyData key;
+ HeapTuple tup;
+ List *parents = NIL;
+
+ inheritsRel = table_open(InheritsRelationId, AccessShareLock);
+ ScanKeyInit(&key,
+ Anum_pg_inherits_inhrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(relid));
+ scan = systable_beginscan(inheritsRel, InheritsRelidSeqnoIndexId,
+ true, NULL, 1, &key);
+ while (HeapTupleIsValid(tup = systable_getnext(scan)))
+ {
+ Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(tup);
+
+ parents = lappend_oid(parents, inh->inhparent);
+ }
+ systable_endscan(scan);
+ table_close(inheritsRel, AccessShareLock);
+
+ return parents;
+}
+
+/*
+ * lookup_qualified_relname
+ * Return the schema-qualified, identifier-quoted name of a relation,
+ * or raise ERRCODE_UNDEFINED_OBJECT if the relation has disappeared.
+ *
+ * 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_qualified_relname(Oid relid)
+{
+ 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);
+ 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;
+}
+
+/*
+ * lookup_relname_for_emit
+ * Return either the schema-qualified or the bare quoted name of a
+ * relation, depending on the schema_qualified flag.
+ *
+ * When schema_qualified is true this is identical to
+ * lookup_qualified_relname. 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 still returned, because cross-schema references
+ * (for example an inheritance parent or foreign key target in a
+ * different schema) are not safe to strip.
+ *
+ * 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 *result;
+
+ if (schema_qualified)
+ return lookup_qualified_relname(relid);
+
+ 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);
+ if (reltup->relnamespace != base_namespace)
+ {
+ /* Cross-schema reference: qualification is required for correctness. */
+ ReleaseSysCache(tp);
+ return lookup_qualified_relname(relid);
+ }
+
+ result = pstrdup(quote_identifier(NameStr(reltup->relname)));
+ ReleaseSysCache(tp);
+
+ return result;
+}
+
+/*
+ * strip_schema_prefix
+ * Return a palloc'd copy of stmt with every occurrence of prefix
+ * removed.
+ *
+ * Used to strip the qualified-schema prefix (for example "old_schema.")
+ * from DDL emitted by the always-qualified ruleutils helpers
+ * (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) when the caller has
+ * requested unqualified output via schema_qualified=false. The prefix
+ * is matched as a plain substring, which is safe for DDL output but in
+ * theory could mis-edit a string literal whose contents contain the
+ * literal text "<schema>." — accept that as a rare edge case.
+ */
+static char *
+strip_schema_prefix(const char *stmt, const char *prefix)
+{
+ StringInfoData buf;
+ size_t plen = strlen(prefix);
+ const char *p = stmt;
+ const char *q;
+
+ initStringInfo(&buf);
+ while ((q = strstr(p, prefix)) != NULL)
+ {
+ appendBinaryStringInfo(&buf, p, q - p);
+ p = q + plen;
+ }
+ appendStringInfoString(&buf, p);
+ return buf.data;
+}
+
+/*
+ * 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;
+
+ if (!entries[attnum].is_auto && 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, ',');
+ *first = false;
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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_constraints,
+ 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 emitted by the INHERITS clause
+ * (once implemented), not the column list, unless the child
+ * redeclared them locally (attislocal=true).
+ */
+ if (!att->attislocal)
+ continue;
+
+ if (!first)
+ appendStringInfoChar(buf, ',');
+ first = false;
+
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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))
+ {
+ const char *storage = NULL;
+
+ switch (att->attstorage)
+ {
+ case TYPSTORAGE_PLAIN:
+ storage = "PLAIN";
+ break;
+ case TYPSTORAGE_EXTERNAL:
+ storage = "EXTERNAL";
+ break;
+ case TYPSTORAGE_MAIN:
+ storage = "MAIN";
+ break;
+ case TYPSTORAGE_EXTENDED:
+ storage = "EXTENDED";
+ break;
+ }
+ if (storage)
+ appendStringInfo(buf, " STORAGE %s", storage);
+ }
+
+ /* COMPRESSION clause, only if explicitly set on the column. */
+ if (CompressionMethodIsValid(att->attcompression))
+ {
+ const char *cm = NULL;
+
+ switch (att->attcompression)
+ {
+ case TOAST_PGLZ_COMPRESSION:
+ cm = "pglz";
+ break;
+ case TOAST_LZ4_COMPRESSION:
+ cm = "lz4";
+ break;
+ }
+ if (cm)
+ appendStringInfo(buf, " COMPRESSION %s", cm);
+ }
+
+ /*
+ * 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_constraints)
+ 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_constraints,
+ 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, ',');
+ first = false;
+ if (pretty)
+ appendStringInfoString(&inner, "\n ");
+ else
+ appendStringInfoChar(&inner, ' ');
+
+ 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_constraints)
+ 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 / append_stripped_stmt
+ * Push ctx->buf onto ctx->statements.
+ *
+ * append_stmt is used for emissions where the table reference is built
+ * from ctx->qualname (already partial-qualified per schema_qualified).
+ * append_stripped_stmt is used after invoking always-qualified ruleutils
+ * helpers (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) — when
+ * schema_qualified is false, the base-schema prefix is stripped from
+ * their output so the emitted DDL is replay-relocatable.
+ */
+static void
+append_stmt(TableDdlContext *ctx)
+{
+ ctx->statements = lappend(ctx->statements, pstrdup(ctx->buf.data));
+}
+
+static void
+append_stripped_stmt(TableDdlContext *ctx)
+{
+ if (ctx->schema_prefix != NULL)
+ ctx->statements = lappend(ctx->statements,
+ strip_schema_prefix(ctx->buf.data,
+ ctx->schema_prefix));
+ else
+ 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,
+ ctx->include_constraints,
+ ctx->nn_entries);
+ }
+ else
+ {
+ List *parents;
+ ListCell *lc;
+ bool first;
+
+ appendStringInfoString(&ctx->buf, " (");
+
+ append_column_defs(&ctx->buf, rel, ctx->pretty,
+ ctx->include_constraints,
+ ctx->schema_qualified, ctx->nn_entries);
+
+ if (ctx->pretty)
+ appendStringInfoString(&ctx->buf, "\n)");
+ else
+ appendStringInfoChar(&ctx->buf, ')');
+
+ parents = get_inheritance_parents(ctx->relid);
+ 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 (!ctx->include_indexes)
+ 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_string(idxoid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", idxdef);
+ append_stripped_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). User-named NOT NULL
+ * constraints are emitted inline by the column-emit helpers and
+ * are skipped via skip_notnull_oids; auto-named NOT NULLs fall
+ * through here and are dedup'd by name at replay.
+ */
+static void
+emit_local_constraints(TableDdlContext *ctx)
+{
+ Relation conRel;
+ SysScanDesc conScan;
+ ScanKeyData conKey;
+ HeapTuple conTup;
+ bool is_partition = ctx->rel->rd_rel->relispartition;
+
+ if (!ctx->include_constraints)
+ return;
+
+ 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);
+ char *condef;
+
+ if (!con->conislocal)
+ continue;
+ if (con->contype == CONSTRAINT_CHECK && !is_partition)
+ continue;
+ if (con->contype == CONSTRAINT_NOTNULL && !is_partition &&
+ list_member_oid(ctx->skip_notnull_oids, con->oid))
+ continue;
+
+ condef = pg_get_constraintdef_command(con->oid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", condef);
+ append_stripped_stmt(ctx);
+ pfree(condef);
+ }
+ 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 (!ctx->include_rules || 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;
+ Datum ruledef;
+ char *ruledef_str;
+
+ ruledef = OidFunctionCall1(F_PG_GET_RULEDEF_OID,
+ ObjectIdGetDatum(ruleid));
+ ruledef_str = TextDatumGetCString(ruledef);
+ resetStringInfo(&ctx->buf);
+ appendStringInfoString(&ctx->buf, ruledef_str);
+ append_stripped_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 (!ctx->include_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_string(stat->oid);
+
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", statdef);
+ append_stripped_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 (!ctx->include_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 (!ctx->include_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 (!ctx->include_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);
+ List *childstmts;
+
+ childstmts = pg_get_table_ddl_internal(childoid, ctx->pretty,
+ ctx->no_owner,
+ ctx->no_tablespace,
+ ctx->include_indexes,
+ ctx->include_constraints,
+ ctx->include_rules,
+ ctx->include_statistics,
+ ctx->include_triggers,
+ ctx->include_policies,
+ ctx->include_rls,
+ ctx->include_replica_identity,
+ ctx->include_partition,
+ ctx->schema_qualified);
+ 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 first list element is the CREATE TABLE statement. Subsequent
+ * elements are the ALTER TABLE / CREATE INDEX / CREATE RULE /
+ * CREATE STATISTICS statements needed to restore the table's full
+ * definition.
+ *
+ * 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(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition,
+ bool schema_qualified)
+{
+ TableDdlContext ctx = {0};
+ Relation rel;
+ char relkind;
+ AclResult aclresult;
+
+ rel = table_open(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(relid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLE,
+ RelationGetRelationName(rel));
+
+ ctx.rel = rel;
+ ctx.relid = relid;
+ ctx.pretty = pretty;
+ ctx.no_owner = no_owner;
+ ctx.no_tablespace = no_tablespace;
+ ctx.include_indexes = include_indexes;
+ ctx.include_constraints = include_constraints;
+ ctx.include_rules = include_rules;
+ ctx.include_statistics = include_statistics;
+ ctx.include_triggers = include_triggers;
+ ctx.include_policies = include_policies;
+ ctx.include_rls = include_rls;
+ ctx.include_replica_identity = include_replica_identity;
+ ctx.include_partition = include_partition;
+ ctx.schema_qualified = schema_qualified;
+ ctx.base_namespace = RelationGetNamespace(rel);
+ ctx.qualname = lookup_relname_for_emit(relid, schema_qualified,
+ ctx.base_namespace);
+
+ /*
+ * When schema_qualified is false, pre-compute "<schema>." so that
+ * the sub-object loops can strip it from helper output that always
+ * schema-qualifies (constraintdef_command, indexdef_string, ruledef,
+ * statisticsobjdef_string). Cross-schema references inside those
+ * statements (different schema name) are unaffected.
+ */
+ if (!schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx.base_namespace);
+
+ if (nspname != NULL)
+ {
+ ctx.schema_prefix = psprintf("%s.", quote_identifier(nspname));
+ 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);
+
+ /*
+ * Each pass below appends zero or more finished statements to
+ * ctx.statements via append_stmt / append_stripped_stmt. 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); and partition children last so the parent
+ * already exists at replay time.
+ */
+ 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 (include_triggers)
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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 (include_policies)
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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);
+ }
+#else
+ (void) include_triggers;
+ (void) include_policies;
+#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);
+ if (ctx.schema_prefix != NULL)
+ pfree(ctx.schema_prefix);
+ list_free(ctx.skip_notnull_oids);
+
+ 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;
+ Oid relid;
+ DdlOption opts[] = {
+ {"pretty", DDL_OPT_BOOL},
+ {"owner", DDL_OPT_BOOL},
+ {"tablespace", DDL_OPT_BOOL},
+ {"includes_indexes", DDL_OPT_BOOL},
+ {"includes_constraints", DDL_OPT_BOOL},
+ {"includes_rules", DDL_OPT_BOOL},
+ {"includes_statistics", DDL_OPT_BOOL},
+ {"includes_triggers", DDL_OPT_BOOL},
+ {"includes_policies", DDL_OPT_BOOL},
+ {"includes_rls", DDL_OPT_BOOL},
+ {"includes_replica_identity", DDL_OPT_BOOL},
+ {"includes_partition", DDL_OPT_BOOL},
+ {"schema_qualified", DDL_OPT_BOOL},
+ };
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ relid = PG_GETARG_OID(0);
+ parse_ddl_options(fcinfo, 1, opts, lengthof(opts));
+
+ statements = pg_get_table_ddl_internal(relid,
+ opts[0].isset && opts[0].boolval,
+ opts[1].isset && !opts[1].boolval,
+ opts[2].isset && !opts[2].boolval,
+ !opts[3].isset || opts[3].boolval,
+ !opts[4].isset || opts[4].boolval,
+ !opts[5].isset || opts[5].boolval,
+ !opts[6].isset || opts[6].boolval,
+ !opts[7].isset || opts[7].boolval,
+ !opts[8].isset || opts[8].boolval,
+ !opts[9].isset || opts[9].boolval,
+ !opts[10].isset || opts[10].boolval,
+ opts[11].isset && opts[11].boolval,
+ !opts[12].isset || opts[12].boolval);
+ 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/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..e456c226f53 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,13 @@
proargtypes => 'regdatabase text', proallargtypes => '{regdatabase,text}',
proargmodes => '{i,v}', proargdefaults => '{NULL}',
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..2085027c9b1 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);
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..436f92e94fd
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,638 @@
+--
+-- 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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);
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_always_not_null NOT NULL id_always;
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_default_not_null NOT NULL id_default;
+(3 rows)
+
+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);
+ ALTER TABLE pgtbl_ddl_test.id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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)
+
+-- 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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(5 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 parent (RANGE and HASH).
+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)
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0);
+(1 row)
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(1 row)
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx ( a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx ADD CONSTRAINT ri_idx_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(4 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)));
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_b_not_null NOT NULL b;
+(3 rows)
+
+-- Temporary tables + ON COMMIT. Temp tables live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_default ( id integer);
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_delete ( id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_drop'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+ 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+(1 row)
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+ pg_get_table_ddl
+------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+(1 row)
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+ pg_get_table_ddl
+---------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+(1 row)
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range ( id integer, k integer) PARTITION BY RANGE (id);
+ 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;
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(4 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_id_not_null NOT NULL id;
+ ALTER TABLE basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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_id_not_null NOT NULL id;
+ 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);
+(4 rows)
+
+-- 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);
+ ALTER TABLE id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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
+-- User-named NOT NULL constraint must be emitted inline as
+-- "CONSTRAINT name NOT NULL" so a replay does not collide with the
+-- auto-named NOT NULL that PostgreSQL would otherwise create from a
+-- plain inline "NOT NULL". The constraint loop skips the user-named
+-- entry to avoid double-emission.
+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);
+ ALTER TABLE pgtbl_ddl_test.nn_named ADD CONSTRAINT nn_named_b_not_null NOT NULL b;
+(2 rows)
+
+DROP TABLE nn_named;
+-- 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.
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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 25 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 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 rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ 'owner', 'false') WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 20 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 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 rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..75e07f3505f
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,368 @@
+--
+-- 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');
+
+-- 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 parent (RANGE and HASH).
+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');
+
+-- 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 live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS 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');
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+
+-- 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');
+
+-- 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;
+
+-- User-named NOT NULL constraint must be emitted inline as
+-- "CONSTRAINT name NOT NULL" so a replay does not collide with the
+-- auto-named NOT NULL that PostgreSQL would otherwise create from a
+-- plain inline "NOT NULL". The constraint loop skips the user-named
+-- entry to avoid double-emission.
+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;
+
+-- 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');
+
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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;
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-09 19:17 Zsolt Parragi <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Zsolt Parragi @ 2026-06-09 19:17 UTC (permalink / raw)
To: [email protected]
Thanks, I can confirm that the previous bugs were fixed, however the
bugfixes also introduce a new issue, where inherited not null
constraints are missing:
CREATE TABLE par (a int);
CREATE TABLE ch (b int) INHERITS (par);
ALTER TABLE ch ADD CONSTRAINT my_nn NOT NULL a;
SELECT * FROM pg_get_table_ddl('ch','owner','false');
-- CREATE TABLE public.ch ( b integer) INHERITS (public.par);
The schema_qualified => false part also doesn't work as described:
+ <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.
but:
CREATE SCHEMA s1;
CREATE SEQUENCE s1.myseq;
CREATE FUNCTION s1.f(int) RETURNS int LANGUAGE sql IMMUTABLE AS 'SELECT $1';
CREATE TABLE s1.t (
id int DEFAULT nextval('s1.myseq'),
val int,
CONSTRAINT chk CHECK (s1.f(val) > 0)
);
SELECT * FROM pg_get_table_ddl('s1.t', 'owner','false',
'schema_qualified','false');
-- CREATE TABLE t ( id integer DEFAULT nextval('s1.myseq'::regclass),
val integer, CONSTRAINT chk CHECK ((s1.f(val) > 0)));
s1 appears twice in the output.
It also has an issue with strings containing the schema:
CREATE SCHEMA myschema;
CREATE TABLE myschema.p (id int, note text) PARTITION BY RANGE (id);
CREATE TABLE myschema.pc PARTITION OF myschema.p
(CONSTRAINT chk CHECK (note <> 'myschema.secret')) FOR VALUES FROM
(0) TO (100);
SELECT * FROM pg_get_table_ddl('myschema.pc', 'owner','false',
'schema_qualified','false');
-- CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
-- ALTER TABLE pc ADD CONSTRAINT chk CHECK ((note <> 'secret'::text));
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-10 11:24 Akshay Joshi <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-06-10 11:24 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: [email protected]
Thanks for the review. I have fixed all the issues you mentioned.
Because this is a major restructuring of the DDL function involving
multiple child elements and complex syntaxes, some edge cases might still
pop up, though I have tried to cover as much as possible.
The v4 patch is ready for your review.
On Wed, Jun 10, 2026 at 12:47 AM Zsolt Parragi <[email protected]>
wrote:
> Thanks, I can confirm that the previous bugs were fixed, however the
> bugfixes also introduce a new issue, where inherited not null
> constraints are missing:
>
> CREATE TABLE par (a int);
> CREATE TABLE ch (b int) INHERITS (par);
> ALTER TABLE ch ADD CONSTRAINT my_nn NOT NULL a;
> SELECT * FROM pg_get_table_ddl('ch','owner','false');
> -- CREATE TABLE public.ch ( b integer) INHERITS (public.par);
>
> The schema_qualified => false part also doesn't work as described:
>
> + <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.
>
> but:
>
> CREATE SCHEMA s1;
> CREATE SEQUENCE s1.myseq;
> CREATE FUNCTION s1.f(int) RETURNS int LANGUAGE sql IMMUTABLE AS 'SELECT
> $1';
> CREATE TABLE s1.t (
> id int DEFAULT nextval('s1.myseq'),
> val int,
> CONSTRAINT chk CHECK (s1.f(val) > 0)
> );
> SELECT * FROM pg_get_table_ddl('s1.t', 'owner','false',
> 'schema_qualified','false');
> -- CREATE TABLE t ( id integer DEFAULT nextval('s1.myseq'::regclass),
> val integer, CONSTRAINT chk CHECK ((s1.f(val) > 0)));
>
> s1 appears twice in the output.
>
> It also has an issue with strings containing the schema:
>
> CREATE SCHEMA myschema;
> CREATE TABLE myschema.p (id int, note text) PARTITION BY RANGE (id);
> CREATE TABLE myschema.pc PARTITION OF myschema.p
> (CONSTRAINT chk CHECK (note <> 'myschema.secret')) FOR VALUES FROM
> (0) TO (100);
> SELECT * FROM pg_get_table_ddl('myschema.pc', 'owner','false',
> 'schema_qualified','false');
> -- CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
> -- ALTER TABLE pc ADD CONSTRAINT chk CHECK ((note <> 'secret'::text));
>
>
>
Attachments:
[application/octet-stream] v4-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (117.8K, ../../CANxoLDc6UFMsyzbXKF8SpMoXdoKE=-yRPqtvyBMKKJ2zOUKX4A@mail.gmail.com/3-v4-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From 8afd12261f6f2bb872addc4265d1b5c805b5e78e Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v4] 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_string, constraints
(PRIMARY KEY, UNIQUE, FOREIGN KEY, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_command, rules via pg_get_ruledef, extended
statistics via pg_get_statisticsobjdef_string, REPLICA IDENTITY
NOTHING/FULL/USING INDEX, ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY,
and child-local DEFAULT overrides on inheritance/partition children.
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.
A regression test under src/test/regress covers ordinary tables,
identity (default and custom sequence options), generated columns,
STORAGE/COMPRESSION, constraints (CHECK/UNIQUE/FK with deferrable),
functional and partial indexes, inheritance and partitioning,
partition children with FOR VALUES FROM/TO, WITH modulus/remainder, and
DEFAULT, rules, extended statistics, RLS toggles, REPLICA IDENTITY,
UNLOGGED with reloptions, per-column attoptions, child DEFAULT
overrides, pretty mode, owner=false, and the error paths for views,
sequences, NULL, unknown options, and odd-variadic argument counts.
Author: Akshay Joshi <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 79 +
src/backend/commands/tablecmds.c | 27 +
src/backend/utils/adt/ddlutils.c | 1800 +++++++++++++++++
src/include/catalog/pg_proc.dat | 7 +
src/include/commands/tablecmds.h | 1 +
.../regress/expected/pg_get_table_ddl.out | 700 +++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 410 ++++
8 files changed, 3025 insertions(+), 1 deletion(-)
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 00f64f50ceb..6a34ded9628 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3961,6 +3961,85 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
<literal>TABLESPACE</literal>.
</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 a family of
+ <literal>includes_<replaceable>category</replaceable></literal>
+ booleans that gate emission of optional sub-objects:
+ <literal>includes_indexes</literal>,
+ <literal>includes_constraints</literal>,
+ <literal>includes_rules</literal>,
+ <literal>includes_statistics</literal>,
+ <literal>includes_triggers</literal>,
+ <literal>includes_policies</literal>,
+ <literal>includes_rls</literal> (the
+ <command>ENABLE</command>/<command>FORCE ROW LEVEL SECURITY</command>
+ toggles), and
+ <literal>includes_replica_identity</literal>. Each of these
+ defaults to <literal>true</literal>. The
+ <literal>includes_partition</literal> option, which defaults to
+ <literal>false</literal>, controls whether the DDL for partition
+ children is appended after a partitioned-table parent.
+ </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/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a33e22e8e61..6aa401d943c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19558,6 +19558,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 f32fcd453ef..3d2deb2a327 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -21,12 +21,27 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.h"
#include "catalog/pg_auth_members.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_class.h"
+#include "catalog/dependency.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_am.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 "catalog/partition.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
+#include "rewrite/prs2lock.h"
+#include "nodes/nodes.h"
#include "commands/tablespace.h"
#include "common/relpath.h"
#include "funcapi.h"
@@ -87,6 +102,105 @@ static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool i
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, schema_prefix, 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 include_indexes;
+ bool include_constraints;
+ bool include_rules;
+ bool include_statistics;
+ bool include_triggers;
+ bool include_policies;
+ bool include_rls;
+ bool include_replica_identity;
+ bool include_partition;
+ bool schema_qualified;
+
+ /* Derived during setup */
+ Oid base_namespace;
+ char *qualname;
+ char *schema_prefix; /* NULL when schema_qualified */
+ 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(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition,
+ bool schema_qualified);
+static void append_column_defs(StringInfo buf, Relation rel, bool pretty,
+ bool include_constraints,
+ bool schema_qualified,
+ LocalNotNullEntry * nn_entries);
+static void append_typed_column_overrides(StringInfo buf, Relation rel,
+ bool pretty, bool include_constraints,
+ 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_qualified_relname(Oid relid);
+static char *lookup_relname_for_emit(Oid relid, bool schema_qualified,
+ Oid base_namespace);
+static char *strip_schema_prefix(const char *stmt, const char *prefix);
+static LocalNotNullEntry * collect_local_not_null(Relation rel,
+ List **skip_oids);
+static List *get_inheritance_parents(Oid relid);
+
+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
@@ -1185,3 +1299,1689 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
}
+
+/*
+ * get_inheritance_parents
+ * Return a List of parent OIDs for relid, ordered by inhseqno.
+ *
+ * find_inheritance_children() walks the opposite direction (parent->children),
+ * so we scan pg_inherits directly here using the (inhrelid, inhseqno) index,
+ * which yields rows in the order they need to appear in the INHERITS clause.
+ * Partition children also have a pg_inherits entry, so callers must skip the
+ * INHERITS clause when relispartition is true.
+ */
+static List *
+get_inheritance_parents(Oid relid)
+{
+ Relation inheritsRel;
+ SysScanDesc scan;
+ ScanKeyData key;
+ HeapTuple tup;
+ List *parents = NIL;
+
+ inheritsRel = table_open(InheritsRelationId, AccessShareLock);
+ ScanKeyInit(&key,
+ Anum_pg_inherits_inhrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(relid));
+ scan = systable_beginscan(inheritsRel, InheritsRelidSeqnoIndexId,
+ true, NULL, 1, &key);
+ while (HeapTupleIsValid(tup = systable_getnext(scan)))
+ {
+ Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(tup);
+
+ parents = lappend_oid(parents, inh->inhparent);
+ }
+ systable_endscan(scan);
+ table_close(inheritsRel, AccessShareLock);
+
+ return parents;
+}
+
+/*
+ * lookup_qualified_relname
+ * Return the schema-qualified, identifier-quoted name of a relation,
+ * or raise ERRCODE_UNDEFINED_OBJECT if the relation has disappeared.
+ *
+ * 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_qualified_relname(Oid relid)
+{
+ 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);
+ 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;
+}
+
+/*
+ * lookup_relname_for_emit
+ * Return either the schema-qualified or the bare quoted name of a
+ * relation, depending on the schema_qualified flag.
+ *
+ * When schema_qualified is true this is identical to
+ * lookup_qualified_relname. 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 still returned, because cross-schema references
+ * (for example an inheritance parent or foreign key target in a
+ * different schema) are not safe to strip.
+ *
+ * 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 *result;
+
+ if (schema_qualified)
+ return lookup_qualified_relname(relid);
+
+ 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);
+ if (reltup->relnamespace != base_namespace)
+ {
+ /* Cross-schema reference: qualification is required for correctness. */
+ ReleaseSysCache(tp);
+ return lookup_qualified_relname(relid);
+ }
+
+ result = pstrdup(quote_identifier(NameStr(reltup->relname)));
+ ReleaseSysCache(tp);
+
+ return result;
+}
+
+/*
+ * strip_schema_prefix
+ * Return a palloc'd copy of stmt with every occurrence of prefix
+ * removed, except occurrences that fall inside a single-quoted SQL
+ * string literal.
+ *
+ * Used to strip the qualified-schema prefix (for example "old_schema.")
+ * from DDL emitted by the always-qualified ruleutils helpers
+ * (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) when the caller has
+ * requested unqualified output via schema_qualified=false. A plain
+ * substring strip would corrupt string literals whose contents happen
+ * to contain "<schema>." (for example a CHECK comparing against
+ * 'myschema.secret'), so we track quoting state and skip matches that
+ * occur while we are inside a string. Escaped quotes ('') inside a
+ * string are handled by keeping the in-string flag set across them.
+ */
+static char *
+strip_schema_prefix(const char *stmt, const char *prefix)
+{
+ StringInfoData buf;
+ size_t plen = strlen(prefix);
+ const char *p = stmt;
+ bool in_string = false;
+
+ initStringInfo(&buf);
+ while (*p)
+ {
+ if (*p == '\'')
+ {
+ /* SQL '' escape: emit both quotes and stay inside the string. */
+ if (in_string && p[1] == '\'')
+ {
+ appendStringInfoChar(&buf, '\'');
+ appendStringInfoChar(&buf, '\'');
+ p += 2;
+ continue;
+ }
+ in_string = !in_string;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ if (!in_string && strncmp(p, prefix, plen) == 0)
+ {
+ p += plen;
+ continue;
+ }
+ appendStringInfoChar(&buf, *p);
+ p++;
+ }
+ return buf.data;
+}
+
+/*
+ * 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 "CONSTRAINT name NOT NULL" only happens for
+ * columns that the column list actually emits, i.e. attislocal
+ * columns. For a locally-declared NOT NULL sitting on an inherited
+ * column the inline path never fires, so we must leave the constraint
+ * OID out of skip_oids and let the post-CREATE constraint loop emit
+ * ALTER TABLE ... ADD CONSTRAINT.
+ */
+ if (!entries[attnum].is_auto && 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, ',');
+ *first = false;
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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_constraints,
+ 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 emitted by the INHERITS clause
+ * (once implemented), not the column list, unless the child
+ * redeclared them locally (attislocal=true).
+ */
+ if (!att->attislocal)
+ continue;
+
+ if (!first)
+ appendStringInfoChar(buf, ',');
+ first = false;
+
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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))
+ {
+ const char *storage = NULL;
+
+ switch (att->attstorage)
+ {
+ case TYPSTORAGE_PLAIN:
+ storage = "PLAIN";
+ break;
+ case TYPSTORAGE_EXTERNAL:
+ storage = "EXTERNAL";
+ break;
+ case TYPSTORAGE_MAIN:
+ storage = "MAIN";
+ break;
+ case TYPSTORAGE_EXTENDED:
+ storage = "EXTENDED";
+ break;
+ }
+ if (storage)
+ appendStringInfo(buf, " STORAGE %s", storage);
+ }
+
+ /* COMPRESSION clause, only if explicitly set on the column. */
+ if (CompressionMethodIsValid(att->attcompression))
+ {
+ const char *cm = NULL;
+
+ switch (att->attcompression)
+ {
+ case TOAST_PGLZ_COMPRESSION:
+ cm = "pglz";
+ break;
+ case TOAST_LZ4_COMPRESSION:
+ cm = "lz4";
+ break;
+ }
+ if (cm)
+ appendStringInfo(buf, " COMPRESSION %s", cm);
+ }
+
+ /*
+ * 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_constraints)
+ 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_constraints,
+ 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, ',');
+ first = false;
+ if (pretty)
+ appendStringInfoString(&inner, "\n ");
+ else
+ appendStringInfoChar(&inner, ' ');
+
+ 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_constraints)
+ 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 / append_stripped_stmt
+ * Push ctx->buf onto ctx->statements.
+ *
+ * append_stmt is used for emissions where the table reference is built
+ * from ctx->qualname (already partial-qualified per schema_qualified).
+ * append_stripped_stmt is used after invoking always-qualified ruleutils
+ * helpers (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) — when
+ * schema_qualified is false, the base-schema prefix is stripped from
+ * their output so the emitted DDL is replay-relocatable.
+ */
+static void
+append_stmt(TableDdlContext * ctx)
+{
+ ctx->statements = lappend(ctx->statements, pstrdup(ctx->buf.data));
+}
+
+static void
+append_stripped_stmt(TableDdlContext * ctx)
+{
+ if (ctx->schema_prefix != NULL)
+ ctx->statements = lappend(ctx->statements,
+ strip_schema_prefix(ctx->buf.data,
+ ctx->schema_prefix));
+ else
+ 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,
+ ctx->include_constraints,
+ ctx->nn_entries);
+ }
+ else
+ {
+ List *parents;
+ ListCell *lc;
+ bool first;
+
+ appendStringInfoString(&ctx->buf, " (");
+
+ append_column_defs(&ctx->buf, rel, ctx->pretty,
+ ctx->include_constraints,
+ ctx->schema_qualified, ctx->nn_entries);
+
+ if (ctx->pretty)
+ appendStringInfoString(&ctx->buf, "\n)");
+ else
+ appendStringInfoChar(&ctx->buf, ')');
+
+ parents = get_inheritance_parents(ctx->relid);
+ 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 (!ctx->include_indexes)
+ 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_string(idxoid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", idxdef);
+ append_stripped_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). User-named NOT NULL
+ * constraints are emitted inline by the column-emit helpers and
+ * are skipped via skip_notnull_oids; auto-named NOT NULLs fall
+ * through here and are dedup'd by name at replay.
+ */
+static void
+emit_local_constraints(TableDdlContext * ctx)
+{
+ Relation conRel;
+ SysScanDesc conScan;
+ ScanKeyData conKey;
+ HeapTuple conTup;
+ bool is_partition = ctx->rel->rd_rel->relispartition;
+
+ if (!ctx->include_constraints)
+ return;
+
+ 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);
+ char *condef;
+
+ if (!con->conislocal)
+ continue;
+ if (con->contype == CONSTRAINT_CHECK && !is_partition)
+ continue;
+ if (con->contype == CONSTRAINT_NOTNULL && !is_partition &&
+ list_member_oid(ctx->skip_notnull_oids, con->oid))
+ continue;
+
+ condef = pg_get_constraintdef_command(con->oid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", condef);
+ append_stripped_stmt(ctx);
+ pfree(condef);
+ }
+ 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 (!ctx->include_rules || 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;
+ Datum ruledef;
+ char *ruledef_str;
+
+ ruledef = OidFunctionCall1(F_PG_GET_RULEDEF_OID,
+ ObjectIdGetDatum(ruleid));
+ ruledef_str = TextDatumGetCString(ruledef);
+ resetStringInfo(&ctx->buf);
+ appendStringInfoString(&ctx->buf, ruledef_str);
+ append_stripped_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 (!ctx->include_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_string(stat->oid);
+
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", statdef);
+ append_stripped_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 (!ctx->include_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 (!ctx->include_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 (!ctx->include_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);
+ List *childstmts;
+
+ childstmts = pg_get_table_ddl_internal(childoid, ctx->pretty,
+ ctx->no_owner,
+ ctx->no_tablespace,
+ ctx->include_indexes,
+ ctx->include_constraints,
+ ctx->include_rules,
+ ctx->include_statistics,
+ ctx->include_triggers,
+ ctx->include_policies,
+ ctx->include_rls,
+ ctx->include_replica_identity,
+ ctx->include_partition,
+ ctx->schema_qualified);
+ 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 first list element is the CREATE TABLE statement. Subsequent
+ * elements are the ALTER TABLE / CREATE INDEX / CREATE RULE /
+ * CREATE STATISTICS statements needed to restore the table's full
+ * definition.
+ *
+ * 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(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition,
+ bool schema_qualified)
+{
+ TableDdlContext ctx = {0};
+ Relation rel;
+ char relkind;
+ AclResult aclresult;
+
+ rel = table_open(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(relid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLE,
+ RelationGetRelationName(rel));
+
+ ctx.rel = rel;
+ ctx.relid = relid;
+ ctx.pretty = pretty;
+ ctx.no_owner = no_owner;
+ ctx.no_tablespace = no_tablespace;
+ ctx.include_indexes = include_indexes;
+ ctx.include_constraints = include_constraints;
+ ctx.include_rules = include_rules;
+ ctx.include_statistics = include_statistics;
+ ctx.include_triggers = include_triggers;
+ ctx.include_policies = include_policies;
+ ctx.include_rls = include_rls;
+ ctx.include_replica_identity = include_replica_identity;
+ ctx.include_partition = include_partition;
+ ctx.schema_qualified = schema_qualified;
+ ctx.base_namespace = RelationGetNamespace(rel);
+ ctx.save_nestlevel = -1;
+ ctx.qualname = lookup_relname_for_emit(relid, 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, etc. come out unqualified. 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.
+ *
+ * Pre-compute "<schema>." too — the always-qualified helpers
+ * (constraintdef_command, indexdef_string, ruledef,
+ * statisticsobjdef_string) ignore search_path for the target's own "ALTER
+ * TABLE schema.tbl"/"ON schema.tbl"/"TO schema.tbl" prefix and for the
+ * qualified statistics name; we strip that string ourselves via
+ * strip_schema_prefix.
+ */
+ if (!schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx.base_namespace);
+
+ if (nspname != NULL)
+ {
+ const char *qnsp = quote_identifier(nspname);
+
+ ctx.schema_prefix = psprintf("%s.", qnsp);
+ 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);
+
+ /*
+ * Each pass below appends zero or more finished statements to
+ * ctx.statements via append_stmt / append_stripped_stmt. 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); and partition children
+ * last so the parent already exists at replay time.
+ */
+ 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 (include_triggers)
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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 (include_policies)
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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);
+ }
+#else
+ (void) include_triggers;
+ (void) include_policies;
+#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);
+ if (ctx.schema_prefix != NULL)
+ pfree(ctx.schema_prefix);
+ 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;
+ Oid relid;
+ DdlOption opts[] = {
+ {"pretty", DDL_OPT_BOOL},
+ {"owner", DDL_OPT_BOOL},
+ {"tablespace", DDL_OPT_BOOL},
+ {"includes_indexes", DDL_OPT_BOOL},
+ {"includes_constraints", DDL_OPT_BOOL},
+ {"includes_rules", DDL_OPT_BOOL},
+ {"includes_statistics", DDL_OPT_BOOL},
+ {"includes_triggers", DDL_OPT_BOOL},
+ {"includes_policies", DDL_OPT_BOOL},
+ {"includes_rls", DDL_OPT_BOOL},
+ {"includes_replica_identity", DDL_OPT_BOOL},
+ {"includes_partition", DDL_OPT_BOOL},
+ {"schema_qualified", DDL_OPT_BOOL},
+ };
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ relid = PG_GETARG_OID(0);
+ parse_ddl_options(fcinfo, 1, opts, lengthof(opts));
+
+ statements = pg_get_table_ddl_internal(relid,
+ opts[0].isset && opts[0].boolval,
+ opts[1].isset && !opts[1].boolval,
+ opts[2].isset && !opts[2].boolval,
+ !opts[3].isset || opts[3].boolval,
+ !opts[4].isset || opts[4].boolval,
+ !opts[5].isset || opts[5].boolval,
+ !opts[6].isset || opts[6].boolval,
+ !opts[7].isset || opts[7].boolval,
+ !opts[8].isset || opts[8].boolval,
+ !opts[9].isset || opts[9].boolval,
+ !opts[10].isset || opts[10].boolval,
+ opts[11].isset && opts[11].boolval,
+ !opts[12].isset || opts[12].boolval);
+ 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/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..e456c226f53 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,13 @@
proargtypes => 'regdatabase text', proallargtypes => '{regdatabase,text}',
proargmodes => '{i,v}', proargdefaults => '{NULL}',
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..2085027c9b1 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);
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..fe522c479e7
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,700 @@
+--
+-- 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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);
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_always_not_null NOT NULL id_always;
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_default_not_null NOT NULL id_default;
+(3 rows)
+
+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);
+ ALTER TABLE pgtbl_ddl_test.id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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)
+
+-- 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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(5 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 parent (RANGE and HASH).
+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)
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0);
+(1 row)
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(1 row)
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx ( a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx ADD CONSTRAINT ri_idx_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(4 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)));
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_b_not_null NOT NULL b;
+(3 rows)
+
+-- Temporary tables + ON COMMIT. Temp tables live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_default ( id integer);
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_delete ( id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_drop'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+ 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+(1 row)
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+ pg_get_table_ddl
+------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+(1 row)
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+ pg_get_table_ddl
+---------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+(1 row)
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range ( id integer, k integer) PARTITION BY RANGE (id);
+ 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;
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(4 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_id_not_null NOT NULL id;
+ ALTER TABLE basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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_id_not_null NOT NULL id;
+ 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);
+(4 rows)
+
+-- 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);
+ ALTER TABLE id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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
+-- User-named NOT NULL constraint must be emitted inline as
+-- "CONSTRAINT name NOT NULL" so a replay does not collide with the
+-- auto-named NOT NULL that PostgreSQL would otherwise create from a
+-- plain inline "NOT NULL". The constraint loop skips the user-named
+-- entry to avoid double-emission.
+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);
+ ALTER TABLE pgtbl_ddl_test.nn_named ADD CONSTRAINT nn_named_b_not_null NOT NULL b;
+(2 rows)
+
+DROP TABLE nn_named;
+-- 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
+-- 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.
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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 25 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 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 rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ 'owner', 'false') WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 20 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 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 rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..f632ffd38c2
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,410 @@
+--
+-- 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');
+
+-- 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 parent (RANGE and HASH).
+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');
+
+-- 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 live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS 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');
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+
+-- 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');
+
+-- 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;
+
+-- User-named NOT NULL constraint must be emitted inline as
+-- "CONSTRAINT name NOT NULL" so a replay does not collide with the
+-- auto-named NOT NULL that PostgreSQL would otherwise create from a
+-- plain inline "NOT NULL". The constraint loop skips the user-named
+-- entry to avoid double-emission.
+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;
+
+-- 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;
+
+-- 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');
+
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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;
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-10 20:43 Zsolt Parragi <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Zsolt Parragi @ 2026-06-10 20:43 UTC (permalink / raw)
To: [email protected]
Thanks for the update! The new version looks mostly good, I only found
one corner case that doesn't work, double quoted literals can still
get over-stripped:
CREATE SCHEMA s;
CREATE TABLE s.p (id int, "s.weird" int) PARTITION BY RANGE (id);
CREATE TABLE s.pc PARTITION OF s.p
(CONSTRAINT chk CHECK ("s.weird" > 0)) FOR VALUES FROM (0) TO (100);
SELECT * FROM pg_get_table_ddl('s.pc', 'owner', 'false',
'schema_qualified', 'false');
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-11 07:48 Akshay Joshi <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 3 replies; 54+ messages in thread
From: Akshay Joshi @ 2026-06-11 07:48 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: [email protected]
On Thu, Jun 11, 2026 at 2:13 AM Zsolt Parragi <[email protected]>
wrote:
> Thanks for the update! The new version looks mostly good, I only found
> one corner case that doesn't work, double quoted literals can still
> get over-stripped:
>
> CREATE SCHEMA s;
> CREATE TABLE s.p (id int, "s.weird" int) PARTITION BY RANGE (id);
> CREATE TABLE s.pc PARTITION OF s.p
> (CONSTRAINT chk CHECK ("s.weird" > 0)) FOR VALUES FROM (0) TO (100);
> SELECT * FROM pg_get_table_ddl('s.pc', 'owner', 'false',
> 'schema_qualified', 'false');
>
Fixed the issue above. The v5 patch is ready for review/testing.
Attachments:
[application/octet-stream] v5-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (122.6K, ../../CANxoLDco7z7Fj7_pj_3YQLvjq8ZH2Ai4c3GcOamed01fZhJ77Q@mail.gmail.com/3-v5-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From dc90c22ba99dd48c27e0223954215a326ee7b34c Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v5] 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_string, constraints
(PRIMARY KEY, UNIQUE, FOREIGN KEY, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_command, rules via pg_get_ruledef, extended
statistics via pg_get_statisticsobjdef_string, REPLICA IDENTITY
NOTHING/FULL/USING INDEX, ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY,
and child-local DEFAULT overrides on inheritance/partition children.
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.
A regression test under src/test/regress covers ordinary tables,
identity (default and custom sequence options), generated columns,
STORAGE/COMPRESSION, constraints (CHECK/UNIQUE/FK with deferrable),
functional and partial indexes, inheritance and partitioning,
partition children with FOR VALUES FROM/TO, WITH modulus/remainder, and
DEFAULT, rules, extended statistics, RLS toggles, REPLICA IDENTITY,
UNLOGGED with reloptions, per-column attoptions, child DEFAULT
overrides, pretty mode, owner=false, and the error paths for views,
sequences, NULL, unknown options, and odd-variadic argument counts.
Author: Akshay Joshi <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 79 +
src/backend/commands/tablecmds.c | 27 +
src/backend/utils/adt/ddlutils.c | 1830 +++++++++++++++++
src/include/catalog/pg_proc.dat | 7 +
src/include/commands/tablecmds.h | 1 +
.../regress/expected/pg_get_table_ddl.out | 742 +++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 440 ++++
8 files changed, 3127 insertions(+), 1 deletion(-)
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 00f64f50ceb..6a34ded9628 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3961,6 +3961,85 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
<literal>TABLESPACE</literal>.
</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 a family of
+ <literal>includes_<replaceable>category</replaceable></literal>
+ booleans that gate emission of optional sub-objects:
+ <literal>includes_indexes</literal>,
+ <literal>includes_constraints</literal>,
+ <literal>includes_rules</literal>,
+ <literal>includes_statistics</literal>,
+ <literal>includes_triggers</literal>,
+ <literal>includes_policies</literal>,
+ <literal>includes_rls</literal> (the
+ <command>ENABLE</command>/<command>FORCE ROW LEVEL SECURITY</command>
+ toggles), and
+ <literal>includes_replica_identity</literal>. Each of these
+ defaults to <literal>true</literal>. The
+ <literal>includes_partition</literal> option, which defaults to
+ <literal>false</literal>, controls whether the DDL for partition
+ children is appended after a partitioned-table parent.
+ </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/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a33e22e8e61..6aa401d943c 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19558,6 +19558,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 f32fcd453ef..54abc7249c1 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -21,12 +21,27 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.h"
#include "catalog/pg_auth_members.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_class.h"
+#include "catalog/dependency.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_am.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 "catalog/partition.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
+#include "rewrite/prs2lock.h"
+#include "nodes/nodes.h"
#include "commands/tablespace.h"
#include "common/relpath.h"
#include "funcapi.h"
@@ -87,6 +102,105 @@ static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool i
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, schema_prefix, 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 include_indexes;
+ bool include_constraints;
+ bool include_rules;
+ bool include_statistics;
+ bool include_triggers;
+ bool include_policies;
+ bool include_rls;
+ bool include_replica_identity;
+ bool include_partition;
+ bool schema_qualified;
+
+ /* Derived during setup */
+ Oid base_namespace;
+ char *qualname;
+ char *schema_prefix; /* NULL when schema_qualified */
+ 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(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition,
+ bool schema_qualified);
+static void append_column_defs(StringInfo buf, Relation rel, bool pretty,
+ bool include_constraints,
+ bool schema_qualified,
+ LocalNotNullEntry * nn_entries);
+static void append_typed_column_overrides(StringInfo buf, Relation rel,
+ bool pretty, bool include_constraints,
+ 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_qualified_relname(Oid relid);
+static char *lookup_relname_for_emit(Oid relid, bool schema_qualified,
+ Oid base_namespace);
+static char *strip_schema_prefix(const char *stmt, const char *prefix);
+static LocalNotNullEntry * collect_local_not_null(Relation rel,
+ List **skip_oids);
+static List *get_inheritance_parents(Oid relid);
+
+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
@@ -1185,3 +1299,1719 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
}
+
+/*
+ * get_inheritance_parents
+ * Return a List of parent OIDs for relid, ordered by inhseqno.
+ *
+ * find_inheritance_children() walks the opposite direction (parent->children),
+ * so we scan pg_inherits directly here using the (inhrelid, inhseqno) index,
+ * which yields rows in the order they need to appear in the INHERITS clause.
+ * Partition children also have a pg_inherits entry, so callers must skip the
+ * INHERITS clause when relispartition is true.
+ */
+static List *
+get_inheritance_parents(Oid relid)
+{
+ Relation inheritsRel;
+ SysScanDesc scan;
+ ScanKeyData key;
+ HeapTuple tup;
+ List *parents = NIL;
+
+ inheritsRel = table_open(InheritsRelationId, AccessShareLock);
+ ScanKeyInit(&key,
+ Anum_pg_inherits_inhrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(relid));
+ scan = systable_beginscan(inheritsRel, InheritsRelidSeqnoIndexId,
+ true, NULL, 1, &key);
+ while (HeapTupleIsValid(tup = systable_getnext(scan)))
+ {
+ Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(tup);
+
+ parents = lappend_oid(parents, inh->inhparent);
+ }
+ systable_endscan(scan);
+ table_close(inheritsRel, AccessShareLock);
+
+ return parents;
+}
+
+/*
+ * lookup_qualified_relname
+ * Return the schema-qualified, identifier-quoted name of a relation,
+ * or raise ERRCODE_UNDEFINED_OBJECT if the relation has disappeared.
+ *
+ * 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_qualified_relname(Oid relid)
+{
+ 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);
+ 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;
+}
+
+/*
+ * lookup_relname_for_emit
+ * Return either the schema-qualified or the bare quoted name of a
+ * relation, depending on the schema_qualified flag.
+ *
+ * When schema_qualified is true this is identical to
+ * lookup_qualified_relname. 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 still returned, because cross-schema references
+ * (for example an inheritance parent or foreign key target in a
+ * different schema) are not safe to strip.
+ *
+ * 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 *result;
+
+ if (schema_qualified)
+ return lookup_qualified_relname(relid);
+
+ 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);
+ if (reltup->relnamespace != base_namespace)
+ {
+ /* Cross-schema reference: qualification is required for correctness. */
+ ReleaseSysCache(tp);
+ return lookup_qualified_relname(relid);
+ }
+
+ result = pstrdup(quote_identifier(NameStr(reltup->relname)));
+ ReleaseSysCache(tp);
+
+ return result;
+}
+
+/*
+ * strip_schema_prefix
+ * Return a palloc'd copy of stmt with every occurrence of prefix
+ * removed, except occurrences that fall inside a single-quoted SQL
+ * string literal or a double-quoted SQL identifier.
+ *
+ * Used to strip the qualified-schema prefix (for example "old_schema.")
+ * from DDL emitted by the always-qualified ruleutils helpers
+ * (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) when the caller has
+ * requested unqualified output via schema_qualified=false. A plain
+ * substring strip would corrupt string literals whose contents happen
+ * to contain "<schema>." (for example a CHECK comparing against
+ * 'myschema.secret') or double-quoted column/identifier names whose
+ * contents happen to contain "<schema>." (for example a column named
+ * "s.weird" when stripping the prefix "s."), so we track quoting state
+ * and skip matches that occur while we are inside either kind of
+ * quoted token. Escaped quotes ('' inside a string, "" inside an
+ * identifier) are handled by keeping the corresponding flag set across
+ * them.
+ */
+static char *
+strip_schema_prefix(const char *stmt, const char *prefix)
+{
+ StringInfoData buf;
+ size_t plen = strlen(prefix);
+ const char *p = stmt;
+ bool in_string = false;
+ bool in_ident = false;
+
+ initStringInfo(&buf);
+ while (*p)
+ {
+ /*
+ * Try the prefix match BEFORE the quote-state toggles. The
+ * prefix itself may begin with a double quote when the base
+ * schema name requires quoting (e.g. "My-Schema".), and at that
+ * leading " we must strip the whole prefix rather than treat it
+ * as the opening of a regular quoted identifier. The check is
+ * gated on !in_ident && !in_string, so once we have toggled
+ * into a quoted token we never strip a coincidental substring
+ * inside it.
+ */
+ if (!in_string && !in_ident && strncmp(p, prefix, plen) == 0)
+ {
+ p += plen;
+ continue;
+ }
+ if (*p == '\'' && !in_ident)
+ {
+ /* SQL '' escape: emit both quotes and stay inside the string. */
+ if (in_string && p[1] == '\'')
+ {
+ appendStringInfoChar(&buf, '\'');
+ appendStringInfoChar(&buf, '\'');
+ p += 2;
+ continue;
+ }
+ in_string = !in_string;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ if (*p == '"' && !in_string)
+ {
+ /* SQL "" escape: emit both quotes and stay inside the ident. */
+ if (in_ident && p[1] == '"')
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoChar(&buf, '"');
+ p += 2;
+ continue;
+ }
+ in_ident = !in_ident;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ appendStringInfoChar(&buf, *p);
+ p++;
+ }
+ return buf.data;
+}
+
+/*
+ * 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 "CONSTRAINT name NOT NULL" only happens for
+ * columns that the column list actually emits, i.e. attislocal
+ * columns. For a locally-declared NOT NULL sitting on an inherited
+ * column the inline path never fires, so we must leave the constraint
+ * OID out of skip_oids and let the post-CREATE constraint loop emit
+ * ALTER TABLE ... ADD CONSTRAINT.
+ */
+ if (!entries[attnum].is_auto && 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, ',');
+ *first = false;
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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_constraints,
+ 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 emitted by the INHERITS clause
+ * (once implemented), not the column list, unless the child
+ * redeclared them locally (attislocal=true).
+ */
+ if (!att->attislocal)
+ continue;
+
+ if (!first)
+ appendStringInfoChar(buf, ',');
+ first = false;
+
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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))
+ {
+ const char *storage = NULL;
+
+ switch (att->attstorage)
+ {
+ case TYPSTORAGE_PLAIN:
+ storage = "PLAIN";
+ break;
+ case TYPSTORAGE_EXTERNAL:
+ storage = "EXTERNAL";
+ break;
+ case TYPSTORAGE_MAIN:
+ storage = "MAIN";
+ break;
+ case TYPSTORAGE_EXTENDED:
+ storage = "EXTENDED";
+ break;
+ }
+ if (storage)
+ appendStringInfo(buf, " STORAGE %s", storage);
+ }
+
+ /* COMPRESSION clause, only if explicitly set on the column. */
+ if (CompressionMethodIsValid(att->attcompression))
+ {
+ const char *cm = NULL;
+
+ switch (att->attcompression)
+ {
+ case TOAST_PGLZ_COMPRESSION:
+ cm = "pglz";
+ break;
+ case TOAST_LZ4_COMPRESSION:
+ cm = "lz4";
+ break;
+ }
+ if (cm)
+ appendStringInfo(buf, " COMPRESSION %s", cm);
+ }
+
+ /*
+ * 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_constraints)
+ 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_constraints,
+ 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, ',');
+ first = false;
+ if (pretty)
+ appendStringInfoString(&inner, "\n ");
+ else
+ appendStringInfoChar(&inner, ' ');
+
+ 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_constraints)
+ 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 / append_stripped_stmt
+ * Push ctx->buf onto ctx->statements.
+ *
+ * append_stmt is used for emissions where the table reference is built
+ * from ctx->qualname (already partial-qualified per schema_qualified).
+ * append_stripped_stmt is used after invoking always-qualified ruleutils
+ * helpers (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) — when
+ * schema_qualified is false, the base-schema prefix is stripped from
+ * their output so the emitted DDL is replay-relocatable.
+ */
+static void
+append_stmt(TableDdlContext * ctx)
+{
+ ctx->statements = lappend(ctx->statements, pstrdup(ctx->buf.data));
+}
+
+static void
+append_stripped_stmt(TableDdlContext * ctx)
+{
+ if (ctx->schema_prefix != NULL)
+ ctx->statements = lappend(ctx->statements,
+ strip_schema_prefix(ctx->buf.data,
+ ctx->schema_prefix));
+ else
+ 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,
+ ctx->include_constraints,
+ ctx->nn_entries);
+ }
+ else
+ {
+ List *parents;
+ ListCell *lc;
+ bool first;
+
+ appendStringInfoString(&ctx->buf, " (");
+
+ append_column_defs(&ctx->buf, rel, ctx->pretty,
+ ctx->include_constraints,
+ ctx->schema_qualified, ctx->nn_entries);
+
+ if (ctx->pretty)
+ appendStringInfoString(&ctx->buf, "\n)");
+ else
+ appendStringInfoChar(&ctx->buf, ')');
+
+ parents = get_inheritance_parents(ctx->relid);
+ 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 (!ctx->include_indexes)
+ 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_string(idxoid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", idxdef);
+ append_stripped_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). User-named NOT NULL
+ * constraints are emitted inline by the column-emit helpers and
+ * are skipped via skip_notnull_oids; auto-named NOT NULLs fall
+ * through here and are dedup'd by name at replay.
+ */
+static void
+emit_local_constraints(TableDdlContext * ctx)
+{
+ Relation conRel;
+ SysScanDesc conScan;
+ ScanKeyData conKey;
+ HeapTuple conTup;
+ bool is_partition = ctx->rel->rd_rel->relispartition;
+
+ if (!ctx->include_constraints)
+ return;
+
+ 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);
+ char *condef;
+
+ if (!con->conislocal)
+ continue;
+ if (con->contype == CONSTRAINT_CHECK && !is_partition)
+ continue;
+ if (con->contype == CONSTRAINT_NOTNULL && !is_partition &&
+ list_member_oid(ctx->skip_notnull_oids, con->oid))
+ continue;
+
+ condef = pg_get_constraintdef_command(con->oid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", condef);
+ append_stripped_stmt(ctx);
+ pfree(condef);
+ }
+ 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 (!ctx->include_rules || 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;
+ Datum ruledef;
+ char *ruledef_str;
+
+ ruledef = OidFunctionCall1(F_PG_GET_RULEDEF_OID,
+ ObjectIdGetDatum(ruleid));
+ ruledef_str = TextDatumGetCString(ruledef);
+ resetStringInfo(&ctx->buf);
+ appendStringInfoString(&ctx->buf, ruledef_str);
+ append_stripped_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 (!ctx->include_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_string(stat->oid);
+
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", statdef);
+ append_stripped_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 (!ctx->include_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 (!ctx->include_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 (!ctx->include_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);
+ List *childstmts;
+
+ childstmts = pg_get_table_ddl_internal(childoid, ctx->pretty,
+ ctx->no_owner,
+ ctx->no_tablespace,
+ ctx->include_indexes,
+ ctx->include_constraints,
+ ctx->include_rules,
+ ctx->include_statistics,
+ ctx->include_triggers,
+ ctx->include_policies,
+ ctx->include_rls,
+ ctx->include_replica_identity,
+ ctx->include_partition,
+ ctx->schema_qualified);
+ 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 first list element is the CREATE TABLE statement. Subsequent
+ * elements are the ALTER TABLE / CREATE INDEX / CREATE RULE /
+ * CREATE STATISTICS statements needed to restore the table's full
+ * definition.
+ *
+ * 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(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition,
+ bool schema_qualified)
+{
+ TableDdlContext ctx = {0};
+ Relation rel;
+ char relkind;
+ AclResult aclresult;
+
+ rel = table_open(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(relid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLE,
+ RelationGetRelationName(rel));
+
+ ctx.rel = rel;
+ ctx.relid = relid;
+ ctx.pretty = pretty;
+ ctx.no_owner = no_owner;
+ ctx.no_tablespace = no_tablespace;
+ ctx.include_indexes = include_indexes;
+ ctx.include_constraints = include_constraints;
+ ctx.include_rules = include_rules;
+ ctx.include_statistics = include_statistics;
+ ctx.include_triggers = include_triggers;
+ ctx.include_policies = include_policies;
+ ctx.include_rls = include_rls;
+ ctx.include_replica_identity = include_replica_identity;
+ ctx.include_partition = include_partition;
+ ctx.schema_qualified = schema_qualified;
+ ctx.base_namespace = RelationGetNamespace(rel);
+ ctx.save_nestlevel = -1;
+ ctx.qualname = lookup_relname_for_emit(relid, 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, etc. come out unqualified. 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.
+ *
+ * Pre-compute "<schema>." too — the always-qualified helpers
+ * (constraintdef_command, indexdef_string, ruledef,
+ * statisticsobjdef_string) ignore search_path for the target's own "ALTER
+ * TABLE schema.tbl"/"ON schema.tbl"/"TO schema.tbl" prefix and for the
+ * qualified statistics name; we strip that string ourselves via
+ * strip_schema_prefix.
+ */
+ if (!schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx.base_namespace);
+
+ if (nspname != NULL)
+ {
+ const char *qnsp = quote_identifier(nspname);
+
+ ctx.schema_prefix = psprintf("%s.", qnsp);
+ 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);
+
+ /*
+ * Each pass below appends zero or more finished statements to
+ * ctx.statements via append_stmt / append_stripped_stmt. 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); and partition children
+ * last so the parent already exists at replay time.
+ */
+ 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 (include_triggers)
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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 (include_policies)
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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);
+ }
+#else
+ (void) include_triggers;
+ (void) include_policies;
+#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);
+ if (ctx.schema_prefix != NULL)
+ pfree(ctx.schema_prefix);
+ 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;
+ Oid relid;
+ DdlOption opts[] = {
+ {"pretty", DDL_OPT_BOOL},
+ {"owner", DDL_OPT_BOOL},
+ {"tablespace", DDL_OPT_BOOL},
+ {"includes_indexes", DDL_OPT_BOOL},
+ {"includes_constraints", DDL_OPT_BOOL},
+ {"includes_rules", DDL_OPT_BOOL},
+ {"includes_statistics", DDL_OPT_BOOL},
+ {"includes_triggers", DDL_OPT_BOOL},
+ {"includes_policies", DDL_OPT_BOOL},
+ {"includes_rls", DDL_OPT_BOOL},
+ {"includes_replica_identity", DDL_OPT_BOOL},
+ {"includes_partition", DDL_OPT_BOOL},
+ {"schema_qualified", DDL_OPT_BOOL},
+ };
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ relid = PG_GETARG_OID(0);
+ parse_ddl_options(fcinfo, 1, opts, lengthof(opts));
+
+ statements = pg_get_table_ddl_internal(relid,
+ opts[0].isset && opts[0].boolval,
+ opts[1].isset && !opts[1].boolval,
+ opts[2].isset && !opts[2].boolval,
+ !opts[3].isset || opts[3].boolval,
+ !opts[4].isset || opts[4].boolval,
+ !opts[5].isset || opts[5].boolval,
+ !opts[6].isset || opts[6].boolval,
+ !opts[7].isset || opts[7].boolval,
+ !opts[8].isset || opts[8].boolval,
+ !opts[9].isset || opts[9].boolval,
+ !opts[10].isset || opts[10].boolval,
+ opts[11].isset && opts[11].boolval,
+ !opts[12].isset || opts[12].boolval);
+ 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/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..e456c226f53 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,13 @@
proargtypes => 'regdatabase text', proallargtypes => '{regdatabase,text}',
proargmodes => '{i,v}', proargdefaults => '{NULL}',
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..2085027c9b1 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);
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..031c4d9f130
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,742 @@
+--
+-- 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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);
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_always_not_null NOT NULL id_always;
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_default_not_null NOT NULL id_default;
+(3 rows)
+
+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);
+ ALTER TABLE pgtbl_ddl_test.id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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)
+
+-- 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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(5 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 parent (RANGE and HASH).
+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)
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0);
+(1 row)
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(1 row)
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx ( a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx ADD CONSTRAINT ri_idx_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(4 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)));
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_b_not_null NOT NULL b;
+(3 rows)
+
+-- Temporary tables + ON COMMIT. Temp tables live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_default ( id integer);
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_delete ( id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_drop'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+ 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+(1 row)
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+ pg_get_table_ddl
+------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+(1 row)
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+ pg_get_table_ddl
+---------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+(1 row)
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range ( id integer, k integer) PARTITION BY RANGE (id);
+ 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;
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(4 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_id_not_null NOT NULL id;
+ ALTER TABLE basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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_id_not_null NOT NULL id;
+ 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);
+(4 rows)
+
+-- 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);
+ ALTER TABLE id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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
+-- User-named NOT NULL constraint must be emitted inline as
+-- "CONSTRAINT name NOT NULL" so a replay does not collide with the
+-- auto-named NOT NULL that PostgreSQL would otherwise create from a
+-- plain inline "NOT NULL". The constraint loop skips the user-named
+-- entry to avoid double-emission.
+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);
+ ALTER TABLE pgtbl_ddl_test.nn_named ADD CONSTRAINT nn_named_b_not_null NOT NULL b;
+(2 rows)
+
+DROP TABLE nn_named;
+-- 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.
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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 25 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 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 rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ 'owner', 'false') WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 20 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 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 rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..b81ff6dc4b1
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,440 @@
+--
+-- 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');
+
+-- 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 parent (RANGE and HASH).
+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');
+
+-- 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 live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS 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');
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+
+-- 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');
+
+-- 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;
+
+-- User-named NOT NULL constraint must be emitted inline as
+-- "CONSTRAINT name NOT NULL" so a replay does not collide with the
+-- auto-named NOT NULL that PostgreSQL would otherwise create from a
+-- plain inline "NOT NULL". The constraint loop skips the user-named
+-- entry to avoid double-emission.
+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;
+
+-- 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');
+
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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;
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-11 13:07 Marcos Pegoraro <[email protected]>
parent: Akshay Joshi <[email protected]>
2 siblings, 1 reply; 54+ messages in thread
From: Marcos Pegoraro @ 2026-06-11 13:07 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
Em qui., 11 de jun. de 2026 às 04:48, Akshay Joshi <
[email protected]> escreveu:
> Fixed the issue above. The v5 patch is ready for review/testing.
>
One thing I noticed, though I'm not sure if it's the point here, is that
it's not possible to extract only the foreign keys or only the triggers
from the table. So if we want to extract the objects independently by type,
we would need to have all the return types as optional, and we could have
more granularity in the return types.
Just like you have...
if (!ctx->include_indexes)
You could have too
+ if (!ctx->include_create_table)
+ if (!ctx->include_foreign_keys)
+ if (!ctx->include_primary_keys)
Because only in this way can we more or less execute the dump behavior
here, which is to create all the tables beforehand, then primary keys, then
foreign keys, then triggers.
I repeat, sorry if this is not the function's intended purpose.
regards
Marcos
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-11 21:00 Zsolt Parragi <[email protected]>
parent: Akshay Joshi <[email protected]>
2 siblings, 0 replies; 54+ messages in thread
From: Zsolt Parragi @ 2026-06-11 21:00 UTC (permalink / raw)
To: [email protected]
The new version seem to work correctly to me, I didn't find any
further issues. Now the main blockers seem to be the remaining TODOs
related to includes_triggers/includes_policies.
I only have some minor comments about code structuring:
+ * get_inheritance_parents
+ * Return a List of parent OIDs for relid, ordered by inhseqno.
+ *
+ * find_inheritance_children() walks the opposite direction (parent->children),
Shouldn't this follow the same naming and parameter pattern and live
at the same place in pg_inherits?
+static char *
+lookup_qualified_relname(Oid relid)
+...
+static char *
+lookup_relname_for_emit(Oid relid, bool schema_qualified, Oid base_namespace)
Is lookup_qualified_relname needed? It is only called within
lookup_relname_for_emit, and it results in a double syscache lookup,
which could be avoided if these were a single function.
+ /* COMPRESSION clause, only if explicitly set on the column. */
+ if (CompressionMethodIsValid(att->attcompression))
+ {
+ const char *cm = NULL;
+
+ switch (att->attcompression)
+ {
+ case TOAST_PGLZ_COMPRESSION:
+ cm = "pglz";
+ break;
+ case TOAST_LZ4_COMPRESSION:
+ cm = "lz4";
+ break;
+ }
+ if (cm)
+ appendStringInfo(buf, " COMPRESSION %s", cm);
+ }
Isn't this basically GetCompressionMethodName(att->attcompression)?
+ /* STORAGE clause, only if it differs from the type's default. */
+ if (att->attstorage != get_typstorage(att->atttypid))
+ {
+ const char *storage = NULL;
+
+ switch (att->attstorage)
+ {
+ case TYPSTORAGE_PLAIN:
+...
And this seems like storage_name(att->attstorage) from tablecmds.c,
the only issue is that that's currently static
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-12 01:10 Kyotaro Horiguchi <[email protected]>
parent: Akshay Joshi <[email protected]>
2 siblings, 1 reply; 54+ messages in thread
From: Kyotaro Horiguchi @ 2026-06-12 01:10 UTC (permalink / raw)
To: [email protected]; +Cc: [email protected]; [email protected]
Hello.
At Thu, 11 Jun 2026 13:18:07 +0530, Akshay Joshi <[email protected]> wrote in
> Fixed the issue above. The v5 patch is ready for review/testing.
I have not looked at the patch in detail, but I noticed that some
comments in the patch seem to contain non-ASCII characters.
> * * re-emitting it as ALTER TABLE … ADD CONSTRAINT ― the column-emit
I don't think that is recommended in PostgreSQL source comments, so
these should probably be replaced with plain ASCII equivalents.
https://www.postgresql.org/message-id/E1pnhhu-003D6z-Ki%40gemulon.postgresql.org
For reference, I have attached the result of a quick search below.
Regards,
--
Kyotaro Horiguchi
NTT Open Source Software Center
Quick search result:
=========
20 matches in 18 lines for "[^[:ascii:]]" in buffer: v5-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch
565:+ * re-emitting it as ALTER TABLE … ADD CONSTRAINT ― the column-emit
877:+ * defaults ― mirroring pg_get_database_ddl's pattern of
934:+ * SEQUENCE NAME ― omit when it matches the implicit
1042:+ * Table-level CHECK constraints ― emitted inline in the CREATE TABLE
1055:+ * applied per-column overrides ― DEFAULT, NOT NULL, and any locally
1155:+ * pg_get_ruledef, pg_get_statisticsobjdef_string) ― when
1383:+ * ALTER TABLE qualname ALTER COLUMN col SET DEFAULT expr ― one per
1422:+ * ALTER TABLE qualname ALTER COLUMN col SET (...) ― one per column
1468:+ * out-of-line by emit_local_constraints (the ALTER TABLE … ADD
1500:+ * ALTER TABLE … ADD CONSTRAINT for each locally-defined constraint
1617:+ * ALTER TABLE qualname REPLICA IDENTITY … ― emitted only when the
1749:+ * (#if 0) ― they will become a single helper call once the standalone
1830:+ * Pre-compute "<schema>." too ― the always-qualified helpers
1884:+ * Triggers and row-level security policies ― disabled until the
2680:+-- quoting (its prefix starts with "). Both forms of the prefix ―
2681:+-- bare-lowercase and quoted ― must be stripped from outer
3197:+-- quoting (its prefix starts with "). Both forms of the prefix ―
3198:+-- bare-lowercase and quoted ― must be stripped from outer
=========
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-15 07:52 Akshay Joshi <[email protected]>
parent: Marcos Pegoraro <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-06-15 07:52 UTC (permalink / raw)
To: Marcos Pegoraro <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
On Thu, Jun 11, 2026 at 6:38 PM Marcos Pegoraro <[email protected]> wrote:
> Em qui., 11 de jun. de 2026 às 04:48, Akshay Joshi <
> [email protected]> escreveu:
>
>> Fixed the issue above. The v5 patch is ready for review/testing.
>>
>
> One thing I noticed, though I'm not sure if it's the point here, is that
> it's not possible to extract only the foreign keys or only the triggers
> from the table. So if we want to extract the objects independently by type,
> we would need to have all the return types as optional, and we could have
> more granularity in the return types.
>
> Just like you have...
> if (!ctx->include_indexes)
>
> You could have too
> + if (!ctx->include_create_table)
> + if (!ctx->include_foreign_keys)
> + if (!ctx->include_primary_keys)
>
> Because only in this way can we more or less execute the dump behavior
> here, which is to create all the tables beforehand, then primary keys, then
> foreign keys, then triggers.
>
> I repeat, sorry if this is not the function's intended purpose.
>
I don't think per-contype flags are the right shape, though. The existing
toggles group by catalog (indexes, constraints, rules, ...); splitting
constraints into PK/FK/CHECK/UNIQUE/EXCLUDE/NOT NULL adds six flags on a
second axis, and the function already carries nine. Only FKs have the
cross-table dependency-ordering problem; the rest only reference the same
table, so splitting them unlocks nothing new.
On include_create_table, we are reconstructing the DDL for the table
itself, so I don't think we should skip the CREATE TABLE statement. I'd
rather always emit CREATE TABLE.
> regards
> Marcos
>
>
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-15 08:55 Akshay Joshi <[email protected]>
parent: Kyotaro Horiguchi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-06-15 08:55 UTC (permalink / raw)
To: Kyotaro Horiguchi <[email protected]>; +Cc: [email protected]; [email protected]
Hi Kyotaro and Zsolt,
I have incorporated the feedback provided by both of you.
The v6 patch is updated and ready for your review.
On Fri, Jun 12, 2026 at 6:40 AM Kyotaro Horiguchi <[email protected]>
wrote:
> Hello.
>
> At Thu, 11 Jun 2026 13:18:07 +0530, Akshay Joshi <
> [email protected]> wrote in
> > Fixed the issue above. The v5 patch is ready for review/testing.
>
> I have not looked at the patch in detail, but I noticed that some
> comments in the patch seem to contain non-ASCII characters.
>
> > * * re-emitting it as ALTER TABLE … ADD CONSTRAINT ― the column-emit
>
> I don't think that is recommended in PostgreSQL source comments, so
> these should probably be replaced with plain ASCII equivalents.
>
>
> https://www.postgresql.org/message-id/E1pnhhu-003D6z-Ki%40gemulon.postgresql.org
>
> For reference, I have attached the result of a quick search below.
>
> Regards,
>
> --
> Kyotaro Horiguchi
> NTT Open Source Software Center
>
>
> Quick search result:
>
> =========
> 20 matches in 18 lines for "[^[:ascii:]]" in buffer:
> v5-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch
> 565:+ * re-emitting it as ALTER TABLE … ADD CONSTRAINT ― the
> column-emit
> 877:+ * defaults ― mirroring
> pg_get_database_ddl's pattern of
> 934:+ * SEQUENCE NAME ―
> omit when it matches the implicit
> 1042:+ * Table-level CHECK constraints ― emitted inline in the
> CREATE TABLE
> 1055:+ * applied per-column overrides ― DEFAULT, NOT NULL,
> and any locally
> 1155:+ * pg_get_ruledef, pg_get_statisticsobjdef_string) ― when
> 1383:+ * ALTER TABLE qualname ALTER COLUMN col SET DEFAULT
> expr ― one per
> 1422:+ * ALTER TABLE qualname ALTER COLUMN col SET (...) ―
> one per column
> 1468:+ * out-of-line by emit_local_constraints (the ALTER
> TABLE … ADD
> 1500:+ * ALTER TABLE … ADD CONSTRAINT for each
> locally-defined constraint
> 1617:+ * ALTER TABLE qualname REPLICA IDENTITY … ― emitted
> only when the
> 1749:+ * (#if 0) ― they will become a single helper call once the
> standalone
> 1830:+ * Pre-compute "<schema>." too ― the always-qualified
> helpers
> 1884:+ * Triggers and row-level security policies ― disabled
> until the
> 2680:+-- quoting (its prefix starts with "). Both forms of the prefix ―
> 2681:+-- bare-lowercase and quoted ― must be stripped from outer
> 3197:+-- quoting (its prefix starts with "). Both forms of the prefix ―
> 3198:+-- bare-lowercase and quoted ― must be stripped from outer
> =========
>
Attachments:
[application/octet-stream] v6-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (124.4K, ../../CANxoLDcb3AavOjvikn+2fmSX18fStQOpHFq+b7byf6LFUDx0yw@mail.gmail.com/3-v6-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From 98856d781a7d469e9b74830c9966946b0a56b71c Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v6] 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_string, constraints
(PRIMARY KEY, UNIQUE, FOREIGN KEY, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_command, rules via pg_get_ruledef, extended
statistics via pg_get_statisticsobjdef_string, REPLICA IDENTITY
NOTHING/FULL/USING INDEX, ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY,
and child-local DEFAULT overrides on inheritance/partition children.
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.
A regression test under src/test/regress covers ordinary tables,
identity (default and custom sequence options), generated columns,
STORAGE/COMPRESSION, constraints (CHECK/UNIQUE/FK with deferrable),
functional and partial indexes, inheritance and partitioning,
partition children with FOR VALUES FROM/TO, WITH modulus/remainder, and
DEFAULT, rules, extended statistics, RLS toggles, REPLICA IDENTITY,
UNLOGGED with reloptions, per-column attoptions, child DEFAULT
overrides, pretty mode, owner=false, and the error paths for views,
sequences, NULL, unknown options, and odd-variadic argument counts.
Author: Akshay Joshi <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kyotaro Horiguchi <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 79 +
src/backend/catalog/pg_inherits.c | 93 +
src/backend/commands/tablecmds.c | 30 +-
src/backend/utils/adt/ddlutils.c | 1724 +++++++++++++++++
src/include/catalog/pg_inherits.h | 1 +
src/include/catalog/pg_proc.dat | 7 +
src/include/commands/tablecmds.h | 3 +
.../regress/expected/pg_get_table_ddl.out | 742 +++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 440 +++++
10 files changed, 3118 insertions(+), 3 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 8ffa7e83275..384242efb86 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3947,6 +3947,85 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
<literal>TABLESPACE</literal>.
</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 a family of
+ <literal>includes_<replaceable>category</replaceable></literal>
+ booleans that gate emission of optional sub-objects:
+ <literal>includes_indexes</literal>,
+ <literal>includes_constraints</literal>,
+ <literal>includes_rules</literal>,
+ <literal>includes_statistics</literal>,
+ <literal>includes_triggers</literal>,
+ <literal>includes_policies</literal>,
+ <literal>includes_rls</literal> (the
+ <command>ENABLE</command>/<command>FORCE ROW LEVEL SECURITY</command>
+ toggles), and
+ <literal>includes_replica_identity</literal>. Each of these
+ defaults to <literal>true</literal>. The
+ <literal>includes_partition</literal> option, which defaults to
+ <literal>false</literal>, controls whether the DDL for partition
+ children is appended after a partitioned-table parent.
+ </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 38f9ffcd04f..7d2575c05fa 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -739,7 +739,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);
@@ -2515,7 +2514,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)
@@ -19558,6 +19557,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 f32fcd453ef..9e456e512be 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -21,12 +21,25 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.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_am.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 "catalog/partition.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
+#include "nodes/nodes.h"
#include "commands/tablespace.h"
#include "common/relpath.h"
#include "funcapi.h"
@@ -87,6 +100,103 @@ static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool i
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, schema_prefix, 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 include_indexes;
+ bool include_constraints;
+ bool include_rules;
+ bool include_statistics;
+ bool include_triggers;
+ bool include_policies;
+ bool include_rls;
+ bool include_replica_identity;
+ bool include_partition;
+ bool schema_qualified;
+
+ /* Derived during setup */
+ Oid base_namespace;
+ char *qualname;
+ char *schema_prefix; /* NULL when schema_qualified */
+ 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(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition,
+ bool schema_qualified);
+static void append_column_defs(StringInfo buf, Relation rel, bool pretty,
+ bool include_constraints,
+ bool schema_qualified,
+ LocalNotNullEntry * nn_entries);
+static void append_typed_column_overrides(StringInfo buf, Relation rel,
+ bool pretty, bool include_constraints,
+ 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 char *strip_schema_prefix(const char *stmt, const char *prefix);
+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
@@ -1185,3 +1295,1617 @@ 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.
+ *
+ * When schema_qualified is true the schema-qualified name is always
+ * returned. 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 strip.
+ *
+ * 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);
+
+ /* 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;
+}
+
+/*
+ * strip_schema_prefix
+ * Return a palloc'd copy of stmt with every occurrence of prefix
+ * removed, except occurrences that fall inside a single-quoted SQL
+ * string literal or a double-quoted SQL identifier.
+ *
+ * Used to strip the qualified-schema prefix (for example "old_schema.")
+ * from DDL emitted by the always-qualified ruleutils helpers
+ * (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) when the caller has
+ * requested unqualified output via schema_qualified=false. A plain
+ * substring strip would corrupt string literals whose contents happen
+ * to contain "<schema>." (for example a CHECK comparing against
+ * 'myschema.secret') or double-quoted column/identifier names whose
+ * contents happen to contain "<schema>." (for example a column named
+ * "s.weird" when stripping the prefix "s."), so we track quoting state
+ * and skip matches that occur while we are inside either kind of
+ * quoted token. Escaped quotes ('' inside a string, "" inside an
+ * identifier) are handled by keeping the corresponding flag set across
+ * them.
+ */
+static char *
+strip_schema_prefix(const char *stmt, const char *prefix)
+{
+ StringInfoData buf;
+ size_t plen = strlen(prefix);
+ const char *p = stmt;
+ bool in_string = false;
+ bool in_ident = false;
+
+ initStringInfo(&buf);
+ while (*p)
+ {
+ /*
+ * Try the prefix match BEFORE the quote-state toggles. The prefix
+ * itself may begin with a double quote when the base schema name
+ * requires quoting (e.g. "My-Schema".), and at that leading " we must
+ * strip the whole prefix rather than treat it as the opening of a
+ * regular quoted identifier. The check is gated on !in_ident &&
+ * !in_string, so once we have toggled into a quoted token we never
+ * strip a coincidental substring inside it.
+ */
+ if (!in_string && !in_ident && strncmp(p, prefix, plen) == 0)
+ {
+ p += plen;
+ continue;
+ }
+ if (*p == '\'' && !in_ident)
+ {
+ /* SQL '' escape: emit both quotes and stay inside the string. */
+ if (in_string && p[1] == '\'')
+ {
+ appendStringInfoChar(&buf, '\'');
+ appendStringInfoChar(&buf, '\'');
+ p += 2;
+ continue;
+ }
+ in_string = !in_string;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ if (*p == '"' && !in_string)
+ {
+ /* SQL "" escape: emit both quotes and stay inside the ident. */
+ if (in_ident && p[1] == '"')
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoChar(&buf, '"');
+ p += 2;
+ continue;
+ }
+ in_ident = !in_ident;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ appendStringInfoChar(&buf, *p);
+ p++;
+ }
+ return buf.data;
+}
+
+/*
+ * 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 "CONSTRAINT name NOT NULL" only happens for
+ * columns that the column list actually emits, i.e. attislocal
+ * columns. For a locally-declared NOT NULL sitting on an inherited
+ * column the inline path never fires, so we must leave the constraint
+ * OID out of skip_oids and let the post-CREATE constraint loop emit
+ * ALTER TABLE ... ADD CONSTRAINT.
+ */
+ if (!entries[attnum].is_auto && 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, ',');
+ *first = false;
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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_constraints,
+ 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 emitted by the INHERITS clause
+ * (once implemented), not the column list, unless the child
+ * redeclared them locally (attislocal=true).
+ */
+ if (!att->attislocal)
+ continue;
+
+ if (!first)
+ appendStringInfoChar(buf, ',');
+ first = false;
+
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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_constraints)
+ 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_constraints,
+ 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, ',');
+ first = false;
+ if (pretty)
+ appendStringInfoString(&inner, "\n ");
+ else
+ appendStringInfoChar(&inner, ' ');
+
+ 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_constraints)
+ 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 / append_stripped_stmt
+ * Push ctx->buf onto ctx->statements.
+ *
+ * append_stmt is used for emissions where the table reference is built
+ * from ctx->qualname (already partial-qualified per schema_qualified).
+ * append_stripped_stmt is used after invoking always-qualified ruleutils
+ * helpers (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) - when
+ * schema_qualified is false, the base-schema prefix is stripped from
+ * their output so the emitted DDL is replay-relocatable.
+ */
+static void
+append_stmt(TableDdlContext * ctx)
+{
+ ctx->statements = lappend(ctx->statements, pstrdup(ctx->buf.data));
+}
+
+static void
+append_stripped_stmt(TableDdlContext * ctx)
+{
+ if (ctx->schema_prefix != NULL)
+ ctx->statements = lappend(ctx->statements,
+ strip_schema_prefix(ctx->buf.data,
+ ctx->schema_prefix));
+ else
+ 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,
+ ctx->include_constraints,
+ ctx->nn_entries);
+ }
+ else
+ {
+ List *parents;
+ ListCell *lc;
+ bool first;
+
+ appendStringInfoString(&ctx->buf, " (");
+
+ append_column_defs(&ctx->buf, rel, ctx->pretty,
+ ctx->include_constraints,
+ 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 (!ctx->include_indexes)
+ 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_string(idxoid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", idxdef);
+ append_stripped_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). User-named NOT NULL
+ * constraints are emitted inline by the column-emit helpers and
+ * are skipped via skip_notnull_oids; auto-named NOT NULLs fall
+ * through here and are dedup'd by name at replay.
+ */
+static void
+emit_local_constraints(TableDdlContext * ctx)
+{
+ Relation conRel;
+ SysScanDesc conScan;
+ ScanKeyData conKey;
+ HeapTuple conTup;
+ bool is_partition = ctx->rel->rd_rel->relispartition;
+
+ if (!ctx->include_constraints)
+ return;
+
+ 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);
+ char *condef;
+
+ if (!con->conislocal)
+ continue;
+ if (con->contype == CONSTRAINT_CHECK && !is_partition)
+ continue;
+ if (con->contype == CONSTRAINT_NOTNULL && !is_partition &&
+ list_member_oid(ctx->skip_notnull_oids, con->oid))
+ continue;
+
+ condef = pg_get_constraintdef_command(con->oid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", condef);
+ append_stripped_stmt(ctx);
+ pfree(condef);
+ }
+ 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 (!ctx->include_rules || 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;
+ Datum ruledef;
+ char *ruledef_str;
+
+ ruledef = OidFunctionCall1(F_PG_GET_RULEDEF_OID,
+ ObjectIdGetDatum(ruleid));
+ ruledef_str = TextDatumGetCString(ruledef);
+ resetStringInfo(&ctx->buf);
+ appendStringInfoString(&ctx->buf, ruledef_str);
+ append_stripped_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 (!ctx->include_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_string(stat->oid);
+
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", statdef);
+ append_stripped_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 (!ctx->include_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 (!ctx->include_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 (!ctx->include_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);
+ List *childstmts;
+
+ childstmts = pg_get_table_ddl_internal(childoid, ctx->pretty,
+ ctx->no_owner,
+ ctx->no_tablespace,
+ ctx->include_indexes,
+ ctx->include_constraints,
+ ctx->include_rules,
+ ctx->include_statistics,
+ ctx->include_triggers,
+ ctx->include_policies,
+ ctx->include_rls,
+ ctx->include_replica_identity,
+ ctx->include_partition,
+ ctx->schema_qualified);
+ 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 first list element is the CREATE TABLE statement. Subsequent
+ * elements are the ALTER TABLE / CREATE INDEX / CREATE RULE /
+ * CREATE STATISTICS statements needed to restore the table's full
+ * definition.
+ *
+ * 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(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition,
+ bool schema_qualified)
+{
+ TableDdlContext ctx = {0};
+ Relation rel;
+ char relkind;
+ AclResult aclresult;
+
+ rel = table_open(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(relid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLE,
+ RelationGetRelationName(rel));
+
+ ctx.rel = rel;
+ ctx.relid = relid;
+ ctx.pretty = pretty;
+ ctx.no_owner = no_owner;
+ ctx.no_tablespace = no_tablespace;
+ ctx.include_indexes = include_indexes;
+ ctx.include_constraints = include_constraints;
+ ctx.include_rules = include_rules;
+ ctx.include_statistics = include_statistics;
+ ctx.include_triggers = include_triggers;
+ ctx.include_policies = include_policies;
+ ctx.include_rls = include_rls;
+ ctx.include_replica_identity = include_replica_identity;
+ ctx.include_partition = include_partition;
+ ctx.schema_qualified = schema_qualified;
+ ctx.base_namespace = RelationGetNamespace(rel);
+ ctx.save_nestlevel = -1;
+ ctx.qualname = lookup_relname_for_emit(relid, 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, etc. come out unqualified. 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.
+ *
+ * Pre-compute "<schema>." too - the always-qualified helpers
+ * (constraintdef_command, indexdef_string, ruledef,
+ * statisticsobjdef_string) ignore search_path for the target's own "ALTER
+ * TABLE schema.tbl"/"ON schema.tbl"/"TO schema.tbl" prefix and for the
+ * qualified statistics name; we strip that string ourselves via
+ * strip_schema_prefix.
+ */
+ if (!schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx.base_namespace);
+
+ if (nspname != NULL)
+ {
+ const char *qnsp = quote_identifier(nspname);
+
+ ctx.schema_prefix = psprintf("%s.", qnsp);
+ 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);
+
+ /*
+ * Each pass below appends zero or more finished statements to
+ * ctx.statements via append_stmt / append_stripped_stmt. 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); and partition children
+ * last so the parent already exists at replay time.
+ */
+ 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 (include_triggers)
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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 (include_policies)
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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);
+ }
+#else
+ (void) include_triggers;
+ (void) include_policies;
+#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);
+ if (ctx.schema_prefix != NULL)
+ pfree(ctx.schema_prefix);
+ 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;
+ Oid relid;
+ DdlOption opts[] = {
+ {"pretty", DDL_OPT_BOOL},
+ {"owner", DDL_OPT_BOOL},
+ {"tablespace", DDL_OPT_BOOL},
+ {"includes_indexes", DDL_OPT_BOOL},
+ {"includes_constraints", DDL_OPT_BOOL},
+ {"includes_rules", DDL_OPT_BOOL},
+ {"includes_statistics", DDL_OPT_BOOL},
+ {"includes_triggers", DDL_OPT_BOOL},
+ {"includes_policies", DDL_OPT_BOOL},
+ {"includes_rls", DDL_OPT_BOOL},
+ {"includes_replica_identity", DDL_OPT_BOOL},
+ {"includes_partition", DDL_OPT_BOOL},
+ {"schema_qualified", DDL_OPT_BOOL},
+ };
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ relid = PG_GETARG_OID(0);
+ parse_ddl_options(fcinfo, 1, opts, lengthof(opts));
+
+ statements = pg_get_table_ddl_internal(relid,
+ opts[0].isset && opts[0].boolval,
+ opts[1].isset && !opts[1].boolval,
+ opts[2].isset && !opts[2].boolval,
+ !opts[3].isset || opts[3].boolval,
+ !opts[4].isset || opts[4].boolval,
+ !opts[5].isset || opts[5].boolval,
+ !opts[6].isset || opts[6].boolval,
+ !opts[7].isset || opts[7].boolval,
+ !opts[8].isset || opts[8].boolval,
+ !opts[9].isset || opts[9].boolval,
+ !opts[10].isset || opts[10].boolval,
+ opts[11].isset && opts[11].boolval,
+ !opts[12].isset || opts[12].boolval);
+ 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/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 be157a5fbe9..e456c226f53 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,13 @@
proargtypes => 'regdatabase text', proallargtypes => '{regdatabase,text}',
proargmodes => '{i,v}', proargdefaults => '{NULL}',
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/test/regress/expected/pg_get_table_ddl.out b/src/test/regress/expected/pg_get_table_ddl.out
new file mode 100644
index 00000000000..b8cab4290a0
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,742 @@
+--
+-- 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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);
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_always_not_null NOT NULL id_always;
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_default_not_null NOT NULL id_default;
+(3 rows)
+
+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);
+ ALTER TABLE pgtbl_ddl_test.id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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)
+
+-- 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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(5 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 parent (RANGE and HASH).
+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)
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0);
+(1 row)
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(1 row)
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx ( a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx ADD CONSTRAINT ri_idx_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(4 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)));
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_b_not_null NOT NULL b;
+(3 rows)
+
+-- Temporary tables + ON COMMIT. Temp tables live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_default ( id integer);
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_delete ( id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_drop'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+ 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+(1 row)
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+ pg_get_table_ddl
+------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+(1 row)
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+ pg_get_table_ddl
+---------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+(1 row)
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range ( id integer, k integer) PARTITION BY RANGE (id);
+ 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;
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(4 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_id_not_null NOT NULL id;
+ ALTER TABLE basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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_id_not_null NOT NULL id;
+ 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);
+(4 rows)
+
+-- 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);
+ ALTER TABLE id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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
+-- User-named NOT NULL constraint must be emitted inline as
+-- "CONSTRAINT name NOT NULL" so a replay does not collide with the
+-- auto-named NOT NULL that PostgreSQL would otherwise create from a
+-- plain inline "NOT NULL". The constraint loop skips the user-named
+-- entry to avoid double-emission.
+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);
+ ALTER TABLE pgtbl_ddl_test.nn_named ADD CONSTRAINT nn_named_b_not_null NOT NULL b;
+(2 rows)
+
+DROP TABLE nn_named;
+-- 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.
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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 25 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 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 rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ 'owner', 'false') WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 20 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 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 rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..887f565d58e
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,440 @@
+--
+-- 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');
+
+-- 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 parent (RANGE and HASH).
+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');
+
+-- 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 live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS 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');
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+
+-- 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');
+
+-- 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;
+
+-- User-named NOT NULL constraint must be emitted inline as
+-- "CONSTRAINT name NOT NULL" so a replay does not collide with the
+-- auto-named NOT NULL that PostgreSQL would otherwise create from a
+-- plain inline "NOT NULL". The constraint loop skips the user-named
+-- entry to avoid double-emission.
+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;
+
+-- 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');
+
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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;
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-15 21:04 Marcos Pegoraro <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Marcos Pegoraro @ 2026-06-15 21:04 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
Em seg., 15 de jun. de 2026 às 04:52, Akshay Joshi <
[email protected]> escreveu:
> I don't think per-contype flags are the right shape, though. The existing
> toggles group by catalog (indexes, constraints, rules, ...); splitting
> constraints into PK/FK/CHECK/UNIQUE/EXCLUDE/NOT NULL adds six flags on a
> second axis, and the function already carries nine. Only FKs have the
> cross-table dependency-ordering problem; the rest only reference the same
> table, so splitting them unlocks nothing new.
>
Ok, I understand your point. Initially, I saw the usefulness of this
function for cloning a schema, something very common in a multi-tenant
model. But creating the foreign keys along with the create table makes that
unfeasible.
Options are variadic, so you could split your emit_local_constraints into
+emit_local_foreign_keys_constraints(TableDdlContext * ctx)
+ if (!(ctx->include_constraints || ctx->include_foreign_keys)) then
+ return
+emit_local_primary_keys_constraints(TableDdlContext * ctx)
+ if (!(ctx->include_constraints || ctx->include_primary_keys)) then
+ return
pg_get_table_ddl('x','includes_constraints','true') -- would print all
constraints
pg_get_table_ddl('x','include_primary_keys','true') -- would print only
primary key constraints
regards
Marcos
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-19 12:19 Akshay Joshi <[email protected]>
parent: Marcos Pegoraro <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-06-19 12:19 UTC (permalink / raw)
To: Marcos Pegoraro <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
On Tue, Jun 16, 2026 at 2:35 AM Marcos Pegoraro <[email protected]> wrote:
> Em seg., 15 de jun. de 2026 às 04:52, Akshay Joshi <
> [email protected]> escreveu:
>
>> I don't think per-contype flags are the right shape, though. The existing
>> toggles group by catalog (indexes, constraints, rules, ...); splitting
>> constraints into PK/FK/CHECK/UNIQUE/EXCLUDE/NOT NULL adds six flags on a
>> second axis, and the function already carries nine. Only FKs have the
>> cross-table dependency-ordering problem; the rest only reference the same
>> table, so splitting them unlocks nothing new.
>>
>
> Ok, I understand your point. Initially, I saw the usefulness of this
> function for cloning a schema, something very common in a multi-tenant
> model. But creating the foreign keys along with the create table makes that
> unfeasible.
>
> Options are variadic, so you could split your emit_local_constraints into
> +emit_local_foreign_keys_constraints(TableDdlContext * ctx)
> + if (!(ctx->include_constraints || ctx->include_foreign_keys)) then
> + return
>
> +emit_local_primary_keys_constraints(TableDdlContext * ctx)
> + if (!(ctx->include_constraints || ctx->include_primary_keys)) then
> + return
>
> pg_get_table_ddl('x','includes_constraints','true') -- would print all
> constraints
> pg_get_table_ddl('x','include_primary_keys','true') -- would print only
> primary key constraints
>
The schema cloning use case is valid. The v7 patch (attached) adds a single
new option, `includes_foreign_keys` (boolean, default true), which acts as
an additive gate underneath `includes_constraints`. Calling
pg_get_table_ddl(t, 'includes_foreign_keys', 'false') now emits everything
except FOREIGN KEY constraints. This covers the multi-tenant clone
workflow: create tables first without cross-table references, then re-run
with the default to add the constraints once all targets exist.
I held off on the broader split into per-contype options
(includes_primary_keys, includes_unique, etc.) for two reasons. First, only
FOREIGN KEY actually breaks schema cloning. PRIMARY KEY, UNIQUE, CHECK,
EXCLUDE, and named NOT NULL constraints are all table local and don't
reference anything else, so no workflow currently needs to suppress them
independently. Second, the function already exposes thirteen boolean
options; adding five more granular ones without a concrete use case expands
the surface area unnecessarily.
The v7 patch is ready for review.
>
> regards
> Marcos
>
Attachments:
[application/octet-stream] v7-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (126.9K, ../../CANxoLDcWwoWOxvmV_uBBUsbGf+_AZzBV1GkcjoX+omNpcrCNMQ@mail.gmail.com/3-v7-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From 7770e35da9bf3883b72ff0f7bc60443084b764fb Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v7] 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_string, constraints
(PRIMARY KEY, UNIQUE, FOREIGN KEY, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_command, rules via pg_get_ruledef, extended
statistics via pg_get_statisticsobjdef_string, REPLICA IDENTITY
NOTHING/FULL/USING INDEX, ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY,
and child-local DEFAULT overrides on inheritance/partition children.
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.
A regression test under src/test/regress covers ordinary tables,
identity (default and custom sequence options), generated columns,
STORAGE/COMPRESSION, constraints (CHECK/UNIQUE/FK with deferrable),
functional and partial indexes, inheritance and partitioning,
partition children with FOR VALUES FROM/TO, WITH modulus/remainder, and
DEFAULT, rules, extended statistics, RLS toggles, REPLICA IDENTITY,
UNLOGGED with reloptions, per-column attoptions, child DEFAULT
overrides, pretty mode, owner=false, and the error paths for views,
sequences, NULL, unknown options, and odd-variadic argument counts.
Author: Akshay Joshi <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kyotaro Horiguchi <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 89 +
src/backend/catalog/pg_inherits.c | 93 +
src/backend/commands/tablecmds.c | 30 +-
src/backend/utils/adt/ddlutils.c | 1736 +++++++++++++++++
src/include/catalog/pg_inherits.h | 1 +
src/include/catalog/pg_proc.dat | 7 +
src/include/commands/tablecmds.h | 3 +
.../regress/expected/pg_get_table_ddl.out | 754 +++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 447 +++++
10 files changed, 3159 insertions(+), 3 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 8ffa7e83275..18afaafb2bd 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3947,6 +3947,95 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
<literal>TABLESPACE</literal>.
</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 a family of
+ <literal>includes_<replaceable>category</replaceable></literal>
+ booleans that gate emission of optional sub-objects:
+ <literal>includes_indexes</literal>,
+ <literal>includes_constraints</literal>,
+ <literal>includes_foreign_keys</literal>,
+ <literal>includes_rules</literal>,
+ <literal>includes_statistics</literal>,
+ <literal>includes_triggers</literal>,
+ <literal>includes_policies</literal>,
+ <literal>includes_rls</literal> (the
+ <command>ENABLE</command>/<command>FORCE ROW LEVEL SECURITY</command>
+ toggles), and
+ <literal>includes_replica_identity</literal>. Each of these
+ defaults to <literal>true</literal>.
+ <literal>includes_foreign_keys</literal> acts as a finer-grained
+ gate beneath <literal>includes_constraints</literal>: when set
+ to <literal>false</literal>, <literal>FOREIGN KEY</literal>
+ constraints are suppressed while other constraint types
+ (<literal>PRIMARY KEY</literal>, <literal>UNIQUE</literal>,
+ <literal>CHECK</literal>, <literal>EXCLUDE</literal>, and named
+ <literal>NOT NULL</literal>) are still emitted; this is intended
+ for schema-cloning workflows that need to create all tables
+ before any cross-table references exist. The
+ <literal>includes_partition</literal> option, which defaults to
+ <literal>false</literal>, controls whether the DDL for partition
+ children is appended after a partitioned-table parent.
+ </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 265dcfe7fda..a5f46522c33 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -739,7 +739,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);
@@ -2515,7 +2514,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)
@@ -19558,6 +19557,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 f32fcd453ef..d42947ebf9f 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -21,12 +21,25 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.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_am.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 "catalog/partition.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
+#include "nodes/nodes.h"
#include "commands/tablespace.h"
#include "common/relpath.h"
#include "funcapi.h"
@@ -87,6 +100,105 @@ static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool i
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, schema_prefix, 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 include_indexes;
+ bool include_constraints;
+ bool include_foreign_keys;
+ bool include_rules;
+ bool include_statistics;
+ bool include_triggers;
+ bool include_policies;
+ bool include_rls;
+ bool include_replica_identity;
+ bool include_partition;
+ bool schema_qualified;
+
+ /* Derived during setup */
+ Oid base_namespace;
+ char *qualname;
+ char *schema_prefix; /* NULL when schema_qualified */
+ 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(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_foreign_keys,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition,
+ bool schema_qualified);
+static void append_column_defs(StringInfo buf, Relation rel, bool pretty,
+ bool include_constraints,
+ bool schema_qualified,
+ LocalNotNullEntry * nn_entries);
+static void append_typed_column_overrides(StringInfo buf, Relation rel,
+ bool pretty, bool include_constraints,
+ 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 char *strip_schema_prefix(const char *stmt, const char *prefix);
+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
@@ -1185,3 +1297,1627 @@ 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.
+ *
+ * When schema_qualified is true the schema-qualified name is always
+ * returned. 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 strip.
+ *
+ * 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);
+
+ /* 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;
+}
+
+/*
+ * strip_schema_prefix
+ * Return a palloc'd copy of stmt with every occurrence of prefix
+ * removed, except occurrences that fall inside a single-quoted SQL
+ * string literal or a double-quoted SQL identifier.
+ *
+ * Used to strip the qualified-schema prefix (for example "old_schema.")
+ * from DDL emitted by the always-qualified ruleutils helpers
+ * (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) when the caller has
+ * requested unqualified output via schema_qualified=false. A plain
+ * substring strip would corrupt string literals whose contents happen
+ * to contain "<schema>." (for example a CHECK comparing against
+ * 'myschema.secret') or double-quoted column/identifier names whose
+ * contents happen to contain "<schema>." (for example a column named
+ * "s.weird" when stripping the prefix "s."), so we track quoting state
+ * and skip matches that occur while we are inside either kind of
+ * quoted token. Escaped quotes ('' inside a string, "" inside an
+ * identifier) are handled by keeping the corresponding flag set across
+ * them.
+ */
+static char *
+strip_schema_prefix(const char *stmt, const char *prefix)
+{
+ StringInfoData buf;
+ size_t plen = strlen(prefix);
+ const char *p = stmt;
+ bool in_string = false;
+ bool in_ident = false;
+
+ initStringInfo(&buf);
+ while (*p)
+ {
+ /*
+ * Try the prefix match BEFORE the quote-state toggles. The prefix
+ * itself may begin with a double quote when the base schema name
+ * requires quoting (e.g. "My-Schema".), and at that leading " we must
+ * strip the whole prefix rather than treat it as the opening of a
+ * regular quoted identifier. The check is gated on !in_ident &&
+ * !in_string, so once we have toggled into a quoted token we never
+ * strip a coincidental substring inside it.
+ */
+ if (!in_string && !in_ident && strncmp(p, prefix, plen) == 0)
+ {
+ p += plen;
+ continue;
+ }
+ if (*p == '\'' && !in_ident)
+ {
+ /* SQL '' escape: emit both quotes and stay inside the string. */
+ if (in_string && p[1] == '\'')
+ {
+ appendStringInfoChar(&buf, '\'');
+ appendStringInfoChar(&buf, '\'');
+ p += 2;
+ continue;
+ }
+ in_string = !in_string;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ if (*p == '"' && !in_string)
+ {
+ /* SQL "" escape: emit both quotes and stay inside the ident. */
+ if (in_ident && p[1] == '"')
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoChar(&buf, '"');
+ p += 2;
+ continue;
+ }
+ in_ident = !in_ident;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ appendStringInfoChar(&buf, *p);
+ p++;
+ }
+ return buf.data;
+}
+
+/*
+ * 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 "CONSTRAINT name NOT NULL" only happens for
+ * columns that the column list actually emits, i.e. attislocal
+ * columns. For a locally-declared NOT NULL sitting on an inherited
+ * column the inline path never fires, so we must leave the constraint
+ * OID out of skip_oids and let the post-CREATE constraint loop emit
+ * ALTER TABLE ... ADD CONSTRAINT.
+ */
+ if (!entries[attnum].is_auto && 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, ',');
+ *first = false;
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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_constraints,
+ 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 emitted by the INHERITS clause
+ * (once implemented), not the column list, unless the child
+ * redeclared them locally (attislocal=true).
+ */
+ if (!att->attislocal)
+ continue;
+
+ if (!first)
+ appendStringInfoChar(buf, ',');
+ first = false;
+
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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_constraints)
+ 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_constraints,
+ 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, ',');
+ first = false;
+ if (pretty)
+ appendStringInfoString(&inner, "\n ");
+ else
+ appendStringInfoChar(&inner, ' ');
+
+ 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_constraints)
+ 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 / append_stripped_stmt
+ * Push ctx->buf onto ctx->statements.
+ *
+ * append_stmt is used for emissions where the table reference is built
+ * from ctx->qualname (already partial-qualified per schema_qualified).
+ * append_stripped_stmt is used after invoking always-qualified ruleutils
+ * helpers (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) - when
+ * schema_qualified is false, the base-schema prefix is stripped from
+ * their output so the emitted DDL is replay-relocatable.
+ */
+static void
+append_stmt(TableDdlContext * ctx)
+{
+ ctx->statements = lappend(ctx->statements, pstrdup(ctx->buf.data));
+}
+
+static void
+append_stripped_stmt(TableDdlContext * ctx)
+{
+ if (ctx->schema_prefix != NULL)
+ ctx->statements = lappend(ctx->statements,
+ strip_schema_prefix(ctx->buf.data,
+ ctx->schema_prefix));
+ else
+ 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,
+ ctx->include_constraints,
+ ctx->nn_entries);
+ }
+ else
+ {
+ List *parents;
+ ListCell *lc;
+ bool first;
+
+ appendStringInfoString(&ctx->buf, " (");
+
+ append_column_defs(&ctx->buf, rel, ctx->pretty,
+ ctx->include_constraints,
+ 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 (!ctx->include_indexes)
+ 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_string(idxoid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", idxdef);
+ append_stripped_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). User-named NOT NULL
+ * constraints are emitted inline by the column-emit helpers and
+ * are skipped via skip_notnull_oids; auto-named NOT NULLs fall
+ * through here and are dedup'd by name at replay. FOREIGN KEY
+ * emission is further gated by include_foreign_keys so callers
+ * cloning a schema can suppress cross-table references in a first
+ * pass and re-emit them after all targets exist.
+ */
+static void
+emit_local_constraints(TableDdlContext * ctx)
+{
+ Relation conRel;
+ SysScanDesc conScan;
+ ScanKeyData conKey;
+ HeapTuple conTup;
+ bool is_partition = ctx->rel->rd_rel->relispartition;
+
+ if (!ctx->include_constraints)
+ return;
+
+ 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);
+ char *condef;
+
+ if (!con->conislocal)
+ continue;
+ if (con->contype == CONSTRAINT_CHECK && !is_partition)
+ continue;
+ if (con->contype == CONSTRAINT_NOTNULL && !is_partition &&
+ list_member_oid(ctx->skip_notnull_oids, con->oid))
+ continue;
+ if (con->contype == CONSTRAINT_FOREIGN && !ctx->include_foreign_keys)
+ continue;
+
+ condef = pg_get_constraintdef_command(con->oid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", condef);
+ append_stripped_stmt(ctx);
+ pfree(condef);
+ }
+ 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 (!ctx->include_rules || 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;
+ Datum ruledef;
+ char *ruledef_str;
+
+ ruledef = OidFunctionCall1(F_PG_GET_RULEDEF_OID,
+ ObjectIdGetDatum(ruleid));
+ ruledef_str = TextDatumGetCString(ruledef);
+ resetStringInfo(&ctx->buf);
+ appendStringInfoString(&ctx->buf, ruledef_str);
+ append_stripped_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 (!ctx->include_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_string(stat->oid);
+
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", statdef);
+ append_stripped_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 (!ctx->include_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 (!ctx->include_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 (!ctx->include_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);
+ List *childstmts;
+
+ childstmts = pg_get_table_ddl_internal(childoid, ctx->pretty,
+ ctx->no_owner,
+ ctx->no_tablespace,
+ ctx->include_indexes,
+ ctx->include_constraints,
+ ctx->include_foreign_keys,
+ ctx->include_rules,
+ ctx->include_statistics,
+ ctx->include_triggers,
+ ctx->include_policies,
+ ctx->include_rls,
+ ctx->include_replica_identity,
+ ctx->include_partition,
+ ctx->schema_qualified);
+ 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 first list element is the CREATE TABLE statement. Subsequent
+ * elements are the ALTER TABLE / CREATE INDEX / CREATE RULE /
+ * CREATE STATISTICS statements needed to restore the table's full
+ * definition.
+ *
+ * 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(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_foreign_keys,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition,
+ bool schema_qualified)
+{
+ TableDdlContext ctx = {0};
+ Relation rel;
+ char relkind;
+ AclResult aclresult;
+
+ rel = table_open(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(relid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLE,
+ RelationGetRelationName(rel));
+
+ ctx.rel = rel;
+ ctx.relid = relid;
+ ctx.pretty = pretty;
+ ctx.no_owner = no_owner;
+ ctx.no_tablespace = no_tablespace;
+ ctx.include_indexes = include_indexes;
+ ctx.include_constraints = include_constraints;
+ ctx.include_foreign_keys = include_foreign_keys;
+ ctx.include_rules = include_rules;
+ ctx.include_statistics = include_statistics;
+ ctx.include_triggers = include_triggers;
+ ctx.include_policies = include_policies;
+ ctx.include_rls = include_rls;
+ ctx.include_replica_identity = include_replica_identity;
+ ctx.include_partition = include_partition;
+ ctx.schema_qualified = schema_qualified;
+ ctx.base_namespace = RelationGetNamespace(rel);
+ ctx.save_nestlevel = -1;
+ ctx.qualname = lookup_relname_for_emit(relid, 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, etc. come out unqualified. 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.
+ *
+ * Pre-compute "<schema>." too - the always-qualified helpers
+ * (constraintdef_command, indexdef_string, ruledef,
+ * statisticsobjdef_string) ignore search_path for the target's own "ALTER
+ * TABLE schema.tbl"/"ON schema.tbl"/"TO schema.tbl" prefix and for the
+ * qualified statistics name; we strip that string ourselves via
+ * strip_schema_prefix.
+ */
+ if (!schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx.base_namespace);
+
+ if (nspname != NULL)
+ {
+ const char *qnsp = quote_identifier(nspname);
+
+ ctx.schema_prefix = psprintf("%s.", qnsp);
+ 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);
+
+ /*
+ * Each pass below appends zero or more finished statements to
+ * ctx.statements via append_stmt / append_stripped_stmt. 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); and partition children
+ * last so the parent already exists at replay time.
+ */
+ 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 (include_triggers)
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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 (include_policies)
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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);
+ }
+#else
+ (void) include_triggers;
+ (void) include_policies;
+#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);
+ if (ctx.schema_prefix != NULL)
+ pfree(ctx.schema_prefix);
+ 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;
+ Oid relid;
+ DdlOption opts[] = {
+ {"pretty", DDL_OPT_BOOL},
+ {"owner", DDL_OPT_BOOL},
+ {"tablespace", DDL_OPT_BOOL},
+ {"includes_indexes", DDL_OPT_BOOL},
+ {"includes_constraints", DDL_OPT_BOOL},
+ {"includes_foreign_keys", DDL_OPT_BOOL},
+ {"includes_rules", DDL_OPT_BOOL},
+ {"includes_statistics", DDL_OPT_BOOL},
+ {"includes_triggers", DDL_OPT_BOOL},
+ {"includes_policies", DDL_OPT_BOOL},
+ {"includes_rls", DDL_OPT_BOOL},
+ {"includes_replica_identity", DDL_OPT_BOOL},
+ {"includes_partition", DDL_OPT_BOOL},
+ {"schema_qualified", DDL_OPT_BOOL},
+ };
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ relid = PG_GETARG_OID(0);
+ parse_ddl_options(fcinfo, 1, opts, lengthof(opts));
+
+ statements = pg_get_table_ddl_internal(relid,
+ opts[0].isset && opts[0].boolval,
+ opts[1].isset && !opts[1].boolval,
+ opts[2].isset && !opts[2].boolval,
+ !opts[3].isset || opts[3].boolval,
+ !opts[4].isset || opts[4].boolval,
+ !opts[5].isset || opts[5].boolval,
+ !opts[6].isset || opts[6].boolval,
+ !opts[7].isset || opts[7].boolval,
+ !opts[8].isset || opts[8].boolval,
+ !opts[9].isset || opts[9].boolval,
+ !opts[10].isset || opts[10].boolval,
+ !opts[11].isset || opts[11].boolval,
+ opts[12].isset && opts[12].boolval,
+ !opts[13].isset || opts[13].boolval);
+ 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/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 be157a5fbe9..e456c226f53 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,13 @@
proargtypes => 'regdatabase text', proallargtypes => '{regdatabase,text}',
proargmodes => '{i,v}', proargdefaults => '{NULL}',
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/test/regress/expected/pg_get_table_ddl.out b/src/test/regress/expected/pg_get_table_ddl.out
new file mode 100644
index 00000000000..fdb362892e8
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,754 @@
+--
+-- 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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);
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_always_not_null NOT NULL id_always;
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_default_not_null NOT NULL id_default;
+(3 rows)
+
+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);
+ ALTER TABLE pgtbl_ddl_test.id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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)
+
+-- 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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(5 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 parent (RANGE and HASH).
+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)
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0);
+(1 row)
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(1 row)
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx ( a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx ADD CONSTRAINT ri_idx_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(4 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)));
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_b_not_null NOT NULL b;
+(3 rows)
+
+-- Temporary tables + ON COMMIT. Temp tables live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_default ( id integer);
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_delete ( id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_drop'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+ 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_foreign_keys=false suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- schema-cloning workflow: emit tables first without cross-table FKs,
+-- then re-run with the default to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_foreign_keys', '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);
+(2 rows)
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+(1 row)
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+ pg_get_table_ddl
+------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+(1 row)
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+ pg_get_table_ddl
+---------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+(1 row)
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range ( id integer, k integer) PARTITION BY RANGE (id);
+ 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;
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(4 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_id_not_null NOT NULL id;
+ ALTER TABLE basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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_id_not_null NOT NULL id;
+ 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);
+(4 rows)
+
+-- 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);
+ ALTER TABLE id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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
+-- User-named NOT NULL constraint must be emitted inline as
+-- "CONSTRAINT name NOT NULL" so a replay does not collide with the
+-- auto-named NOT NULL that PostgreSQL would otherwise create from a
+-- plain inline "NOT NULL". The constraint loop skips the user-named
+-- entry to avoid double-emission.
+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);
+ ALTER TABLE pgtbl_ddl_test.nn_named ADD CONSTRAINT nn_named_b_not_null NOT NULL b;
+(2 rows)
+
+DROP TABLE nn_named;
+-- 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.
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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 25 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 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 rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ 'owner', 'false') WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 20 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 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 rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..e33ec15aff8
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,447 @@
+--
+-- 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');
+
+-- 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 parent (RANGE and HASH).
+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');
+
+-- 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 live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS 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');
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+
+-- includes_foreign_keys=false suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- schema-cloning workflow: emit tables first without cross-table FKs,
+-- then re-run with the default to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_foreign_keys', 'false');
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+
+-- 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');
+
+-- 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;
+
+-- User-named NOT NULL constraint must be emitted inline as
+-- "CONSTRAINT name NOT NULL" so a replay does not collide with the
+-- auto-named NOT NULL that PostgreSQL would otherwise create from a
+-- plain inline "NOT NULL". The constraint loop skips the user-named
+-- entry to avoid double-emission.
+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;
+
+-- 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');
+
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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;
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-19 19:45 Zsolt Parragi <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Zsolt Parragi @ 2026-06-19 19:45 UTC (permalink / raw)
To: [email protected]
The previous features all look good to me, I only have one question
for the new flag.
> Calling
> pg_get_table_ddl(t, 'includes_foreign_keys', 'false') now emits everything
> except FOREIGN KEY constraints. This covers the multi-tenant clone
> workflow: create tables first without cross-table references, then re-run
> with the default to add the constraints once all targets exist.
I think this feature needs a bit more documentation, an
"only_foreign_keys" flag, or both.
CREATE TABLE refd (id int PRIMARY KEY);
CREATE TABLE cons (a int CHECK(a>0), b int UNIQUE, c int REFERENCES refd(id));
-- pass 1: running without foreign keys
SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','false');
-- execute everything
-- loading data
-- pass 2: running with everything
SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','true');
-- ERROR: relation "cons" already exists (and the unique constraint
also collides)
I could do a "grep FOREIGN KEY" before executing (unless it's a tricky
schema where that phrase appears elsewhere), or since psql continues
on error, it will simply work if I accept a significant error noise,
but then the documentation should be clear about this limitation.
Following the documented approach and getting a bunch of unexpected
errors could be confusing for users.
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-22 06:26 Akshay Joshi <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 3 replies; 54+ messages in thread
From: Akshay Joshi @ 2026-06-22 06:26 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: [email protected]
Thanks for the review; you're right, `includes_foreign_keys=false` on its
own is a half-measure. Re-running with the default to add FKs back collides
with the existing CREATE TABLE, UNIQUE indexes, etc.
I've added an only_foreign_keys option (boolean, default false) as the
natural complement of includes_foreign_keys=false. When set to true, the
function emits only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY
statements and suppresses everything else (CREATE TABLE, owner, indexes,
non-FK constraints, rules, statistics, replica identity, RLS toggles).
Partition-child recursion still runs so child FKs are reached too.
Combining `only_foreign_keys=true` with `includes_foreign_keys=false` is
rejected upfront since it would produce no output.
The documentation paragraph for `includes_foreign_keys` now directs users
to `only_foreign_keys` as the intended second pass. Regression coverage
adds three cases: the FK-only emission for your cons example, the zero-row
result for a table without FKs, and the error path.
The v8 patch is ready for review.
On Sat, Jun 20, 2026 at 1:15 AM Zsolt Parragi <[email protected]>
wrote:
> The previous features all look good to me, I only have one question
> for the new flag.
>
> > Calling
> > pg_get_table_ddl(t, 'includes_foreign_keys', 'false') now emits
> everything
> > except FOREIGN KEY constraints. This covers the multi-tenant clone
> > workflow: create tables first without cross-table references, then re-run
> > with the default to add the constraints once all targets exist.
>
> I think this feature needs a bit more documentation, an
> "only_foreign_keys" flag, or both.
>
> CREATE TABLE refd (id int PRIMARY KEY);
> CREATE TABLE cons (a int CHECK(a>0), b int UNIQUE, c int REFERENCES
> refd(id));
>
> -- pass 1: running without foreign keys
> SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','false');
> -- execute everything
>
> -- loading data
>
> -- pass 2: running with everything
> SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','true');
> -- ERROR: relation "cons" already exists (and the unique constraint
> also collides)
>
> I could do a "grep FOREIGN KEY" before executing (unless it's a tricky
> schema where that phrase appears elsewhere), or since psql continues
> on error, it will simply work if I accept a significant error noise,
> but then the documentation should be clear about this limitation.
> Following the documented approach and getting a bunch of unexpected
> errors could be confusing for users.
>
>
>
Attachments:
[application/octet-stream] v8-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (131.8K, ../../CANxoLDejn2JFXF7kVMtcznW6XyessrJ7WToSaQau2GxBdAGSUg@mail.gmail.com/3-v8-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From f288010d7216b717f9553b1669828d3e07ada734 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v8] 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_string, constraints
(PRIMARY KEY, UNIQUE, FOREIGN KEY, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_command, rules via pg_get_ruledef, extended
statistics via pg_get_statisticsobjdef_string, REPLICA IDENTITY
NOTHING/FULL/USING INDEX, ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY,
and child-local DEFAULT overrides on inheritance/partition children.
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.
A regression test under src/test/regress covers ordinary tables,
identity (default and custom sequence options), generated columns,
STORAGE/COMPRESSION, constraints (CHECK/UNIQUE/FK with deferrable),
functional and partial indexes, inheritance and partitioning,
partition children with FOR VALUES FROM/TO, WITH modulus/remainder, and
DEFAULT, rules, extended statistics, RLS toggles, REPLICA IDENTITY,
UNLOGGED with reloptions, per-column attoptions, child DEFAULT
overrides, pretty mode, owner=false, and the error paths for views,
sequences, NULL, unknown options, and odd-variadic argument counts.
Author: Akshay Joshi <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kyotaro Horiguchi <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 105 +
src/backend/catalog/pg_inherits.c | 93 +
src/backend/commands/tablecmds.c | 30 +-
src/backend/utils/adt/ddlutils.c | 1775 +++++++++++++++++
src/include/catalog/pg_inherits.h | 1 +
src/include/catalog/pg_proc.dat | 7 +
src/include/commands/tablecmds.h | 3 +
.../regress/expected/pg_get_table_ddl.out | 779 ++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 465 +++++
10 files changed, 3257 insertions(+), 3 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 8ffa7e83275..b03b65689d3 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3947,6 +3947,111 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
<literal>TABLESPACE</literal>.
</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 a family of
+ <literal>includes_<replaceable>category</replaceable></literal>
+ booleans that gate emission of optional sub-objects:
+ <literal>includes_indexes</literal>,
+ <literal>includes_constraints</literal>,
+ <literal>includes_foreign_keys</literal>,
+ <literal>includes_rules</literal>,
+ <literal>includes_statistics</literal>,
+ <literal>includes_triggers</literal>,
+ <literal>includes_policies</literal>,
+ <literal>includes_rls</literal> (the
+ <command>ENABLE</command>/<command>FORCE ROW LEVEL SECURITY</command>
+ toggles), and
+ <literal>includes_replica_identity</literal>. Each of these
+ defaults to <literal>true</literal>.
+ <literal>includes_foreign_keys</literal> acts as a finer-grained
+ gate beneath <literal>includes_constraints</literal>: when set
+ to <literal>false</literal>, <literal>FOREIGN KEY</literal>
+ constraints are suppressed while other constraint types
+ (<literal>PRIMARY KEY</literal>, <literal>UNIQUE</literal>,
+ <literal>CHECK</literal>, <literal>EXCLUDE</literal>, and named
+ <literal>NOT NULL</literal>) are still emitted; this is intended
+ for schema-cloning workflows that need to create all tables
+ before any cross-table references exist. The
+ <literal>only_foreign_keys</literal> option (boolean, default
+ <literal>false</literal>) is the natural complement: when set
+ to <literal>true</literal>, the function emits only the
+ <command>ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY</command>
+ statements and suppresses everything else (the
+ <command>CREATE TABLE</command>, owner, indexes, non-FK
+ constraints, rules, statistics, replica identity, and RLS
+ toggles). Combined with a prior call using
+ <literal>includes_foreign_keys</literal> set to
+ <literal>false</literal>, this supports the typical two-pass
+ workflow: create the tables and load data first, then add the
+ cross-table foreign-key references. Setting
+ <literal>only_foreign_keys</literal> to <literal>true</literal>
+ together with <literal>includes_foreign_keys</literal> set to
+ <literal>false</literal> is rejected as it would produce no
+ output. The <literal>includes_partition</literal> option,
+ which defaults to <literal>false</literal>, controls whether
+ the DDL for partition children is appended after a
+ partitioned-table parent.
+ </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 265dcfe7fda..a5f46522c33 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -739,7 +739,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);
@@ -2515,7 +2514,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)
@@ -19558,6 +19557,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 f32fcd453ef..5fbf1d515c0 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -21,12 +21,25 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.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_am.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 "catalog/partition.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
+#include "nodes/nodes.h"
#include "commands/tablespace.h"
#include "common/relpath.h"
#include "funcapi.h"
@@ -87,6 +100,107 @@ static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool i
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, schema_prefix, 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 include_indexes;
+ bool include_constraints;
+ bool include_foreign_keys;
+ bool only_foreign_keys;
+ bool include_rules;
+ bool include_statistics;
+ bool include_triggers;
+ bool include_policies;
+ bool include_rls;
+ bool include_replica_identity;
+ bool include_partition;
+ bool schema_qualified;
+
+ /* Derived during setup */
+ Oid base_namespace;
+ char *qualname;
+ char *schema_prefix; /* NULL when schema_qualified */
+ 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(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_foreign_keys,
+ bool only_foreign_keys,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition,
+ bool schema_qualified);
+static void append_column_defs(StringInfo buf, Relation rel, bool pretty,
+ bool include_constraints,
+ bool schema_qualified,
+ LocalNotNullEntry * nn_entries);
+static void append_typed_column_overrides(StringInfo buf, Relation rel,
+ bool pretty, bool include_constraints,
+ 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 char *strip_schema_prefix(const char *stmt, const char *prefix);
+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
@@ -1185,3 +1299,1664 @@ 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.
+ *
+ * When schema_qualified is true the schema-qualified name is always
+ * returned. 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 strip.
+ *
+ * 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);
+
+ /* 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;
+}
+
+/*
+ * strip_schema_prefix
+ * Return a palloc'd copy of stmt with every occurrence of prefix
+ * removed, except occurrences that fall inside a single-quoted SQL
+ * string literal or a double-quoted SQL identifier.
+ *
+ * Used to strip the qualified-schema prefix (for example "old_schema.")
+ * from DDL emitted by the always-qualified ruleutils helpers
+ * (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) when the caller has
+ * requested unqualified output via schema_qualified=false. A plain
+ * substring strip would corrupt string literals whose contents happen
+ * to contain "<schema>." (for example a CHECK comparing against
+ * 'myschema.secret') or double-quoted column/identifier names whose
+ * contents happen to contain "<schema>." (for example a column named
+ * "s.weird" when stripping the prefix "s."), so we track quoting state
+ * and skip matches that occur while we are inside either kind of
+ * quoted token. Escaped quotes ('' inside a string, "" inside an
+ * identifier) are handled by keeping the corresponding flag set across
+ * them.
+ */
+static char *
+strip_schema_prefix(const char *stmt, const char *prefix)
+{
+ StringInfoData buf;
+ size_t plen = strlen(prefix);
+ const char *p = stmt;
+ bool in_string = false;
+ bool in_ident = false;
+
+ initStringInfo(&buf);
+ while (*p)
+ {
+ /*
+ * Try the prefix match BEFORE the quote-state toggles. The prefix
+ * itself may begin with a double quote when the base schema name
+ * requires quoting (e.g. "My-Schema".), and at that leading " we must
+ * strip the whole prefix rather than treat it as the opening of a
+ * regular quoted identifier. The check is gated on !in_ident &&
+ * !in_string, so once we have toggled into a quoted token we never
+ * strip a coincidental substring inside it.
+ */
+ if (!in_string && !in_ident && strncmp(p, prefix, plen) == 0)
+ {
+ p += plen;
+ continue;
+ }
+ if (*p == '\'' && !in_ident)
+ {
+ /* SQL '' escape: emit both quotes and stay inside the string. */
+ if (in_string && p[1] == '\'')
+ {
+ appendStringInfoChar(&buf, '\'');
+ appendStringInfoChar(&buf, '\'');
+ p += 2;
+ continue;
+ }
+ in_string = !in_string;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ if (*p == '"' && !in_string)
+ {
+ /* SQL "" escape: emit both quotes and stay inside the ident. */
+ if (in_ident && p[1] == '"')
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoChar(&buf, '"');
+ p += 2;
+ continue;
+ }
+ in_ident = !in_ident;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ appendStringInfoChar(&buf, *p);
+ p++;
+ }
+ return buf.data;
+}
+
+/*
+ * 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 "CONSTRAINT name NOT NULL" only happens for
+ * columns that the column list actually emits, i.e. attislocal
+ * columns. For a locally-declared NOT NULL sitting on an inherited
+ * column the inline path never fires, so we must leave the constraint
+ * OID out of skip_oids and let the post-CREATE constraint loop emit
+ * ALTER TABLE ... ADD CONSTRAINT.
+ */
+ if (!entries[attnum].is_auto && 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, ',');
+ *first = false;
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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_constraints,
+ 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 emitted by the INHERITS clause
+ * (once implemented), not the column list, unless the child
+ * redeclared them locally (attislocal=true).
+ */
+ if (!att->attislocal)
+ continue;
+
+ if (!first)
+ appendStringInfoChar(buf, ',');
+ first = false;
+
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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_constraints)
+ 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_constraints,
+ 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, ',');
+ first = false;
+ if (pretty)
+ appendStringInfoString(&inner, "\n ");
+ else
+ appendStringInfoChar(&inner, ' ');
+
+ 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_constraints)
+ 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 / append_stripped_stmt
+ * Push ctx->buf onto ctx->statements.
+ *
+ * append_stmt is used for emissions where the table reference is built
+ * from ctx->qualname (already partial-qualified per schema_qualified).
+ * append_stripped_stmt is used after invoking always-qualified ruleutils
+ * helpers (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) - when
+ * schema_qualified is false, the base-schema prefix is stripped from
+ * their output so the emitted DDL is replay-relocatable.
+ */
+static void
+append_stmt(TableDdlContext * ctx)
+{
+ ctx->statements = lappend(ctx->statements, pstrdup(ctx->buf.data));
+}
+
+static void
+append_stripped_stmt(TableDdlContext * ctx)
+{
+ if (ctx->schema_prefix != NULL)
+ ctx->statements = lappend(ctx->statements,
+ strip_schema_prefix(ctx->buf.data,
+ ctx->schema_prefix));
+ else
+ 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,
+ ctx->include_constraints,
+ ctx->nn_entries);
+ }
+ else
+ {
+ List *parents;
+ ListCell *lc;
+ bool first;
+
+ appendStringInfoString(&ctx->buf, " (");
+
+ append_column_defs(&ctx->buf, rel, ctx->pretty,
+ ctx->include_constraints,
+ 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 (!ctx->include_indexes)
+ 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_string(idxoid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", idxdef);
+ append_stripped_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). User-named NOT NULL
+ * constraints are emitted inline by the column-emit helpers and
+ * are skipped via skip_notnull_oids; auto-named NOT NULLs fall
+ * through here and are dedup'd by name at replay. FOREIGN KEY
+ * emission is further gated by include_foreign_keys so callers
+ * cloning a schema can suppress cross-table references in a first
+ * pass and re-emit them after all targets exist. When
+ * only_foreign_keys is set, the loop emits FOREIGN KEY rows alone
+ * and skips everything else - that is the natural complement of
+ * the includes_foreign_keys=false first pass, used to add the
+ * cross-table references once data has been loaded.
+ */
+static void
+emit_local_constraints(TableDdlContext * ctx)
+{
+ Relation conRel;
+ SysScanDesc conScan;
+ ScanKeyData conKey;
+ HeapTuple conTup;
+ bool is_partition = ctx->rel->rd_rel->relispartition;
+
+ if (!ctx->include_constraints)
+ return;
+
+ 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);
+ char *condef;
+
+ if (!con->conislocal)
+ continue;
+ if (ctx->only_foreign_keys && con->contype != CONSTRAINT_FOREIGN)
+ continue;
+ if (con->contype == CONSTRAINT_CHECK && !is_partition)
+ continue;
+ if (con->contype == CONSTRAINT_NOTNULL && !is_partition &&
+ list_member_oid(ctx->skip_notnull_oids, con->oid))
+ continue;
+ if (con->contype == CONSTRAINT_FOREIGN && !ctx->include_foreign_keys)
+ continue;
+
+ condef = pg_get_constraintdef_command(con->oid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", condef);
+ append_stripped_stmt(ctx);
+ pfree(condef);
+ }
+ 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 (!ctx->include_rules || 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;
+ Datum ruledef;
+ char *ruledef_str;
+
+ ruledef = OidFunctionCall1(F_PG_GET_RULEDEF_OID,
+ ObjectIdGetDatum(ruleid));
+ ruledef_str = TextDatumGetCString(ruledef);
+ resetStringInfo(&ctx->buf);
+ appendStringInfoString(&ctx->buf, ruledef_str);
+ append_stripped_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 (!ctx->include_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_string(stat->oid);
+
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", statdef);
+ append_stripped_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 (!ctx->include_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 (!ctx->include_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 (!ctx->include_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);
+ List *childstmts;
+
+ childstmts = pg_get_table_ddl_internal(childoid, ctx->pretty,
+ ctx->no_owner,
+ ctx->no_tablespace,
+ ctx->include_indexes,
+ ctx->include_constraints,
+ ctx->include_foreign_keys,
+ ctx->only_foreign_keys,
+ ctx->include_rules,
+ ctx->include_statistics,
+ ctx->include_triggers,
+ ctx->include_policies,
+ ctx->include_rls,
+ ctx->include_replica_identity,
+ ctx->include_partition,
+ ctx->schema_qualified);
+ 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 first list element is the CREATE TABLE statement. Subsequent
+ * elements are the ALTER TABLE / CREATE INDEX / CREATE RULE /
+ * CREATE STATISTICS statements needed to restore the table's full
+ * definition.
+ *
+ * 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(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_foreign_keys,
+ bool only_foreign_keys,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition,
+ bool schema_qualified)
+{
+ TableDdlContext ctx = {0};
+ Relation rel;
+ char relkind;
+ AclResult aclresult;
+
+ rel = table_open(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(relid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLE,
+ RelationGetRelationName(rel));
+
+ /*
+ * only_foreign_keys is meant to pair with a prior call that suppressed
+ * FOREIGN KEY emission via includes_foreign_keys=false. Combining
+ * only_foreign_keys=true with includes_foreign_keys=false would emit
+ * nothing useful, so reject that combination up front.
+ */
+ if (only_foreign_keys && !include_foreign_keys)
+ {
+ table_close(rel, AccessShareLock);
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"only_foreign_keys\" requires \"includes_foreign_keys\" to be true")));
+ }
+
+ ctx.rel = rel;
+ ctx.relid = relid;
+ ctx.pretty = pretty;
+ ctx.no_owner = no_owner;
+ ctx.no_tablespace = no_tablespace;
+ ctx.include_indexes = include_indexes;
+ ctx.include_constraints = include_constraints;
+ ctx.include_foreign_keys = include_foreign_keys;
+ ctx.only_foreign_keys = only_foreign_keys;
+ ctx.include_rules = include_rules;
+ ctx.include_statistics = include_statistics;
+ ctx.include_triggers = include_triggers;
+ ctx.include_policies = include_policies;
+ ctx.include_rls = include_rls;
+ ctx.include_replica_identity = include_replica_identity;
+ ctx.include_partition = include_partition;
+ ctx.schema_qualified = schema_qualified;
+ ctx.base_namespace = RelationGetNamespace(rel);
+ ctx.save_nestlevel = -1;
+ ctx.qualname = lookup_relname_for_emit(relid, 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, etc. come out unqualified. 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.
+ *
+ * Pre-compute "<schema>." too - the always-qualified helpers
+ * (constraintdef_command, indexdef_string, ruledef,
+ * statisticsobjdef_string) ignore search_path for the target's own "ALTER
+ * TABLE schema.tbl"/"ON schema.tbl"/"TO schema.tbl" prefix and for the
+ * qualified statistics name; we strip that string ourselves via
+ * strip_schema_prefix.
+ */
+ if (!schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx.base_namespace);
+
+ if (nspname != NULL)
+ {
+ const char *qnsp = quote_identifier(nspname);
+
+ ctx.schema_prefix = psprintf("%s.", qnsp);
+ 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);
+
+ /*
+ * Each pass below appends zero or more finished statements to
+ * ctx.statements via append_stmt / append_stripped_stmt. 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); and partition children
+ * last so the parent already exists at replay time.
+ *
+ * In only_foreign_keys mode the table itself and every non-FK sub-object
+ * already exist (the caller produced them in a prior pass via
+ * includes_foreign_keys=false), so we skip directly to the constraint
+ * loop, which then emits FOREIGN KEY rows only. Partition-child
+ * recursion still runs so child FKs are reached too.
+ */
+ if (!only_foreign_keys)
+ {
+ emit_create_table_stmt(&ctx);
+ emit_owner_stmt(&ctx);
+ emit_child_default_overrides(&ctx);
+ emit_attoptions(&ctx);
+ emit_indexes(&ctx);
+ }
+ emit_local_constraints(&ctx);
+ if (!only_foreign_keys)
+ {
+ 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 (include_triggers)
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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 (include_policies)
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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);
+ }
+#else
+ (void) include_triggers;
+ (void) include_policies;
+#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);
+ if (ctx.schema_prefix != NULL)
+ pfree(ctx.schema_prefix);
+ 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;
+ Oid relid;
+ DdlOption opts[] = {
+ {"pretty", DDL_OPT_BOOL},
+ {"owner", DDL_OPT_BOOL},
+ {"tablespace", DDL_OPT_BOOL},
+ {"includes_indexes", DDL_OPT_BOOL},
+ {"includes_constraints", DDL_OPT_BOOL},
+ {"includes_foreign_keys", DDL_OPT_BOOL},
+ {"only_foreign_keys", DDL_OPT_BOOL},
+ {"includes_rules", DDL_OPT_BOOL},
+ {"includes_statistics", DDL_OPT_BOOL},
+ {"includes_triggers", DDL_OPT_BOOL},
+ {"includes_policies", DDL_OPT_BOOL},
+ {"includes_rls", DDL_OPT_BOOL},
+ {"includes_replica_identity", DDL_OPT_BOOL},
+ {"includes_partition", DDL_OPT_BOOL},
+ {"schema_qualified", DDL_OPT_BOOL},
+ };
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ relid = PG_GETARG_OID(0);
+ parse_ddl_options(fcinfo, 1, opts, lengthof(opts));
+
+ statements = pg_get_table_ddl_internal(relid,
+ opts[0].isset && opts[0].boolval,
+ opts[1].isset && !opts[1].boolval,
+ opts[2].isset && !opts[2].boolval,
+ !opts[3].isset || opts[3].boolval,
+ !opts[4].isset || opts[4].boolval,
+ !opts[5].isset || opts[5].boolval,
+ opts[6].isset && opts[6].boolval,
+ !opts[7].isset || opts[7].boolval,
+ !opts[8].isset || opts[8].boolval,
+ !opts[9].isset || opts[9].boolval,
+ !opts[10].isset || opts[10].boolval,
+ !opts[11].isset || opts[11].boolval,
+ !opts[12].isset || opts[12].boolval,
+ opts[13].isset && opts[13].boolval,
+ !opts[14].isset || opts[14].boolval);
+ 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/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 be157a5fbe9..e456c226f53 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,13 @@
proargtypes => 'regdatabase text', proallargtypes => '{regdatabase,text}',
proargmodes => '{i,v}', proargdefaults => '{NULL}',
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/test/regress/expected/pg_get_table_ddl.out b/src/test/regress/expected/pg_get_table_ddl.out
new file mode 100644
index 00000000000..dc164c5b788
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,779 @@
+--
+-- 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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);
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_always_not_null NOT NULL id_always;
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_default_not_null NOT NULL id_default;
+(3 rows)
+
+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);
+ ALTER TABLE pgtbl_ddl_test.id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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)
+
+-- 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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(5 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 parent (RANGE and HASH).
+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)
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0);
+(1 row)
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(1 row)
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx ( a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx ADD CONSTRAINT ri_idx_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(4 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)));
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_b_not_null NOT NULL b;
+(3 rows)
+
+-- Temporary tables + ON COMMIT. Temp tables live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_default ( id integer);
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_delete ( id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_drop'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+ 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_foreign_keys=false suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- schema-cloning workflow: emit tables first without cross-table FKs,
+-- then run pg_get_table_ddl(..., only_foreign_keys=>true) after data
+-- has been loaded to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_foreign_keys', '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);
+(2 rows)
+
+-- only_foreign_keys=true is the complement of the previous call: it
+-- emits only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY rows
+-- and suppresses the CREATE TABLE and every non-FK sub-object.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'only_foreign_keys', 'true');
+ 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 in only_foreign_keys
+-- mode.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, 'owner', 'false',
+ 'only_foreign_keys', 'true');
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- Combining only_foreign_keys=true with includes_foreign_keys=false
+-- would emit nothing useful, so the combination is rejected.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'only_foreign_keys', 'true',
+ 'includes_foreign_keys', 'false');
+ERROR: "only_foreign_keys" requires "includes_foreign_keys" to be true
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+(1 row)
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+ pg_get_table_ddl
+------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+(1 row)
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+ pg_get_table_ddl
+---------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+(1 row)
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range ( id integer, k integer) PARTITION BY RANGE (id);
+ 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;
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(4 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_id_not_null NOT NULL id;
+ ALTER TABLE basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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_id_not_null NOT NULL id;
+ 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);
+(4 rows)
+
+-- 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);
+ ALTER TABLE id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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
+-- User-named NOT NULL constraint must be emitted inline as
+-- "CONSTRAINT name NOT NULL" so a replay does not collide with the
+-- auto-named NOT NULL that PostgreSQL would otherwise create from a
+-- plain inline "NOT NULL". The constraint loop skips the user-named
+-- entry to avoid double-emission.
+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);
+ ALTER TABLE pgtbl_ddl_test.nn_named ADD CONSTRAINT nn_named_b_not_null NOT NULL b;
+(2 rows)
+
+DROP TABLE nn_named;
+-- 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.
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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 25 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 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 rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ 'owner', 'false') WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 20 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 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 rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..43fa6d744e5
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,465 @@
+--
+-- 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');
+
+-- 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 parent (RANGE and HASH).
+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');
+
+-- 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 live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS 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');
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+
+-- includes_foreign_keys=false suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- schema-cloning workflow: emit tables first without cross-table FKs,
+-- then run pg_get_table_ddl(..., only_foreign_keys=>true) after data
+-- has been loaded to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_foreign_keys', 'false');
+
+-- only_foreign_keys=true is the complement of the previous call: it
+-- emits only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY rows
+-- and suppresses the CREATE TABLE and every non-FK sub-object.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'only_foreign_keys', 'true');
+
+-- A table with no foreign keys produces no rows in only_foreign_keys
+-- mode.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, 'owner', 'false',
+ 'only_foreign_keys', 'true');
+
+-- Combining only_foreign_keys=true with includes_foreign_keys=false
+-- would emit nothing useful, so the combination is rejected.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'only_foreign_keys', 'true',
+ 'includes_foreign_keys', 'false');
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+
+-- 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');
+
+-- 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;
+
+-- User-named NOT NULL constraint must be emitted inline as
+-- "CONSTRAINT name NOT NULL" so a replay does not collide with the
+-- auto-named NOT NULL that PostgreSQL would otherwise create from a
+-- plain inline "NOT NULL". The constraint loop skips the user-named
+-- entry to avoid double-emission.
+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;
+
+-- 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');
+
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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;
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-22 06:54 Chao Li <[email protected]>
parent: Akshay Joshi <[email protected]>
2 siblings, 1 reply; 54+ messages in thread
From: Chao Li @ 2026-06-22 06:54 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
> On Jun 22, 2026, at 14:26, Akshay Joshi <[email protected]> wrote:
>
> Thanks for the review; you're right, `includes_foreign_keys=false` on its own is a half-measure. Re-running with the default to add FKs back collides with the existing CREATE TABLE, UNIQUE indexes, etc.
>
> I've added an only_foreign_keys option (boolean, default false) as the natural complement of includes_foreign_keys=false. When set to true, the function emits only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY statements and suppresses everything else (CREATE TABLE, owner, indexes, non-FK constraints, rules, statistics, replica identity, RLS toggles). Partition-child recursion still runs so child FKs are reached too. Combining `only_foreign_keys=true` with `includes_foreign_keys=false` is rejected upfront since it would produce no output.
>
> The documentation paragraph for `includes_foreign_keys` now directs users to `only_foreign_keys` as the intended second pass. Regression coverage adds three cases: the FK-only emission for your cons example, the zero-row result for a table without FKs, and the error path.
>
> The v8 patch is ready for review.
>
> On Sat, Jun 20, 2026 at 1:15 AM Zsolt Parragi <[email protected]> wrote:
> The previous features all look good to me, I only have one question
> for the new flag.
>
> > Calling
> > pg_get_table_ddl(t, 'includes_foreign_keys', 'false') now emits everything
> > except FOREIGN KEY constraints. This covers the multi-tenant clone
> > workflow: create tables first without cross-table references, then re-run
> > with the default to add the constraints once all targets exist.
>
> I think this feature needs a bit more documentation, an
> "only_foreign_keys" flag, or both.
>
> CREATE TABLE refd (id int PRIMARY KEY);
> CREATE TABLE cons (a int CHECK(a>0), b int UNIQUE, c int REFERENCES refd(id));
>
> -- pass 1: running without foreign keys
> SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','false');
> -- execute everything
>
> -- loading data
>
> -- pass 2: running with everything
> SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','true');
> -- ERROR: relation "cons" already exists (and the unique constraint
> also collides)
>
> I could do a "grep FOREIGN KEY" before executing (unless it's a tricky
> schema where that phrase appears elsewhere), or since psql continues
> on error, it will simply work if I accept a significant error noise,
> but then the documentation should be clear about this limitation.
> Following the documented approach and getting a bunch of unexpected
> errors could be confusing for users.
>
>
> <v8-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch>
I have a comment, or maybe a question:
```
+{ 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' },
```
Since provariadic is text, I wonder if proallargtypes should be {regclass,_text}, with _text meaning an array of text.
I’m asking because I have had this suspicion for some time. I saw a few other procs using the same pattern, for example:
```
{ oid => '6501', descr => 'get DDL to recreate a role',
proname => 'pg_get_role_ddl', prorows => '10', provariadic => 'text',
proisstrict => 'f', proretset => 't', provolatile => 's',
pronargdefaults => '1', prorettype => 'text', proargtypes => 'regrole text',
proallargtypes => '{regrole,text}', proargmodes => '{i,v}',
proargdefaults => '{NULL}', prosrc => 'pg_get_role_ddl' },
```
But for jsonb_delete etc procs, _text is used:
```
{ oid => '3343',
proname => 'jsonb_delete', provariadic => 'text', prorettype => 'jsonb',
proargtypes => 'jsonb _text', proallargtypes => '{jsonb,_text}',
proargmodes => '{i,v}', proargnames => '{from_json,path_elems}',
prosrc => 'jsonb_delete_array' },
```
So I wonder whether “text” rather than “_text" is intentionally used in proallargtypes, or if this was just never noticed.
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-22 12:40 Akshay Joshi <[email protected]>
parent: Chao Li <[email protected]>
0 siblings, 2 replies; 54+ messages in thread
From: Akshay Joshi @ 2026-06-22 12:40 UTC (permalink / raw)
To: Chao Li <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
You're right, and thanks for spotting this. The existing pattern in
pg_proc.dat for variadic-text functions (e.g., jsonb_delete,
json_extract_path) uses _text at the variadic position in both proargtypes
and proallargtypes, with provariadic => 'text'. That is the convention
documented by the sanity check in src/test/regress/sql/opr_sanity.sql.
The same issue applies to *pg_get_role_ddl*, *pg_get_tablespace_ddl* (both
variants), and *pg_get_database_ddl*, but that will require a separate
patch.
The v9 patch is ready for review.
On Mon, Jun 22, 2026 at 12:25 PM Chao Li <[email protected]> wrote:
>
>
> > On Jun 22, 2026, at 14:26, Akshay Joshi <[email protected]>
> wrote:
> >
> > Thanks for the review; you're right, `includes_foreign_keys=false` on
> its own is a half-measure. Re-running with the default to add FKs back
> collides with the existing CREATE TABLE, UNIQUE indexes, etc.
> >
> > I've added an only_foreign_keys option (boolean, default false) as the
> natural complement of includes_foreign_keys=false. When set to true, the
> function emits only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY
> statements and suppresses everything else (CREATE TABLE, owner, indexes,
> non-FK constraints, rules, statistics, replica identity, RLS toggles).
> Partition-child recursion still runs so child FKs are reached too.
> Combining `only_foreign_keys=true` with `includes_foreign_keys=false` is
> rejected upfront since it would produce no output.
> >
> > The documentation paragraph for `includes_foreign_keys` now directs
> users to `only_foreign_keys` as the intended second pass. Regression
> coverage adds three cases: the FK-only emission for your cons example, the
> zero-row result for a table without FKs, and the error path.
> >
> > The v8 patch is ready for review.
> >
> > On Sat, Jun 20, 2026 at 1:15 AM Zsolt Parragi <[email protected]>
> wrote:
> > The previous features all look good to me, I only have one question
> > for the new flag.
> >
> > > Calling
> > > pg_get_table_ddl(t, 'includes_foreign_keys', 'false') now emits
> everything
> > > except FOREIGN KEY constraints. This covers the multi-tenant clone
> > > workflow: create tables first without cross-table references, then
> re-run
> > > with the default to add the constraints once all targets exist.
> >
> > I think this feature needs a bit more documentation, an
> > "only_foreign_keys" flag, or both.
> >
> > CREATE TABLE refd (id int PRIMARY KEY);
> > CREATE TABLE cons (a int CHECK(a>0), b int UNIQUE, c int REFERENCES
> refd(id));
> >
> > -- pass 1: running without foreign keys
> > SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','false');
> > -- execute everything
> >
> > -- loading data
> >
> > -- pass 2: running with everything
> > SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','true');
> > -- ERROR: relation "cons" already exists (and the unique constraint
> > also collides)
> >
> > I could do a "grep FOREIGN KEY" before executing (unless it's a tricky
> > schema where that phrase appears elsewhere), or since psql continues
> > on error, it will simply work if I accept a significant error noise,
> > but then the documentation should be clear about this limitation.
> > Following the documented approach and getting a bunch of unexpected
> > errors could be confusing for users.
> >
> >
> > <v8-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch>
>
> I have a comment, or maybe a question:
> ```
> +{ 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' },
> ```
>
> Since provariadic is text, I wonder if proallargtypes should be
> {regclass,_text}, with _text meaning an array of text.
>
> I’m asking because I have had this suspicion for some time. I saw a few
> other procs using the same pattern, for example:
> ```
> { oid => '6501', descr => 'get DDL to recreate a role',
> proname => 'pg_get_role_ddl', prorows => '10', provariadic => 'text',
> proisstrict => 'f', proretset => 't', provolatile => 's',
> pronargdefaults => '1', prorettype => 'text', proargtypes => 'regrole
> text',
> proallargtypes => '{regrole,text}', proargmodes => '{i,v}',
> proargdefaults => '{NULL}', prosrc => 'pg_get_role_ddl' },
> ```
>
> But for jsonb_delete etc procs, _text is used:
> ```
> { oid => '3343',
> proname => 'jsonb_delete', provariadic => 'text', prorettype => 'jsonb',
> proargtypes => 'jsonb _text', proallargtypes => '{jsonb,_text}',
> proargmodes => '{i,v}', proargnames => '{from_json,path_elems}',
> prosrc => 'jsonb_delete_array' },
> ```
>
> So I wonder whether “text” rather than “_text" is intentionally used in
> proallargtypes, or if this was just never noticed.
>
> Best regards,
> --
> Chao Li (Evan)
> HighGo Software Co., Ltd.
> https://www.highgo.com/
>
>
>
>
>
Attachments:
[application/octet-stream] v9-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (134.9K, ../../CANxoLDe_GOXyyZs7GN3VUJoT+o1DwqZGhid-TWWvS0D8d5ggYw@mail.gmail.com/3-v9-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From f02916e2ff52f1096bc7270686578462da9aef22 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v9] 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_string, constraints
(PRIMARY KEY, UNIQUE, FOREIGN KEY, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_command, rules via pg_get_ruledef, extended
statistics via pg_get_statisticsobjdef_string, REPLICA IDENTITY
NOTHING/FULL/USING INDEX, ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY,
and child-local DEFAULT overrides on inheritance/partition children.
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.
A regression test under src/test/regress covers ordinary tables,
identity (default and custom sequence options), generated columns,
STORAGE/COMPRESSION, constraints (CHECK/UNIQUE/FK with deferrable),
functional and partial indexes, inheritance and partitioning,
partition children with FOR VALUES FROM/TO, WITH modulus/remainder, and
DEFAULT, rules, extended statistics, RLS toggles, REPLICA IDENTITY,
UNLOGGED with reloptions, per-column attoptions, child DEFAULT
overrides, pretty mode, owner=false, and the error paths for views,
sequences, NULL, unknown options, and odd-variadic argument counts.
Author: Akshay Joshi <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kyotaro Horiguchi <[email protected]>
Reviewed-by: Chao Li <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 105 +
src/backend/catalog/pg_inherits.c | 93 +
src/backend/commands/tablecmds.c | 30 +-
src/backend/utils/adt/ddlutils.c | 1775 +++++++++++++++++
src/include/catalog/pg_inherits.h | 1 +
src/include/catalog/pg_proc.dat | 7 +
src/include/commands/tablecmds.h | 3 +
.../regress/expected/pg_get_table_ddl.out | 818 ++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 482 +++++
10 files changed, 3313 insertions(+), 3 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 8ffa7e83275..b03b65689d3 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3947,6 +3947,111 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
<literal>TABLESPACE</literal>.
</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 a family of
+ <literal>includes_<replaceable>category</replaceable></literal>
+ booleans that gate emission of optional sub-objects:
+ <literal>includes_indexes</literal>,
+ <literal>includes_constraints</literal>,
+ <literal>includes_foreign_keys</literal>,
+ <literal>includes_rules</literal>,
+ <literal>includes_statistics</literal>,
+ <literal>includes_triggers</literal>,
+ <literal>includes_policies</literal>,
+ <literal>includes_rls</literal> (the
+ <command>ENABLE</command>/<command>FORCE ROW LEVEL SECURITY</command>
+ toggles), and
+ <literal>includes_replica_identity</literal>. Each of these
+ defaults to <literal>true</literal>.
+ <literal>includes_foreign_keys</literal> acts as a finer-grained
+ gate beneath <literal>includes_constraints</literal>: when set
+ to <literal>false</literal>, <literal>FOREIGN KEY</literal>
+ constraints are suppressed while other constraint types
+ (<literal>PRIMARY KEY</literal>, <literal>UNIQUE</literal>,
+ <literal>CHECK</literal>, <literal>EXCLUDE</literal>, and named
+ <literal>NOT NULL</literal>) are still emitted; this is intended
+ for schema-cloning workflows that need to create all tables
+ before any cross-table references exist. The
+ <literal>only_foreign_keys</literal> option (boolean, default
+ <literal>false</literal>) is the natural complement: when set
+ to <literal>true</literal>, the function emits only the
+ <command>ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY</command>
+ statements and suppresses everything else (the
+ <command>CREATE TABLE</command>, owner, indexes, non-FK
+ constraints, rules, statistics, replica identity, and RLS
+ toggles). Combined with a prior call using
+ <literal>includes_foreign_keys</literal> set to
+ <literal>false</literal>, this supports the typical two-pass
+ workflow: create the tables and load data first, then add the
+ cross-table foreign-key references. Setting
+ <literal>only_foreign_keys</literal> to <literal>true</literal>
+ together with <literal>includes_foreign_keys</literal> set to
+ <literal>false</literal> is rejected as it would produce no
+ output. The <literal>includes_partition</literal> option,
+ which defaults to <literal>false</literal>, controls whether
+ the DDL for partition children is appended after a
+ partitioned-table parent.
+ </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 265dcfe7fda..a5f46522c33 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -739,7 +739,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);
@@ -2515,7 +2514,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)
@@ -19558,6 +19557,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 f32fcd453ef..5fbf1d515c0 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -21,12 +21,25 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.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_am.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 "catalog/partition.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
+#include "nodes/nodes.h"
#include "commands/tablespace.h"
#include "common/relpath.h"
#include "funcapi.h"
@@ -87,6 +100,107 @@ static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool i
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, schema_prefix, 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 include_indexes;
+ bool include_constraints;
+ bool include_foreign_keys;
+ bool only_foreign_keys;
+ bool include_rules;
+ bool include_statistics;
+ bool include_triggers;
+ bool include_policies;
+ bool include_rls;
+ bool include_replica_identity;
+ bool include_partition;
+ bool schema_qualified;
+
+ /* Derived during setup */
+ Oid base_namespace;
+ char *qualname;
+ char *schema_prefix; /* NULL when schema_qualified */
+ 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(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_foreign_keys,
+ bool only_foreign_keys,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition,
+ bool schema_qualified);
+static void append_column_defs(StringInfo buf, Relation rel, bool pretty,
+ bool include_constraints,
+ bool schema_qualified,
+ LocalNotNullEntry * nn_entries);
+static void append_typed_column_overrides(StringInfo buf, Relation rel,
+ bool pretty, bool include_constraints,
+ 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 char *strip_schema_prefix(const char *stmt, const char *prefix);
+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
@@ -1185,3 +1299,1664 @@ 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.
+ *
+ * When schema_qualified is true the schema-qualified name is always
+ * returned. 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 strip.
+ *
+ * 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);
+
+ /* 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;
+}
+
+/*
+ * strip_schema_prefix
+ * Return a palloc'd copy of stmt with every occurrence of prefix
+ * removed, except occurrences that fall inside a single-quoted SQL
+ * string literal or a double-quoted SQL identifier.
+ *
+ * Used to strip the qualified-schema prefix (for example "old_schema.")
+ * from DDL emitted by the always-qualified ruleutils helpers
+ * (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) when the caller has
+ * requested unqualified output via schema_qualified=false. A plain
+ * substring strip would corrupt string literals whose contents happen
+ * to contain "<schema>." (for example a CHECK comparing against
+ * 'myschema.secret') or double-quoted column/identifier names whose
+ * contents happen to contain "<schema>." (for example a column named
+ * "s.weird" when stripping the prefix "s."), so we track quoting state
+ * and skip matches that occur while we are inside either kind of
+ * quoted token. Escaped quotes ('' inside a string, "" inside an
+ * identifier) are handled by keeping the corresponding flag set across
+ * them.
+ */
+static char *
+strip_schema_prefix(const char *stmt, const char *prefix)
+{
+ StringInfoData buf;
+ size_t plen = strlen(prefix);
+ const char *p = stmt;
+ bool in_string = false;
+ bool in_ident = false;
+
+ initStringInfo(&buf);
+ while (*p)
+ {
+ /*
+ * Try the prefix match BEFORE the quote-state toggles. The prefix
+ * itself may begin with a double quote when the base schema name
+ * requires quoting (e.g. "My-Schema".), and at that leading " we must
+ * strip the whole prefix rather than treat it as the opening of a
+ * regular quoted identifier. The check is gated on !in_ident &&
+ * !in_string, so once we have toggled into a quoted token we never
+ * strip a coincidental substring inside it.
+ */
+ if (!in_string && !in_ident && strncmp(p, prefix, plen) == 0)
+ {
+ p += plen;
+ continue;
+ }
+ if (*p == '\'' && !in_ident)
+ {
+ /* SQL '' escape: emit both quotes and stay inside the string. */
+ if (in_string && p[1] == '\'')
+ {
+ appendStringInfoChar(&buf, '\'');
+ appendStringInfoChar(&buf, '\'');
+ p += 2;
+ continue;
+ }
+ in_string = !in_string;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ if (*p == '"' && !in_string)
+ {
+ /* SQL "" escape: emit both quotes and stay inside the ident. */
+ if (in_ident && p[1] == '"')
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoChar(&buf, '"');
+ p += 2;
+ continue;
+ }
+ in_ident = !in_ident;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ appendStringInfoChar(&buf, *p);
+ p++;
+ }
+ return buf.data;
+}
+
+/*
+ * 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 "CONSTRAINT name NOT NULL" only happens for
+ * columns that the column list actually emits, i.e. attislocal
+ * columns. For a locally-declared NOT NULL sitting on an inherited
+ * column the inline path never fires, so we must leave the constraint
+ * OID out of skip_oids and let the post-CREATE constraint loop emit
+ * ALTER TABLE ... ADD CONSTRAINT.
+ */
+ if (!entries[attnum].is_auto && 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, ',');
+ *first = false;
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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_constraints,
+ 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 emitted by the INHERITS clause
+ * (once implemented), not the column list, unless the child
+ * redeclared them locally (attislocal=true).
+ */
+ if (!att->attislocal)
+ continue;
+
+ if (!first)
+ appendStringInfoChar(buf, ',');
+ first = false;
+
+ if (pretty)
+ appendStringInfoString(buf, "\n ");
+ else
+ appendStringInfoChar(buf, ' ');
+
+ 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_constraints)
+ 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_constraints,
+ 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, ',');
+ first = false;
+ if (pretty)
+ appendStringInfoString(&inner, "\n ");
+ else
+ appendStringInfoChar(&inner, ' ');
+
+ 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_constraints)
+ 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 / append_stripped_stmt
+ * Push ctx->buf onto ctx->statements.
+ *
+ * append_stmt is used for emissions where the table reference is built
+ * from ctx->qualname (already partial-qualified per schema_qualified).
+ * append_stripped_stmt is used after invoking always-qualified ruleutils
+ * helpers (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) - when
+ * schema_qualified is false, the base-schema prefix is stripped from
+ * their output so the emitted DDL is replay-relocatable.
+ */
+static void
+append_stmt(TableDdlContext * ctx)
+{
+ ctx->statements = lappend(ctx->statements, pstrdup(ctx->buf.data));
+}
+
+static void
+append_stripped_stmt(TableDdlContext * ctx)
+{
+ if (ctx->schema_prefix != NULL)
+ ctx->statements = lappend(ctx->statements,
+ strip_schema_prefix(ctx->buf.data,
+ ctx->schema_prefix));
+ else
+ 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,
+ ctx->include_constraints,
+ ctx->nn_entries);
+ }
+ else
+ {
+ List *parents;
+ ListCell *lc;
+ bool first;
+
+ appendStringInfoString(&ctx->buf, " (");
+
+ append_column_defs(&ctx->buf, rel, ctx->pretty,
+ ctx->include_constraints,
+ 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 (!ctx->include_indexes)
+ 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_string(idxoid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", idxdef);
+ append_stripped_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). User-named NOT NULL
+ * constraints are emitted inline by the column-emit helpers and
+ * are skipped via skip_notnull_oids; auto-named NOT NULLs fall
+ * through here and are dedup'd by name at replay. FOREIGN KEY
+ * emission is further gated by include_foreign_keys so callers
+ * cloning a schema can suppress cross-table references in a first
+ * pass and re-emit them after all targets exist. When
+ * only_foreign_keys is set, the loop emits FOREIGN KEY rows alone
+ * and skips everything else - that is the natural complement of
+ * the includes_foreign_keys=false first pass, used to add the
+ * cross-table references once data has been loaded.
+ */
+static void
+emit_local_constraints(TableDdlContext * ctx)
+{
+ Relation conRel;
+ SysScanDesc conScan;
+ ScanKeyData conKey;
+ HeapTuple conTup;
+ bool is_partition = ctx->rel->rd_rel->relispartition;
+
+ if (!ctx->include_constraints)
+ return;
+
+ 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);
+ char *condef;
+
+ if (!con->conislocal)
+ continue;
+ if (ctx->only_foreign_keys && con->contype != CONSTRAINT_FOREIGN)
+ continue;
+ if (con->contype == CONSTRAINT_CHECK && !is_partition)
+ continue;
+ if (con->contype == CONSTRAINT_NOTNULL && !is_partition &&
+ list_member_oid(ctx->skip_notnull_oids, con->oid))
+ continue;
+ if (con->contype == CONSTRAINT_FOREIGN && !ctx->include_foreign_keys)
+ continue;
+
+ condef = pg_get_constraintdef_command(con->oid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", condef);
+ append_stripped_stmt(ctx);
+ pfree(condef);
+ }
+ 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 (!ctx->include_rules || 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;
+ Datum ruledef;
+ char *ruledef_str;
+
+ ruledef = OidFunctionCall1(F_PG_GET_RULEDEF_OID,
+ ObjectIdGetDatum(ruleid));
+ ruledef_str = TextDatumGetCString(ruledef);
+ resetStringInfo(&ctx->buf);
+ appendStringInfoString(&ctx->buf, ruledef_str);
+ append_stripped_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 (!ctx->include_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_string(stat->oid);
+
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", statdef);
+ append_stripped_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 (!ctx->include_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 (!ctx->include_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 (!ctx->include_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);
+ List *childstmts;
+
+ childstmts = pg_get_table_ddl_internal(childoid, ctx->pretty,
+ ctx->no_owner,
+ ctx->no_tablespace,
+ ctx->include_indexes,
+ ctx->include_constraints,
+ ctx->include_foreign_keys,
+ ctx->only_foreign_keys,
+ ctx->include_rules,
+ ctx->include_statistics,
+ ctx->include_triggers,
+ ctx->include_policies,
+ ctx->include_rls,
+ ctx->include_replica_identity,
+ ctx->include_partition,
+ ctx->schema_qualified);
+ 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 first list element is the CREATE TABLE statement. Subsequent
+ * elements are the ALTER TABLE / CREATE INDEX / CREATE RULE /
+ * CREATE STATISTICS statements needed to restore the table's full
+ * definition.
+ *
+ * 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(Oid relid, bool pretty,
+ bool no_owner, bool no_tablespace,
+ bool include_indexes,
+ bool include_constraints,
+ bool include_foreign_keys,
+ bool only_foreign_keys,
+ bool include_rules,
+ bool include_statistics,
+ bool include_triggers,
+ bool include_policies,
+ bool include_rls,
+ bool include_replica_identity,
+ bool include_partition,
+ bool schema_qualified)
+{
+ TableDdlContext ctx = {0};
+ Relation rel;
+ char relkind;
+ AclResult aclresult;
+
+ rel = table_open(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(relid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLE,
+ RelationGetRelationName(rel));
+
+ /*
+ * only_foreign_keys is meant to pair with a prior call that suppressed
+ * FOREIGN KEY emission via includes_foreign_keys=false. Combining
+ * only_foreign_keys=true with includes_foreign_keys=false would emit
+ * nothing useful, so reject that combination up front.
+ */
+ if (only_foreign_keys && !include_foreign_keys)
+ {
+ table_close(rel, AccessShareLock);
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"only_foreign_keys\" requires \"includes_foreign_keys\" to be true")));
+ }
+
+ ctx.rel = rel;
+ ctx.relid = relid;
+ ctx.pretty = pretty;
+ ctx.no_owner = no_owner;
+ ctx.no_tablespace = no_tablespace;
+ ctx.include_indexes = include_indexes;
+ ctx.include_constraints = include_constraints;
+ ctx.include_foreign_keys = include_foreign_keys;
+ ctx.only_foreign_keys = only_foreign_keys;
+ ctx.include_rules = include_rules;
+ ctx.include_statistics = include_statistics;
+ ctx.include_triggers = include_triggers;
+ ctx.include_policies = include_policies;
+ ctx.include_rls = include_rls;
+ ctx.include_replica_identity = include_replica_identity;
+ ctx.include_partition = include_partition;
+ ctx.schema_qualified = schema_qualified;
+ ctx.base_namespace = RelationGetNamespace(rel);
+ ctx.save_nestlevel = -1;
+ ctx.qualname = lookup_relname_for_emit(relid, 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, etc. come out unqualified. 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.
+ *
+ * Pre-compute "<schema>." too - the always-qualified helpers
+ * (constraintdef_command, indexdef_string, ruledef,
+ * statisticsobjdef_string) ignore search_path for the target's own "ALTER
+ * TABLE schema.tbl"/"ON schema.tbl"/"TO schema.tbl" prefix and for the
+ * qualified statistics name; we strip that string ourselves via
+ * strip_schema_prefix.
+ */
+ if (!schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx.base_namespace);
+
+ if (nspname != NULL)
+ {
+ const char *qnsp = quote_identifier(nspname);
+
+ ctx.schema_prefix = psprintf("%s.", qnsp);
+ 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);
+
+ /*
+ * Each pass below appends zero or more finished statements to
+ * ctx.statements via append_stmt / append_stripped_stmt. 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); and partition children
+ * last so the parent already exists at replay time.
+ *
+ * In only_foreign_keys mode the table itself and every non-FK sub-object
+ * already exist (the caller produced them in a prior pass via
+ * includes_foreign_keys=false), so we skip directly to the constraint
+ * loop, which then emits FOREIGN KEY rows only. Partition-child
+ * recursion still runs so child FKs are reached too.
+ */
+ if (!only_foreign_keys)
+ {
+ emit_create_table_stmt(&ctx);
+ emit_owner_stmt(&ctx);
+ emit_child_default_overrides(&ctx);
+ emit_attoptions(&ctx);
+ emit_indexes(&ctx);
+ }
+ emit_local_constraints(&ctx);
+ if (!only_foreign_keys)
+ {
+ 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 (include_triggers)
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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 (include_policies)
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(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);
+ }
+#else
+ (void) include_triggers;
+ (void) include_policies;
+#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);
+ if (ctx.schema_prefix != NULL)
+ pfree(ctx.schema_prefix);
+ 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;
+ Oid relid;
+ DdlOption opts[] = {
+ {"pretty", DDL_OPT_BOOL},
+ {"owner", DDL_OPT_BOOL},
+ {"tablespace", DDL_OPT_BOOL},
+ {"includes_indexes", DDL_OPT_BOOL},
+ {"includes_constraints", DDL_OPT_BOOL},
+ {"includes_foreign_keys", DDL_OPT_BOOL},
+ {"only_foreign_keys", DDL_OPT_BOOL},
+ {"includes_rules", DDL_OPT_BOOL},
+ {"includes_statistics", DDL_OPT_BOOL},
+ {"includes_triggers", DDL_OPT_BOOL},
+ {"includes_policies", DDL_OPT_BOOL},
+ {"includes_rls", DDL_OPT_BOOL},
+ {"includes_replica_identity", DDL_OPT_BOOL},
+ {"includes_partition", DDL_OPT_BOOL},
+ {"schema_qualified", DDL_OPT_BOOL},
+ };
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ relid = PG_GETARG_OID(0);
+ parse_ddl_options(fcinfo, 1, opts, lengthof(opts));
+
+ statements = pg_get_table_ddl_internal(relid,
+ opts[0].isset && opts[0].boolval,
+ opts[1].isset && !opts[1].boolval,
+ opts[2].isset && !opts[2].boolval,
+ !opts[3].isset || opts[3].boolval,
+ !opts[4].isset || opts[4].boolval,
+ !opts[5].isset || opts[5].boolval,
+ opts[6].isset && opts[6].boolval,
+ !opts[7].isset || opts[7].boolval,
+ !opts[8].isset || opts[8].boolval,
+ !opts[9].isset || opts[9].boolval,
+ !opts[10].isset || opts[10].boolval,
+ !opts[11].isset || opts[11].boolval,
+ !opts[12].isset || opts[12].boolval,
+ opts[13].isset && opts[13].boolval,
+ !opts[14].isset || opts[14].boolval);
+ 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/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 be157a5fbe9..d13cf10eb43 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,13 @@
proargtypes => 'regdatabase text', proallargtypes => '{regdatabase,text}',
proargmodes => '{i,v}', proargdefaults => '{NULL}',
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/test/regress/expected/pg_get_table_ddl.out b/src/test/regress/expected/pg_get_table_ddl.out
new file mode 100644
index 00000000000..94005e50029
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,818 @@
+--
+-- 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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);
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_always_not_null NOT NULL id_always;
+ ALTER TABLE pgtbl_ddl_test.id_cols ADD CONSTRAINT id_cols_id_default_not_null NOT NULL id_default;
+(3 rows)
+
+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);
+ ALTER TABLE pgtbl_ddl_test.id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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)
+
+-- 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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(5 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 parent (RANGE and HASH).
+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)
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0);
+(1 row)
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(1 row)
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx ( a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx ADD CONSTRAINT ri_idx_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(4 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)));
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_a_not_null NOT NULL a;
+ ALTER TABLE pgtbl_ddl_test.typed_over ADD CONSTRAINT typed_over_b_not_null NOT NULL b;
+(3 rows)
+
+-- Temporary tables + ON COMMIT. Temp tables live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_default ( id integer);
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_delete ( id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_drop'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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)
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+ 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_id_not_null NOT NULL id;
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_foreign_keys=false suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- schema-cloning workflow: emit tables first without cross-table FKs,
+-- then run pg_get_table_ddl(..., only_foreign_keys=>true) after data
+-- has been loaded to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_foreign_keys', '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);
+(2 rows)
+
+-- only_foreign_keys=true is the complement of the previous call: it
+-- emits only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY rows
+-- and suppresses the CREATE TABLE and every non-FK sub-object.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'only_foreign_keys', 'true');
+ 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 in only_foreign_keys
+-- mode.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, 'owner', 'false',
+ 'only_foreign_keys', 'true');
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- Combining only_foreign_keys=true with includes_foreign_keys=false
+-- would emit nothing useful, so the combination is rejected.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'only_foreign_keys', 'true',
+ 'includes_foreign_keys', 'false');
+ERROR: "only_foreign_keys" requires "includes_foreign_keys" to be true
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt ( id integer);
+(1 row)
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx ( a integer, b integer, c integer);
+(1 row)
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+ pg_get_table_ddl
+------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls ( id integer);
+(1 row)
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+ pg_get_table_ddl
+---------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full ( a integer);
+(1 row)
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range ( id integer, k integer) PARTITION BY RANGE (id);
+ 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;
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(4 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_id_not_null NOT NULL id;
+ ALTER TABLE basic ADD CONSTRAINT basic_name_not_null NOT NULL name;
+ ALTER TABLE basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(4 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_id_not_null NOT NULL id;
+ 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);
+(4 rows)
+
+-- 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);
+ ALTER TABLE id_custom ADD CONSTRAINT id_custom_v_not_null NOT NULL v;
+(2 rows)
+
+-- 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
+-- User-named NOT NULL constraint must be emitted inline as
+-- "CONSTRAINT name NOT NULL" so a replay does not collide with the
+-- auto-named NOT NULL that PostgreSQL would otherwise create from a
+-- plain inline "NOT NULL". The constraint loop skips the user-named
+-- entry to avoid double-emission.
+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);
+ ALTER TABLE pgtbl_ddl_test.nn_named ADD CONSTRAINT nn_named_b_not_null NOT NULL b;
+(2 rows)
+
+DROP TABLE nn_named;
+-- 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.
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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 25 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 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 rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ 'owner', 'false') WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 20 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 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 rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..636d75b9072
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,482 @@
+--
+-- 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');
+
+-- 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 parent (RANGE and HASH).
+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');
+
+-- 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 live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS 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[]);
+
+-- includes_* gating: each sub-object category can be suppressed individually.
+-- includes_indexes=false hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'includes_indexes', 'false');
+
+-- includes_constraints=false hides both inline CHECK in CREATE TABLE and
+-- the ALTER TABLE ... ADD CONSTRAINT lines.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_constraints', 'false');
+
+-- includes_foreign_keys=false suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- schema-cloning workflow: emit tables first without cross-table FKs,
+-- then run pg_get_table_ddl(..., only_foreign_keys=>true) after data
+-- has been loaded to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'includes_foreign_keys', 'false');
+
+-- only_foreign_keys=true is the complement of the previous call: it
+-- emits only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY rows
+-- and suppresses the CREATE TABLE and every non-FK sub-object.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'only_foreign_keys', 'true');
+
+-- A table with no foreign keys produces no rows in only_foreign_keys
+-- mode.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, 'owner', 'false',
+ 'only_foreign_keys', 'true');
+
+-- Combining only_foreign_keys=true with includes_foreign_keys=false
+-- would emit nothing useful, so the combination is rejected.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'only_foreign_keys', 'true',
+ 'includes_foreign_keys', 'false');
+
+-- includes_rules=false hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'includes_rules', 'false');
+
+-- includes_statistics=false hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'includes_statistics', 'false');
+
+-- includes_rls=false hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'includes_rls', 'false');
+
+-- includes_replica_identity=false hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'includes_replica_identity', 'false');
+
+-- includes_partition: default is false, so the partitioned-table parent
+-- DDL on its own does not include its children. Setting it to true
+-- appends the children's DDL after the parent.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'includes_partition', 'true');
+
+-- 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');
+
+-- 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;
+
+-- User-named NOT NULL constraint must be emitted inline as
+-- "CONSTRAINT name NOT NULL" so a replay does not collide with the
+-- auto-named NOT NULL that PostgreSQL would otherwise create from a
+-- plain inline "NOT NULL". The constraint loop skips the user-named
+-- entry to avoid double-emission.
+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;
+
+-- 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');
+
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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;
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-22 14:00 Chao Li <[email protected]>
parent: Akshay Joshi <[email protected]>
1 sibling, 1 reply; 54+ messages in thread
From: Chao Li @ 2026-06-22 14:00 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
> On Jun 22, 2026, at 20:40, Akshay Joshi <[email protected]> wrote:
>
> You're right, and thanks for spotting this. The existing pattern in pg_proc.dat for variadic-text functions (e.g., jsonb_delete, json_extract_path) uses _text at the variadic position in both proargtypes and proallargtypes, with provariadic => 'text'. That is the convention documented by the sanity check in src/test/regress/sql/opr_sanity.sql.
>
> The same issue applies to pg_get_role_ddl, pg_get_tablespace_ddl (both variants), and pg_get_database_ddl, but that will require a separate patch.
>
Thanks for confirming. Then I will file a patch tomorrow to fix those.
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-22 14:11 Akshay Joshi <[email protected]>
parent: Chao Li <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-06-22 14:11 UTC (permalink / raw)
To: Chao Li <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
On Mon, Jun 22, 2026 at 7:30 PM Chao Li <[email protected]> wrote:
>
>
> > On Jun 22, 2026, at 20:40, Akshay Joshi <[email protected]>
> wrote:
> >
> > You're right, and thanks for spotting this. The existing pattern in
> pg_proc.dat for variadic-text functions (e.g., jsonb_delete,
> json_extract_path) uses _text at the variadic position in both proargtypes
> and proallargtypes, with provariadic => 'text'. That is the convention
> documented by the sanity check in src/test/regress/sql/opr_sanity.sql.
> >
> > The same issue applies to pg_get_role_ddl, pg_get_tablespace_ddl (both
> variants), and pg_get_database_ddl, but that will require a separate patch.
> >
>
> Thanks for confirming. Then I will file a patch tomorrow to fix those.
>
I started working on it, but if you want to take the lead, just let me
know and I won't send my version over
>
> Best regards,
> --
> Chao Li (Evan)
> HighGo Software Co., Ltd.
> https://www.highgo.com/
>
>
>
>
>
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-22 14:54 Marcos Pegoraro <[email protected]>
parent: Akshay Joshi <[email protected]>
2 siblings, 1 reply; 54+ messages in thread
From: Marcos Pegoraro @ 2026-06-22 14:54 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
Em seg., 22 de jun. de 2026 às 03:27, Akshay Joshi <
[email protected]> escreveu:
> The documentation paragraph for `includes_foreign_keys` now directs users
> to `only_foreign_keys` as the intended second pass. Regression coverage
> adds three cases: the FK-only emission for your cons example, the zero-row
> result for a table without FKs, and the error path.
>
>>
I still think this model of only having options for foreign keys is
incomplete, maybe wrong.
Imagine then cloning a schema from a publication server to be executed on a
subscription server. So I don't want any other constraints besides the
primary key, for example. The way you implemented it is not possible.
Furthermore having only_foreign_keys and includes_foreign_keys seems
confuse.
regards
Marcos
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-22 18:56 Zsolt Parragi <[email protected]>
parent: Akshay Joshi <[email protected]>
2 siblings, 0 replies; 54+ messages in thread
From: Zsolt Parragi @ 2026-06-22 18:56 UTC (permalink / raw)
To: [email protected]
Thanks, I can confirm that only_foreign_keys works properly.
> Combining `only_foreign_keys=true` with `includes_foreign_keys=false` is
> rejected upfront since it would produce no output.
Shouldn't only_foreign_keys=true with include_constraints=false also error out?
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-23 02:18 Chao Li <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 0 replies; 54+ messages in thread
From: Chao Li @ 2026-06-23 02:18 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
> On Jun 22, 2026, at 22:11, Akshay Joshi <[email protected]> wrote:
>
>
>
> On Mon, Jun 22, 2026 at 7:30 PM Chao Li <[email protected]> wrote:
>
>
> > On Jun 22, 2026, at 20:40, Akshay Joshi <[email protected]> wrote:
> >
> > You're right, and thanks for spotting this. The existing pattern in pg_proc.dat for variadic-text functions (e.g., jsonb_delete, json_extract_path) uses _text at the variadic position in both proargtypes and proallargtypes, with provariadic => 'text'. That is the convention documented by the sanity check in src/test/regress/sql/opr_sanity.sql.
> >
> > The same issue applies to pg_get_role_ddl, pg_get_tablespace_ddl (both variants), and pg_get_database_ddl, but that will require a separate patch.
> >
>
> Thanks for confirming. Then I will file a patch tomorrow to fix those.
>
> I started working on it, but if you want to take the lead, just let me know and I won't send my version over
>
The changes to pg_proc.dat have been in my local tree for some time. Today I also fixed the sanity check in opr_sanity so that it can now report this mismatch. Could you please take a look at my patch [1]?
[1] https://www.postgresql.org/message-id/D41A334E-ED9E-42EE-830D-28D4D36E9317%40gmail.com
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-23 06:15 Kyotaro Horiguchi <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 0 replies; 54+ messages in thread
From: Kyotaro Horiguchi @ 2026-06-23 06:15 UTC (permalink / raw)
To: [email protected]; +Cc: [email protected]; [email protected]
At Mon, 15 Jun 2026 14:25:06 +0530, Akshay Joshi <[email protected]> wrote in
> Hi Kyotaro and Zsolt,
>
> I have incorporated the feedback provided by both of you.
> The v6 patch is updated and ready for your review.
>
> On Fri, Jun 12, 2026 at 6:40 AM Kyotaro Horiguchi <[email protected]>
> wrote:
> >
> > I have not looked at the patch in detail, but I noticed that some
> > comments in the patch seem to contain non-ASCII characters.
Thanks for the update.
That issue appears to be resolved in this version.
Regards,
--
Kyotaro Horiguchi
NTT Open Source Software Center
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-23 07:21 Kyotaro Horiguchi <[email protected]>
parent: Akshay Joshi <[email protected]>
1 sibling, 1 reply; 54+ messages in thread
From: Kyotaro Horiguchi @ 2026-06-23 07:21 UTC (permalink / raw)
To: [email protected]; +Cc: [email protected]; [email protected]; [email protected]
At Mon, 22 Jun 2026 18:10:53 +0530, Akshay Joshi <[email protected]> wrote in
> The v9 patch is ready for review.
I have not looked closely at the DDL generation logic itself, but I
have a few comments on how pg_get_table_ddl handles its options.
Since pg_get_table_ddl_internal() appears to copy these values into
TableDdlContext almost immediately, I wonder whether TableDdlContext
could be initialized by the caller instead.
Using positional boolean arguments is probably fine when there are
only a handful of options, but with around fifteen of them the current
approach seems somewhat error-prone.
It might also be clearer to initialize the default values first, and
then override only the fields corresponding to explicitly specified
options, rather than folding the default handling and option lookup
into the same expression.
Regards,
--
Kyotaro Horiguchi
NTT Open Source Software Center
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-23 08:57 Akshay Joshi <[email protected]>
parent: Marcos Pegoraro <[email protected]>
0 siblings, 0 replies; 54+ messages in thread
From: Akshay Joshi @ 2026-06-23 08:57 UTC (permalink / raw)
To: Marcos Pegoraro <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
On Mon, Jun 22, 2026 at 8:25 PM Marcos Pegoraro <[email protected]> wrote:
> Em seg., 22 de jun. de 2026 às 03:27, Akshay Joshi <
> [email protected]> escreveu:
>
>> The documentation paragraph for `includes_foreign_keys` now directs users
>> to `only_foreign_keys` as the intended second pass. Regression coverage
>> adds three cases: the FK-only emission for your cons example, the zero-row
>> result for a table without FKs, and the error path.
>>
>>>
> I still think this model of only having options for foreign keys is
> incomplete, maybe wrong.
> Imagine then cloning a schema from a publication server to be executed on
> a subscription server. So I don't want any other constraints besides the
> primary key, for example. The way you implemented it is not possible.
>
> Furthermore having only_foreign_keys and includes_foreign_keys seems
> confuse.
>
OK. I'd like to change the model, not just the flag names. Drop the
entire *includes_** family and *only_foreign_keys*, replace them with two
mutually-exclusive variadic keys:
- include => 'kind1,kind2,...' — emit only these kinds
- exclude => 'kind1,kind2,...' — emit everything except these
Setting both is an error. Setting neither emits everything (today's
default behavior, which is preserved).
*Vocabulary*: indexes, primary_key, unique, check, foreign_keys,
exclusion, rules, statistics, triggers, policies, rls, replica_identity,
partitions. Unknown kind → parse-time error, which also
catches typos that the boolean version silently accepted.
If everyone approves the model above, I'll try implementing it.
regards
> Marcos
>
>
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-23 09:22 Akshay Joshi <[email protected]>
parent: Kyotaro Horiguchi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-06-23 09:22 UTC (permalink / raw)
To: Kyotaro Horiguchi <[email protected]>; +Cc: [email protected]; [email protected]; [email protected]
On Tue, Jun 23, 2026 at 12:51 PM Kyotaro Horiguchi <[email protected]>
wrote:
> At Mon, 22 Jun 2026 18:10:53 +0530, Akshay Joshi <
> [email protected]> wrote in
> > The v9 patch is ready for review.
>
> I have not looked closely at the DDL generation logic itself, but I
> have a few comments on how pg_get_table_ddl handles its options.
>
> Since pg_get_table_ddl_internal() appears to copy these values into
> TableDdlContext almost immediately, I wonder whether TableDdlContext
> could be initialized by the caller instead.
>
> Using positional boolean arguments is probably fine when there are
> only a handful of options, but with around fifteen of them the current
> approach seems somewhat error-prone.
>
> It might also be clearer to initialize the default values first, and
> then override only the fields corresponding to explicitly specified
> options, rather than folding the default handling and option lookup
> into the same expression.
>
I assume that changing the implementation model, as I mentioned in my other
email, will solve this problem as well. We can drop the entire includes_*
family and only_foreign_keys, and replace them with two mutually exclusive
variadic keys:
include => 'kind1,kind2,...' — emit only these kinds
exclude => 'kind1,kind2,...' — emit everything except these kinds
>
>
> Regards,
>
> --
> Kyotaro Horiguchi
> NTT Open Source Software Center
>
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-23 13:34 Akshay Joshi <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 2 replies; 54+ messages in thread
From: Akshay Joshi @ 2026-06-23 13:34 UTC (permalink / raw)
To: Kyotaro Horiguchi <[email protected]>; +Cc: [email protected]; [email protected]; [email protected]
All,
A quick note on a design change I made in the *pg_get_table_ddl* option
surface.
The earlier draft exposed one boolean option per sub-object kind:
include_indexes, include_primary_key, include_check, include_foreign_keys,
include_rules, include_statistics, include_rls, include_replica_identity,
include_partitions, and so on. Every new kind we wanted to gate (triggers,
policies, exclusion constraints…) meant another option name. Callers
wanting "everything except FKs" had to flip nine flags, and the
include/exclude polarity was not symmetric: to drop one item, you toggled
one flag, but to keep only one item, you toggled all the others.
*I have replaced those with two options:*
- include — comma-separated list of kinds; emit only the listed ones.
- exclude — comma-separated list of kinds; emit everything except the
listed ones.
*Vocabulary*: table, indexes, primary_key, unique, check, foreign_keys,
exclusion, rules, statistics, triggers, policies, rls, replica_identity,
partitions.
NOT NULL is intentionally omitted from the vocabulary; it's always emitted
to avoid silently producing schemas that accept NULLs when the source would
have rejected them.
The v10 patch is ready for review.
On Tue, Jun 23, 2026 at 2:52 PM Akshay Joshi <[email protected]>
wrote:
>
>
> On Tue, Jun 23, 2026 at 12:51 PM Kyotaro Horiguchi <
> [email protected]> wrote:
>
>> At Mon, 22 Jun 2026 18:10:53 +0530, Akshay Joshi <
>> [email protected]> wrote in
>> > The v9 patch is ready for review.
>>
>> I have not looked closely at the DDL generation logic itself, but I
>> have a few comments on how pg_get_table_ddl handles its options.
>>
>> Since pg_get_table_ddl_internal() appears to copy these values into
>> TableDdlContext almost immediately, I wonder whether TableDdlContext
>> could be initialized by the caller instead.
>>
>> Using positional boolean arguments is probably fine when there are
>> only a handful of options, but with around fifteen of them the current
>> approach seems somewhat error-prone.
>>
>> It might also be clearer to initialize the default values first, and
>> then override only the fields corresponding to explicitly specified
>> options, rather than folding the default handling and option lookup
>> into the same expression.
>>
>
> I assume that changing the implementation model, as I mentioned in my
> other email, will solve this problem as well. We can drop the entire
> includes_* family and only_foreign_keys, and replace them with two mutually
> exclusive variadic keys:
> include => 'kind1,kind2,...' — emit only these kinds
> exclude => 'kind1,kind2,...' — emit everything except these kinds
>>
>>
>> Regards,
>>
>> --
>> Kyotaro Horiguchi
>> NTT Open Source Software Center
>>
>
Attachments:
[application/octet-stream] v10-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (149.6K, ../../CANxoLDdEBYzKfj3NGrP5PjoTa-eCViWPkFR-sXLq4bUnbokyPA@mail.gmail.com/3-v10-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From 07d30797beb4aab778f46fa1dabcebaba5250f23 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v10] 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_string, constraints
(PRIMARY KEY, UNIQUE, FOREIGN KEY, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_command, rules via pg_get_ruledef, extended
statistics via pg_get_statisticsobjdef_string, 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). Object-class filtering is expressed through two
mutually-exclusive options, include and exclude, each taking a
comma-separated list drawn from the kind vocabulary
{table, indexes, primary_key, unique, check, foreign_keys, exclusion,
rules, statistics, triggers, policies, rls, replica_identity,
partitions}. When include is set, only the listed kinds are emitted;
when exclude 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 <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kyotaro Horiguchi <[email protected]>
Reviewed-by: Chao Li <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 120 +
src/backend/catalog/pg_inherits.c | 93 +
src/backend/commands/tablecmds.c | 30 +-
src/backend/utils/adt/ddlutils.c | 1996 +++++++++++++++++
src/include/catalog/pg_inherits.h | 1 +
src/include/catalog/pg_proc.dat | 7 +
src/include/commands/tablecmds.h | 3 +
.../regress/expected/pg_get_table_ddl.out | 885 ++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 542 +++++
10 files changed, 3676 insertions(+), 3 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 8ffa7e83275..d1759dbde67 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3947,6 +3947,126 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
<literal>TABLESPACE</literal>.
</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>include</literal> and <literal>exclude</literal>
+ options described below for filtering which object classes are
+ emitted.
+ </para>
+ <para>
+ The <literal>include</literal> and <literal>exclude</literal>
+ options each take a comma-separated list of object-class kind
+ names and are mutually exclusive (specifying both raises an
+ error). When <literal>include</literal> is set, only the
+ listed kinds are emitted; when <literal>exclude</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>indexes</literal>,
+ <literal>primary_key</literal>,
+ <literal>unique</literal>,
+ <literal>check</literal>,
+ <literal>foreign_keys</literal>,
+ <literal>exclusion</literal>,
+ <literal>rules</literal>,
+ <literal>statistics</literal>,
+ <literal>triggers</literal>,
+ <literal>policies</literal>,
+ <literal>rls</literal> (the
+ <command>ENABLE</command>/<command>FORCE ROW LEVEL SECURITY</command>
+ toggles),
+ <literal>replica_identity</literal>, and
+ <literal>partitions</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>include => 'foreign_keys'</literal>, and the
+ pub/sub-style clone that keeps a primary key but drops every
+ other constraint can be written as
+ <literal>exclude => 'unique,check,foreign_keys,exclusion'</literal>.
+ </para>
+ <para>
+ Excluding <literal>primary_key</literal> on a table whose
+ <literal>REPLICA IDENTITY</literal> is set to
+ <literal>USING INDEX</literal> of the primary key raises an
+ error unless <literal>replica_identity</literal> is also
+ excluded; otherwise the emitted DDL would reference an index
+ the same DDL just dropped.
+ </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 265dcfe7fda..a5f46522c33 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -739,7 +739,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);
@@ -2515,7 +2514,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)
@@ -19558,6 +19557,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 f32fcd453ef..9e00061c62b 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -21,12 +21,25 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.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_am.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 "catalog/partition.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
+#include "nodes/nodes.h"
#include "commands/tablespace.h"
#include "common/relpath.h"
#include "funcapi.h"
@@ -73,8 +86,55 @@ typedef struct DdlOption
} DdlOption;
+/*
+ * Object-class kinds that the include/exclude 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_INDEXES,
+ TABLE_DDL_KIND_PRIMARY_KEY,
+ TABLE_DDL_KIND_UNIQUE,
+ TABLE_DDL_KIND_CHECK,
+ TABLE_DDL_KIND_FOREIGN_KEYS,
+ TABLE_DDL_KIND_EXCLUSION,
+ TABLE_DDL_KIND_RULES,
+ TABLE_DDL_KIND_STATISTICS,
+ TABLE_DDL_KIND_TRIGGERS,
+ TABLE_DDL_KIND_POLICIES,
+ TABLE_DDL_KIND_RLS,
+ TABLE_DDL_KIND_REPLICA_IDENTITY,
+ TABLE_DDL_KIND_PARTITIONS,
+} TableDdlKind;
+
+static const struct
+{
+ const char *name;
+ TableDdlKind kind;
+} table_ddl_kind_names[] =
+{
+ {"table", TABLE_DDL_KIND_TABLE},
+ {"indexes", TABLE_DDL_KIND_INDEXES},
+ {"primary_key", TABLE_DDL_KIND_PRIMARY_KEY},
+ {"unique", TABLE_DDL_KIND_UNIQUE},
+ {"check", TABLE_DDL_KIND_CHECK},
+ {"foreign_keys", TABLE_DDL_KIND_FOREIGN_KEYS},
+ {"exclusion", TABLE_DDL_KIND_EXCLUSION},
+ {"rules", TABLE_DDL_KIND_RULES},
+ {"statistics", TABLE_DDL_KIND_STATISTICS},
+ {"triggers", TABLE_DDL_KIND_TRIGGERS},
+ {"policies", TABLE_DDL_KIND_POLICIES},
+ {"rls", TABLE_DDL_KIND_RLS},
+ {"replica_identity", TABLE_DDL_KIND_REPLICA_IDENTITY},
+ {"partitions", TABLE_DDL_KIND_PARTITIONS},
+};
+
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);
@@ -87,6 +147,95 @@ static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool i
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, schema_prefix, 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 included_kinds is non-NULL, only the
+ * kinds in that set are emitted; if excluded_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 *included_kinds;
+ Bitmapset *excluded_kinds;
+
+ /* Derived during setup */
+ Oid base_namespace;
+ char *qualname;
+ char *schema_prefix; /* NULL when schema_qualified */
+ 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 char *strip_schema_prefix(const char *stmt, const char *prefix);
+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
@@ -227,6 +376,102 @@ parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start,
}
}
+/*
+ * 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++;
+
+ 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->included_kinds != NULL)
+ return bms_is_member((int) kind, ctx->included_kinds);
+ if (ctx->excluded_kinds != NULL)
+ return !bms_is_member((int) kind, ctx->excluded_kinds);
+ return true;
+}
+
/*
* Helper to append a formatted string with optional pretty-printing.
*/
@@ -1185,3 +1430,1754 @@ 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.
+ *
+ * When schema_qualified is true the schema-qualified name is always
+ * returned. 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 strip.
+ *
+ * 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);
+
+ /* 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;
+}
+
+/*
+ * strip_schema_prefix
+ * Return a palloc'd copy of stmt with every occurrence of prefix
+ * removed, except occurrences that fall inside a single-quoted SQL
+ * string literal or a double-quoted SQL identifier.
+ *
+ * Used to strip the qualified-schema prefix (for example "old_schema.")
+ * from DDL emitted by the always-qualified ruleutils helpers
+ * (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) when the caller has
+ * requested unqualified output via schema_qualified=false. A plain
+ * substring strip would corrupt string literals whose contents happen
+ * to contain "<schema>." (for example a CHECK comparing against
+ * 'myschema.secret') or double-quoted column/identifier names whose
+ * contents happen to contain "<schema>." (for example a column named
+ * "s.weird" when stripping the prefix "s."), so we track quoting state
+ * and skip matches that occur while we are inside either kind of
+ * quoted token. Escaped quotes ('' inside a string, "" inside an
+ * identifier) are handled by keeping the corresponding flag set across
+ * them.
+ */
+static char *
+strip_schema_prefix(const char *stmt, const char *prefix)
+{
+ StringInfoData buf;
+ size_t plen = strlen(prefix);
+ const char *p = stmt;
+ bool in_string = false;
+ bool in_ident = false;
+
+ initStringInfo(&buf);
+ while (*p)
+ {
+ /*
+ * Try the prefix match BEFORE the quote-state toggles. The prefix
+ * itself may begin with a double quote when the base schema name
+ * requires quoting (e.g. "My-Schema".), and at that leading " we must
+ * strip the whole prefix rather than treat it as the opening of a
+ * regular quoted identifier. The check is gated on !in_ident &&
+ * !in_string, so once we have toggled into a quoted token we never
+ * strip a coincidental substring inside it.
+ */
+ if (!in_string && !in_ident && strncmp(p, prefix, plen) == 0)
+ {
+ p += plen;
+ continue;
+ }
+ if (*p == '\'' && !in_ident)
+ {
+ /* SQL '' escape: emit both quotes and stay inside the string. */
+ if (in_string && p[1] == '\'')
+ {
+ appendStringInfoChar(&buf, '\'');
+ appendStringInfoChar(&buf, '\'');
+ p += 2;
+ continue;
+ }
+ in_string = !in_string;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ if (*p == '"' && !in_string)
+ {
+ /* SQL "" escape: emit both quotes and stay inside the ident. */
+ if (in_ident && p[1] == '"')
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoChar(&buf, '"');
+ p += 2;
+ continue;
+ }
+ in_ident = !in_ident;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ appendStringInfoChar(&buf, *p);
+ p++;
+ }
+ return buf.data;
+}
+
+/*
+ * 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 emitted by the INHERITS clause
+ * (once implemented), 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 / append_stripped_stmt
+ * Push ctx->buf onto ctx->statements.
+ *
+ * append_stmt is used for emissions where the table reference is built
+ * from ctx->qualname (already partial-qualified per schema_qualified).
+ * append_stripped_stmt is used after invoking always-qualified ruleutils
+ * helpers (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) - when
+ * schema_qualified is false, the base-schema prefix is stripped from
+ * their output so the emitted DDL is replay-relocatable.
+ */
+static void
+append_stmt(TableDdlContext * ctx)
+{
+ ctx->statements = lappend(ctx->statements, pstrdup(ctx->buf.data));
+}
+
+static void
+append_stripped_stmt(TableDdlContext * ctx)
+{
+ if (ctx->schema_prefix != NULL)
+ ctx->statements = lappend(ctx->statements,
+ strip_schema_prefix(ctx->buf.data,
+ ctx->schema_prefix));
+ else
+ 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_INDEXES))
+ 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_string(idxoid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", idxdef);
+ append_stripped_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 include /
+ * exclude vocabulary, so callers can produce e.g. an FK-only pass
+ * (include => 'foreign_keys') or a pub/sub clone that keeps only
+ * the primary key (exclude => 'unique,check,foreign_keys,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);
+ char *condef;
+
+ if (!con->conislocal)
+ continue;
+
+ /*
+ * Each contype is gated on its kind in the include/exclude
+ * 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_partition)
+ continue;
+ if (!is_kind_included(ctx, TABLE_DDL_KIND_CHECK))
+ continue;
+ break;
+ case CONSTRAINT_FOREIGN:
+ if (!is_kind_included(ctx, TABLE_DDL_KIND_FOREIGN_KEYS))
+ 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
+ * include=foreign_keys 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;
+ }
+
+ condef = pg_get_constraintdef_command(con->oid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", condef);
+ append_stripped_stmt(ctx);
+ pfree(condef);
+ }
+ 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_RULES) ||
+ 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;
+ Datum ruledef;
+ char *ruledef_str;
+
+ ruledef = OidFunctionCall1(F_PG_GET_RULEDEF_OID,
+ ObjectIdGetDatum(ruleid));
+ ruledef_str = TextDatumGetCString(ruledef);
+ resetStringInfo(&ctx->buf);
+ appendStringInfoString(&ctx->buf, ruledef_str);
+ append_stripped_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_string(stat->oid);
+
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", statdef);
+ append_stripped_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_PARTITIONS) ||
+ 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; the included_kinds / excluded_kinds
+ * bitmapsets are shared by pointer (read-only, freed once by the
+ * top-level caller's memory context cleanup).
+ */
+ 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.included_kinds = ctx->included_kinds;
+ childctx.excluded_kinds = ctx->excluded_kinds;
+
+ 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,
+ * included_kinds, excluded_kinds). This function opens the relation,
+ * validates access, populates the derived fields (rel, qualname,
+ * schema_prefix, 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 include or exclude 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: excluding "primary_key" while the table's REPLICA IDENTITY
+ * is set to USING INDEX of the PK would produce DDL that references an
+ * index the same DDL just dropped. Require the user to also exclude
+ * "replica_identity" in that case, or to not exclude the PK.
+ */
+ if (ctx->excluded_kinds != NULL &&
+ bms_is_member((int) TABLE_DDL_KIND_PRIMARY_KEY,
+ ctx->excluded_kinds) &&
+ !bms_is_member((int) TABLE_DDL_KIND_REPLICA_IDENTITY,
+ ctx->excluded_kinds) &&
+ rel->rd_rel->relreplident == REPLICA_IDENTITY_INDEX)
+ {
+ Oid replidx = RelationGetReplicaIndex(rel);
+
+ if (OidIsValid(replidx))
+ {
+ HeapTuple idxTup = SearchSysCache1(INDEXRELID,
+ ObjectIdGetDatum(replidx));
+
+ if (HeapTupleIsValid(idxTup))
+ {
+ bool is_pk = ((Form_pg_index) GETSTRUCT(idxTup))->indisprimary;
+
+ ReleaseSysCache(idxTup);
+
+ if (is_pk)
+ {
+ char *relname = pstrdup(RelationGetRelationName(rel));
+
+ table_close(rel, AccessShareLock);
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot exclude \"primary_key\" without also excluding \"replica_identity\" for table \"%s\"",
+ relname),
+ errdetail("The table's REPLICA IDENTITY references the primary key index.")));
+ }
+ }
+ }
+ }
+
+ /*
+ * Populate derived fields now that the relation is open and validated.
+ * The remaining derived fields (schema_prefix, nn_entries,
+ * skip_notnull_oids, buf, statements) start zeroed via the caller's
+ * `TableDdlContext ctx = {0}` initializer; schema_prefix is set
+ * conditionally below, 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, etc. come out unqualified. 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.
+ *
+ * Pre-compute "<schema>." too - the always-qualified helpers
+ * (constraintdef_command, indexdef_string, ruledef,
+ * statisticsobjdef_string) ignore search_path for the target's own "ALTER
+ * TABLE schema.tbl"/"ON schema.tbl"/"TO schema.tbl" prefix and for the
+ * qualified statistics name; we strip that string ourselves via
+ * strip_schema_prefix.
+ */
+ if (!ctx->schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx->base_namespace);
+
+ if (nspname != NULL)
+ {
+ const char *qnsp = quote_identifier(nspname);
+
+ ctx->schema_prefix = psprintf("%s.", qnsp);
+ 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. include => 'foreign_keys' 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_TRIGGERS))
+ {
+ 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_POLICIES))
+ {
+ 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);
+ if (ctx->schema_prefix != NULL)
+ pfree(ctx->schema_prefix);
+ 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},
+ {"include", DDL_OPT_TEXT},
+ {"exclude", DDL_OPT_TEXT},
+ };
+ enum
+ {
+ OPT_PRETTY,
+ OPT_OWNER,
+ OPT_TABLESPACE,
+ OPT_SCHEMA_QUALIFIED,
+ OPT_INCLUDE,
+ OPT_EXCLUDE,
+ };
+
+ 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_INCLUDE].isset && opts[OPT_EXCLUDE].isset)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"include\" and \"exclude\" 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.included_kinds = NULL;
+ ctx.excluded_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_INCLUDE].isset)
+ ctx.included_kinds = parse_kind_list("include",
+ opts[OPT_INCLUDE].textval);
+ if (opts[OPT_EXCLUDE].isset)
+ ctx.excluded_kinds = parse_kind_list("exclude",
+ opts[OPT_EXCLUDE].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/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 be157a5fbe9..d13cf10eb43 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,13 @@
proargtypes => 'regdatabase text', proallargtypes => '{regdatabase,text}',
proargmodes => '{i,v}', proargdefaults => '{NULL}',
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/test/regress/expected/pg_get_table_ddl.out b/src/test/regress/expected/pg_get_table_ddl.out
new file mode 100644
index 00000000000..200aced7a73
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,885 @@
+--
+-- 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)
+
+-- 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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- Inheritance, including a child DEFAULT override on an inherited column.
+CREATE TABLE par (a int DEFAULT 1, b text);
+CREATE TABLE ch (c int) INHERITS (par);
+ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+SELECT * FROM pg_get_table_ddl('ch'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+---------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ch (c integer) INHERITS (pgtbl_ddl_test.par);
+ ALTER TABLE pgtbl_ddl_test.ch ALTER COLUMN a SET DEFAULT 999;
+(2 rows)
+
+-- Per-column attoptions: emitted as ALTER COLUMN SET (...).
+CREATE TABLE attopt (a int, b text);
+ALTER TABLE attopt ALTER COLUMN a SET (n_distinct = 100);
+SELECT * FROM pg_get_table_ddl('attopt'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.attopt (a integer, b text);
+ ALTER TABLE pgtbl_ddl_test.attopt ALTER COLUMN a SET (n_distinct='100');
+(2 rows)
+
+-- Partitioned table parent (RANGE and HASH).
+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)
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0);
+(1 row)
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(1 row)
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx (a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(3 rows)
+
+-- UNLOGGED + reloptions.
+CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70);
+SELECT * FROM pg_get_table_ddl('uno'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE UNLOGGED TABLE pgtbl_ddl_test.uno (id integer) WITH (fillfactor='70');
+(1 row)
+
+-- Typed table (CREATE TABLE OF type_name). Columns inherited from the
+-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come
+-- out through a single "(col WITH OPTIONS ...)" list.
+CREATE TYPE typed_t AS (a int, b text);
+CREATE TABLE typed_plain OF typed_t;
+SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_plain OF pgtbl_ddl_test.typed_t;
+(1 row)
+
+CREATE TABLE typed_over OF typed_t (
+ a WITH OPTIONS DEFAULT 7 NOT NULL,
+ b WITH OPTIONS NOT NULL,
+ CONSTRAINT b_nonempty CHECK (length(b) > 0)
+);
+SELECT * FROM pg_get_table_ddl('typed_over'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_over OF pgtbl_ddl_test.typed_t (a WITH OPTIONS DEFAULT 7 NOT NULL, b WITH OPTIONS NOT NULL, CONSTRAINT b_nonempty CHECK ((length(b) > 0)));
+(1 row)
+
+-- Temporary tables + ON COMMIT. Temp tables live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+-----------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_default (id integer);
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+--------------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_delete (id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_drop'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+-----------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.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)
+
+-- include / exclude gating: each emits either only the listed kinds
+-- (include) or every kind except the listed ones (exclude). The two
+-- are mutually exclusive. Kind vocabulary: table, indexes,
+-- primary_key, unique, check, foreign_keys, exclusion, rules,
+-- statistics, triggers, policies, rls, replica_identity, partitions.
+-- NOT NULL is not in the vocabulary - always emitted to prevent
+-- producing schemas that silently accept NULLs the source would
+-- have rejected.
+--
+-- exclude=indexes hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'exclude', 'indexes');
+ 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)
+
+-- exclude=primary_key,unique,check,foreign_keys,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',
+ 'exclude',
+ 'primary_key,unique,check,foreign_keys,exclusion');
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- exclude=foreign_keys 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 include=foreign_keys
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'exclude', 'foreign_keys');
+ 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)
+
+-- include=foreign_keys 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',
+ 'include', 'foreign_keys');
+ 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
+-- include=foreign_keys.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, 'owner', 'false',
+ 'include', 'foreign_keys');
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- include=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',
+ 'include', 'table');
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+(1 row)
+
+-- include and exclude are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'include', 'foreign_keys',
+ 'exclude', 'foreign_keys');
+ERROR: "include" and "exclude" 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',
+ 'exclude',
+ 'unique,check,foreign_keys,exclusion');
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- exclude=rules hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'exclude', 'rules');
+ pg_get_table_ddl
+----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+(1 row)
+
+-- exclude=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'exclude', 'statistics');
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+(1 row)
+
+-- exclude=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'exclude', 'rls');
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+(1 row)
+
+-- exclude=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'exclude', 'replica_identity');
+ pg_get_table_ddl
+--------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+(1 row)
+
+-- Partition children are emitted by default; exclude=partitions
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'exclude', 'partitions');
+ 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',
+ 'exclude', 'no_such_kind');
+ERROR: unrecognized kind "no_such_kind" in option "exclude"
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'exclude', ' INDEXES , RULES ');
+ 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)
+
+-- 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.
+-- exclude=primary_key on a table whose REPLICA IDENTITY references
+-- the PK would emit a REPLICA IDENTITY USING INDEX clause that
+-- referenced an index the same DDL just dropped. Require the user
+-- to also exclude replica_identity in that case.
+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',
+ 'exclude', 'primary_key');
+ERROR: cannot exclude "primary_key" without also excluding "replica_identity" for table "ri_pk_excl"
+DETAIL: The table's REPLICA IDENTITY references the primary key index.
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, 'owner', 'false',
+ 'exclude', '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;
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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 25 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 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 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 21 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 25 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 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 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..9e332194db0
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,542 @@
+--
+-- 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');
+
+-- 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 parent (RANGE and HASH).
+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');
+
+-- 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 live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS 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[]);
+
+-- include / exclude gating: each emits either only the listed kinds
+-- (include) or every kind except the listed ones (exclude). The two
+-- are mutually exclusive. Kind vocabulary: table, indexes,
+-- primary_key, unique, check, foreign_keys, exclusion, rules,
+-- statistics, triggers, policies, rls, replica_identity, partitions.
+-- NOT NULL is not in the vocabulary - always emitted to prevent
+-- producing schemas that silently accept NULLs the source would
+-- have rejected.
+--
+-- exclude=indexes hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'exclude', 'indexes');
+
+-- exclude=primary_key,unique,check,foreign_keys,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',
+ 'exclude',
+ 'primary_key,unique,check,foreign_keys,exclusion');
+
+-- exclude=foreign_keys 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 include=foreign_keys
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'exclude', 'foreign_keys');
+
+-- include=foreign_keys 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',
+ 'include', 'foreign_keys');
+
+-- A table with no foreign keys produces no rows under
+-- include=foreign_keys.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, 'owner', 'false',
+ 'include', 'foreign_keys');
+
+-- include=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',
+ 'include', 'table');
+
+-- include and exclude are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'include', 'foreign_keys',
+ 'exclude', 'foreign_keys');
+
+-- 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',
+ 'exclude',
+ 'unique,check,foreign_keys,exclusion');
+
+-- exclude=rules hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false',
+ 'exclude', 'rules');
+
+-- exclude=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false',
+ 'exclude', 'statistics');
+
+-- exclude=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false',
+ 'exclude', 'rls');
+
+-- exclude=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false',
+ 'exclude', 'replica_identity');
+
+-- Partition children are emitted by default; exclude=partitions
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, 'owner', 'false',
+ 'exclude', 'partitions');
+
+-- Unknown kind name is rejected at option-parse time.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, 'owner', 'false',
+ 'exclude', 'no_such_kind');
+
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'exclude', ' INDEXES , RULES ');
+
+-- 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');
+
+-- 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');
+
+-- exclude=primary_key on a table whose REPLICA IDENTITY references
+-- the PK would emit a REPLICA IDENTITY USING INDEX clause that
+-- referenced an index the same DDL just dropped. Require the user
+-- to also exclude replica_identity in that case.
+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',
+ 'exclude', 'primary_key');
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, 'owner', 'false',
+ 'exclude', 'primary_key,replica_identity');
+DROP TABLE ri_pk_excl;
+
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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;
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-23 14:03 Marcos Pegoraro <[email protected]>
parent: Akshay Joshi <[email protected]>
1 sibling, 1 reply; 54+ messages in thread
From: Marcos Pegoraro @ 2026-06-23 14:03 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: Kyotaro Horiguchi <[email protected]>; [email protected]; [email protected]; [email protected]
Em ter., 23 de jun. de 2026 às 10:34, Akshay Joshi <
[email protected]> escreveu:
> A quick note on a design change I made in the *pg_get_table_ddl* option
> surface
>
Much better now.
All options can be present or omitted, so why couldn't the owner also be
part of the include/exclude options too ?
Just one addition, some options are plural and others are not.
We have indexes, policies, triggers, but we also have check, unique,
exclusion.
You don't know how many indices or checks you have, so wouldn't it be
better to have them in singular form ?
regards
Marcos
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-23 20:05 Zsolt Parragi <[email protected]>
parent: Akshay Joshi <[email protected]>
1 sibling, 1 reply; 54+ messages in thread
From: Zsolt Parragi @ 2026-06-23 20:05 UTC (permalink / raw)
To: [email protected]
The new design is definitely an improvement as a more generic
interface. I'm unsure about the "include" wording, to me that suggest
"also includes", while the actual behavior is "only includes". To me
it would be a bit surprising pg_get_table_ddl including something not
including the basic create table statement.
I also found a few issues with include in v10.
Include check doesn't seem to work:
CREATE TABLE t (
id int PRIMARY KEY,
qty int CHECK (qty > 0),
CONSTRAINT t_id_pos CHECK (id > 0)
);
SELECT * FROM pg_get_table_ddl('t', 'include', 'check'); -- empty?
The include partitions clause also doesn't work:
CREATE TABLE p (id int, val text) PARTITION BY RANGE (id);
CREATE TABLE p_a PARTITION OF p FOR VALUES FROM (0) TO (100);
CREATE TABLE p_b PARTITION OF p FOR VALUES FROM (100) TO (200);
SELECT * FROM pg_get_table_ddl('p','include','partitions'); -- empty
There's also an issue with replica identity non primary key unique indexes:
CREATE TABLE t2 (a int NOT NULL UNIQUE, b int);
ALTER TABLE t2 REPLICA IDENTITY USING INDEX t2_a_key;
SELECT * FROM pg_get_table_ddl('t2','exclude','unique');
-- ALTER TABLE public.t2 REPLICA IDENTITY USING INDEX t2_a_key;
-- but there's no such index
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-24 08:23 Akshay Joshi <[email protected]>
parent: Marcos Pegoraro <[email protected]>
0 siblings, 0 replies; 54+ messages in thread
From: Akshay Joshi @ 2026-06-24 08:23 UTC (permalink / raw)
To: Marcos Pegoraro <[email protected]>; +Cc: Kyotaro Horiguchi <[email protected]>; [email protected]; [email protected]; [email protected]
On Tue, Jun 23, 2026 at 7:33 PM Marcos Pegoraro <[email protected]> wrote:
> Em ter., 23 de jun. de 2026 às 10:34, Akshay Joshi <
> [email protected]> escreveu:
>
>> A quick note on a design change I made in the *pg_get_table_ddl* option
>> surface
>>
>
> Much better now.
>
> All options can be present or omitted, so why couldn't the owner also be
> part of the include/exclude options too ?
>
> Just one addition, some options are plural and others are not.
> We have indexes, policies, triggers, but we also have check, unique,
> exclusion.
> You don't know how many indices or checks you have, so wouldn't it be
> better to have them in singular form ?
>
Regarding moving owner into the include/exclude syntax, I prefer keeping it
as a top-level boolean for now:
1) It aligns with existing sibling functions (pg_get_role_ddl,
pg_get_tablespace_ddl, and pg_get_database_ddl), which all use a boolean
owner.
2) Existing regression tests and early callers pass owner => false for
output stability. Moving this into the exclude list would create
unnecessary churn that I'd prefer to avoid bundling here.
As for the naming inconsistency (singular vs. plural), you're entirely
right mixing indexes/policies/triggers with checks/uniques/exclusions was
an oversight. Note that I left statistics alone to match the CREATE
STATISTICS keyword.
I'll address the naming consistency in the next patch.
>
> regards
> Marcos
>
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-24 09:16 Akshay Joshi <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-06-24 09:16 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: [email protected]
On Wed, Jun 24, 2026 at 1:35 AM Zsolt Parragi <[email protected]>
wrote:
> The new design is definitely an improvement as a more generic
> interface. I'm unsure about the "include" wording, to me that suggest
> "also includes", while the actual behavior is "only includes". To me
> it would be a bit surprising pg_get_table_ddl including something not
> including the basic create table statement.
>
> I also found a few issues with include in v10.
>
> Include check doesn't seem to work:
>
> CREATE TABLE t (
> id int PRIMARY KEY,
> qty int CHECK (qty > 0),
> CONSTRAINT t_id_pos CHECK (id > 0)
> );
>
> SELECT * FROM pg_get_table_ddl('t', 'include', 'check'); -- empty?
>
> The include partitions clause also doesn't work:
>
> CREATE TABLE p (id int, val text) PARTITION BY RANGE (id);
> CREATE TABLE p_a PARTITION OF p FOR VALUES FROM (0) TO (100);
> CREATE TABLE p_b PARTITION OF p FOR VALUES FROM (100) TO (200);
>
> SELECT * FROM pg_get_table_ddl('p','include','partitions'); -- empty
>
> There's also an issue with replica identity non primary key unique indexes:
>
> CREATE TABLE t2 (a int NOT NULL UNIQUE, b int);
> ALTER TABLE t2 REPLICA IDENTITY USING INDEX t2_a_key;
> SELECT * FROM pg_get_table_ddl('t2','exclude','unique');
> -- ALTER TABLE public.t2 REPLICA IDENTITY USING INDEX t2_a_key;
> -- but there's no such index
>
> Thanks for the review. In the attached patch, I’ve renamed
*include/exclude* to *only* and *except*. If you have any other suggestions
or a better name, please let me know.
Fixed all the above issues as well.
The semantics remain unchanged:
1) only => 'kind1,kind2,...' — emits only the listed kinds.
2) except => 'kind1,kind2,...' — emits everything except the listed kinds.
3) Setting both raises an error; setting neither emits all kinds (the
default).
I also dropped the trailing "s" from the multi-item kind names so the
vocabulary reads naturally with only/except (e.g., index, foreign_key,
rule, trigger, policy, partition). statistics and rls retain their existing
spellings. The documentation, regression tests, and func-info.sgml have all
been updated accordingly.
The v11 patch is now ready for review and testing.
Attachments:
[application/octet-stream] v11-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (178.5K, ../../CANxoLDe308ogjUZD4DLjaMysJzHZ4z3SOXbUTyC7SAEKvSgqcw@mail.gmail.com/3-v11-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From b291ba01311f35b89c0f2b860315612d7b74e77d Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v11] 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_string, constraints
(PRIMARY KEY, UNIQUE, FOREIGN KEY, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_command, rules via pg_get_ruledef, extended
statistics via pg_get_statisticsobjdef_string, 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). Object-class filtering is expressed through two
mutually-exclusive options, include and exclude, each taking a
comma-separated list drawn from the kind vocabulary
{table, indexes, primary_key, unique, check, foreign_keys, exclusion,
rules, statistics, triggers, policies, rls, replica_identity,
partitions}. When include is set, only the listed kinds are emitted;
when exclude 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 <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kyotaro Horiguchi <[email protected]>
Reviewed-by: Chao Li <[email protected]>
---
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 | 2071 +++++++++++++++++
src/include/catalog/pg_inherits.h | 1 +
src/include/catalog/pg_proc.dat | 7 +
src/include/commands/tablecmds.h | 3 +
.../regress/expected/pg_get_table_ddl.out | 1222 ++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 717 ++++++
10 files changed, 4267 insertions(+), 3 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 34f4019690f..4edacf4bd87 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3947,6 +3947,130 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
<literal>TABLESPACE</literal>.
</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 265dcfe7fda..a5f46522c33 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -739,7 +739,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);
@@ -2515,7 +2514,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)
@@ -19558,6 +19557,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 f32fcd453ef..742c8f1423a 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -21,12 +21,25 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.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_am.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 "catalog/partition.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
+#include "nodes/nodes.h"
#include "commands/tablespace.h"
#include "common/relpath.h"
#include "funcapi.h"
@@ -73,8 +86,55 @@ typedef struct DdlOption
} 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);
@@ -87,6 +147,95 @@ static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid, bool i
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, schema_prefix, 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;
+ char *schema_prefix; /* NULL when schema_qualified */
+ 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 char *strip_schema_prefix(const char *stmt, const char *prefix);
+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
@@ -227,6 +376,115 @@ parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start,
}
}
+/*
+ * 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.
*/
@@ -1185,3 +1443,1816 @@ 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.
+ *
+ * When schema_qualified is true the schema-qualified name is always
+ * returned. 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 strip.
+ *
+ * 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);
+
+ /* 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;
+}
+
+/*
+ * strip_schema_prefix
+ * Return a palloc'd copy of stmt with every occurrence of prefix
+ * removed, except occurrences that fall inside a single-quoted SQL
+ * string literal or a double-quoted SQL identifier.
+ *
+ * Used to strip the qualified-schema prefix (for example "old_schema.")
+ * from DDL emitted by the always-qualified ruleutils helpers
+ * (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) when the caller has
+ * requested unqualified output via schema_qualified=false. A plain
+ * substring strip would corrupt string literals whose contents happen
+ * to contain "<schema>." (for example a CHECK comparing against
+ * 'myschema.secret') or double-quoted column/identifier names whose
+ * contents happen to contain "<schema>." (for example a column named
+ * "s.weird" when stripping the prefix "s."), so we track quoting state
+ * and skip matches that occur while we are inside either kind of
+ * quoted token. Escaped quotes ('' inside a string, "" inside an
+ * identifier) are handled by keeping the corresponding flag set across
+ * them.
+ */
+static char *
+strip_schema_prefix(const char *stmt, const char *prefix)
+{
+ StringInfoData buf;
+ size_t plen = strlen(prefix);
+ const char *p = stmt;
+ bool in_string = false;
+ bool in_ident = false;
+
+ initStringInfo(&buf);
+ while (*p)
+ {
+ /*
+ * Try the prefix match BEFORE the quote-state toggles. The prefix
+ * itself may begin with a double quote when the base schema name
+ * requires quoting (e.g. "My-Schema".), and at that leading " we must
+ * strip the whole prefix rather than treat it as the opening of a
+ * regular quoted identifier. The check is gated on !in_ident &&
+ * !in_string, so once we have toggled into a quoted token we never
+ * strip a coincidental substring inside it.
+ */
+ if (!in_string && !in_ident && strncmp(p, prefix, plen) == 0)
+ {
+ p += plen;
+ continue;
+ }
+ if (*p == '\'' && !in_ident)
+ {
+ /* SQL '' escape: emit both quotes and stay inside the string. */
+ if (in_string && p[1] == '\'')
+ {
+ appendStringInfoChar(&buf, '\'');
+ appendStringInfoChar(&buf, '\'');
+ p += 2;
+ continue;
+ }
+ in_string = !in_string;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ if (*p == '"' && !in_string)
+ {
+ /* SQL "" escape: emit both quotes and stay inside the ident. */
+ if (in_ident && p[1] == '"')
+ {
+ appendStringInfoChar(&buf, '"');
+ appendStringInfoChar(&buf, '"');
+ p += 2;
+ continue;
+ }
+ in_ident = !in_ident;
+ appendStringInfoChar(&buf, *p);
+ p++;
+ continue;
+ }
+ appendStringInfoChar(&buf, *p);
+ p++;
+ }
+ return buf.data;
+}
+
+/*
+ * 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 emitted by the INHERITS clause
+ * (once implemented), 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 / append_stripped_stmt
+ * Push ctx->buf onto ctx->statements.
+ *
+ * append_stmt is used for emissions where the table reference is built
+ * from ctx->qualname (already partial-qualified per schema_qualified).
+ * append_stripped_stmt is used after invoking always-qualified ruleutils
+ * helpers (pg_get_constraintdef_command, pg_get_indexdef_string,
+ * pg_get_ruledef, pg_get_statisticsobjdef_string) - when
+ * schema_qualified is false, the base-schema prefix is stripped from
+ * their output so the emitted DDL is replay-relocatable.
+ */
+static void
+append_stmt(TableDdlContext * ctx)
+{
+ ctx->statements = lappend(ctx->statements, pstrdup(ctx->buf.data));
+}
+
+static void
+append_stripped_stmt(TableDdlContext * ctx)
+{
+ if (ctx->schema_prefix != NULL)
+ ctx->statements = lappend(ctx->statements,
+ strip_schema_prefix(ctx->buf.data,
+ ctx->schema_prefix));
+ else
+ 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_string(idxoid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", idxdef);
+ append_stripped_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);
+ char *condef;
+
+ 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;
+ }
+
+ condef = pg_get_constraintdef_command(con->oid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", condef);
+ append_stripped_stmt(ctx);
+ pfree(condef);
+ }
+ 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;
+ Datum ruledef;
+ char *ruledef_str;
+
+ ruledef = OidFunctionCall1(F_PG_GET_RULEDEF_OID,
+ ObjectIdGetDatum(ruleid));
+ ruledef_str = TextDatumGetCString(ruledef);
+ resetStringInfo(&ctx->buf);
+ appendStringInfoString(&ctx->buf, ruledef_str);
+ append_stripped_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_string(stat->oid);
+
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", statdef);
+ append_stripped_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,
+ * schema_prefix, 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 (schema_prefix, nn_entries,
+ * skip_notnull_oids, buf, statements) start zeroed via the caller's
+ * `TableDdlContext ctx = {0}` initializer; schema_prefix is set
+ * conditionally below, 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, etc. come out unqualified. 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.
+ *
+ * Pre-compute "<schema>." too - the always-qualified helpers
+ * (constraintdef_command, indexdef_string, ruledef,
+ * statisticsobjdef_string) ignore search_path for the target's own "ALTER
+ * TABLE schema.tbl"/"ON schema.tbl"/"TO schema.tbl" prefix and for the
+ * qualified statistics name; we strip that string ourselves via
+ * strip_schema_prefix.
+ */
+ if (!ctx->schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx->base_namespace);
+
+ if (nspname != NULL)
+ {
+ const char *qnsp = quote_identifier(nspname);
+
+ ctx->schema_prefix = psprintf("%s.", qnsp);
+ 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);
+ if (ctx->schema_prefix != NULL)
+ pfree(ctx->schema_prefix);
+ 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/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 fa76c7923f0..d1b87a764d4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8615,6 +8615,13 @@
proargtypes => 'regdatabase _text', proallargtypes => '{regdatabase,_text}',
proargmodes => '{i,v}', proargdefaults => '{NULL}',
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/test/regress/expected/pg_get_table_ddl.out b/src/test/regress/expected/pg_get_table_ddl.out
new file mode 100644
index 00000000000..b2b645094a5
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,1222 @@
+--
+-- 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)
+
+-- 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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- Inheritance, including a child DEFAULT override on an inherited column.
+CREATE TABLE par (a int DEFAULT 1, b text);
+CREATE TABLE ch (c int) INHERITS (par);
+ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+SELECT * FROM pg_get_table_ddl('ch'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+---------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ch (c integer) INHERITS (pgtbl_ddl_test.par);
+ ALTER TABLE pgtbl_ddl_test.ch ALTER COLUMN a SET DEFAULT 999;
+(2 rows)
+
+-- Per-column attoptions: emitted as ALTER COLUMN SET (...).
+CREATE TABLE attopt (a int, b text);
+ALTER TABLE attopt ALTER COLUMN a SET (n_distinct = 100);
+SELECT * FROM pg_get_table_ddl('attopt'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.attopt (a integer, b text);
+ ALTER TABLE pgtbl_ddl_test.attopt ALTER COLUMN a SET (n_distinct='100');
+(2 rows)
+
+-- Partitioned table parent (RANGE and HASH).
+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)
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0);
+(1 row)
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(1 row)
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+----------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx (a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(3 rows)
+
+-- UNLOGGED + reloptions.
+CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70);
+SELECT * FROM pg_get_table_ddl('uno'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE UNLOGGED TABLE pgtbl_ddl_test.uno (id integer) WITH (fillfactor='70');
+(1 row)
+
+-- Typed table (CREATE TABLE OF type_name). Columns inherited from the
+-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come
+-- out through a single "(col WITH OPTIONS ...)" list.
+CREATE TYPE typed_t AS (a int, b text);
+CREATE TABLE typed_plain OF typed_t;
+SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_plain OF pgtbl_ddl_test.typed_t;
+(1 row)
+
+CREATE TABLE typed_over OF typed_t (
+ a WITH OPTIONS DEFAULT 7 NOT NULL,
+ b WITH OPTIONS NOT NULL,
+ CONSTRAINT b_nonempty CHECK (length(b) > 0)
+);
+SELECT * FROM pg_get_table_ddl('typed_over'::regclass, 'owner', 'false');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_over OF pgtbl_ddl_test.typed_t (a WITH OPTIONS DEFAULT 7 NOT NULL, b WITH OPTIONS NOT NULL, CONSTRAINT b_nonempty CHECK ((length(b) > 0)));
+(1 row)
+
+-- Temporary tables + ON COMMIT. Temp tables live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+-----------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_default (id integer);
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+--------------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.temp_delete (id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_drop'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+ line
+-----------------------------------------------------------------------
+ CREATE TEMPORARY TABLE pg_temp.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)
+
+-- 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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'only', '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 pgtbl_ddl_test.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 pgtbl_ddl_test.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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'except', '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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- Multi-kind compositions.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, 'owner', 'false',
+ 'only', 'table,index');
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, 'owner', 'false',
+ 'only', '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 pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+DROP TABLE excl_fix;
+-- only=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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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 25 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 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 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 21 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 25 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 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 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..f6b73ff717b
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,717 @@
+--
+-- 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');
+
+-- 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 parent (RANGE and HASH).
+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');
+
+-- 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 live in a session-local
+-- namespace whose name varies between runs, so we strip the schema
+-- prefix. 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 regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_default'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS line
+FROM pg_get_table_ddl('temp_delete'::regclass, 'owner', 'false') AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT regexp_replace(line, '"?pg_temp_?[0-9]+"?\.', 'pg_temp.') AS 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');
+
+-- 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'), ('storage_cols'),
+ ('refd'), ('cons'), ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('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', 'storage_cols',
+ 'refd', 'cons', 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ '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;
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-28 16:44 Rui Zhao <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Rui Zhao @ 2026-06-28 16:44 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: [email protected]; [email protected]; [email protected]; [email protected]; Zsolt Parragi <[email protected]>
Hi Akshay,
Nice patch -- server-side CREATE TABLE reconstruction is something we want in
production. I tested v11 on current master and it round-trips correctly
across the documented coverage, including the recent grammar (VIRTUAL
columns, NOT ENFORCED, temporal WITHOUT OVERLAPS / PERIOD) and state set
later via ALTER. A few comments:
1. Real bug in schema_qualified => false. strip_schema_prefix() strips the
base prefix anywhere in code position with no token-boundary check, so a
cross-schema name whose schema ends with the base schema's name gets
mangled:
postgres=# CREATE SCHEMA xs;
CREATE SCHEMA
postgres=# CREATE TABLE xs.reftbl(id int PRIMARY KEY);
CREATE TABLE
postgres=# CREATE SCHEMA s;
CREATE SCHEMA
postgres=# CREATE TABLE s.orders(id int PRIMARY KEY, ref int
REFERENCES xs.reftbl(id));
CREATE TABLE
postgres=# SELECT d FROM
pg_get_table_ddl('s.orders'::regclass,'schema_qualified','false') d;
d
---------------------------------------------------------------------------------------------
CREATE TABLE orders (id integer NOT NULL, ref integer);
ALTER TABLE orders OWNER TO postgres;
ALTER TABLE orders ADD CONSTRAINT orders_pkey PRIMARY KEY (id);
ALTER TABLE orders ADD CONSTRAINT orders_ref_fkey FOREIGN KEY (ref)
REFERENCES xreftbl(id);
(4 rows)
The last FK line says REFERENCES xreftbl(id); it should be xs.reftbl(id),
and the result doesn't replay. Gating the match on a token boundary fixes
it -- though see (2), which would remove this code path (and the bug) entirely.
2. Bigger picture: is schema_qualified needed at all? None of the existing
pg_get_*def / pg_get_*ddl functions have such a knob, so this would be the
lone exception. The established convention is to let search_path decide
(generate_relation_name): pg_get_viewdef and pg_get_constraintdef already
work that way, and pg_get_indexdef supports it too via
pg_get_indexdef(idx, 0, true) (the PRETTYFLAG_SCHEMA code path). It is
genuinely what we want in production -- the caller
controls qualification through search_path. Set it to the table's schema for
unqualified output, or to pg_catalog (or '') for fully-qualified
schema.table; pg_dump itself dumps under an empty search_path
(ALWAYS_SECURE_SEARCH_PATH_SQL) for exactly this reason. Following the
convention would also drop the option and the strip_schema_prefix code (and
this bug).
That last point matters: there's no robust way to strip a schema prefix out
of already-generated SQL by text processing. Doing it safely means
re-tokenizing arbitrary SQL (string literals, quoted identifiers, dollar
quotes, comments, casts, operators, ...), and strip_schema_prefix is a
hand-rolled partial scanner of exactly that. It shows -- it has already
needed several over-stripping fixes during review (the base name appearing
inside a string literal, and inside a quoted identifier), and the
token-boundary case in (1) is yet another. Deciding qualification at
generation time (generate_relation_name) avoids the whole class: the
backend's real deparser already gets this right, rather than a post-hoc
string pass trying to re-derive it.
3. typedefs.list is missing TableDdlContext and LocalNotNullEntry, so
pgindent leaves the "Type * var" pointer spacing in ~30 places -- for
example the forward declarations at ddlutils.c:226-237:
static void append_stmt(TableDdlContext * ctx);
There is also a stale comment at ddlutils.c:1830 in append_column_defs():
inherited columns are described as "emitted by the INHERITS clause (once
implemented)", but INHERITS is implemented now.
4. Related to (2): a temporary table's default output qualifies it with the
session-private pg_temp_NN schema (e.g. CREATE TEMPORARY TABLE pg_temp_3.t
...), which won't replay anywhere else. A reconstructed temp table should
just be CREATE TEMPORARY TABLE t (...) -- the TEMPORARY keyword already puts
it in pg_temp, so the schema name should never be emitted. This also falls
out for free under the search_path convention in (2): pg_temp is always in
the effective search_path, so the table is visible and wouldn't be qualified
in the first place.
5. The commit message is out of sync with the code and func-info.sgml on the
option interface: it still says "include and exclude" and lists plural kind
names (indexes, foreign_keys, triggers, policies, partitions), whereas the
code and docs use only/except and singular names (index, foreign_key,
trigger, policy, partition). Looks like a leftover from the
include/exclude -> only/except rename. (FWIW on the naming itself,
include/exclude is the more common convention for this kind of list
parameter -- pg_dump has --exclude-table/-schema/-extension, etc. -- while
only/except reads more like the SQL keywords.)
Otherwise it looks good.
Thanks,
Rui
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-29 10:40 Akshay Joshi <[email protected]>
parent: Rui Zhao <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-06-29 10:40 UTC (permalink / raw)
To: Rui Zhao <[email protected]>; +Cc: [email protected]; [email protected]; [email protected]; [email protected]; Zsolt Parragi <[email protected]>
On Sun, Jun 28, 2026 at 10:14 PM Rui Zhao <[email protected]> wrote:
> Hi Akshay,
>
> Nice patch -- server-side CREATE TABLE reconstruction is something we want
> in
> production. I tested v11 on current master and it round-trips correctly
> across the documented coverage, including the recent grammar (VIRTUAL
> columns, NOT ENFORCED, temporal WITHOUT OVERLAPS / PERIOD) and state set
> later via ALTER. A few comments:
>
> 1. Real bug in schema_qualified => false. strip_schema_prefix() strips the
> base prefix anywhere in code position with no token-boundary check, so a
> cross-schema name whose schema ends with the base schema's name gets
> mangled:
>
> postgres=# CREATE SCHEMA xs;
> CREATE SCHEMA
> postgres=# CREATE TABLE xs.reftbl(id int PRIMARY KEY);
> CREATE TABLE
> postgres=# CREATE SCHEMA s;
> CREATE SCHEMA
> postgres=# CREATE TABLE s.orders(id int PRIMARY KEY, ref int
> REFERENCES xs.reftbl(id));
> CREATE TABLE
> postgres=# SELECT d FROM
> pg_get_table_ddl('s.orders'::regclass,'schema_qualified','false') d;
> d
>
> ---------------------------------------------------------------------------------------------
> CREATE TABLE orders (id integer NOT NULL, ref integer);
> ALTER TABLE orders OWNER TO postgres;
> ALTER TABLE orders ADD CONSTRAINT orders_pkey PRIMARY KEY (id);
> ALTER TABLE orders ADD CONSTRAINT orders_ref_fkey FOREIGN KEY (ref)
> REFERENCES xreftbl(id);
> (4 rows)
>
> The last FK line says REFERENCES xreftbl(id); it should be xs.reftbl(id),
> and the result doesn't replay. Gating the match on a token boundary fixes
> it -- though see (2), which would remove this code path (and the bug)
> entirely.
>
Fixed.
>
> 2. Bigger picture: is schema_qualified needed at all? None of the existing
> pg_get_*def / pg_get_*ddl functions have such a knob, so this would be the
> lone exception. The established convention is to let search_path decide
> (generate_relation_name): pg_get_viewdef and pg_get_constraintdef already
> work that way, and pg_get_indexdef supports it too via
> pg_get_indexdef(idx, 0, true) (the PRETTYFLAG_SCHEMA code path). It is
> genuinely what we want in production -- the caller
> controls qualification through search_path. Set it to the table's schema
> for
> unqualified output, or to pg_catalog (or '') for fully-qualified
> schema.table; pg_dump itself dumps under an empty search_path
> (ALWAYS_SECURE_SEARCH_PATH_SQL) for exactly this reason. Following the
> convention would also drop the option and the strip_schema_prefix code (and
> this bug).
>
> That last point matters: there's no robust way to strip a schema prefix out
> of already-generated SQL by text processing. Doing it safely means
> re-tokenizing arbitrary SQL (string literals, quoted identifiers, dollar
> quotes, comments, casts, operators, ...), and strip_schema_prefix is a
> hand-rolled partial scanner of exactly that. It shows -- it has already
> needed several over-stripping fixes during review (the base name appearing
> inside a string literal, and inside a quoted identifier), and the
> token-boundary case in (1) is yet another. Deciding qualification at
> generation time (generate_relation_name) avoids the whole class: the
> backend's real deparser already gets this right, rather than a post-hoc
> string pass trying to re-derive it.
>
Done. strip_schema_prefix and append_stripped_stmt are gone. Instead of
post-processing, I added four thin wrappers in ruleutils.c. Let the
deparser decide qualification at generation time:
- pg_get_indexdef_ddl passes PRETTYFLAG_SCHEMA to the worker, enabling
generate_relation_name instead of generate_qualified_relation_name.
- pg_get_ruledef_ddl — same flag.
- pg_get_constraintdef_body — returns body only (FK targets already use
generate_relation_name); emit_local_constraints now builds the ALTER TABLE
ctx->qualname ADD CONSTRAINT prefix itself.
- pg_get_statisticsobjdef_ddl — uses StatisticsObjIsVisible() to qualify
the statistics object name
*The schema_qualified=false path still narrows search_path to the base
schema, which is what makes all four helpers emit unqualified names for
same-schema objects. The option is kept for per-call convenience.*
> 3. typedefs.list is missing TableDdlContext and LocalNotNullEntry, so
> pgindent leaves the "Type * var" pointer spacing in ~30 places -- for
> example the forward declarations at ddlutils.c:226-237:
>
> static void append_stmt(TableDdlContext * ctx);
>
> There is also a stale comment at ddlutils.c:1830 in append_column_defs():
> inherited columns are described as "emitted by the INHERITS clause (once
> implemented)", but INHERITS is implemented now.
>
> 4. Related to (2): a temporary table's default output qualifies it with the
> session-private pg_temp_NN schema (e.g. CREATE TEMPORARY TABLE pg_temp_3.t
> ...), which won't replay anywhere else. A reconstructed temp table should
> just be CREATE TEMPORARY TABLE t (...) -- the TEMPORARY keyword already
> puts
> it in pg_temp, so the schema name should never be emitted. This also falls
> out for free under the search_path convention in (2): pg_temp is always in
> the effective search_path, so the table is visible and wouldn't be
> qualified
> in the first place.
>
> 5. The commit message is out of sync with the code and func-info.sgml on
> the
> option interface: it still says "include and exclude" and lists plural kind
> names (indexes, foreign_keys, triggers, policies, partitions), whereas the
> code and docs use only/except and singular names (index, foreign_key,
> trigger, policy, partition). Looks like a leftover from the
> include/exclude -> only/except rename. (FWIW on the naming itself,
> include/exclude is the more common convention for this kind of list
> parameter -- pg_dump has --exclude-table/-schema/-extension, etc. -- while
> only/except reads more like the SQL keywords.)
>
Fixed 3, 4 and 5.
The v12 patch is ready for review/test.
>
> Otherwise it looks good.
>
> Thanks,
> Rui
>
Attachments:
[application/octet-stream] v12-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (189.8K, ../../CANxoLDcAy+46QesKdK95XFM3z9eT6PsiJ4Tx_GSQmtbg1O7Evw@mail.gmail.com/3-v12-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From c1f03685fe93e3c033c0935eea9989640a9f8afa Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
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 <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kyotaro Horiguchi <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Rui Zhao <[email protected]>
---
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
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-29 13:38 Akshay Joshi <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-06-29 13:38 UTC (permalink / raw)
To: Rui Zhao <[email protected]>; +Cc: [email protected]; [email protected]; [email protected]; [email protected]; Zsolt Parragi <[email protected]>
Fixed some compiler warnings. The *v13* patch is ready for review/test.
On Mon, Jun 29, 2026 at 4:10 PM Akshay Joshi <[email protected]>
wrote:
>
>
> On Sun, Jun 28, 2026 at 10:14 PM Rui Zhao <[email protected]> wrote:
>
>> Hi Akshay,
>>
>> Nice patch -- server-side CREATE TABLE reconstruction is something we
>> want in
>> production. I tested v11 on current master and it round-trips correctly
>> across the documented coverage, including the recent grammar (VIRTUAL
>> columns, NOT ENFORCED, temporal WITHOUT OVERLAPS / PERIOD) and state set
>> later via ALTER. A few comments:
>>
>> 1. Real bug in schema_qualified => false. strip_schema_prefix() strips the
>> base prefix anywhere in code position with no token-boundary check, so a
>> cross-schema name whose schema ends with the base schema's name gets
>> mangled:
>>
>> postgres=# CREATE SCHEMA xs;
>> CREATE SCHEMA
>> postgres=# CREATE TABLE xs.reftbl(id int PRIMARY KEY);
>> CREATE TABLE
>> postgres=# CREATE SCHEMA s;
>> CREATE SCHEMA
>> postgres=# CREATE TABLE s.orders(id int PRIMARY KEY, ref int
>> REFERENCES xs.reftbl(id));
>> CREATE TABLE
>> postgres=# SELECT d FROM
>> pg_get_table_ddl('s.orders'::regclass,'schema_qualified','false') d;
>> d
>>
>> ---------------------------------------------------------------------------------------------
>> CREATE TABLE orders (id integer NOT NULL, ref integer);
>> ALTER TABLE orders OWNER TO postgres;
>> ALTER TABLE orders ADD CONSTRAINT orders_pkey PRIMARY KEY (id);
>> ALTER TABLE orders ADD CONSTRAINT orders_ref_fkey FOREIGN KEY (ref)
>> REFERENCES xreftbl(id);
>> (4 rows)
>>
>> The last FK line says REFERENCES xreftbl(id); it should be xs.reftbl(id),
>> and the result doesn't replay. Gating the match on a token boundary fixes
>> it -- though see (2), which would remove this code path (and the bug)
>> entirely.
>>
>
> Fixed.
>
>>
>> 2. Bigger picture: is schema_qualified needed at all? None of the existing
>> pg_get_*def / pg_get_*ddl functions have such a knob, so this would be the
>> lone exception. The established convention is to let search_path decide
>> (generate_relation_name): pg_get_viewdef and pg_get_constraintdef already
>> work that way, and pg_get_indexdef supports it too via
>> pg_get_indexdef(idx, 0, true) (the PRETTYFLAG_SCHEMA code path). It is
>> genuinely what we want in production -- the caller
>> controls qualification through search_path. Set it to the table's schema
>> for
>> unqualified output, or to pg_catalog (or '') for fully-qualified
>> schema.table; pg_dump itself dumps under an empty search_path
>> (ALWAYS_SECURE_SEARCH_PATH_SQL) for exactly this reason. Following the
>> convention would also drop the option and the strip_schema_prefix code
>> (and
>> this bug).
>>
>> That last point matters: there's no robust way to strip a schema prefix
>> out
>> of already-generated SQL by text processing. Doing it safely means
>> re-tokenizing arbitrary SQL (string literals, quoted identifiers, dollar
>> quotes, comments, casts, operators, ...), and strip_schema_prefix is a
>> hand-rolled partial scanner of exactly that. It shows -- it has already
>> needed several over-stripping fixes during review (the base name appearing
>> inside a string literal, and inside a quoted identifier), and the
>> token-boundary case in (1) is yet another. Deciding qualification at
>> generation time (generate_relation_name) avoids the whole class: the
>> backend's real deparser already gets this right, rather than a post-hoc
>> string pass trying to re-derive it.
>>
>
> Done. strip_schema_prefix and append_stripped_stmt are gone. Instead of
> post-processing, I added four thin wrappers in ruleutils.c. Let the
> deparser decide qualification at generation time:
> - pg_get_indexdef_ddl passes PRETTYFLAG_SCHEMA to the worker, enabling
> generate_relation_name instead of generate_qualified_relation_name.
> - pg_get_ruledef_ddl — same flag.
> - pg_get_constraintdef_body — returns body only (FK targets already use
> generate_relation_name); emit_local_constraints now builds the ALTER TABLE
> ctx->qualname ADD CONSTRAINT prefix itself.
> - pg_get_statisticsobjdef_ddl — uses StatisticsObjIsVisible() to qualify
> the statistics object name
> *The schema_qualified=false path still narrows search_path to the base
> schema, which is what makes all four helpers emit unqualified names for
> same-schema objects. The option is kept for per-call convenience.*
>
>> 3. typedefs.list is missing TableDdlContext and LocalNotNullEntry, so
>> pgindent leaves the "Type * var" pointer spacing in ~30 places -- for
>> example the forward declarations at ddlutils.c:226-237:
>>
>> static void append_stmt(TableDdlContext * ctx);
>>
>> There is also a stale comment at ddlutils.c:1830 in append_column_defs():
>> inherited columns are described as "emitted by the INHERITS clause (once
>> implemented)", but INHERITS is implemented now.
>>
>> 4. Related to (2): a temporary table's default output qualifies it with
>> the
>> session-private pg_temp_NN schema (e.g. CREATE TEMPORARY TABLE pg_temp_3.t
>> ...), which won't replay anywhere else. A reconstructed temp table should
>> just be CREATE TEMPORARY TABLE t (...) -- the TEMPORARY keyword already
>> puts
>> it in pg_temp, so the schema name should never be emitted. This also falls
>> out for free under the search_path convention in (2): pg_temp is always in
>> the effective search_path, so the table is visible and wouldn't be
>> qualified
>> in the first place.
>>
>> 5. The commit message is out of sync with the code and func-info.sgml on
>> the
>> option interface: it still says "include and exclude" and lists plural
>> kind
>> names (indexes, foreign_keys, triggers, policies, partitions), whereas the
>> code and docs use only/except and singular names (index, foreign_key,
>> trigger, policy, partition). Looks like a leftover from the
>> include/exclude -> only/except rename. (FWIW on the naming itself,
>> include/exclude is the more common convention for this kind of list
>> parameter -- pg_dump has --exclude-table/-schema/-extension, etc. -- while
>> only/except reads more like the SQL keywords.)
>>
>
> Fixed 3, 4 and 5.
>
> The v12 patch is ready for review/test.
>
>>
>> Otherwise it looks good.
>>
>> Thanks,
>> Rui
>>
>
Attachments:
[application/octet-stream] v13-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (190.2K, ../../CANxoLDd0KT+5fHOyTi0Fu5Jwme0+a5S2aAVj=iCVsqkpvF_2TA@mail.gmail.com/3-v13-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From bff9b579b9e7d70b9b6fca598cf7a6274a512540 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v13] 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 <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kyotaro Horiguchi <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Rui Zhao <[email protected]>
---
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 | 87 +-
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, 4497 insertions(+), 14 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..7186cf83e1d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -23,6 +23,7 @@
#include "access/htup_details.h"
#include "access/relation.h"
#include "access/table.h"
+#include "catalog/namespace.h"
#include "catalog/pg_aggregate.h"
#include "catalog/pg_am.h"
#include "catalog/pg_authid.h"
@@ -370,7 +371,7 @@ static void make_propgraphdef_elements(StringInfo buf, Oid pgrelid, char pgekind
static void make_propgraphdef_labels(StringInfo buf, Oid elid, const char *elalias, Oid elrelid);
static void make_propgraphdef_properties(StringInfo buf, Oid ellabelid, Oid elrelid);
static char *pg_get_statisticsobj_worker(Oid statextid, bool columns_only,
- bool missing_ok);
+ bool missing_ok, int prettyFlags);
static char *pg_get_partkeydef_worker(Oid relid, int prettyFlags,
bool attrsOnly, bool missing_ok);
static char *pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
@@ -603,6 +604,19 @@ pg_get_ruledef_ext(PG_FUNCTION_ARGS)
PG_RETURN_TEXT_P(string_to_text(res));
}
+/*
+ * pg_get_ruledef_ddl
+ * Like pg_get_ruledef but uses the active search_path to decide
+ * whether to schema-qualify the rule's target relation name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_ruledef_ddl(Oid ruleoid)
+{
+ return pg_get_ruledef_worker(ruleoid, PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA);
+}
+
static char *
pg_get_ruledef_worker(Oid ruleoid, int prettyFlags)
@@ -1241,6 +1255,22 @@ pg_get_indexdef_string(Oid indexrelid)
0, false);
}
+/*
+ * pg_get_indexdef_ddl
+ * Like pg_get_indexdef_string but uses the active search_path to
+ * decide whether to schema-qualify the indexed relation name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_indexdef_ddl(Oid indexrelid)
+{
+ return pg_get_indexdef_worker(indexrelid, 0, NULL,
+ false, false,
+ true, true,
+ PRETTYFLAG_SCHEMA, false);
+}
+
/* Internal version that just reports the key-column definitions */
char *
pg_get_indexdef_columns(Oid indexrelid, bool pretty)
@@ -1969,7 +1999,7 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS)
Oid statextid = PG_GETARG_OID(0);
char *res;
- res = pg_get_statisticsobj_worker(statextid, false, true);
+ res = pg_get_statisticsobj_worker(statextid, false, true, 0);
if (res == NULL)
PG_RETURN_NULL();
@@ -1984,7 +2014,20 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS)
char *
pg_get_statisticsobjdef_string(Oid statextid)
{
- return pg_get_statisticsobj_worker(statextid, false, false);
+ return pg_get_statisticsobj_worker(statextid, false, false, 0);
+}
+
+/*
+ * pg_get_statisticsobjdef_ddl
+ * Like pg_get_statisticsobjdef_string but uses the active search_path
+ * to decide whether to schema-qualify the statistics object name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_statisticsobjdef_ddl(Oid statextid)
+{
+ return pg_get_statisticsobj_worker(statextid, false, false, PRETTYFLAG_SCHEMA);
}
/*
@@ -1997,7 +2040,7 @@ pg_get_statisticsobjdef_columns(PG_FUNCTION_ARGS)
Oid statextid = PG_GETARG_OID(0);
char *res;
- res = pg_get_statisticsobj_worker(statextid, true, true);
+ res = pg_get_statisticsobj_worker(statextid, true, true, 0);
if (res == NULL)
PG_RETURN_NULL();
@@ -2009,7 +2052,8 @@ pg_get_statisticsobjdef_columns(PG_FUNCTION_ARGS)
* Internal workhorse to decompile an extended statistics object.
*/
static char *
-pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
+pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok,
+ int prettyFlags)
{
Form_pg_statistic_ext statextrec;
HeapTuple statexttup;
@@ -2069,10 +2113,18 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
if (!columns_only)
{
- nsp = get_namespace_name_or_temp(statextrec->stxnamespace);
- appendStringInfo(&buf, "CREATE STATISTICS %s",
- quote_qualified_identifier(nsp,
- NameStr(statextrec->stxname)));
+ if ((prettyFlags & PRETTYFLAG_SCHEMA) &&
+ StatisticsObjIsVisible(statextid))
+ appendStringInfo(&buf, "CREATE STATISTICS %s",
+ quote_identifier(NameStr(statextrec->stxname)));
+ else
+ {
+ nsp = get_namespace_name_or_temp(statextrec->stxnamespace);
+ appendStringInfo(&buf, "CREATE STATISTICS %s",
+ quote_qualified_identifier(nsp,
+ NameStr(statextrec->stxname)));
+ pfree(nsp);
+ }
/*
* Decode the stxkind column so that we know which stats types to
@@ -2163,10 +2215,9 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
{
Node *expr = (Node *) lfirst(lc);
char *str;
- int prettyFlags = PRETTYFLAG_PAREN;
str = deparse_expression_pretty(expr, context, false, false,
- prettyFlags, 0);
+ PRETTYFLAG_PAREN, 0);
if (colno > 0)
appendStringInfoString(&buf, ", ");
@@ -2544,6 +2595,20 @@ pg_get_constraintdef_command(Oid constraintId)
return pg_get_constraintdef_worker(constraintId, true, 0, false);
}
+/*
+ * pg_get_constraintdef_body
+ * Returns the constraint definition body without the
+ * "ALTER TABLE name ADD CONSTRAINT cname" prefix.
+ * FK target relation names in the body already use
+ * generate_relation_name (search_path-aware). For use by
+ * pg_get_table_ddl which builds the ALTER TABLE prefix itself.
+ */
+char *
+pg_get_constraintdef_body(Oid constraintId)
+{
+ return pg_get_constraintdef_worker(constraintId, false, 0, false);
+}
+
/*
* As of 9.4, we now use an MVCC snapshot for this.
*/
diff --git a/src/include/catalog/pg_inherits.h b/src/include/catalog/pg_inherits.h
index cc874abaabb..1e463b0106d 100644
--- a/src/include/catalog/pg_inherits.h
+++ b/src/include/catalog/pg_inherits.h
@@ -55,6 +55,7 @@ DECLARE_INDEX(pg_inherits_parent_index, 2187, InheritsParentIndexId, pg_inherits
extern List *find_inheritance_children(Oid parentrelId, LOCKMODE lockmode);
extern List *find_inheritance_children_extended(Oid parentrelId, bool omit_detached,
LOCKMODE lockmode, bool *detached_exist, TransactionId *detached_xmin);
+extern List *find_inheritance_parents(Oid relid, LOCKMODE lockmode);
extern List *find_all_inheritors(Oid parentrelId, LOCKMODE lockmode,
List **numparents);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 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
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-06-29 21:31 Zsolt Parragi <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Zsolt Parragi @ 2026-06-29 21:31 UTC (permalink / raw)
To: [email protected]
Hello
I noticed that there's an inconsistency with schema qualification, if
the schema is in the search path:
CREATE SCHEMA s;
CREATE TABLE s.parent (id int PRIMARY KEY);
CREATE TABLE s.t (id int PRIMARY KEY, pid int REFERENCES s.parent(id),
name text);
CREATE INDEX t_name_idx ON s.t (name);
CREATE STATISTICS s.t_stat ON id, pid FROM s.t;
SET search_path = s, public;
SELECT pg_get_table_ddl('s.t', 'owner', 'false');
outputs:
CREATE TABLE s.t (id integer NOT NULL, pid integer, name text);
CREATE INDEX t_name_idx ON t USING btree (name); -- should be s.t
ALTER TABLE s.t ADD CONSTRAINT t_pid_fkey FOREIGN KEY (pid) REFERENCES
parent(id); -- should be s.parent
ALTER TABLE s.t ADD CONSTRAINT t_pkey PRIMARY KEY (id);
CREATE STATISTICS t_stat ON id, pid FROM t; -- should be s.t
In the included testcase:
+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
is this error expected, doesn't it break the test?
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-01 14:12 Akshay Joshi <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 3 replies; 54+ messages in thread
From: Akshay Joshi @ 2026-07-01 14:12 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: [email protected]
Thanks for the continued review. I've rebased and updated the patch.
Andrew's commit replaced the VARIADIC text[] option interface on
pg_get_role_ddl(),
pg_get_tablespace_ddl(), and pg_get_database_ddl() with typed named boolean
parameters. pg_get_table_ddl() now follows the exact same convention:
pretty, owner, tablespace, and schema_qualified are plain boolean
parameters with DEFAULT values, using PG_GETARG_BOOL() directly rather than
parse_ddl_options(). The shared DdlOptType / DdlOption / parse_ddl_options
infrastructure is gone entirely.
*Filtering parameters:* *only_kinds* / *except_kinds* as text[]
Based on review feedback the filtering parameters(*include/exclude*) have
been revised:
The two mutually-exclusive filter arguments are now named *only_kinds* and
*except_kinds* (matching the SQL set-operation vocabulary) and take text[]
rather than a comma-separated text value (e.g. only_kinds =>
ARRAY['index','foreign_key']). This provides type safety and allows
named-argument syntax.
*Note*: only and except are the keywords, and I couldn't find any better
name. Suggestions are welcome.
The *v14* patch is ready for review/test.
On Tue, Jun 30, 2026 at 3:01 AM Zsolt Parragi <[email protected]>
wrote:
> Hello
>
> I noticed that there's an inconsistency with schema qualification, if
> the schema is in the search path:
>
> CREATE SCHEMA s;
> CREATE TABLE s.parent (id int PRIMARY KEY);
> CREATE TABLE s.t (id int PRIMARY KEY, pid int REFERENCES s.parent(id),
> name text);
> CREATE INDEX t_name_idx ON s.t (name);
> CREATE STATISTICS s.t_stat ON id, pid FROM s.t;
> SET search_path = s, public;
> SELECT pg_get_table_ddl('s.t', 'owner', 'false');
>
> outputs:
>
> CREATE TABLE s.t (id integer NOT NULL, pid integer, name text);
> CREATE INDEX t_name_idx ON t USING btree (name); -- should be s.t
> ALTER TABLE s.t ADD CONSTRAINT t_pid_fkey FOREIGN KEY (pid) REFERENCES
> parent(id); -- should be s.parent
> ALTER TABLE s.t ADD CONSTRAINT t_pkey PRIMARY KEY (id);
> CREATE STATISTICS t_stat ON id, pid FROM t; -- should be s.t
>
>
> In the included testcase:
>
> +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
>
> is this error expected, doesn't it break the test?
>
>
>
Attachments:
[application/octet-stream] v14-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (193.3K, ../../CANxoLDdVEjCCKch1O-5J6ghw89b2fkWC1ADPYTUwxu8WB-hQbw@mail.gmail.com/3-v14-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From e467df19b0807ca4351be5c9ad246d3959929c5f Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v14] Add pg_get_table_ddl() to reconstruct CREATE TABLE
statements
The function reconstructs the CREATE TABLE statement for an ordinary or
partitioned table, followed by the ALTER TABLE / CREATE INDEX /
CREATE RULE / CREATE STATISTICS statements needed to restore its full
definition. Each statement is returned as a separate row.
Supported per-column features: data type with type modifiers, COLLATE,
STORAGE, COMPRESSION (pglz / lz4), GENERATED ALWAYS AS (expr)
STORED/VIRTUAL, GENERATED ALWAYS|BY DEFAULT AS IDENTITY (with sequence
options), DEFAULT, NOT NULL (including named NOT NULL constraints), and
per-column attoptions emitted as ALTER COLUMN SET (...).
Supported table-level features: UNLOGGED, INHERITS, PARTITION BY (RANGE
/ LIST / HASH parents), PARTITION OF parent FOR VALUES (FROM/TO, WITH
modulus/remainder, DEFAULT), USING table access method, WITH
(reloptions), TABLESPACE, and inline CHECK constraints in the CREATE
TABLE body.
Supported sub-objects (re-using existing deparse helpers from
ruleutils.c): indexes (including partial and functional) via
pg_get_indexdef_ddl; constraints (PRIMARY KEY with WITHOUT OVERLAPS for
temporal keys, UNIQUE with NULLS NOT DISTINCT and INCLUDE columns,
FOREIGN KEY with ON DELETE/UPDATE referential actions and MATCH clause,
NOT ENFORCED foreign keys, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_body; rules via pg_get_ruledef_ddl; extended
statistics via pg_get_statisticsobjdef_ddl; REPLICA IDENTITY
NOTHING/FULL/USING INDEX; ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY;
and child-local DEFAULT overrides on inheritance/partition children.
DDL for partition children of a partitioned-table parent is appended
after the parent by default.
The function signature follows the named-parameter convention
established by pg_get_role_ddl(), pg_get_tablespace_ddl(), and
pg_get_database_ddl():
pg_get_table_ddl(relation regclass,
pretty boolean DEFAULT false,
owner boolean DEFAULT true,
tablespace boolean DEFAULT true,
schema_qualified boolean DEFAULT true,
only_kinds text[] DEFAULT NULL,
except_kinds text[] DEFAULT NULL)
pretty controls pretty-printed output. owner controls emission of the
ALTER TABLE ... OWNER TO statement. tablespace controls the TABLESPACE
clause on CREATE TABLE. schema_qualified controls whether object names
are emitted with their schema prefix: when true (the default) the
active search_path is temporarily narrowed to pg_catalog so every
deparse helper produces fully-qualified names; when false it is narrowed
to the target table's own schema so same-schema references come out
unqualified while cross-schema references remain qualified for
correctness. Temporary tables are never schema-qualified regardless of
this setting: the TEMPORARY keyword already places them in pg_temp, and
emitting pg_temp_NN.relname would produce non-replayable DDL.
Object-class filtering uses two mutually-exclusive text-array
parameters, only_kinds and except_kinds. When only_kinds is set, only
the listed kinds are emitted; when except_kinds is set, every kind
except the listed ones is emitted; when neither is set every kind is
emitted. Each array element is matched case-insensitively with
leading/trailing whitespace trimmed; an unrecognized name raises an
error. The kind vocabulary is:
table, index, primary_key, unique, check, foreign_key, exclusion,
rule, statistics, rls, replica_identity, partition
trigger and policy are accepted in the vocabulary but currently produce
no output; they are reserved for when standalone pg_get_trigger_ddl()
and pg_get_policy_ddl() helpers become available.
NOT NULL is not part of the vocabulary: it is always emitted to prevent
producing schemas that silently accept NULLs the source would reject.
When replica_identity is in the active filter and the table's REPLICA
IDENTITY USING INDEX references an index that the filter would suppress,
the function raises an error before emitting any output, so the
generated DDL never references an index it did not produce.
Default omission convention: every optional clause is omitted when its
value matches what the server would reapply on round-trip, including
type-default COLLATE, per-type STORAGE, the auto-generated identity
sequence name and parameter defaults, heap access method, default
REPLICA IDENTITY, disabled RLS toggles, empty reloptions, and the
default tablespace.
Author: Akshay Joshi <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kyotaro Horiguchi <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Rui Zhao <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 138 ++
src/backend/catalog/pg_inherits.c | 93 +
src/backend/commands/tablecmds.c | 30 +-
src/backend/utils/adt/ddlutils.c | 1959 ++++++++++++++++-
src/backend/utils/adt/ruleutils.c | 87 +-
src/include/catalog/pg_inherits.h | 1 +
src/include/catalog/pg_proc.dat | 8 +
src/include/commands/tablecmds.h | 3 +
src/include/utils/ruleutils.h | 4 +
.../regress/expected/pg_get_table_ddl.out | 1274 +++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 740 +++++++
src/tools/pgindent/typedefs.list | 2 +
13 files changed, 4324 insertions(+), 17 deletions(-)
create mode 100644 src/test/regress/expected/pg_get_table_ddl.out
create mode 100644 src/test/regress/sql/pg_get_table_ddl.sql
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 69ef3857cfa..fa2431bef4a 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3960,6 +3960,144 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
is false, the <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>relation</parameter> <type>regclass</type>
+ <optional>, <parameter>pretty</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> false</optional>
+ <optional>, <parameter>owner</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>tablespace</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>schema_qualified</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>only_kinds</parameter> <type>text[]</type>
+ <literal>DEFAULT</literal> NULL</optional>
+ <optional>, <parameter>except_kinds</parameter> <type>text[]</type>
+ <literal>DEFAULT</literal> NULL</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.
+ When <parameter>pretty</parameter> is true, the output is
+ pretty-printed. When <parameter>owner</parameter> is false, the
+ <command>ALTER TABLE ... OWNER TO</command> statement is omitted.
+ When <parameter>tablespace</parameter> is false, the
+ <literal>TABLESPACE</literal> clause is omitted from the
+ <command>CREATE TABLE</command> statement. The
+ <parameter>only_kinds</parameter> and <parameter>except_kinds</parameter>
+ parameters described below control which object classes are emitted.
+ </para>
+ <para>
+ The <parameter>only_kinds</parameter> and <parameter>except_kinds</parameter>
+ parameters each take a text array of object-class kind names
+ and are mutually exclusive (specifying both raises an
+ error). When <parameter>only_kinds</parameter> is set, only the
+ listed kinds are emitted; when <parameter>except_kinds</parameter> is
+ set, every kind <emphasis>except</emphasis> the listed ones is
+ emitted. When neither is set (the default), every kind is
+ emitted. Leading and trailing whitespace in each array element
+ is ignored and matching is case-insensitive; an unrecognized
+ kind name raises an error.
+ The kind vocabulary is
+ <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).
+ Note that <literal>trigger</literal> and <literal>policy</literal>
+ are accepted in the vocabulary but currently produce no output;
+ they are reserved for future use when standalone
+ <function>pg_get_trigger_ddl</function> and
+ <function>pg_get_policy_ddl</function> helpers become available. 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_kinds => ARRAY['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_kinds => ARRAY['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 <parameter>schema_qualified</parameter> parameter (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..b56f59f0f41 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -5,7 +5,7 @@
*
* This file contains the pg_get_*_ddl family of functions that generate
* DDL statements to recreate database objects such as roles, tablespaces,
- * and databases, along with common infrastructure for option parsing and
+ * databases, and tables, along with common infrastructure for
* pretty-printing.
*
* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
@@ -21,30 +21,89 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.h"
+#include "catalog/namespace.h"
+#include "catalog/partition.h"
+#include "catalog/pg_am.h"
#include "catalog/pg_auth_members.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_collation.h"
+#include "catalog/pg_constraint.h"
#include "catalog/pg_database.h"
#include "catalog/pg_db_role_setting.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_policy.h"
+#include "catalog/pg_sequence.h"
+#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_tablespace.h"
+#include "catalog/pg_trigger.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
#include "commands/tablespace.h"
-#include "common/relpath.h"
#include "funcapi.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "utils/acl.h"
+#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/datetime.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
-#include "utils/pg_locale.h"
#include "utils/rel.h"
#include "utils/ruleutils.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
#include "utils/varlena.h"
+/*
+ * Object-class kinds that the only / except options on
+ * pg_get_table_ddl can filter on. Members are stored as integers in a
+ * Bitmapset on TableDdlContext. Keep table_ddl_kind_names[] in sync
+ * with the order of additions here.
+ */
+typedef enum TableDdlKind
+{
+ TABLE_DDL_KIND_TABLE,
+ TABLE_DDL_KIND_INDEX,
+ TABLE_DDL_KIND_PRIMARY_KEY,
+ TABLE_DDL_KIND_UNIQUE,
+ TABLE_DDL_KIND_CHECK,
+ TABLE_DDL_KIND_FOREIGN_KEY,
+ TABLE_DDL_KIND_EXCLUSION,
+ TABLE_DDL_KIND_RULE,
+ TABLE_DDL_KIND_STATISTICS,
+ TABLE_DDL_KIND_TRIGGER,
+ TABLE_DDL_KIND_POLICY,
+ TABLE_DDL_KIND_RLS,
+ TABLE_DDL_KIND_REPLICA_IDENTITY,
+ TABLE_DDL_KIND_PARTITION,
+} TableDdlKind;
+
+static const struct
+{
+ const char *name;
+ TableDdlKind kind;
+} table_ddl_kind_names[] =
+{
+ {"table", TABLE_DDL_KIND_TABLE},
+ {"index", TABLE_DDL_KIND_INDEX},
+ {"primary_key", TABLE_DDL_KIND_PRIMARY_KEY},
+ {"unique", TABLE_DDL_KIND_UNIQUE},
+ {"check", TABLE_DDL_KIND_CHECK},
+ {"foreign_key", TABLE_DDL_KIND_FOREIGN_KEY},
+ {"exclusion", TABLE_DDL_KIND_EXCLUSION},
+ {"rule", TABLE_DDL_KIND_RULE},
+ {"statistics", TABLE_DDL_KIND_STATISTICS},
+ {"trigger", TABLE_DDL_KIND_TRIGGER},
+ {"policy", TABLE_DDL_KIND_POLICY},
+ {"rls", TABLE_DDL_KIND_RLS},
+ {"replica_identity", TABLE_DDL_KIND_REPLICA_IDENTITY},
+ {"partition", TABLE_DDL_KIND_PARTITION},
+};
+
+static Bitmapset *parse_kind_array(const char *paramname, ArrayType *arr);
static void append_ddl_option(StringInfo buf, bool pretty, int indent,
const char *fmt, ...)
pg_attribute_printf(4, 5);
@@ -57,6 +116,183 @@ static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid);
static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
bool no_owner, bool no_tablespace);
+/*
+ * Per-column cache of locally-declared NOT NULL constraints. Built once
+ * by collect_local_not_null() and consulted by the column-emit helpers
+ * and the post-CREATE constraint loop. Entries with conoid == InvalidOid
+ * mean the column has no local NOT NULL constraint row in pg_constraint
+ * (the column may still have attnotnull=true on a pre-PG-18-upgraded
+ * catalog, in which case emit plain inline NOT NULL).
+ */
+typedef struct LocalNotNullEntry
+{
+ Oid conoid;
+ char *name;
+ bool is_auto; /* matches "<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_kind_array
+ * Parse a text[] of object-class kind names into a Bitmapset of
+ * TableDdlKind values.
+ *
+ * Each element is matched case-insensitively; surrounding whitespace is
+ * stripped. NULL elements are rejected. Unknown kind names raise an error
+ * citing the supplied parameter name. An empty array is also rejected.
+ * Duplicate entries are silently de-duplicated by the Bitmapset.
+ */
+static Bitmapset *
+parse_kind_array(const char *paramname, ArrayType *arr)
+{
+ Datum *elems;
+ bool *nulls;
+ int nelems;
+ Bitmapset *result = NULL;
+
+ deconstruct_array_builtin(arr, TEXTOID, &elems, &nulls, &nelems);
+
+ if (nelems == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" must specify at least one kind",
+ paramname)));
+
+ for (int i = 0; i < nelems; i++)
+ {
+ char *raw;
+ char *token;
+ char *end;
+ bool found = false;
+
+ if (nulls[i])
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" must not contain NULL elements",
+ paramname)));
+
+ raw = text_to_cstring(DatumGetTextPP(elems[i]));
+
+ /* Trim leading whitespace. */
+ token = raw;
+ while (*token == ' ' || *token == '\t')
+ token++;
+
+ /* Trim trailing whitespace. */
+ end = token + strlen(token);
+ while (end > token && (end[-1] == ' ' || end[-1] == '\t'))
+ end--;
+ *end = '\0';
+
+ for (size_t j = 0; j < lengthof(table_ddl_kind_names); j++)
+ {
+ if (pg_strcasecmp(token, table_ddl_kind_names[j].name) == 0)
+ {
+ result = bms_add_member(result,
+ (int) table_ddl_kind_names[j].kind);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("unrecognized kind \"%s\" in parameter \"%s\"",
+ token, paramname)));
+
+ pfree(raw);
+ }
+
+ return result;
+}
+
+/*
+ * is_kind_included
+ * Determine whether DDL for the given object-class kind should be
+ * emitted under the current options.
+ */
+static bool
+is_kind_included(const TableDdlContext *ctx, TableDdlKind kind)
+{
+ if (ctx->only_kinds != NULL)
+ return bms_is_member((int) kind, ctx->only_kinds);
+ if (ctx->except_kinds != NULL)
+ return !bms_is_member((int) kind, ctx->except_kinds);
+ return true;
+}
/*
* Helper to append a formatted string with optional pretty-printing.
@@ -975,3 +1211,1720 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
}
+
+/*
+ * lookup_relname_for_emit
+ * Return either the schema-qualified or the bare quoted name of a
+ * relation, depending on the schema_qualified flag.
+ *
+ * Temporary relations are never schema-qualified regardless of
+ * schema_qualified: the TEMPORARY keyword in CREATE TEMPORARY TABLE
+ * already places the table in the session's temp schema, so emitting
+ * pg_temp_NN.relname would produce DDL that cannot be replayed.
+ *
+ * When schema_qualified is true the schema-qualified name is always
+ * returned for non-temporary relations. When false, the bare relname
+ * is returned only if the target relation lives in base_namespace (the
+ * namespace of the table whose DDL is being generated); otherwise the
+ * schema-qualified form is returned, because cross-schema references
+ * (for example an inheritance parent or foreign key target in a
+ * different schema) are not safe to omit.
+ *
+ * This replaces the unsafe pattern
+ * quote_qualified_identifier(get_namespace_name(get_rel_namespace(oid)),
+ * get_rel_name(oid))
+ * which dereferences NULL when a concurrent transaction has dropped the
+ * referenced relation (or its schema) between when we cached its OID and
+ * when we ask the syscache for its name. Holding AccessShareLock on a
+ * dependent relation makes this race vanishingly unlikely in practice, but
+ * we still defend against it because the alternative is a SIGSEGV.
+ *
+ * Caller is responsible for pfree()ing the result.
+ */
+static char *
+lookup_relname_for_emit(Oid relid, bool schema_qualified, Oid base_namespace)
+{
+ HeapTuple tp;
+ Form_pg_class reltup;
+ char *nspname;
+ char *result;
+
+ tp = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ if (!HeapTupleIsValid(tp))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("relation with OID %u does not exist", relid),
+ errdetail("It may have been concurrently dropped.")));
+
+ reltup = (Form_pg_class) GETSTRUCT(tp);
+
+ /*
+ * Temporary relations are never schema-qualified: the TEMPORARY keyword
+ * already places them in pg_temp, and pg_temp_NN.relname cannot be
+ * replayed in any other session.
+ */
+ if (isTempNamespace(reltup->relnamespace))
+ {
+ result = pstrdup(quote_identifier(NameStr(reltup->relname)));
+ ReleaseSysCache(tp);
+ return result;
+ }
+
+ /* Bare name only when caller asked and target is in the base namespace. */
+ if (!schema_qualified && reltup->relnamespace == base_namespace)
+ {
+ result = pstrdup(quote_identifier(NameStr(reltup->relname)));
+ ReleaseSysCache(tp);
+ return result;
+ }
+
+ nspname = get_namespace_name(reltup->relnamespace);
+ if (nspname == NULL)
+ {
+ Oid nspoid = reltup->relnamespace;
+
+ ReleaseSysCache(tp);
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("schema with OID %u does not exist", nspoid),
+ errdetail("It may have been concurrently dropped.")));
+ }
+
+ result = quote_qualified_identifier(nspname, NameStr(reltup->relname));
+
+ pfree(nspname);
+ ReleaseSysCache(tp);
+
+ return result;
+}
+
+/*
+ * collect_local_not_null
+ * Scan pg_constraint once for locally-declared NOT NULL constraints
+ * on rel, returning a palloc'd array indexed by attnum (1..natts).
+ *
+ * Entries with conoid==InvalidOid mean "no local NOT NULL row for this
+ * column". When the constraint name does not match the auto-generated
+ * pattern "<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);
+
+ /*
+ * Temporarily override search_path so that the ruleutils helpers
+ * (pg_get_indexdef_ddl, pg_get_constraintdef_body,
+ * pg_get_statisticsobjdef_ddl, pg_get_ruledef_ddl, etc.) produce names
+ * that match the schema_qualified flag. Those helpers decide whether to
+ * qualify a name by calling RelationIsVisible(), which checks whether the
+ * object's schema appears in the active search_path.
+ *
+ * schema_qualified = false: narrow to the base schema so that same-schema
+ * references in DEFAULT expressions, FK targets, indexes, rules, and
+ * statistics come out unqualified automatically. Cross-schema references
+ * stay qualified, which is the correctness requirement.
+ *
+ * schema_qualified = true: narrow to pg_catalog only so that objects in
+ * the base schema (or any user schema the caller placed on search_path)
+ * are not reachable without qualification, forcing fully-qualified output
+ * from every helper regardless of the caller's session search_path.
+ *
+ * AtEOXact_GUC cleans up at xact end if anything throws between here and
+ * the explicit restore below; on the normal path we restore right before
+ * returning.
+ */
+ if (!ctx->schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx->base_namespace);
+
+ if (nspname != NULL)
+ {
+ const char *qnsp = quote_identifier(nspname);
+
+ ctx->save_nestlevel = NewGUCNestLevel();
+ (void) set_config_option("search_path", qnsp,
+ PGC_USERSET, PGC_S_SESSION,
+ GUC_ACTION_SAVE, true, 0, false);
+ pfree(nspname);
+ }
+ }
+ else
+ {
+ ctx->save_nestlevel = NewGUCNestLevel();
+ (void) set_config_option("search_path", "pg_catalog",
+ PGC_USERSET, PGC_S_SESSION,
+ GUC_ACTION_SAVE, true, 0, false);
+ }
+
+ /*
+ * Cache locally-declared NOT NULL constraint metadata so the column- emit
+ * helpers can produce "CONSTRAINT name NOT NULL" inline for user-named
+ * constraints, and so the constraint loop can avoid double-emitting them.
+ */
+ ctx->nn_entries = collect_local_not_null(rel, &ctx->skip_notnull_oids);
+
+ initStringInfo(&ctx->buf);
+
+ /*
+ * Emission passes. Order is significant: CREATE TABLE first; OWNER and
+ * the per-column ALTER COLUMN passes before sub-object emission;
+ * sub-objects in dependency-friendly order (indexes before constraints,
+ * since constraint-backed indexes are emitted out-of-line by the
+ * constraint loop); partition children last so the parent already exists
+ * at replay time. Each helper gates itself on is_kind_included for the
+ * relevant TABLE_DDL_KIND_*; the four "table itself" passes are grouped
+ * here under KIND_TABLE so all of CREATE TABLE / OWNER / SET DEFAULT /
+ * SET (...) drop out together when the user asks for only sub-objects
+ * (e.g. only => 'foreign_key' for the second pass of a two-pass FK
+ * clone).
+ */
+ if (is_kind_included(ctx, TABLE_DDL_KIND_TABLE))
+ {
+ emit_create_table_stmt(ctx);
+ emit_owner_stmt(ctx);
+ emit_child_default_overrides(ctx);
+ emit_attoptions(ctx);
+ }
+ emit_indexes(ctx);
+ emit_local_constraints(ctx);
+ emit_rules(ctx);
+ emit_statistics(ctx);
+ emit_replica_identity(ctx);
+ emit_rls_toggles(ctx);
+
+ /*
+ * Triggers and row-level security policies - disabled until the
+ * standalone pg_get_trigger_ddl() and pg_get_policy_ddl() helpers land.
+ * The scan + lock + filter scaffolding below is preserved (inside #if 0)
+ * so wiring up each emission becomes a one-liner in the loop body once
+ * those helpers are available.
+ */
+#if 0
+ if (is_kind_included(ctx, TABLE_DDL_KIND_TRIGGER))
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(ctx->relid));
+ trigScan = systable_beginscan(trigRel, TriggerRelidNameIndexId,
+ true, NULL, 1, &trigKey);
+ while (HeapTupleIsValid(trigTup = systable_getnext(trigScan)))
+ {
+ Form_pg_trigger trg = (Form_pg_trigger) GETSTRUCT(trigTup);
+
+ if (trg->tgisinternal)
+ continue;
+
+ /* TODO: append pg_get_trigger_ddl(trg->oid) output here. */
+ }
+ systable_endscan(trigScan);
+ table_close(trigRel, AccessShareLock);
+ }
+
+ if (is_kind_included(ctx, TABLE_DDL_KIND_POLICY))
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(ctx->relid));
+ polScan = systable_beginscan(polRel, PolicyPolrelidPolnameIndexId,
+ true, NULL, 1, &polKey);
+ while (HeapTupleIsValid(polTup = systable_getnext(polScan)))
+ {
+ /* TODO: append pg_get_policy_ddl(relid, polname) output here. */
+ }
+ systable_endscan(polScan);
+ table_close(polRel, AccessShareLock);
+ }
+#endif
+
+ emit_partition_children(ctx);
+
+ {
+ int natts = RelationGetDescr(rel)->natts;
+
+ for (int i = 1; i <= natts; i++)
+ if (ctx->nn_entries[i].name != NULL)
+ pfree(ctx->nn_entries[i].name);
+ }
+ pfree(ctx->nn_entries);
+
+ table_close(rel, AccessShareLock);
+ pfree(ctx->buf.data);
+ pfree(ctx->qualname);
+ list_free(ctx->skip_notnull_oids);
+
+ /*
+ * Pop the narrowed search_path now that all helpers have run. Errors
+ * thrown earlier are cleaned up by AtEOXact_GUC at xact end.
+ */
+ if (ctx->save_nestlevel >= 0)
+ AtEOXact_GUC(true, ctx->save_nestlevel);
+
+ return ctx->statements;
+}
+
+/*
+ * pg_get_table_ddl
+ * Return DDL to recreate a table as a set of text rows.
+ */
+Datum
+pg_get_table_ddl(PG_FUNCTION_ARGS)
+{
+ FuncCallContext *funcctx;
+ List *statements;
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcontext;
+ TableDdlContext ctx = {0};
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ if (!PG_ARGISNULL(5) && !PG_ARGISNULL(6))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"only_kinds\" and \"except_kinds\" parameters are mutually exclusive")));
+
+ /* Option defaults (match proargdefaults in pg_proc.dat). */
+ ctx.relid = PG_GETARG_OID(0);
+ ctx.pretty = false;
+ ctx.no_owner = false; /* owner DEFAULT true → no_owner = false */
+ ctx.no_tablespace = false; /* tablespace DEFAULT true → no_tablespace = false */
+ ctx.schema_qualified = true;
+ ctx.only_kinds = NULL;
+ ctx.except_kinds = NULL;
+
+ /* Override defaults with any explicitly supplied values. */
+ if (!PG_ARGISNULL(1))
+ ctx.pretty = PG_GETARG_BOOL(1);
+ if (!PG_ARGISNULL(2))
+ ctx.no_owner = !PG_GETARG_BOOL(2);
+ if (!PG_ARGISNULL(3))
+ ctx.no_tablespace = !PG_GETARG_BOOL(3);
+ if (!PG_ARGISNULL(4))
+ ctx.schema_qualified = PG_GETARG_BOOL(4);
+ if (!PG_ARGISNULL(5))
+ ctx.only_kinds = parse_kind_array("only_kinds",
+ PG_GETARG_ARRAYTYPE_P(5));
+ if (!PG_ARGISNULL(6))
+ ctx.except_kinds = parse_kind_array("except_kinds",
+ PG_GETARG_ARRAYTYPE_P(6));
+
+ statements = pg_get_table_ddl_internal(&ctx);
+ funcctx->user_fctx = statements;
+ funcctx->max_calls = list_length(statements);
+
+ MemoryContextSwitchTo(oldcontext);
+ }
+
+ funcctx = SRF_PERCALL_SETUP();
+ statements = (List *) funcctx->user_fctx;
+
+ if (funcctx->call_cntr < funcctx->max_calls)
+ {
+ char *stmt;
+
+ stmt = list_nth(statements, funcctx->call_cntr);
+
+ SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+ }
+ else
+ {
+ list_free_deep(statements);
+ SRF_RETURN_DONE(funcctx);
+ }
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 88de5c0481c..7186cf83e1d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -23,6 +23,7 @@
#include "access/htup_details.h"
#include "access/relation.h"
#include "access/table.h"
+#include "catalog/namespace.h"
#include "catalog/pg_aggregate.h"
#include "catalog/pg_am.h"
#include "catalog/pg_authid.h"
@@ -370,7 +371,7 @@ static void make_propgraphdef_elements(StringInfo buf, Oid pgrelid, char pgekind
static void make_propgraphdef_labels(StringInfo buf, Oid elid, const char *elalias, Oid elrelid);
static void make_propgraphdef_properties(StringInfo buf, Oid ellabelid, Oid elrelid);
static char *pg_get_statisticsobj_worker(Oid statextid, bool columns_only,
- bool missing_ok);
+ bool missing_ok, int prettyFlags);
static char *pg_get_partkeydef_worker(Oid relid, int prettyFlags,
bool attrsOnly, bool missing_ok);
static char *pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
@@ -603,6 +604,19 @@ pg_get_ruledef_ext(PG_FUNCTION_ARGS)
PG_RETURN_TEXT_P(string_to_text(res));
}
+/*
+ * pg_get_ruledef_ddl
+ * Like pg_get_ruledef but uses the active search_path to decide
+ * whether to schema-qualify the rule's target relation name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_ruledef_ddl(Oid ruleoid)
+{
+ return pg_get_ruledef_worker(ruleoid, PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA);
+}
+
static char *
pg_get_ruledef_worker(Oid ruleoid, int prettyFlags)
@@ -1241,6 +1255,22 @@ pg_get_indexdef_string(Oid indexrelid)
0, false);
}
+/*
+ * pg_get_indexdef_ddl
+ * Like pg_get_indexdef_string but uses the active search_path to
+ * decide whether to schema-qualify the indexed relation name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_indexdef_ddl(Oid indexrelid)
+{
+ return pg_get_indexdef_worker(indexrelid, 0, NULL,
+ false, false,
+ true, true,
+ PRETTYFLAG_SCHEMA, false);
+}
+
/* Internal version that just reports the key-column definitions */
char *
pg_get_indexdef_columns(Oid indexrelid, bool pretty)
@@ -1969,7 +1999,7 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS)
Oid statextid = PG_GETARG_OID(0);
char *res;
- res = pg_get_statisticsobj_worker(statextid, false, true);
+ res = pg_get_statisticsobj_worker(statextid, false, true, 0);
if (res == NULL)
PG_RETURN_NULL();
@@ -1984,7 +2014,20 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS)
char *
pg_get_statisticsobjdef_string(Oid statextid)
{
- return pg_get_statisticsobj_worker(statextid, false, false);
+ return pg_get_statisticsobj_worker(statextid, false, false, 0);
+}
+
+/*
+ * pg_get_statisticsobjdef_ddl
+ * Like pg_get_statisticsobjdef_string but uses the active search_path
+ * to decide whether to schema-qualify the statistics object name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_statisticsobjdef_ddl(Oid statextid)
+{
+ return pg_get_statisticsobj_worker(statextid, false, false, PRETTYFLAG_SCHEMA);
}
/*
@@ -1997,7 +2040,7 @@ pg_get_statisticsobjdef_columns(PG_FUNCTION_ARGS)
Oid statextid = PG_GETARG_OID(0);
char *res;
- res = pg_get_statisticsobj_worker(statextid, true, true);
+ res = pg_get_statisticsobj_worker(statextid, true, true, 0);
if (res == NULL)
PG_RETURN_NULL();
@@ -2009,7 +2052,8 @@ pg_get_statisticsobjdef_columns(PG_FUNCTION_ARGS)
* Internal workhorse to decompile an extended statistics object.
*/
static char *
-pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
+pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok,
+ int prettyFlags)
{
Form_pg_statistic_ext statextrec;
HeapTuple statexttup;
@@ -2069,10 +2113,18 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
if (!columns_only)
{
- nsp = get_namespace_name_or_temp(statextrec->stxnamespace);
- appendStringInfo(&buf, "CREATE STATISTICS %s",
- quote_qualified_identifier(nsp,
- NameStr(statextrec->stxname)));
+ if ((prettyFlags & PRETTYFLAG_SCHEMA) &&
+ StatisticsObjIsVisible(statextid))
+ appendStringInfo(&buf, "CREATE STATISTICS %s",
+ quote_identifier(NameStr(statextrec->stxname)));
+ else
+ {
+ nsp = get_namespace_name_or_temp(statextrec->stxnamespace);
+ appendStringInfo(&buf, "CREATE STATISTICS %s",
+ quote_qualified_identifier(nsp,
+ NameStr(statextrec->stxname)));
+ pfree(nsp);
+ }
/*
* Decode the stxkind column so that we know which stats types to
@@ -2163,10 +2215,9 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
{
Node *expr = (Node *) lfirst(lc);
char *str;
- int prettyFlags = PRETTYFLAG_PAREN;
str = deparse_expression_pretty(expr, context, false, false,
- prettyFlags, 0);
+ PRETTYFLAG_PAREN, 0);
if (colno > 0)
appendStringInfoString(&buf, ", ");
@@ -2544,6 +2595,20 @@ pg_get_constraintdef_command(Oid constraintId)
return pg_get_constraintdef_worker(constraintId, true, 0, false);
}
+/*
+ * pg_get_constraintdef_body
+ * Returns the constraint definition body without the
+ * "ALTER TABLE name ADD CONSTRAINT cname" prefix.
+ * FK target relation names in the body already use
+ * generate_relation_name (search_path-aware). For use by
+ * pg_get_table_ddl which builds the ALTER TABLE prefix itself.
+ */
+char *
+pg_get_constraintdef_body(Oid constraintId)
+{
+ return pg_get_constraintdef_worker(constraintId, false, 0, false);
+}
+
/*
* As of 9.4, we now use an MVCC snapshot for this.
*/
diff --git a/src/include/catalog/pg_inherits.h b/src/include/catalog/pg_inherits.h
index cc874abaabb..1e463b0106d 100644
--- a/src/include/catalog/pg_inherits.h
+++ b/src/include/catalog/pg_inherits.h
@@ -55,6 +55,7 @@ DECLARE_INDEX(pg_inherits_parent_index, 2187, InheritsParentIndexId, pg_inherits
extern List *find_inheritance_children(Oid parentrelId, LOCKMODE lockmode);
extern List *find_inheritance_children_extended(Oid parentrelId, bool omit_detached,
LOCKMODE lockmode, bool *detached_exist, TransactionId *detached_xmin);
+extern List *find_inheritance_parents(Oid relid, LOCKMODE lockmode);
extern List *find_all_inheritors(Oid parentrelId, LOCKMODE lockmode,
List **numparents);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 73bb7fbb430..8209acf4683 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8620,6 +8620,14 @@
proargtypes => 'regdatabase bool bool bool',
proargnames => '{database,pretty,owner,tablespace}',
proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' },
+{ oid => '8215', descr => 'get DDL to recreate a table',
+ proname => 'pg_get_table_ddl', prorows => '50',
+ proisstrict => 'f', proretset => 't', provolatile => 's', proparallel => 'r',
+ pronargdefaults => '6', prorettype => 'text',
+ proargtypes => 'regclass bool bool bool bool _text _text',
+ proargnames => '{relation,pretty,owner,tablespace,schema_qualified,only_kinds,except_kinds}',
+ proargdefaults => '{false,true,true,true,NULL,NULL}',
+ prosrc => 'pg_get_table_ddl' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index c3d8518cb62..d33563fdead 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -92,6 +92,7 @@ extern void check_of_type(HeapTuple typetuple);
extern void register_on_commit_action(Oid relid, OnCommitAction action);
extern void remove_on_commit_action(Oid relid);
+extern OnCommitAction get_on_commit_action(Oid relid);
extern void PreCommit_on_commit_actions(void);
extern void AtEOXact_on_commit_actions(bool isCommit);
@@ -108,4 +109,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
List *partConstraint);
+extern const char *storage_name(char c);
+
#endif /* TABLECMDS_H */
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 25c05e2f649..c3b3dc65c93 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -25,6 +25,7 @@ typedef struct PlannedStmt PlannedStmt;
#define RULE_INDEXDEF_KEYS_ONLY 0x02 /* ignore included attributes */
extern char *pg_get_indexdef_string(Oid indexrelid);
+extern char *pg_get_indexdef_ddl(Oid indexrelid);
extern char *pg_get_indexdef_columns(Oid indexrelid, bool pretty);
extern char *pg_get_indexdef_columns_extended(Oid indexrelid,
uint16 flags);
@@ -34,6 +35,7 @@ extern char *pg_get_partkeydef_columns(Oid relid, bool pretty);
extern char *pg_get_partconstrdef_string(Oid partitionId, char *aliasname);
extern char *pg_get_constraintdef_command(Oid constraintId);
+extern char *pg_get_constraintdef_body(Oid constraintId);
extern char *deparse_expression(Node *expr, List *dpcontext,
bool forceprefix, bool showimplicit);
extern List *deparse_context_for(const char *aliasname, Oid relid);
@@ -54,5 +56,7 @@ extern char *get_range_partbound_string(List *bound_datums);
extern void get_reloptions(StringInfo buf, Datum reloptions);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern char *pg_get_statisticsobjdef_ddl(Oid statextid);
+extern char *pg_get_ruledef_ddl(Oid ruleoid);
#endif /* RULEUTILS_H */
diff --git a/src/test/regress/expected/pg_get_table_ddl.out b/src/test/regress/expected/pg_get_table_ddl.out
new file mode 100644
index 00000000000..879584f101a
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,1274 @@
+--
+-- pg_get_table_ddl
+--
+-- All tests pass owner=>false so the ALTER TABLE OWNER TO line is not
+-- emitted, keeping output stable across test runners (which may run under
+-- different role names).
+--
+CREATE SCHEMA pgtbl_ddl_test;
+SET search_path = pgtbl_ddl_test;
+-- Basic table with PRIMARY KEY, NOT NULL, DEFAULT, COLLATE.
+CREATE TABLE basic (
+ id int PRIMARY KEY,
+ name text NOT NULL DEFAULT 'anon' COLLATE "C"
+);
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.basic (id integer NOT NULL, name text COLLATE "C" DEFAULT 'anon'::text NOT NULL);
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- Identity columns (with default and custom sequence options).
+CREATE TABLE id_cols (
+ id_always int GENERATED ALWAYS AS IDENTITY,
+ id_default int GENERATED BY DEFAULT AS IDENTITY
+);
+SELECT * FROM pg_get_table_ddl('id_cols'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.id_cols (id_always integer GENERATED ALWAYS AS IDENTITY NOT NULL, id_default integer GENERATED BY DEFAULT AS IDENTITY NOT NULL);
+(1 row)
+
+CREATE TABLE id_custom (
+ v int GENERATED ALWAYS AS IDENTITY (
+ SEQUENCE NAME id_custom_v_seq
+ START WITH 100 INCREMENT BY 5
+ MINVALUE 50 MAXVALUE 1000
+ CACHE 10 CYCLE
+ )
+);
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.id_custom (v integer GENERATED ALWAYS AS IDENTITY (START WITH 100 INCREMENT BY 5 MINVALUE 50 MAXVALUE 1000 CACHE 10 CYCLE) NOT NULL);
+(1 row)
+
+-- Generated stored column.
+CREATE TABLE gen_cols (
+ cents int,
+ dollars numeric GENERATED ALWAYS AS (cents / 100.0) STORED
+);
+SELECT * FROM pg_get_table_ddl('gen_cols'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.gen_cols (cents integer, dollars numeric GENERATED ALWAYS AS (((cents)::numeric / 100.0)) STORED);
+(1 row)
+
+-- Generated virtual column.
+CREATE TABLE gen_virtual (
+ base int,
+ derived int GENERATED ALWAYS AS (base * 2) VIRTUAL
+);
+SELECT * FROM pg_get_table_ddl('gen_virtual'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.gen_virtual (base integer, derived integer GENERATED ALWAYS AS ((base * 2)) VIRTUAL);
+(1 row)
+
+-- STORAGE and COMPRESSION (only emitted when non-default for the type).
+CREATE TABLE storage_cols (
+ a text STORAGE EXTERNAL,
+ b text STORAGE MAIN,
+ c text COMPRESSION pglz
+);
+SELECT * FROM pg_get_table_ddl('storage_cols'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.storage_cols (a text STORAGE EXTERNAL, b text STORAGE MAIN, c text COMPRESSION pglz);
+(1 row)
+
+-- Constraints: CHECK, UNIQUE, FOREIGN KEY (DEFERRABLE).
+CREATE TABLE refd (id int PRIMARY KEY);
+CREATE TABLE cons (
+ a int CHECK (a > 0),
+ b int UNIQUE,
+ c int REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED
+);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(3 rows)
+
+-- UNIQUE NULLS NOT DISTINCT (PG 15): null values are treated as equal for
+-- uniqueness purposes, so the NULLS NOT DISTINCT clause must be emitted.
+CREATE TABLE nulls_nd (
+ a int,
+ b int,
+ UNIQUE NULLS NOT DISTINCT (a, b)
+);
+SELECT * FROM pg_get_table_ddl('nulls_nd'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.nulls_nd (a integer, b integer);
+ ALTER TABLE pgtbl_ddl_test.nulls_nd ADD CONSTRAINT nulls_nd_a_b_key UNIQUE NULLS NOT DISTINCT (a, b);
+(2 rows)
+
+-- INCLUDE columns on a constraint-backed UNIQUE index (PG 11): the
+-- covering columns appear in an INCLUDE (...) clause on the ALTER TABLE
+-- ADD CONSTRAINT statement, not in the key column list.
+CREATE TABLE idx_inc (
+ id int PRIMARY KEY,
+ name text,
+ extra text,
+ UNIQUE (name) INCLUDE (extra)
+);
+SELECT * FROM pg_get_table_ddl('idx_inc'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idx_inc (id integer NOT NULL, name text, extra text);
+ ALTER TABLE pgtbl_ddl_test.idx_inc ADD CONSTRAINT idx_inc_name_extra_key UNIQUE (name) INCLUDE (extra);
+ ALTER TABLE pgtbl_ddl_test.idx_inc ADD CONSTRAINT idx_inc_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- FOREIGN KEY with ON DELETE / ON UPDATE referential actions and MATCH
+-- clause. Each variant must appear verbatim in the reconstructed DDL.
+CREATE TABLE fk_acts_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_acts (
+ a int,
+ b int,
+ c int,
+ d int,
+ CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES fk_acts_tgt(id)
+ ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET NULL,
+ CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET DEFAULT,
+ CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES fk_acts_tgt(id)
+ MATCH FULL
+);
+SELECT * FROM pg_get_table_ddl('fk_acts'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.fk_acts (a integer, b integer, c integer, d integer);
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON UPDATE RESTRICT ON DELETE CASCADE;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) MATCH FULL;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON DELETE SET DEFAULT;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON DELETE SET NULL;
+(5 rows)
+
+-- NOT ENFORCED foreign key (PG 17): the constraint is recorded in the
+-- catalog but not validated by the engine; NOT ENFORCED must appear in
+-- the reconstructed DDL.
+CREATE TABLE notenf_tgt (id int PRIMARY KEY);
+CREATE TABLE notenf (
+ a int,
+ CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES notenf_tgt(id)
+ NOT ENFORCED
+);
+SELECT * FROM pg_get_table_ddl('notenf'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.notenf (a integer);
+ ALTER TABLE pgtbl_ddl_test.notenf ADD CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.notenf_tgt(id) NOT ENFORCED;
+(2 rows)
+
+-- WITHOUT OVERLAPS on a PRIMARY KEY (PG 17): the range-type period
+-- column forms a temporal primary key; WITHOUT OVERLAPS must appear
+-- inside the key column list in the reconstructed ALTER TABLE.
+-- Both columns use range types with native GiST support so no
+-- btree_gist extension is needed.
+CREATE TABLE temporal_pk (
+ grp int4range,
+ during daterange,
+ PRIMARY KEY (grp, during WITHOUT OVERLAPS)
+);
+SELECT * FROM pg_get_table_ddl('temporal_pk'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.temporal_pk (grp int4range NOT NULL, during daterange NOT NULL);
+ ALTER TABLE pgtbl_ddl_test.temporal_pk ADD CONSTRAINT temporal_pk_pkey PRIMARY KEY (grp, during WITHOUT OVERLAPS);
+(2 rows)
+
+-- Indexes: functional and partial. Constraint-backing indexes are
+-- suppressed (they are emitted by the constraint loop).
+CREATE TABLE idxd (id int PRIMARY KEY, name text);
+CREATE INDEX idxd_lower ON idxd (lower(name));
+CREATE INDEX idxd_partial ON idxd (id) WHERE id > 100;
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- Inheritance, including a child DEFAULT override on an inherited column.
+CREATE TABLE par (a int DEFAULT 1, b text);
+CREATE TABLE ch (c int) INHERITS (par);
+ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ch (c integer) INHERITS (pgtbl_ddl_test.par);
+ ALTER TABLE pgtbl_ddl_test.ch ALTER COLUMN a SET DEFAULT 999;
+(2 rows)
+
+-- Per-column attoptions: emitted as ALTER COLUMN SET (...).
+CREATE TABLE attopt (a int, b text);
+ALTER TABLE attopt ALTER COLUMN a SET (n_distinct = 100);
+SELECT * FROM pg_get_table_ddl('attopt'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.attopt (a integer, b text);
+ ALTER TABLE pgtbl_ddl_test.attopt ALTER COLUMN a SET (n_distinct='100');
+(2 rows)
+
+-- Partitioned table parents (RANGE, HASH, and LIST).
+CREATE TABLE parted_range (id int, k int) PARTITION BY RANGE (id);
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range (id integer, k integer) PARTITION BY RANGE (id);
+(1 row)
+
+CREATE TABLE parted_hash (id int) PARTITION BY HASH (id);
+SELECT * FROM pg_get_table_ddl('parted_hash'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash (id integer) PARTITION BY HASH (id);
+(1 row)
+
+CREATE TABLE parted_list (id int, region text) PARTITION BY LIST (region);
+SELECT * FROM pg_get_table_ddl('parted_list'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_list (id integer, region text) PARTITION BY LIST (region);
+(1 row)
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0);
+(1 row)
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(1 row)
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO pgtbl_ddl_test.rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM pgtbl_ddl_test.stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx (a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(3 rows)
+
+-- UNLOGGED + reloptions.
+CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70);
+SELECT * FROM pg_get_table_ddl('uno'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE UNLOGGED TABLE pgtbl_ddl_test.uno (id integer) WITH (fillfactor='70');
+(1 row)
+
+-- Typed table (CREATE TABLE OF type_name). Columns inherited from the
+-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come
+-- out through a single "(col WITH OPTIONS ...)" list.
+CREATE TYPE typed_t AS (a int, b text);
+CREATE TABLE typed_plain OF typed_t;
+SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_plain OF pgtbl_ddl_test.typed_t;
+(1 row)
+
+CREATE TABLE typed_over OF typed_t (
+ a WITH OPTIONS DEFAULT 7 NOT NULL,
+ b WITH OPTIONS NOT NULL,
+ CONSTRAINT b_nonempty CHECK (length(b) > 0)
+);
+SELECT * FROM pg_get_table_ddl('typed_over'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_over OF pgtbl_ddl_test.typed_t (a WITH OPTIONS DEFAULT 7 NOT NULL, b WITH OPTIONS NOT NULL, CONSTRAINT b_nonempty CHECK ((length(b) > 0)));
+(1 row)
+
+-- Temporary tables + ON COMMIT. Temp tables must never be emitted with
+-- the session-private pg_temp_NN schema prefix -- the TEMPORARY keyword
+-- already places the table in pg_temp and the prefix is non-replayable.
+-- ON COMMIT DROP only fires at commit, so the queries that need to see
+-- the catalog entry must run in the same transaction as the CREATE.
+BEGIN;
+CREATE TEMP TABLE temp_default (id int);
+CREATE TEMP TABLE temp_delete (id int) ON COMMIT DELETE ROWS;
+CREATE TEMP TABLE temp_drop (id int) ON COMMIT DROP;
+SELECT line FROM pg_get_table_ddl('temp_default'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------
+ CREATE TEMPORARY TABLE temp_default (id integer);
+(1 row)
+
+SELECT line FROM pg_get_table_ddl('temp_delete'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE temp_delete (id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT line FROM pg_get_table_ddl('temp_drop'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------
+ CREATE TEMPORARY TABLE temp_drop (id integer) ON COMMIT DROP;
+(1 row)
+
+ROLLBACK;
+-- Pretty mode.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, pretty => true);
+ pg_get_table_ddl
+------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.basic ( +
+ id integer NOT NULL, +
+ name text COLLATE "C" DEFAULT 'anon'::text NOT NULL +
+ );
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- only_kinds / except_kinds gating: each emits either only the listed kinds
+-- (only_kinds) or every kind except the listed ones (except_kinds). The two
+-- are mutually exclusive. Kind vocabulary: table, index,
+-- primary_key, unique, check, foreign_key, exclusion, rule,
+-- statistics, trigger, policy, rls, replica_identity, partition.
+-- NOT NULL is not in the vocabulary - always emitted to prevent
+-- producing schemas that silently accept NULLs the source would
+-- have rejected.
+--
+-- except_kinds=index hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['index']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- except_kinds=primary_key,unique,check,foreign_key,exclusion hides every
+-- table-level constraint. Inline CHECK in the CREATE TABLE body is
+-- suppressed too.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['primary_key','unique','check','foreign_key','exclusion']);
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=foreign_key suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- first pass of the two-pass FK-clone workflow: emit tables without
+-- cross-table FKs, load data, then re-run with only_kinds=foreign_key
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+(2 rows)
+
+-- only_kinds=foreign_key is the second pass of the two-pass FK-clone
+-- workflow: emit only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN
+-- KEY rows; the CREATE TABLE and every non-FK sub-object are
+-- suppressed (the table already exists from the first pass).
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(1 row)
+
+-- A table with no foreign keys produces no rows under
+-- only_kinds=foreign_key.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- only_kinds=table emits only the bare CREATE TABLE (with any inline
+-- column-level NOT NULL and CHECK) - no indexes, no PRIMARY KEY ALTER,
+-- no other sub-objects. Useful for capturing a table's shape without
+-- pulling in everything attached to it.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table']);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+(1 row)
+
+-- only_kinds=check on a non-partition table. The constraint loop must emit
+-- column-level and table-level CHECK constraints via ALTER TABLE here,
+-- because the inline path in CREATE TABLE never runs (KIND_TABLE is
+-- not in the filter).
+CREATE TABLE chk_only (
+ id int PRIMARY KEY,
+ qty int CHECK (qty > 0),
+ CONSTRAINT chk_only_id_pos CHECK (id > 0)
+);
+SELECT * FROM pg_get_table_ddl('chk_only'::regclass, owner => false, only_kinds => ARRAY['check']);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.chk_only ADD CONSTRAINT chk_only_id_pos CHECK ((id > 0));
+ ALTER TABLE pgtbl_ddl_test.chk_only ADD CONSTRAINT chk_only_qty_check CHECK ((qty > 0));
+(2 rows)
+
+DROP TABLE chk_only;
+-- only_kinds=partition on a partitioned-table parent: each child's full DDL
+-- is emitted (CREATE TABLE ... PARTITION OF ...). The "partition"
+-- keyword is a gate at the parent level; it is stripped from the
+-- propagated filter so the child does not exclude its own
+-- CREATE TABLE.
+CREATE TABLE p_only (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE p_only_a PARTITION OF p_only FOR VALUES FROM (0) TO (100);
+CREATE TABLE p_only_b PARTITION OF p_only FOR VALUES FROM (100) TO (200);
+SELECT * FROM pg_get_table_ddl('p_only'::regclass, owner => false, only_kinds => ARRAY['partition']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.p_only_a PARTITION OF pgtbl_ddl_test.p_only FOR VALUES FROM (0) TO (100);
+ CREATE TABLE pgtbl_ddl_test.p_only_b PARTITION OF pgtbl_ddl_test.p_only FOR VALUES FROM (100) TO (200);
+(2 rows)
+
+DROP TABLE p_only;
+-- only_kinds and except_kinds are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false,
+ only_kinds => ARRAY['foreign_key'],
+ except_kinds => ARRAY['foreign_key']);
+ERROR: "only_kinds" and "except_kinds" parameters are mutually exclusive
+-- Pub/sub schema clone: keep the table and its primary key, drop the
+-- rest of the constraints so the subscriber can replicate without
+-- pulling cross-table dependencies along.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique','check','foreign_key','exclusion']);
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=rule hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, except_kinds => ARRAY['rule']);
+ pg_get_table_ddl
+----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+(1 row)
+
+-- except_kinds=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, except_kinds => ARRAY['statistics']);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, except_kinds => ARRAY['rls']);
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+(1 row)
+
+-- except_kinds=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, except_kinds => ARRAY['replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+(1 row)
+
+-- Partition children are emitted by default; except_kinds=partition
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false, except_kinds => ARRAY['partition']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range (id integer, k integer) PARTITION BY RANGE (id);
+(1 row)
+
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY[' INDEX ',' RULE ']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- schema_qualified=false strips the schema from the target table itself
+-- and from same-schema sibling references (inheritance/partition
+-- parents, identity sequences in the same schema), while cross-schema
+-- references are kept qualified for correctness.
+--
+-- Bare CREATE TABLE for an ordinary table.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE basic (id integer NOT NULL, name text COLLATE "C" DEFAULT 'anon'::text NOT NULL);
+ ALTER TABLE basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- INHERITS parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------
+ CREATE TABLE ch (c integer) INHERITS (par);
+ ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+(2 rows)
+
+-- Partition parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE parted_range_1 PARTITION OF parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+-- Cross-schema FK target keeps qualification. The 'refd' table is in
+-- pgtbl_ddl_test; place an FK target in a different schema and verify it
+-- stays qualified even with schema_qualified=false.
+CREATE SCHEMA pgtbl_ddl_other;
+CREATE TABLE pgtbl_ddl_other.parent (id int PRIMARY KEY);
+CREATE TABLE xschema_fk (
+ id int PRIMARY KEY,
+ pid int REFERENCES pgtbl_ddl_other.parent
+);
+SELECT * FROM pg_get_table_ddl('xschema_fk'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE xschema_fk (id integer NOT NULL, pid integer);
+ ALTER TABLE xschema_fk ADD CONSTRAINT xschema_fk_pid_fkey FOREIGN KEY (pid) REFERENCES pgtbl_ddl_other.parent(id);
+ ALTER TABLE xschema_fk ADD CONSTRAINT xschema_fk_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- Cross-schema FK whose target schema name ends with the base schema name
+-- (e.g. "xpgtbl_ddl_test" ends with "pgtbl_ddl_test"). A post-hoc text
+-- strip would mangle "xpgtbl_ddl_test.reftbl" into "xreftbl"; deciding
+-- qualification at generation time avoids the problem entirely.
+CREATE SCHEMA xpgtbl_ddl_test;
+CREATE TABLE xpgtbl_ddl_test.reftbl (id int PRIMARY KEY);
+CREATE TABLE suffix_schema_fk (
+ id int PRIMARY KEY,
+ ref int REFERENCES xpgtbl_ddl_test.reftbl
+);
+SELECT * FROM pg_get_table_ddl('suffix_schema_fk'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE suffix_schema_fk (id integer NOT NULL, ref integer);
+ ALTER TABLE suffix_schema_fk ADD CONSTRAINT suffix_schema_fk_pkey PRIMARY KEY (id);
+ ALTER TABLE suffix_schema_fk ADD CONSTRAINT suffix_schema_fk_ref_fkey FOREIGN KEY (ref) REFERENCES xpgtbl_ddl_test.reftbl(id);
+(3 rows)
+
+DROP TABLE suffix_schema_fk;
+DROP SCHEMA xpgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to table xpgtbl_ddl_test.reftbl
+-- Identity column with a custom sequence name in the same schema is
+-- emitted unqualified.
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE id_custom (v integer GENERATED ALWAYS AS IDENTITY (START WITH 100 INCREMENT BY 5 MINVALUE 50 MAXVALUE 1000 CACHE 10 CYCLE) NOT NULL);
+(1 row)
+
+-- Round-trip: with schema_qualified=false, after SET search_path the
+-- DDL can be replayed into a different target schema.
+CREATE SCHEMA pgtbl_ddl_replay;
+DO $$
+DECLARE
+ stmt text;
+BEGIN
+ SET LOCAL search_path = pgtbl_ddl_replay;
+ FOR stmt IN
+ SELECT line FROM pg_get_table_ddl('pgtbl_ddl_test.basic'::regclass,
+ owner => false,
+ schema_qualified => false) AS line
+ LOOP
+ EXECUTE stmt;
+ END LOOP;
+END $$;
+SELECT relnamespace::regnamespace::text, relname
+FROM pg_class WHERE oid = 'pgtbl_ddl_replay.basic'::regclass;
+ relnamespace | relname
+------------------+---------
+ pgtbl_ddl_replay | basic
+(1 row)
+
+DROP SCHEMA pgtbl_ddl_replay CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_replay.basic
+DROP TABLE xschema_fk;
+DROP SCHEMA pgtbl_ddl_other CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_other.parent
+-- Locally-declared CHECK on a partition child has no inline column
+-- list to live in (PARTITION OF form), so it must come out as a
+-- separate ALTER TABLE ... ADD CONSTRAINT statement.
+CREATE TABLE parted_chk (id int, val int) PARTITION BY RANGE (id);
+CREATE TABLE parted_chk_child PARTITION OF parted_chk
+ (CONSTRAINT chk_inline CHECK (val > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('parted_chk_child'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_chk_child PARTITION OF pgtbl_ddl_test.parted_chk FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_chk_child ADD CONSTRAINT chk_inline CHECK ((val > 0));
+(2 rows)
+
+DROP TABLE parted_chk;
+-- Inheritance children of a parent with a generated column should not
+-- emit an ALTER COLUMN ... SET DEFAULT for the inherited generated
+-- column: the GENERATED expression carries down through inheritance,
+-- and SET DEFAULT would fail at replay against a generated column.
+CREATE TABLE par_gen (
+ id int,
+ g int GENERATED ALWAYS AS (id * 2) STORED
+);
+CREATE TABLE ch_gen () INHERITS (par_gen);
+SELECT * FROM pg_get_table_ddl('ch_gen'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ch_gen () INHERITS (pgtbl_ddl_test.par_gen);
+(1 row)
+
+DROP TABLE par_gen CASCADE;
+NOTICE: drop cascades to table ch_gen
+-- A user-named NOT NULL on a local column is emitted inline as
+-- "CONSTRAINT name NOT NULL" so the original name is preserved. An
+-- auto-named NOT NULL on a local column is emitted as a plain inline
+-- "NOT NULL" - PG re-creates the auto-named constraint when CREATE
+-- TABLE runs, so the constraint loop skips both forms to avoid the
+-- redundant (and on table-rename or sequence-collision, broken)
+-- ALTER TABLE ... ADD CONSTRAINT ... NOT NULL statement.
+CREATE TABLE nn_named (
+ a int CONSTRAINT my_nn NOT NULL,
+ b int NOT NULL
+);
+SELECT * FROM pg_get_table_ddl('nn_named'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.nn_named (a integer CONSTRAINT my_nn NOT NULL, b integer NOT NULL);
+(1 row)
+
+DROP TABLE nn_named;
+-- Renaming the table after an IDENTITY NOT NULL column was declared
+-- leaves the auto-named constraint frozen at the original name, which
+-- no longer matches the post-rename "<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. String
+-- literal text is output verbatim by the deparser and is unaffected by
+-- search_path narrowing.
+CREATE SCHEMA pgtbl_ddl_str;
+CREATE TABLE pgtbl_ddl_str.p (id int, note text) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_str.pc PARTITION OF pgtbl_ddl_str.p
+ (CONSTRAINT chk CHECK (note <> 'pgtbl_ddl_str.secret'))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_str.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK ((note <> 'pgtbl_ddl_str.secret'::text));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_str CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_str.p
+-- schema_qualified=false must NOT corrupt double-quoted identifiers
+-- whose contents happen to contain the schema name followed by a dot
+-- (for example a column literally named "<schema>.weird" inside that
+-- schema). The mechanism is purely search_path narrowing: the deparser
+-- outputs attribute names verbatim from pg_attribute, so the quoted
+-- identifier content is never examined for schema prefixes. This holds
+-- even when the schema name is a single letter.
+CREATE SCHEMA pgtbl_ddl_qid;
+CREATE TABLE pgtbl_ddl_qid.p (id int, "pgtbl_ddl_qid.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_qid.pc PARTITION OF pgtbl_ddl_qid.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_qid.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_qid.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK (("pgtbl_ddl_qid.weird" > 0));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_qid CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_qid.p
+-- Same with a short (single-letter) schema name to confirm there is no
+-- minimum-length assumption in the stripping path.
+CREATE SCHEMA pgtbl_ddl_q1;
+CREATE TABLE pgtbl_ddl_q1.p (id int, "pgtbl_ddl_q1.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_q1.pc PARTITION OF pgtbl_ddl_q1.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_q1.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_q1.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK (("pgtbl_ddl_q1.weird" > 0));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_q1 CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_q1.p
+-- schema_qualified=false when the base schema name itself requires
+-- quoting (its prefix starts with "). Both forms of the prefix -
+-- bare-lowercase and quoted - must be stripped from outer
+-- ALTER TABLE / ON / CREATE INDEX prefixes, while inner quoted
+-- identifiers that happen to begin the same way must be preserved.
+CREATE SCHEMA "PgTbl-Ddl-Q";
+CREATE TABLE "PgTbl-Ddl-Q"."T" (id int, val int,
+ CONSTRAINT chk CHECK (val > 0));
+CREATE INDEX "T_val_idx" ON "PgTbl-Ddl-Q"."T" (val);
+SELECT * FROM pg_get_table_ddl('"PgTbl-Ddl-Q"."T"'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE TABLE "T" (id integer, val integer, CONSTRAINT chk CHECK ((val > 0)));
+ CREATE INDEX "T_val_idx" ON "T" USING btree (val);
+(2 rows)
+
+DROP SCHEMA "PgTbl-Ddl-Q" CASCADE;
+NOTICE: drop cascades to table "PgTbl-Ddl-Q"."T"
+-- Error: not an ordinary or partitioned table.
+CREATE VIEW v AS SELECT 1 AS x;
+SELECT * FROM pg_get_table_ddl('v'::regclass);
+ERROR: "v" is not an ordinary or partitioned table
+CREATE SEQUENCE s;
+SELECT * FROM pg_get_table_ddl('s'::regclass);
+ERROR: "s" is not an ordinary or partitioned table
+-- NULL argument returns no rows.
+SELECT * FROM pg_get_table_ddl(NULL);
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- REPLICA IDENTITY USING INDEX validation: if the referenced index
+-- would not be emitted (because its source kind is suppressed) but
+-- replica_identity itself would be, the function raises an error so
+-- the emitted DDL never dangles. Exercise all three constraint-backed
+-- forms (PK, UNIQUE, EXCLUSION) and a bare CREATE UNIQUE INDEX, in
+-- both the except-list and only-list shapes.
+--
+-- PK-backed: except_kinds=primary_key without except_kinds=replica_identity.
+CREATE TABLE ri_pk_excl (a int PRIMARY KEY);
+ALTER TABLE ri_pk_excl REPLICA IDENTITY USING INDEX ri_pk_excl_pkey;
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+ERROR: REPLICA IDENTITY for table "ri_pk_excl" requires kind "primary_key" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "primary_key" kind, which is not in the active filter.
+HINT: Either add "primary_key" to the filter or remove "replica_identity" from it.
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_pk_excl (a integer NOT NULL);
+(1 row)
+
+DROP TABLE ri_pk_excl;
+-- UNIQUE-constraint-backed replica identity index.
+CREATE TABLE ri_uniq_excl (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_uniq_excl REPLICA IDENTITY USING INDEX ri_uniq_excl_a_key;
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique']);
+ERROR: REPLICA IDENTITY for table "ri_uniq_excl" requires kind "unique" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "unique" kind, which is not in the active filter.
+HINT: Either add "unique" to the filter or remove "replica_identity" from it.
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique','replica_identity']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_uniq_excl (a integer NOT NULL, b integer);
+(1 row)
+
+DROP TABLE ri_uniq_excl;
+-- Bare CREATE UNIQUE INDEX (no backing constraint) used as REPLICA
+-- IDENTITY: the index is emitted by the "index" kind, so excluding
+-- "index" without also excluding "replica_identity" must error.
+CREATE TABLE ri_idx_excl (a int NOT NULL, b int);
+CREATE UNIQUE INDEX ri_idx_excl_a_uniq ON ri_idx_excl (a);
+ALTER TABLE ri_idx_excl REPLICA IDENTITY USING INDEX ri_idx_excl_a_uniq;
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index']);
+ERROR: REPLICA IDENTITY for table "ri_idx_excl" requires kind "index" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "index" kind, which is not in the active filter.
+HINT: Either add "index" to the filter or remove "replica_identity" from it.
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx_excl (a integer NOT NULL, b integer);
+(1 row)
+
+DROP TABLE ri_idx_excl;
+-- Include-side analog: only_kinds=replica_identity must include the kind
+-- that emits the index, or the function errors.
+CREATE TABLE ri_only (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ERROR: REPLICA IDENTITY for table "ri_only" requires kind "unique" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "unique" kind, which is not in the active filter.
+HINT: Either add "unique" to the filter or remove "replica_identity" from it.
+-- Including both kinds succeeds.
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['unique','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.ri_only ADD CONSTRAINT ri_only_a_key UNIQUE (a);
+ ALTER TABLE pgtbl_ddl_test.ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+(2 rows)
+
+DROP TABLE ri_only;
+-- Per-kind only_kinds / except_kinds coverage. For each kind in the vocabulary,
+-- verify that only_kinds=K emits just that kind's output and that except_kinds=K
+-- omits just that kind's output. Kinds with no fixture above (trigger,
+-- policy) are still recognized but currently emit nothing; only_kinds=K
+-- returns zero rows for them. Fixtures used:
+-- idxd: PK + two secondary CREATE INDEX
+-- cons: PK + UNIQUE + CHECK + FK (constraint-backed)
+-- rt: CREATE RULE
+-- stx: CREATE STATISTICS
+-- rls: ENABLE/FORCE ROW LEVEL SECURITY
+-- ri_full: REPLICA IDENTITY FULL (no source-kind dependency)
+CREATE TABLE excl_fix (rng int4range,
+ EXCLUDE USING gist (rng WITH &&));
+-- only_kinds=K: emit only kind K's statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['unique']);
+ pg_get_table_ddl
+-----------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.excl_fix ADD CONSTRAINT excl_fix_rng_excl EXCLUDE USING gist (rng WITH &&);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, only_kinds => ARRAY['rule']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO pgtbl_ddl_test.rt_log (id)+
+ VALUES (new.id);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM pgtbl_ddl_test.stx;
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, only_kinds => ARRAY['rls']);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(1 row)
+
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['policy']);
+-- except_kinds=K: emit everything except kind K's statements. PK/UNIQUE/
+-- INDEX are exercised on tables without REPLICA IDENTITY USING INDEX
+-- so the validation does not trip.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['check']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, except_kinds => ARRAY['exclusion']);
+ pg_get_table_ddl
+-------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.excl_fix (rng int4range);
+(1 row)
+
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['policy']);
+-- except_kinds=table suppresses the CREATE TABLE + OWNER + child-default
+-- SET DEFAULT + attoptions passes, leaving only the sub-object passes.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['table']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- Multi-kind compositions.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table','index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['primary_key','foreign_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(1 row)
+
+-- Duplicate entries in the list are silently de-duplicated by the
+-- Bitmapset.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index','index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+DROP TABLE excl_fix;
+-- only_kinds=K on a table that does not have any K of that kind is a valid
+-- filter and returns zero rows. Tested previously with foreign_key
+-- on 'refd'; here also rule, statistics, rls, replica_identity, and
+-- exclusion on 'basic' (which has only PK + columns).
+SELECT count(*) AS rule_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rule']);
+ rule_rows
+-----------
+ 0
+(1 row)
+
+SELECT count(*) AS stats_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+ stats_rows
+------------
+ 0
+(1 row)
+
+SELECT count(*) AS rls_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rls']);
+ rls_rows
+----------
+ 0
+(1 row)
+
+SELECT count(*) AS ri_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ ri_rows
+---------
+ 0
+(1 row)
+
+SELECT count(*) AS excl_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+ excl_rows
+-----------
+ 0
+(1 row)
+
+-- Input-validation edge cases for the kind array. All
+-- of these raise an error; the function never reaches relation open.
+-- Empty array:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[]::text[]);
+ERROR: parameter "only_kinds" must specify at least one kind
+-- NULL element:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[NULL::text]);
+ERROR: parameter "only_kinds" must not contain NULL elements
+-- Unrecognized kind:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY['no_such_kind']);
+ERROR: unrecognized kind "no_such_kind" in parameter "only_kinds"
+-- Same kind specified through both parameters is still mutex-rejected.
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ only_kinds => ARRAY['index'],
+ except_kinds => ARRAY['rule']);
+ERROR: "only_kinds" and "except_kinds" parameters are mutually exclusive
+-- Round-trip verification. For every test table, capture the DDL the
+-- function emits, drop the schema, replay each table's DDL in
+-- dependency order, and confirm that pg_get_table_ddl on the recreated
+-- relation matches the original line-for-line. The final SELECT must
+-- return zero rows.
+CREATE TEMP TABLE pgtbl_ddl_rt_orig (name text, ord int, line text);
+INSERT INTO pgtbl_ddl_rt_orig
+SELECT t.name, o.ord, o.line
+FROM (VALUES
+ ('basic'), ('id_cols'), ('id_custom'), ('gen_cols'), ('gen_virtual'), ('storage_cols'),
+ ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'),
+ ('fk_acts_tgt'), ('fk_acts'), ('notenf_tgt'), ('notenf'), ('temporal_pk'),
+ ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('parted_list'),
+ ('rt_log'), ('rt'),
+ ('stx'), ('rls'), ('ri_full'), ('ri_idx'), ('uno')
+ ) AS t(name),
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord);
+DO $$
+DECLARE
+ tables CONSTANT text[] := ARRAY[
+ 'basic', 'id_cols', 'id_custom', 'gen_cols', 'gen_virtual', 'storage_cols',
+ 'refd', 'cons', 'nulls_nd', 'idx_inc',
+ 'fk_acts_tgt', 'fk_acts', 'notenf_tgt', 'notenf', 'temporal_pk',
+ 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ 'parted_list',
+ 'rt_log', 'rt',
+ 'stx', 'rls', 'ri_full', 'ri_idx', 'uno'
+ ];
+ t text;
+ stmt text;
+BEGIN
+ DROP SCHEMA pgtbl_ddl_test CASCADE;
+ CREATE SCHEMA pgtbl_ddl_test;
+ FOREACH t IN ARRAY tables LOOP
+ FOR stmt IN
+ SELECT line FROM pgtbl_ddl_rt_orig WHERE name = t ORDER BY ord
+ LOOP
+ BEGIN
+ EXECUTE stmt;
+ EXCEPTION WHEN duplicate_table THEN
+ NULL; -- partition child already created by its parent's DDL
+ END;
+ END LOOP;
+ END LOOP;
+END $$;
+NOTICE: drop cascades to 34 other objects
+DETAIL: drop cascades to table basic
+drop cascades to table id_cols
+drop cascades to table id_custom
+drop cascades to table gen_cols
+drop cascades to table gen_virtual
+drop cascades to table storage_cols
+drop cascades to table refd
+drop cascades to table cons
+drop cascades to table nulls_nd
+drop cascades to table idx_inc
+drop cascades to table fk_acts_tgt
+drop cascades to table fk_acts
+drop cascades to table notenf_tgt
+drop cascades to table notenf
+drop cascades to table temporal_pk
+drop cascades to table idxd
+drop cascades to table par
+drop cascades to table ch
+drop cascades to table attopt
+drop cascades to table parted_range
+drop cascades to table parted_hash
+drop cascades to table parted_list
+drop cascades to table rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 29 other objects
+DETAIL: drop cascades to table basic
+drop cascades to table id_cols
+drop cascades to table id_custom
+drop cascades to table gen_cols
+drop cascades to table gen_virtual
+drop cascades to table storage_cols
+drop cascades to table refd
+drop cascades to table cons
+drop cascades to table nulls_nd
+drop cascades to table idx_inc
+drop cascades to table fk_acts_tgt
+drop cascades to table fk_acts
+drop cascades to table notenf_tgt
+drop cascades to table notenf
+drop cascades to table temporal_pk
+drop cascades to table idxd
+drop cascades to table par
+drop cascades to table ch
+drop cascades to table attopt
+drop cascades to table parted_range
+drop cascades to table parted_hash
+drop cascades to table parted_list
+drop cascades to table rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..f7bf58ab213
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,740 @@
+--
+-- pg_get_table_ddl
+--
+-- All tests pass owner=>false so the ALTER TABLE OWNER TO line is not
+-- emitted, keeping output stable across test runners (which may run under
+-- different role names).
+--
+CREATE SCHEMA pgtbl_ddl_test;
+SET search_path = pgtbl_ddl_test;
+
+-- Basic table with PRIMARY KEY, NOT NULL, DEFAULT, COLLATE.
+CREATE TABLE basic (
+ id int PRIMARY KEY,
+ name text NOT NULL DEFAULT 'anon' COLLATE "C"
+);
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false);
+
+-- Identity columns (with default and custom sequence options).
+CREATE TABLE id_cols (
+ id_always int GENERATED ALWAYS AS IDENTITY,
+ id_default int GENERATED BY DEFAULT AS IDENTITY
+);
+SELECT * FROM pg_get_table_ddl('id_cols'::regclass, owner => false);
+
+CREATE TABLE id_custom (
+ v int GENERATED ALWAYS AS IDENTITY (
+ SEQUENCE NAME id_custom_v_seq
+ START WITH 100 INCREMENT BY 5
+ MINVALUE 50 MAXVALUE 1000
+ CACHE 10 CYCLE
+ )
+);
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false);
+
+-- Generated stored column.
+CREATE TABLE gen_cols (
+ cents int,
+ dollars numeric GENERATED ALWAYS AS (cents / 100.0) STORED
+);
+SELECT * FROM pg_get_table_ddl('gen_cols'::regclass, owner => false);
+
+-- Generated virtual column.
+CREATE TABLE gen_virtual (
+ base int,
+ derived int GENERATED ALWAYS AS (base * 2) VIRTUAL
+);
+SELECT * FROM pg_get_table_ddl('gen_virtual'::regclass, owner => false);
+
+-- STORAGE and COMPRESSION (only emitted when non-default for the type).
+CREATE TABLE storage_cols (
+ a text STORAGE EXTERNAL,
+ b text STORAGE MAIN,
+ c text COMPRESSION pglz
+);
+SELECT * FROM pg_get_table_ddl('storage_cols'::regclass, owner => false);
+
+-- Constraints: CHECK, UNIQUE, FOREIGN KEY (DEFERRABLE).
+CREATE TABLE refd (id int PRIMARY KEY);
+CREATE TABLE cons (
+ a int CHECK (a > 0),
+ b int UNIQUE,
+ c int REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED
+);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false);
+
+-- UNIQUE NULLS NOT DISTINCT (PG 15): null values are treated as equal for
+-- uniqueness purposes, so the NULLS NOT DISTINCT clause must be emitted.
+CREATE TABLE nulls_nd (
+ a int,
+ b int,
+ UNIQUE NULLS NOT DISTINCT (a, b)
+);
+SELECT * FROM pg_get_table_ddl('nulls_nd'::regclass, owner => false);
+
+-- INCLUDE columns on a constraint-backed UNIQUE index (PG 11): the
+-- covering columns appear in an INCLUDE (...) clause on the ALTER TABLE
+-- ADD CONSTRAINT statement, not in the key column list.
+CREATE TABLE idx_inc (
+ id int PRIMARY KEY,
+ name text,
+ extra text,
+ UNIQUE (name) INCLUDE (extra)
+);
+SELECT * FROM pg_get_table_ddl('idx_inc'::regclass, owner => false);
+
+-- FOREIGN KEY with ON DELETE / ON UPDATE referential actions and MATCH
+-- clause. Each variant must appear verbatim in the reconstructed DDL.
+CREATE TABLE fk_acts_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_acts (
+ a int,
+ b int,
+ c int,
+ d int,
+ CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES fk_acts_tgt(id)
+ ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET NULL,
+ CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET DEFAULT,
+ CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES fk_acts_tgt(id)
+ MATCH FULL
+);
+SELECT * FROM pg_get_table_ddl('fk_acts'::regclass, owner => false);
+
+-- NOT ENFORCED foreign key (PG 17): the constraint is recorded in the
+-- catalog but not validated by the engine; NOT ENFORCED must appear in
+-- the reconstructed DDL.
+CREATE TABLE notenf_tgt (id int PRIMARY KEY);
+CREATE TABLE notenf (
+ a int,
+ CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES notenf_tgt(id)
+ NOT ENFORCED
+);
+SELECT * FROM pg_get_table_ddl('notenf'::regclass, owner => false);
+
+-- WITHOUT OVERLAPS on a PRIMARY KEY (PG 17): the range-type period
+-- column forms a temporal primary key; WITHOUT OVERLAPS must appear
+-- inside the key column list in the reconstructed ALTER TABLE.
+-- Both columns use range types with native GiST support so no
+-- btree_gist extension is needed.
+CREATE TABLE temporal_pk (
+ grp int4range,
+ during daterange,
+ PRIMARY KEY (grp, during WITHOUT OVERLAPS)
+);
+SELECT * FROM pg_get_table_ddl('temporal_pk'::regclass, owner => false);
+
+-- Indexes: functional and partial. Constraint-backing indexes are
+-- suppressed (they are emitted by the constraint loop).
+CREATE TABLE idxd (id int PRIMARY KEY, name text);
+CREATE INDEX idxd_lower ON idxd (lower(name));
+CREATE INDEX idxd_partial ON idxd (id) WHERE id > 100;
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false);
+
+-- Inheritance, including a child DEFAULT override on an inherited column.
+CREATE TABLE par (a int DEFAULT 1, b text);
+CREATE TABLE ch (c int) INHERITS (par);
+ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false);
+
+-- Per-column attoptions: emitted as ALTER COLUMN SET (...).
+CREATE TABLE attopt (a int, b text);
+ALTER TABLE attopt ALTER COLUMN a SET (n_distinct = 100);
+SELECT * FROM pg_get_table_ddl('attopt'::regclass, owner => false);
+
+-- Partitioned table parents (RANGE, HASH, and LIST).
+CREATE TABLE parted_range (id int, k int) PARTITION BY RANGE (id);
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false);
+
+CREATE TABLE parted_hash (id int) PARTITION BY HASH (id);
+SELECT * FROM pg_get_table_ddl('parted_hash'::regclass, owner => false);
+
+CREATE TABLE parted_list (id int, region text) PARTITION BY LIST (region);
+SELECT * FROM pg_get_table_ddl('parted_list'::regclass, owner => false);
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false);
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, owner => false);
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, owner => false);
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false);
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false);
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false);
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false);
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, owner => false);
+
+-- UNLOGGED + reloptions.
+CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70);
+SELECT * FROM pg_get_table_ddl('uno'::regclass, owner => false);
+
+-- Typed table (CREATE TABLE OF type_name). Columns inherited from the
+-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come
+-- out through a single "(col WITH OPTIONS ...)" list.
+CREATE TYPE typed_t AS (a int, b text);
+CREATE TABLE typed_plain OF typed_t;
+SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, owner => false);
+
+CREATE TABLE typed_over OF typed_t (
+ a WITH OPTIONS DEFAULT 7 NOT NULL,
+ b WITH OPTIONS NOT NULL,
+ CONSTRAINT b_nonempty CHECK (length(b) > 0)
+);
+SELECT * FROM pg_get_table_ddl('typed_over'::regclass, owner => false);
+
+-- Temporary tables + ON COMMIT. Temp tables must never be emitted with
+-- the session-private pg_temp_NN schema prefix -- the TEMPORARY keyword
+-- already places the table in pg_temp and the prefix is non-replayable.
+-- ON COMMIT DROP only fires at commit, so the queries that need to see
+-- the catalog entry must run in the same transaction as the CREATE.
+BEGIN;
+CREATE TEMP TABLE temp_default (id int);
+CREATE TEMP TABLE temp_delete (id int) ON COMMIT DELETE ROWS;
+CREATE TEMP TABLE temp_drop (id int) ON COMMIT DROP;
+
+SELECT line FROM pg_get_table_ddl('temp_default'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT line FROM pg_get_table_ddl('temp_delete'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT line FROM pg_get_table_ddl('temp_drop'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ROLLBACK;
+
+-- Pretty mode.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, pretty => true);
+
+-- only_kinds / except_kinds gating: each emits either only the listed kinds
+-- (only_kinds) or every kind except the listed ones (except_kinds). The two
+-- are mutually exclusive. Kind vocabulary: table, index,
+-- primary_key, unique, check, foreign_key, exclusion, rule,
+-- statistics, trigger, policy, rls, replica_identity, partition.
+-- NOT NULL is not in the vocabulary - always emitted to prevent
+-- producing schemas that silently accept NULLs the source would
+-- have rejected.
+--
+-- except_kinds=index hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['index']);
+
+-- except_kinds=primary_key,unique,check,foreign_key,exclusion hides every
+-- table-level constraint. Inline CHECK in the CREATE TABLE body is
+-- suppressed too.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['primary_key','unique','check','foreign_key','exclusion']);
+
+-- except_kinds=foreign_key suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- first pass of the two-pass FK-clone workflow: emit tables without
+-- cross-table FKs, load data, then re-run with only_kinds=foreign_key
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['foreign_key']);
+
+-- only_kinds=foreign_key is the second pass of the two-pass FK-clone
+-- workflow: emit only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN
+-- KEY rows; the CREATE TABLE and every non-FK sub-object are
+-- suppressed (the table already exists from the first pass).
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+
+-- A table with no foreign keys produces no rows under
+-- only_kinds=foreign_key.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+
+-- only_kinds=table emits only the bare CREATE TABLE (with any inline
+-- column-level NOT NULL and CHECK) - no indexes, no PRIMARY KEY ALTER,
+-- no other sub-objects. Useful for capturing a table's shape without
+-- pulling in everything attached to it.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table']);
+
+-- only_kinds=check on a non-partition table. The constraint loop must emit
+-- column-level and table-level CHECK constraints via ALTER TABLE here,
+-- because the inline path in CREATE TABLE never runs (KIND_TABLE is
+-- not in the filter).
+CREATE TABLE chk_only (
+ id int PRIMARY KEY,
+ qty int CHECK (qty > 0),
+ CONSTRAINT chk_only_id_pos CHECK (id > 0)
+);
+SELECT * FROM pg_get_table_ddl('chk_only'::regclass, owner => false, only_kinds => ARRAY['check']);
+DROP TABLE chk_only;
+
+-- only_kinds=partition on a partitioned-table parent: each child's full DDL
+-- is emitted (CREATE TABLE ... PARTITION OF ...). The "partition"
+-- keyword is a gate at the parent level; it is stripped from the
+-- propagated filter so the child does not exclude its own
+-- CREATE TABLE.
+CREATE TABLE p_only (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE p_only_a PARTITION OF p_only FOR VALUES FROM (0) TO (100);
+CREATE TABLE p_only_b PARTITION OF p_only FOR VALUES FROM (100) TO (200);
+SELECT * FROM pg_get_table_ddl('p_only'::regclass, owner => false, only_kinds => ARRAY['partition']);
+DROP TABLE p_only;
+
+-- only_kinds and except_kinds are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false,
+ only_kinds => ARRAY['foreign_key'],
+ except_kinds => ARRAY['foreign_key']);
+
+-- Pub/sub schema clone: keep the table and its primary key, drop the
+-- rest of the constraints so the subscriber can replicate without
+-- pulling cross-table dependencies along.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique','check','foreign_key','exclusion']);
+
+-- except_kinds=rule hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, except_kinds => ARRAY['rule']);
+
+-- except_kinds=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, except_kinds => ARRAY['statistics']);
+
+-- except_kinds=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, except_kinds => ARRAY['rls']);
+
+-- except_kinds=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, except_kinds => ARRAY['replica_identity']);
+
+-- Partition children are emitted by default; except_kinds=partition
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false, except_kinds => ARRAY['partition']);
+
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY[' INDEX ',' RULE ']);
+
+-- schema_qualified=false strips the schema from the target table itself
+-- and from same-schema sibling references (inheritance/partition
+-- parents, identity sequences in the same schema), while cross-schema
+-- references are kept qualified for correctness.
+--
+-- Bare CREATE TABLE for an ordinary table.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false,
+ schema_qualified => false);
+
+-- INHERITS parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Partition parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Cross-schema FK target keeps qualification. The 'refd' table is in
+-- pgtbl_ddl_test; place an FK target in a different schema and verify it
+-- stays qualified even with schema_qualified=false.
+CREATE SCHEMA pgtbl_ddl_other;
+CREATE TABLE pgtbl_ddl_other.parent (id int PRIMARY KEY);
+CREATE TABLE xschema_fk (
+ id int PRIMARY KEY,
+ pid int REFERENCES pgtbl_ddl_other.parent
+);
+SELECT * FROM pg_get_table_ddl('xschema_fk'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Cross-schema FK whose target schema name ends with the base schema name
+-- (e.g. "xpgtbl_ddl_test" ends with "pgtbl_ddl_test"). A post-hoc text
+-- strip would mangle "xpgtbl_ddl_test.reftbl" into "xreftbl"; deciding
+-- qualification at generation time avoids the problem entirely.
+CREATE SCHEMA xpgtbl_ddl_test;
+CREATE TABLE xpgtbl_ddl_test.reftbl (id int PRIMARY KEY);
+CREATE TABLE suffix_schema_fk (
+ id int PRIMARY KEY,
+ ref int REFERENCES xpgtbl_ddl_test.reftbl
+);
+SELECT * FROM pg_get_table_ddl('suffix_schema_fk'::regclass, owner => false,
+ schema_qualified => false);
+DROP TABLE suffix_schema_fk;
+DROP SCHEMA xpgtbl_ddl_test CASCADE;
+
+-- Identity column with a custom sequence name in the same schema is
+-- emitted unqualified.
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Round-trip: with schema_qualified=false, after SET search_path the
+-- DDL can be replayed into a different target schema.
+CREATE SCHEMA pgtbl_ddl_replay;
+DO $$
+DECLARE
+ stmt text;
+BEGIN
+ SET LOCAL search_path = pgtbl_ddl_replay;
+ FOR stmt IN
+ SELECT line FROM pg_get_table_ddl('pgtbl_ddl_test.basic'::regclass,
+ owner => false,
+ schema_qualified => false) AS line
+ LOOP
+ EXECUTE stmt;
+ END LOOP;
+END $$;
+SELECT relnamespace::regnamespace::text, relname
+FROM pg_class WHERE oid = 'pgtbl_ddl_replay.basic'::regclass;
+DROP SCHEMA pgtbl_ddl_replay CASCADE;
+DROP TABLE xschema_fk;
+DROP SCHEMA pgtbl_ddl_other CASCADE;
+
+-- Locally-declared CHECK on a partition child has no inline column
+-- list to live in (PARTITION OF form), so it must come out as a
+-- separate ALTER TABLE ... ADD CONSTRAINT statement.
+CREATE TABLE parted_chk (id int, val int) PARTITION BY RANGE (id);
+CREATE TABLE parted_chk_child PARTITION OF parted_chk
+ (CONSTRAINT chk_inline CHECK (val > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('parted_chk_child'::regclass, owner => false);
+DROP TABLE parted_chk;
+
+-- Inheritance children of a parent with a generated column should not
+-- emit an ALTER COLUMN ... SET DEFAULT for the inherited generated
+-- column: the GENERATED expression carries down through inheritance,
+-- and SET DEFAULT would fail at replay against a generated column.
+CREATE TABLE par_gen (
+ id int,
+ g int GENERATED ALWAYS AS (id * 2) STORED
+);
+CREATE TABLE ch_gen () INHERITS (par_gen);
+SELECT * FROM pg_get_table_ddl('ch_gen'::regclass, owner => false);
+DROP TABLE par_gen CASCADE;
+
+-- A user-named NOT NULL on a local column is emitted inline as
+-- "CONSTRAINT name NOT NULL" so the original name is preserved. An
+-- auto-named NOT NULL on a local column is emitted as a plain inline
+-- "NOT NULL" - PG re-creates the auto-named constraint when CREATE
+-- TABLE runs, so the constraint loop skips both forms to avoid the
+-- redundant (and on table-rename or sequence-collision, broken)
+-- ALTER TABLE ... ADD CONSTRAINT ... NOT NULL statement.
+CREATE TABLE nn_named (
+ a int CONSTRAINT my_nn NOT NULL,
+ b int NOT NULL
+);
+SELECT * FROM pg_get_table_ddl('nn_named'::regclass, owner => false);
+DROP TABLE nn_named;
+
+-- Renaming the table after an IDENTITY NOT NULL column was declared
+-- leaves the auto-named constraint frozen at the original name, which
+-- no longer matches the post-rename "<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. String
+-- literal text is output verbatim by the deparser and is unaffected by
+-- search_path narrowing.
+CREATE SCHEMA pgtbl_ddl_str;
+CREATE TABLE pgtbl_ddl_str.p (id int, note text) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_str.pc PARTITION OF pgtbl_ddl_str.p
+ (CONSTRAINT chk CHECK (note <> 'pgtbl_ddl_str.secret'))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_str.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_str CASCADE;
+
+-- schema_qualified=false must NOT corrupt double-quoted identifiers
+-- whose contents happen to contain the schema name followed by a dot
+-- (for example a column literally named "<schema>.weird" inside that
+-- schema). The mechanism is purely search_path narrowing: the deparser
+-- outputs attribute names verbatim from pg_attribute, so the quoted
+-- identifier content is never examined for schema prefixes. This holds
+-- even when the schema name is a single letter.
+CREATE SCHEMA pgtbl_ddl_qid;
+CREATE TABLE pgtbl_ddl_qid.p (id int, "pgtbl_ddl_qid.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_qid.pc PARTITION OF pgtbl_ddl_qid.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_qid.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_qid.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_qid CASCADE;
+-- Same with a short (single-letter) schema name to confirm there is no
+-- minimum-length assumption in the stripping path.
+CREATE SCHEMA pgtbl_ddl_q1;
+CREATE TABLE pgtbl_ddl_q1.p (id int, "pgtbl_ddl_q1.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_q1.pc PARTITION OF pgtbl_ddl_q1.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_q1.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_q1.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_q1 CASCADE;
+
+-- schema_qualified=false when the base schema name itself requires
+-- quoting (its prefix starts with "). Both forms of the prefix -
+-- bare-lowercase and quoted - must be stripped from outer
+-- ALTER TABLE / ON / CREATE INDEX prefixes, while inner quoted
+-- identifiers that happen to begin the same way must be preserved.
+CREATE SCHEMA "PgTbl-Ddl-Q";
+CREATE TABLE "PgTbl-Ddl-Q"."T" (id int, val int,
+ CONSTRAINT chk CHECK (val > 0));
+CREATE INDEX "T_val_idx" ON "PgTbl-Ddl-Q"."T" (val);
+SELECT * FROM pg_get_table_ddl('"PgTbl-Ddl-Q"."T"'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA "PgTbl-Ddl-Q" CASCADE;
+
+-- Error: not an ordinary or partitioned table.
+CREATE VIEW v AS SELECT 1 AS x;
+SELECT * FROM pg_get_table_ddl('v'::regclass);
+
+CREATE SEQUENCE s;
+SELECT * FROM pg_get_table_ddl('s'::regclass);
+
+-- NULL argument returns no rows.
+SELECT * FROM pg_get_table_ddl(NULL);
+
+-- REPLICA IDENTITY USING INDEX validation: if the referenced index
+-- would not be emitted (because its source kind is suppressed) but
+-- replica_identity itself would be, the function raises an error so
+-- the emitted DDL never dangles. Exercise all three constraint-backed
+-- forms (PK, UNIQUE, EXCLUSION) and a bare CREATE UNIQUE INDEX, in
+-- both the except-list and only-list shapes.
+--
+-- PK-backed: except_kinds=primary_key without except_kinds=replica_identity.
+CREATE TABLE ri_pk_excl (a int PRIMARY KEY);
+ALTER TABLE ri_pk_excl REPLICA IDENTITY USING INDEX ri_pk_excl_pkey;
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key','replica_identity']);
+DROP TABLE ri_pk_excl;
+
+-- UNIQUE-constraint-backed replica identity index.
+CREATE TABLE ri_uniq_excl (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_uniq_excl REPLICA IDENTITY USING INDEX ri_uniq_excl_a_key;
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique','replica_identity']);
+DROP TABLE ri_uniq_excl;
+
+-- Bare CREATE UNIQUE INDEX (no backing constraint) used as REPLICA
+-- IDENTITY: the index is emitted by the "index" kind, so excluding
+-- "index" without also excluding "replica_identity" must error.
+CREATE TABLE ri_idx_excl (a int NOT NULL, b int);
+CREATE UNIQUE INDEX ri_idx_excl_a_uniq ON ri_idx_excl (a);
+ALTER TABLE ri_idx_excl REPLICA IDENTITY USING INDEX ri_idx_excl_a_uniq;
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index']);
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index','replica_identity']);
+DROP TABLE ri_idx_excl;
+
+-- Include-side analog: only_kinds=replica_identity must include the kind
+-- that emits the index, or the function errors.
+CREATE TABLE ri_only (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+-- Including both kinds succeeds.
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['unique','replica_identity']);
+DROP TABLE ri_only;
+
+-- Per-kind only_kinds / except_kinds coverage. For each kind in the vocabulary,
+-- verify that only_kinds=K emits just that kind's output and that except_kinds=K
+-- omits just that kind's output. Kinds with no fixture above (trigger,
+-- policy) are still recognized but currently emit nothing; only_kinds=K
+-- returns zero rows for them. Fixtures used:
+-- idxd: PK + two secondary CREATE INDEX
+-- cons: PK + UNIQUE + CHECK + FK (constraint-backed)
+-- rt: CREATE RULE
+-- stx: CREATE STATISTICS
+-- rls: ENABLE/FORCE ROW LEVEL SECURITY
+-- ri_full: REPLICA IDENTITY FULL (no source-kind dependency)
+CREATE TABLE excl_fix (rng int4range,
+ EXCLUDE USING gist (rng WITH &&));
+
+-- only_kinds=K: emit only kind K's statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index']);
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['primary_key']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, only_kinds => ARRAY['rule']);
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, only_kinds => ARRAY['rls']);
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['policy']);
+
+-- except_kinds=K: emit everything except kind K's statements. PK/UNIQUE/
+-- INDEX are exercised on tables without REPLICA IDENTITY USING INDEX
+-- so the validation does not trip.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['check']);
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, except_kinds => ARRAY['exclusion']);
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['policy']);
+-- except_kinds=table suppresses the CREATE TABLE + OWNER + child-default
+-- SET DEFAULT + attoptions passes, leaving only the sub-object passes.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['table']);
+
+-- Multi-kind compositions.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table','index']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['primary_key','foreign_key']);
+-- Duplicate entries in the list are silently de-duplicated by the
+-- Bitmapset.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index','index']);
+
+DROP TABLE excl_fix;
+
+-- only_kinds=K on a table that does not have any K of that kind is a valid
+-- filter and returns zero rows. Tested previously with foreign_key
+-- on 'refd'; here also rule, statistics, rls, replica_identity, and
+-- exclusion on 'basic' (which has only PK + columns).
+SELECT count(*) AS rule_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rule']);
+SELECT count(*) AS stats_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+SELECT count(*) AS rls_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rls']);
+SELECT count(*) AS ri_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+SELECT count(*) AS excl_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+
+-- Input-validation edge cases for the kind array. All
+-- of these raise an error; the function never reaches relation open.
+-- Empty array:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[]::text[]);
+-- NULL element:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[NULL::text]);
+-- Unrecognized kind:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY['no_such_kind']);
+-- Same kind specified through both parameters is still mutex-rejected.
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ only_kinds => ARRAY['index'],
+ except_kinds => ARRAY['rule']);
+
+-- Round-trip verification. For every test table, capture the DDL the
+-- function emits, drop the schema, replay each table's DDL in
+-- dependency order, and confirm that pg_get_table_ddl on the recreated
+-- relation matches the original line-for-line. The final SELECT must
+-- return zero rows.
+CREATE TEMP TABLE pgtbl_ddl_rt_orig (name text, ord int, line text);
+INSERT INTO pgtbl_ddl_rt_orig
+SELECT t.name, o.ord, o.line
+FROM (VALUES
+ ('basic'), ('id_cols'), ('id_custom'), ('gen_cols'), ('gen_virtual'), ('storage_cols'),
+ ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'),
+ ('fk_acts_tgt'), ('fk_acts'), ('notenf_tgt'), ('notenf'), ('temporal_pk'),
+ ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('parted_list'),
+ ('rt_log'), ('rt'),
+ ('stx'), ('rls'), ('ri_full'), ('ri_idx'), ('uno')
+ ) AS t(name),
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord);
+
+DO $$
+DECLARE
+ tables CONSTANT text[] := ARRAY[
+ 'basic', 'id_cols', 'id_custom', 'gen_cols', 'gen_virtual', 'storage_cols',
+ 'refd', 'cons', 'nulls_nd', 'idx_inc',
+ 'fk_acts_tgt', 'fk_acts', 'notenf_tgt', 'notenf', 'temporal_pk',
+ 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ 'parted_list',
+ 'rt_log', 'rt',
+ 'stx', 'rls', 'ri_full', 'ri_idx', 'uno'
+ ];
+ t text;
+ stmt text;
+BEGIN
+ DROP SCHEMA pgtbl_ddl_test CASCADE;
+ CREATE SCHEMA pgtbl_ddl_test;
+ FOREACH t IN ARRAY tables LOOP
+ FOR stmt IN
+ SELECT line FROM pgtbl_ddl_rt_orig WHERE name = t ORDER BY ord
+ LOOP
+ BEGIN
+ EXECUTE stmt;
+ EXCEPTION WHEN duplicate_table THEN
+ NULL; -- partition child already created by its parent's DDL
+ END;
+ END LOOP;
+ END LOOP;
+END $$;
+
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3a2720fb5f9..49a7811050c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1645,6 +1645,7 @@ ListenerEntry
LoInfo
LoadStmt
LocalBufferLookupEnt
+LocalNotNullEntry
LocalPgBackendStatus
LocalTransactionId
Location
@@ -3149,6 +3150,7 @@ TableFuncScan
TableFuncScanState
TableFuncType
TableInfo
+TableDdlContext
TableLikeClause
TableSampleClause
TableScanDesc
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-01 15:10 Marcos Pegoraro <[email protected]>
parent: Akshay Joshi <[email protected]>
2 siblings, 1 reply; 54+ messages in thread
From: Marcos Pegoraro @ 2026-07-01 15:10 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
Em qua., 1 de jul. de 2026 às 11:12, Akshay Joshi <
[email protected]> escreveu:
> pretty, owner, tablespace, and schema_qualified are plain boolean
> parameters with DEFAULT values
> *Filtering parameters:* *only_kinds* / *except_kinds* as text[]
>
>> If all parameters are optional, and all parameters are boolean, perhaps
you could also make pretty, owner, tablespace, and schema_qualified as
optional parts of only_kinds and except_kinds.
Therefore, we could call these two ways and the result would be the same.
pg_get_table_ddl('idxd'::regclass, owner => false, tablespace =>
false, except_kinds
=> '{primary_key}');
pg_get_table_ddl('idxd'::regclass, except_kinds => '{primary_key,tablespace,
owner}');
Obviously this way you have to know if owner param is false or it exists on
except_kinds.
What do you think ?
regards
Marcos
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-01 22:19 Zsolt Parragi <[email protected]>
parent: Akshay Joshi <[email protected]>
2 siblings, 1 reply; 54+ messages in thread
From: Zsolt Parragi @ 2026-07-01 22:19 UTC (permalink / raw)
To: [email protected]
I did some more testing, I noticed one more issue with self
referencing foreign keys:
CREATE TABLE t (id int PRIMARY KEY, parent_id int REFERENCES t(id));
SELECT pg_get_table_ddl('t'::regclass);
-- CREATE TABLE public.t (id integer NOT NULL, parent_id integer);
-- ALTER TABLE public.t OWNER TO postgres;
-- ALTER TABLE public.t ADD CONSTRAINT t_parent_id_fkey FOREIGN KEY
(parent_id) REFERENCES public.t(id);
-- ALTER TABLE public.t ADD CONSTRAINT t_pkey PRIMARY KEY (id);
It tries to add the foreign key before the primary, and fails with
`ERROR: there is no unique constraint matching given keys for
referenced table "t"`
There's also another issue in schema_qualified false, with partitions
in different schemas:
CREATE SCHEMA s;
CREATE SCHEMA other;
CREATE TABLE s.pt (id int, val int) PARTITION BY RANGE (id);
CREATE TABLE other.pt_c PARTITION OF s.pt FOR VALUES FROM (0) TO (100);
SELECT pg_get_table_ddl('s.pt'::regclass, schema_qualified => false);
-- CREATE TABLE pt (id integer, val integer) PARTITION BY RANGE (id);
-- ALTER TABLE pt OWNER TO postgres;
-- CREATE TABLE pt_c PARTITION OF s.pt FOR VALUES FROM (0) TO (100);
-- ALTER TABLE pt_c OWNER TO postgres;
The second create table statement references pt as s.pt, which seems incorrect.
It is also missing its own schema qualification, which I'm unsure if
it is wrong or not. If I interpret the documentation strictly, it
isn't the target table, so it should appear with its schema
qualification?
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-02 02:17 Kyotaro Horiguchi <[email protected]>
parent: Akshay Joshi <[email protected]>
2 siblings, 1 reply; 54+ messages in thread
From: Kyotaro Horiguchi @ 2026-07-02 02:17 UTC (permalink / raw)
To: [email protected]; +Cc: [email protected]; [email protected]
Hello,
Looking at this, one thing that concerns me is the large amount of
overlap with dumpTableSchema() in pg_dump.
I wonder if it would make sense to separate the SQL generation logic
into frontend/backend-shared code so that it could also be used by
pg_dump. The catalog lookup would naturally remain separate, but
sharing the DDL generation itself would significantly reduce the
duplication.
By the way, a couple of comments use a Unicode RIGHTWARDS ARROW
(U+2192). Please use an ASCII equivalent instead.
Regards,
--
Kyotaro Horiguchi
NTT Open Source Software Center
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-02 07:48 Akshay Joshi <[email protected]>
parent: Marcos Pegoraro <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-07-02 07:48 UTC (permalink / raw)
To: Marcos Pegoraro <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
On Wed, Jul 1, 2026 at 8:41 PM Marcos Pegoraro <[email protected]> wrote:
> Em qua., 1 de jul. de 2026 às 11:12, Akshay Joshi <
> [email protected]> escreveu:
>
>> pretty, owner, tablespace, and schema_qualified are plain boolean
>> parameters with DEFAULT values
>> *Filtering parameters:* *only_kinds* / *except_kinds* as text[]
>>
>>> If all parameters are optional, and all parameters are boolean, perhaps
> you could also make pretty, owner, tablespace, and schema_qualified as
> optional parts of only_kinds and except_kinds.
>
> Therefore, we could call these two ways and the result would be the same.
> pg_get_table_ddl('idxd'::regclass, owner => false, tablespace => false, except_kinds
> => '{primary_key}');
> pg_get_table_ddl('idxd'::regclass, except_kinds => '{primary_key,
> tablespace,owner}');
>
> Obviously this way you have to know if owner param is false or it exists
> on except_kinds.
> What do you think ?
>
*owner* is the one case where it could work, but to make it consistent with
how owner behaves in pg_get_tablespace_ddl and pg_get_database_ddl, we
should not add it.
*tablespace* doesn't map to a kind at all. It controls the inline
TABLESPACE clause within the CREATE TABLE statement body it's a sub-clause,
not a separate statement. If we added tablespace as a kind, except_kinds =>
'{table,tablespace}' would be wrong (if you're skipping the table
statement, there's no inline clause to suppress), and except_kinds =>
'{tablespace}' would imply skipping a standalone statement that doesn't
exist.
*pretty* and *schema_qualified* are rendering/formatting options, not
statement filters. They affect how every statement is rendered —
indentation, name qualification — not which statements are emitted. Putting
them in except_kinds conflates two orthogonal axes: filtering (what to
emit) and formatting (how to emit it).
The current design intentionally keeps these separate:
only_kinds/except_kinds for statement-level filtering, booleans for
rendering and inline-clause control. Merging them would make except_kinds
overloaded and harder to document clearly.
>
> regards
> Marcos
>
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-02 08:48 Akshay Joshi <[email protected]>
parent: Kyotaro Horiguchi <[email protected]>
0 siblings, 0 replies; 54+ messages in thread
From: Akshay Joshi @ 2026-07-02 08:48 UTC (permalink / raw)
To: Kyotaro Horiguchi <[email protected]>; +Cc: [email protected]; [email protected]
On Thu, Jul 2, 2026 at 7:47 AM Kyotaro Horiguchi <[email protected]>
wrote:
> Hello,
>
> Looking at this, one thing that concerns me is the large amount of
> overlap with dumpTableSchema() in pg_dump.
>
> I wonder if it would make sense to separate the SQL generation logic
> into frontend/backend-shared code so that it could also be used by
> pg_dump. The catalog lookup would naturally remain separate, but
> sharing the DDL generation itself would significantly reduce the
> duplication.
>
Thanks for the suggestion. The overlap is real, but sharing the SQL
generation logic at the C level runs into a few structural mismatches:
dumpTableSchema drives column rendering off pre-populated TableInfo arrays
that pg_dump bulk-loaded at startup, while the backend function uses live
syscache lookups - a shared builder would need an adapter layer roughly as
large as the code it replaces. The pattern PostgreSQL already uses for this
kind of sharing is to call backend pg_get_*def functions via SQL from
pg_dump — it already does this 20 times for indexes, constraints, triggers,
rules, and statistics. The natural long-term path would be for pg_dump to
call pg_get_table_ddl() the same way and retire dumpTableSchema, but that
is a substantial refactor in its own right and feels out of scope here.
By the way, a couple of comments use a Unicode RIGHTWARDS ARROW
> (U+2192). Please use an ASCII equivalent instead.
>
Will fix it in the next v15 patch.
>
> Regards,
>
> --
> Kyotaro Horiguchi
> NTT Open Source Software Center
>
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-02 10:39 Akshay Joshi <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-07-02 10:39 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: [email protected]
Thanks for the review, I have fixed the mentioned issue.
The v15 patch is ready for review.
On Thu, Jul 2, 2026 at 3:50 AM Zsolt Parragi <[email protected]>
wrote:
> I did some more testing, I noticed one more issue with self
> referencing foreign keys:
>
> CREATE TABLE t (id int PRIMARY KEY, parent_id int REFERENCES t(id));
> SELECT pg_get_table_ddl('t'::regclass);
> -- CREATE TABLE public.t (id integer NOT NULL, parent_id integer);
> -- ALTER TABLE public.t OWNER TO postgres;
> -- ALTER TABLE public.t ADD CONSTRAINT t_parent_id_fkey FOREIGN KEY
> (parent_id) REFERENCES public.t(id);
> -- ALTER TABLE public.t ADD CONSTRAINT t_pkey PRIMARY KEY (id);
>
> It tries to add the foreign key before the primary, and fails with
> `ERROR: there is no unique constraint matching given keys for
> referenced table "t"`
>
> There's also another issue in schema_qualified false, with partitions
> in different schemas:
>
> CREATE SCHEMA s;
> CREATE SCHEMA other;
> CREATE TABLE s.pt (id int, val int) PARTITION BY RANGE (id);
> CREATE TABLE other.pt_c PARTITION OF s.pt FOR VALUES FROM (0) TO (100);
> SELECT pg_get_table_ddl('s.pt'::regclass, schema_qualified => false);
> -- CREATE TABLE pt (id integer, val integer) PARTITION BY RANGE (id);
> -- ALTER TABLE pt OWNER TO postgres;
> -- CREATE TABLE pt_c PARTITION OF s.pt FOR VALUES FROM (0) TO (100);
> -- ALTER TABLE pt_c OWNER TO postgres;
>
> The second create table statement references pt as s.pt, which seems
> incorrect.
> It is also missing its own schema qualification, which I'm unsure if
> it is wrong or not. If I interpret the documentation strictly, it
> isn't the target table, so it should appear with its schema
> qualification?
>
>
>
Attachments:
[application/octet-stream] v15-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (199.1K, ../../CANxoLDduU1sPUG+RBmPooKXBgFgFuctu4MH_mFuNoQeYy-F=wQ@mail.gmail.com/3-v15-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From c300172dbb52298e39d362acf66f8e993fd155da Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v15] Add pg_get_table_ddl() to reconstruct CREATE TABLE
statements
The function reconstructs the CREATE TABLE statement for an ordinary or
partitioned table, followed by the ALTER TABLE / CREATE INDEX /
CREATE RULE / CREATE STATISTICS statements needed to restore its full
definition. Each statement is returned as a separate row.
Supported per-column features: data type with type modifiers, COLLATE,
STORAGE, COMPRESSION (pglz / lz4), GENERATED ALWAYS AS (expr)
STORED/VIRTUAL, GENERATED ALWAYS|BY DEFAULT AS IDENTITY (with sequence
options), DEFAULT, NOT NULL (including named NOT NULL constraints), and
per-column attoptions emitted as ALTER COLUMN SET (...).
Supported table-level features: UNLOGGED, INHERITS, PARTITION BY (RANGE
/ LIST / HASH parents), PARTITION OF parent FOR VALUES (FROM/TO, WITH
modulus/remainder, DEFAULT), USING table access method, WITH
(reloptions), TABLESPACE, and inline CHECK constraints in the CREATE
TABLE body.
Supported sub-objects (re-using existing deparse helpers from
ruleutils.c): indexes (including partial and functional) via
pg_get_indexdef_ddl; constraints (PRIMARY KEY with WITHOUT OVERLAPS for
temporal keys, UNIQUE with NULLS NOT DISTINCT and INCLUDE columns,
FOREIGN KEY with ON DELETE/UPDATE referential actions and MATCH clause,
NOT ENFORCED foreign keys, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_body; rules via pg_get_ruledef_ddl; extended
statistics via pg_get_statisticsobjdef_ddl; REPLICA IDENTITY
NOTHING/FULL/USING INDEX; ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY;
and child-local DEFAULT overrides on inheritance/partition children.
DDL for partition children of a partitioned-table parent is appended
after the parent by default.
The function signature follows the named-parameter convention
established by pg_get_role_ddl(), pg_get_tablespace_ddl(), and
pg_get_database_ddl():
pg_get_table_ddl(relation regclass,
pretty boolean DEFAULT false,
owner boolean DEFAULT true,
tablespace boolean DEFAULT true,
schema_qualified boolean DEFAULT true,
only_kinds text[] DEFAULT NULL,
except_kinds text[] DEFAULT NULL)
pretty controls pretty-printed output. owner controls emission of the
ALTER TABLE ... OWNER TO statement. tablespace controls the TABLESPACE
clause on CREATE TABLE. schema_qualified controls whether object names
are emitted with their schema prefix: when true (the default) the
active search_path is temporarily narrowed to pg_catalog so every
deparse helper produces fully-qualified names; when false it is narrowed
to the target table's own schema so same-schema references come out
unqualified while cross-schema references remain qualified for
correctness. Temporary tables are never schema-qualified regardless of
this setting: the TEMPORARY keyword already places them in pg_temp, and
emitting pg_temp_NN.relname would produce non-replayable DDL.
Object-class filtering uses two mutually-exclusive text-array
parameters, only_kinds and except_kinds. When only_kinds is set, only
the listed kinds are emitted; when except_kinds is set, every kind
except the listed ones is emitted; when neither is set every kind is
emitted. Each array element is matched case-insensitively with
leading/trailing whitespace trimmed; an unrecognized name raises an
error. The kind vocabulary is:
table, index, primary_key, unique, check, foreign_key, exclusion,
rule, statistics, rls, replica_identity, partition
trigger and policy are accepted in the vocabulary but currently produce
no output; they are reserved for when standalone pg_get_trigger_ddl()
and pg_get_policy_ddl() helpers become available.
NOT NULL is not part of the vocabulary: it is always emitted to prevent
producing schemas that silently accept NULLs the source would reject.
When replica_identity is in the active filter and the table's REPLICA
IDENTITY USING INDEX references an index that the filter would suppress,
the function raises an error before emitting any output, so the
generated DDL never references an index it did not produce.
Default omission convention: every optional clause is omitted when its
value matches what the server would reapply on round-trip, including
type-default COLLATE, per-type STORAGE, the auto-generated identity
sequence name and parameter defaults, heap access method, default
REPLICA IDENTITY, disabled RLS toggles, empty reloptions, and the
default tablespace.
Author: Akshay Joshi <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kyotaro Horiguchi <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Rui Zhao <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 146 ++
src/backend/catalog/pg_inherits.c | 93 +
src/backend/commands/tablecmds.c | 30 +-
src/backend/utils/adt/ddlutils.c | 1992 ++++++++++++++++-
src/backend/utils/adt/ruleutils.c | 87 +-
src/include/catalog/pg_inherits.h | 1 +
src/include/catalog/pg_proc.dat | 8 +
src/include/commands/tablecmds.h | 3 +
src/include/utils/ruleutils.h | 4 +
.../regress/expected/pg_get_table_ddl.out | 1316 +++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 768 +++++++
src/tools/pgindent/typedefs.list | 2 +
13 files changed, 4435 insertions(+), 17 deletions(-)
create mode 100644 src/test/regress/expected/pg_get_table_ddl.out
create mode 100644 src/test/regress/sql/pg_get_table_ddl.sql
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 69ef3857cfa..55c6b873240 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3960,6 +3960,152 @@ 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>relation</parameter> <type>regclass</type>
+ <optional>, <parameter>pretty</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> false</optional>
+ <optional>, <parameter>owner</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>tablespace</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>schema_qualified</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>only_kinds</parameter> <type>text[]</type>
+ <literal>DEFAULT</literal> NULL</optional>
+ <optional>, <parameter>except_kinds</parameter> <type>text[]</type>
+ <literal>DEFAULT</literal> NULL</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.
+ Foreign-key constraints are always emitted after all other
+ constraints so that self-referencing foreign keys (where the
+ referenced primary or unique key is on the same table) can be
+ added once that key already exists.
+ When <parameter>pretty</parameter> is true, the output is
+ pretty-printed. When <parameter>owner</parameter> is false, the
+ <command>ALTER TABLE ... OWNER TO</command> statement is omitted.
+ When <parameter>tablespace</parameter> is false, the
+ <literal>TABLESPACE</literal> clause is omitted from the
+ <command>CREATE TABLE</command> statement. The
+ <parameter>only_kinds</parameter> and <parameter>except_kinds</parameter>
+ parameters described below control which object classes are emitted.
+ </para>
+ <para>
+ The <parameter>only_kinds</parameter> and <parameter>except_kinds</parameter>
+ parameters each take a text array of object-class kind names
+ and are mutually exclusive (specifying both raises an
+ error). When <parameter>only_kinds</parameter> is set, only the
+ listed kinds are emitted; when <parameter>except_kinds</parameter> is
+ set, every kind <emphasis>except</emphasis> the listed ones is
+ emitted. When neither is set (the default), every kind is
+ emitted. Leading and trailing whitespace in each array element
+ is ignored and matching is case-insensitive; an unrecognized
+ kind name raises an error.
+ The kind vocabulary is
+ <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).
+ Note that <literal>trigger</literal> and <literal>policy</literal>
+ are accepted in the vocabulary but currently produce no output;
+ they are reserved for future use when standalone
+ <function>pg_get_trigger_ddl</function> and
+ <function>pg_get_policy_ddl</function> helpers become available. 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_kinds => ARRAY['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_kinds => ARRAY['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 <parameter>schema_qualified</parameter> parameter (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. Partition children that reside in a different
+ schema than the target table are always emitted with full schema
+ qualification regardless of this parameter, because the output
+ would otherwise place the child in the wrong schema when
+ replayed.
+ </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 95abaf4890c..06afa6811ec 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);
@@ -2524,7 +2523,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)
@@ -19837,6 +19836,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..7210a6353e6 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -5,7 +5,7 @@
*
* This file contains the pg_get_*_ddl family of functions that generate
* DDL statements to recreate database objects such as roles, tablespaces,
- * and databases, along with common infrastructure for option parsing and
+ * databases, and tables, along with common infrastructure for
* pretty-printing.
*
* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
@@ -21,30 +21,89 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.h"
+#include "catalog/namespace.h"
+#include "catalog/partition.h"
+#include "catalog/pg_am.h"
#include "catalog/pg_auth_members.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_collation.h"
+#include "catalog/pg_constraint.h"
#include "catalog/pg_database.h"
#include "catalog/pg_db_role_setting.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_policy.h"
+#include "catalog/pg_sequence.h"
+#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_tablespace.h"
+#include "catalog/pg_trigger.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
#include "commands/tablespace.h"
-#include "common/relpath.h"
#include "funcapi.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "utils/acl.h"
+#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/datetime.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
-#include "utils/pg_locale.h"
#include "utils/rel.h"
#include "utils/ruleutils.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
#include "utils/varlena.h"
+/*
+ * Object-class kinds that the only / except options on
+ * pg_get_table_ddl can filter on. Members are stored as integers in a
+ * Bitmapset on TableDdlContext. Keep table_ddl_kind_names[] in sync
+ * with the order of additions here.
+ */
+typedef enum TableDdlKind
+{
+ TABLE_DDL_KIND_TABLE,
+ TABLE_DDL_KIND_INDEX,
+ TABLE_DDL_KIND_PRIMARY_KEY,
+ TABLE_DDL_KIND_UNIQUE,
+ TABLE_DDL_KIND_CHECK,
+ TABLE_DDL_KIND_FOREIGN_KEY,
+ TABLE_DDL_KIND_EXCLUSION,
+ TABLE_DDL_KIND_RULE,
+ TABLE_DDL_KIND_STATISTICS,
+ TABLE_DDL_KIND_TRIGGER,
+ TABLE_DDL_KIND_POLICY,
+ TABLE_DDL_KIND_RLS,
+ TABLE_DDL_KIND_REPLICA_IDENTITY,
+ TABLE_DDL_KIND_PARTITION,
+} TableDdlKind;
+
+static const struct
+{
+ const char *name;
+ TableDdlKind kind;
+} table_ddl_kind_names[] =
+{
+ {"table", TABLE_DDL_KIND_TABLE},
+ {"index", TABLE_DDL_KIND_INDEX},
+ {"primary_key", TABLE_DDL_KIND_PRIMARY_KEY},
+ {"unique", TABLE_DDL_KIND_UNIQUE},
+ {"check", TABLE_DDL_KIND_CHECK},
+ {"foreign_key", TABLE_DDL_KIND_FOREIGN_KEY},
+ {"exclusion", TABLE_DDL_KIND_EXCLUSION},
+ {"rule", TABLE_DDL_KIND_RULE},
+ {"statistics", TABLE_DDL_KIND_STATISTICS},
+ {"trigger", TABLE_DDL_KIND_TRIGGER},
+ {"policy", TABLE_DDL_KIND_POLICY},
+ {"rls", TABLE_DDL_KIND_RLS},
+ {"replica_identity", TABLE_DDL_KIND_REPLICA_IDENTITY},
+ {"partition", TABLE_DDL_KIND_PARTITION},
+};
+
+static Bitmapset *parse_kind_array(const char *paramname, ArrayType *arr);
static void append_ddl_option(StringInfo buf, bool pretty, int indent,
const char *fmt, ...)
pg_attribute_printf(4, 5);
@@ -57,6 +116,183 @@ static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid);
static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
bool no_owner, bool no_tablespace);
+/*
+ * Per-column cache of locally-declared NOT NULL constraints. Built once
+ * by collect_local_not_null() and consulted by the column-emit helpers
+ * and the post-CREATE constraint loop. Entries with conoid == InvalidOid
+ * mean the column has no local NOT NULL constraint row in pg_constraint
+ * (the column may still have attnotnull=true on a pre-PG-18-upgraded
+ * catalog, in which case emit plain inline NOT NULL).
+ */
+typedef struct LocalNotNullEntry
+{
+ Oid conoid;
+ char *name;
+ bool is_auto; /* matches "<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_kind_array
+ * Parse a text[] of object-class kind names into a Bitmapset of
+ * TableDdlKind values.
+ *
+ * Each element is matched case-insensitively; surrounding whitespace is
+ * stripped. NULL elements are rejected. Unknown kind names raise an error
+ * citing the supplied parameter name. An empty array is also rejected.
+ * Duplicate entries are silently de-duplicated by the Bitmapset.
+ */
+static Bitmapset *
+parse_kind_array(const char *paramname, ArrayType *arr)
+{
+ Datum *elems;
+ bool *nulls;
+ int nelems;
+ Bitmapset *result = NULL;
+
+ deconstruct_array_builtin(arr, TEXTOID, &elems, &nulls, &nelems);
+
+ if (nelems == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" must specify at least one kind",
+ paramname)));
+
+ for (int i = 0; i < nelems; i++)
+ {
+ char *raw;
+ char *token;
+ char *end;
+ bool found = false;
+
+ if (nulls[i])
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" must not contain NULL elements",
+ paramname)));
+
+ raw = text_to_cstring(DatumGetTextPP(elems[i]));
+
+ /* Trim leading whitespace. */
+ token = raw;
+ while (*token == ' ' || *token == '\t')
+ token++;
+
+ /* Trim trailing whitespace. */
+ end = token + strlen(token);
+ while (end > token && (end[-1] == ' ' || end[-1] == '\t'))
+ end--;
+ *end = '\0';
+
+ for (size_t j = 0; j < lengthof(table_ddl_kind_names); j++)
+ {
+ if (pg_strcasecmp(token, table_ddl_kind_names[j].name) == 0)
+ {
+ result = bms_add_member(result,
+ (int) table_ddl_kind_names[j].kind);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("unrecognized kind \"%s\" in parameter \"%s\"",
+ token, paramname)));
+
+ pfree(raw);
+ }
+
+ return result;
+}
+
+/*
+ * is_kind_included
+ * Determine whether DDL for the given object-class kind should be
+ * emitted under the current options.
+ */
+static bool
+is_kind_included(const TableDdlContext *ctx, TableDdlKind kind)
+{
+ if (ctx->only_kinds != NULL)
+ return bms_is_member((int) kind, ctx->only_kinds);
+ if (ctx->except_kinds != NULL)
+ return !bms_is_member((int) kind, ctx->except_kinds);
+ return true;
+}
/*
* Helper to append a formatted string with optional pretty-printing.
@@ -975,3 +1211,1753 @@ 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;
+ List *fk_stmts = NIL;
+
+ 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);
+
+ /*
+ * Defer FK statements so they are emitted after all other
+ * constraints. A self-referencing FK (REFERENCES same_table)
+ * requires the PK/UNIQUE it targets to exist first, and because
+ * the catalog scan returns constraints in name order, an FK whose
+ * name sorts before the PK name would otherwise be emitted first
+ * and fail with "there is no unique constraint matching given
+ * keys".
+ */
+ if (con->contype == CONSTRAINT_FOREIGN)
+ fk_stmts = lappend(fk_stmts, pstrdup(ctx->buf.data));
+ else
+ append_stmt(ctx);
+
+ pfree(conbody);
+ }
+ }
+ systable_endscan(conScan);
+ table_close(conRel, AccessShareLock);
+
+ /* Append deferred FK statements after all other constraints. */
+ ctx->statements = list_concat(ctx->statements, fk_stmts);
+}
+
+/*
+ * 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;
+
+ /*
+ * When schema_qualified is false, the contract is that the output
+ * is replayable with the parent's schema in search_path. That
+ * contract cannot hold for a child that lives in a different schema:
+ * its own name would be ambiguous (landing in whatever schema is
+ * first in search_path) and references back to the parent would have
+ * to be schema-qualified anyway. Force full qualification so the
+ * child's DDL is unambiguous regardless of the caller's search_path.
+ */
+ if (!childctx.schema_qualified &&
+ get_rel_namespace(childoid) != ctx->base_namespace)
+ childctx.schema_qualified = true;
+
+ if (childctx.only_kinds != NULL &&
+ bms_is_member((int) TABLE_DDL_KIND_PARTITION,
+ childctx.only_kinds))
+ {
+ Bitmapset *child_only = bms_copy(childctx.only_kinds);
+
+ child_only = bms_del_member(child_only,
+ (int) TABLE_DDL_KIND_PARTITION);
+ if (bms_is_empty(child_only))
+ {
+ bms_free(child_only);
+ childctx.only_kinds = NULL;
+ }
+ else
+ childctx.only_kinds = child_only;
+ }
+
+ childstmts = pg_get_table_ddl_internal(&childctx);
+ ctx->statements = list_concat(ctx->statements, childstmts);
+ }
+ list_free(children);
+}
+
+/*
+ * pg_get_table_ddl_internal
+ * Generate DDL statements to recreate a regular or partitioned table.
+ *
+ * The caller initializes *ctx with the user-supplied option fields
+ * (relid, pretty, no_owner, no_tablespace, schema_qualified,
+ * only_kinds, except_kinds). This function opens the relation,
+ * validates access, populates the derived fields (rel, qualname,
+ * nn_entries, ...), runs the emission passes, and returns the
+ * accumulated statement list.
+ *
+ * Each emission helper consults is_kind_included() to decide whether
+ * it should run. The table-proper passes (CREATE TABLE / OWNER /
+ * ALTER COLUMN ... SET DEFAULT / ALTER COLUMN ... SET (...)) are
+ * grouped under KIND_TABLE so the FK-only / sub-object-only workflow
+ * is expressible as a single only or except list.
+ *
+ * Trigger and policy emission are scaffolded but currently disabled
+ * (#if 0) - they will become a single helper call once the standalone
+ * pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+ */
+static List *
+pg_get_table_ddl_internal(TableDdlContext *ctx)
+{
+ Relation rel;
+ char relkind;
+ AclResult aclresult;
+
+ rel = table_open(ctx->relid, AccessShareLock);
+
+ relkind = rel->rd_rel->relkind;
+
+ /*
+ * The initial cut only supports ordinary and partitioned tables. Views,
+ * matviews, foreign tables, sequences, indexes, composite types, and
+ * TOAST tables are out of scope for now.
+ */
+ if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ char *relname = pstrdup(RelationGetRelationName(rel));
+
+ table_close(rel, AccessShareLock);
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not an ordinary or partitioned table",
+ relname)));
+ }
+
+ /* Caller needs SELECT on the table to read its definition. */
+ aclresult = pg_class_aclcheck(ctx->relid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLE,
+ RelationGetRelationName(rel));
+
+ /*
+ * Validation: if the table has REPLICA IDENTITY USING INDEX and the
+ * referenced index would not be emitted under the active filter, the
+ * emitted REPLICA IDENTITY clause would reference an index the same DDL
+ * never produced. Determine which kind would emit the index (one of
+ * primary_key / unique / exclusion for constraint-backed indexes,
+ * otherwise the generic "index" kind) and require it to be in scope
+ * whenever "replica_identity" is. The check uses is_kind_included so
+ * it covers both forms naturally: an "except" list that omits the
+ * source kind, and an "only" list that omits it.
+ */
+ if (rel->rd_rel->relreplident == REPLICA_IDENTITY_INDEX &&
+ is_kind_included(ctx, TABLE_DDL_KIND_REPLICA_IDENTITY))
+ {
+ Oid replidx = RelationGetReplicaIndex(rel);
+
+ if (OidIsValid(replidx))
+ {
+ TableDdlKind idx_kind = TABLE_DDL_KIND_INDEX;
+ Oid conoid = get_index_constraint(replidx);
+
+ if (OidIsValid(conoid))
+ {
+ HeapTuple conTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conoid));
+
+ if (HeapTupleIsValid(conTup))
+ {
+ Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
+
+ switch (con->contype)
+ {
+ case CONSTRAINT_PRIMARY:
+ idx_kind = TABLE_DDL_KIND_PRIMARY_KEY;
+ break;
+ case CONSTRAINT_UNIQUE:
+ idx_kind = TABLE_DDL_KIND_UNIQUE;
+ break;
+ case CONSTRAINT_EXCLUSION:
+ idx_kind = TABLE_DDL_KIND_EXCLUSION;
+ break;
+ default:
+ break;
+ }
+ ReleaseSysCache(conTup);
+ }
+ }
+
+ if (!is_kind_included(ctx, idx_kind))
+ {
+ char *relname = pstrdup(RelationGetRelationName(rel));
+ const char *idx_name = table_ddl_kind_names[(int) idx_kind].name;
+
+ table_close(rel, AccessShareLock);
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("REPLICA IDENTITY for table \"%s\" requires kind \"%s\" to be emitted",
+ relname, idx_name),
+ errdetail("The table's REPLICA IDENTITY USING INDEX references an index produced by the \"%s\" kind, which is not in the active filter.",
+ idx_name),
+ errhint("Either add \"%s\" to the filter or remove \"replica_identity\" from it.",
+ idx_name)));
+ }
+ }
+ }
+
+ /*
+ * Populate derived fields now that the relation is open and validated.
+ * The remaining derived fields (nn_entries, skip_notnull_oids, buf,
+ * statements) start zeroed via the caller's `TableDdlContext ctx = {0}`
+ * initializer; the NOT NULL cache is populated by collect_local_not_null,
+ * the buffer is initStringInfo'd, and each emit pass appends to
+ * statements via lappend.
+ */
+ ctx->rel = rel;
+ ctx->base_namespace = RelationGetNamespace(rel);
+ ctx->save_nestlevel = -1;
+ ctx->qualname = lookup_relname_for_emit(ctx->relid, ctx->schema_qualified,
+ ctx->base_namespace);
+
+ /*
+ * Temporarily override search_path so that the ruleutils helpers
+ * (pg_get_indexdef_ddl, pg_get_constraintdef_body,
+ * pg_get_statisticsobjdef_ddl, pg_get_ruledef_ddl, etc.) produce names
+ * that match the schema_qualified flag. Those helpers decide whether to
+ * qualify a name by calling RelationIsVisible(), which checks whether the
+ * object's schema appears in the active search_path.
+ *
+ * schema_qualified = false: narrow to the base schema so that same-schema
+ * references in DEFAULT expressions, FK targets, indexes, rules, and
+ * statistics come out unqualified automatically. Cross-schema references
+ * stay qualified, which is the correctness requirement.
+ *
+ * schema_qualified = true: narrow to pg_catalog only so that objects in
+ * the base schema (or any user schema the caller placed on search_path)
+ * are not reachable without qualification, forcing fully-qualified output
+ * from every helper regardless of the caller's session search_path.
+ *
+ * AtEOXact_GUC cleans up at xact end if anything throws between here and
+ * the explicit restore below; on the normal path we restore right before
+ * returning.
+ */
+ if (!ctx->schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx->base_namespace);
+
+ if (nspname != NULL)
+ {
+ const char *qnsp = quote_identifier(nspname);
+
+ ctx->save_nestlevel = NewGUCNestLevel();
+ (void) set_config_option("search_path", qnsp,
+ PGC_USERSET, PGC_S_SESSION,
+ GUC_ACTION_SAVE, true, 0, false);
+ if (qnsp != nspname)
+ pfree((char *) qnsp);
+ pfree(nspname);
+ }
+ }
+ else
+ {
+ ctx->save_nestlevel = NewGUCNestLevel();
+ (void) set_config_option("search_path", "pg_catalog",
+ PGC_USERSET, PGC_S_SESSION,
+ GUC_ACTION_SAVE, true, 0, false);
+ }
+
+ /*
+ * Cache locally-declared NOT NULL constraint metadata so the column- emit
+ * helpers can produce "CONSTRAINT name NOT NULL" inline for user-named
+ * constraints, and so the constraint loop can avoid double-emitting them.
+ */
+ ctx->nn_entries = collect_local_not_null(rel, &ctx->skip_notnull_oids);
+
+ initStringInfo(&ctx->buf);
+
+ /*
+ * Emission passes. Order is significant: CREATE TABLE first; OWNER and
+ * the per-column ALTER COLUMN passes before sub-object emission;
+ * sub-objects in dependency-friendly order (indexes before constraints,
+ * since constraint-backed indexes are emitted out-of-line by the
+ * constraint loop); partition children last so the parent already exists
+ * at replay time. Each helper gates itself on is_kind_included for the
+ * relevant TABLE_DDL_KIND_*; the four "table itself" passes are grouped
+ * here under KIND_TABLE so all of CREATE TABLE / OWNER / SET DEFAULT /
+ * SET (...) drop out together when the user asks for only sub-objects
+ * (e.g. only => 'foreign_key' for the second pass of a two-pass FK
+ * clone).
+ */
+ if (is_kind_included(ctx, TABLE_DDL_KIND_TABLE))
+ {
+ emit_create_table_stmt(ctx);
+ emit_owner_stmt(ctx);
+ emit_child_default_overrides(ctx);
+ emit_attoptions(ctx);
+ }
+ emit_indexes(ctx);
+ emit_local_constraints(ctx);
+ emit_rules(ctx);
+ emit_statistics(ctx);
+ emit_replica_identity(ctx);
+ emit_rls_toggles(ctx);
+
+ /*
+ * Triggers and row-level security policies - disabled until the
+ * standalone pg_get_trigger_ddl() and pg_get_policy_ddl() helpers land.
+ * The scan + lock + filter scaffolding below is preserved (inside #if 0)
+ * so wiring up each emission becomes a one-liner in the loop body once
+ * those helpers are available.
+ */
+#if 0
+ if (is_kind_included(ctx, TABLE_DDL_KIND_TRIGGER))
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(ctx->relid));
+ trigScan = systable_beginscan(trigRel, TriggerRelidNameIndexId,
+ true, NULL, 1, &trigKey);
+ while (HeapTupleIsValid(trigTup = systable_getnext(trigScan)))
+ {
+ Form_pg_trigger trg = (Form_pg_trigger) GETSTRUCT(trigTup);
+
+ if (trg->tgisinternal)
+ continue;
+
+ /* TODO: append pg_get_trigger_ddl(trg->oid) output here. */
+ }
+ systable_endscan(trigScan);
+ table_close(trigRel, AccessShareLock);
+ }
+
+ if (is_kind_included(ctx, TABLE_DDL_KIND_POLICY))
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(ctx->relid));
+ polScan = systable_beginscan(polRel, PolicyPolrelidPolnameIndexId,
+ true, NULL, 1, &polKey);
+ while (HeapTupleIsValid(polTup = systable_getnext(polScan)))
+ {
+ /* TODO: append pg_get_policy_ddl(relid, polname) output here. */
+ }
+ systable_endscan(polScan);
+ table_close(polRel, AccessShareLock);
+ }
+#endif
+
+ emit_partition_children(ctx);
+
+ {
+ int natts = RelationGetDescr(rel)->natts;
+
+ for (int i = 1; i <= natts; i++)
+ if (ctx->nn_entries[i].name != NULL)
+ pfree(ctx->nn_entries[i].name);
+ }
+ pfree(ctx->nn_entries);
+
+ table_close(rel, AccessShareLock);
+ pfree(ctx->buf.data);
+ pfree(ctx->qualname);
+ list_free(ctx->skip_notnull_oids);
+
+ /*
+ * Pop the narrowed search_path now that all helpers have run. Errors
+ * thrown earlier are cleaned up by AtEOXact_GUC at xact end.
+ */
+ if (ctx->save_nestlevel >= 0)
+ AtEOXact_GUC(true, ctx->save_nestlevel);
+
+ return ctx->statements;
+}
+
+/*
+ * pg_get_table_ddl
+ * Return DDL to recreate a table as a set of text rows.
+ */
+Datum
+pg_get_table_ddl(PG_FUNCTION_ARGS)
+{
+ FuncCallContext *funcctx;
+ List *statements;
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcontext;
+ TableDdlContext ctx = {0};
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ if (!PG_ARGISNULL(5) && !PG_ARGISNULL(6))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"only_kinds\" and \"except_kinds\" parameters are mutually exclusive")));
+
+ /* Option defaults (match proargdefaults in pg_proc.dat). */
+ ctx.relid = PG_GETARG_OID(0);
+ ctx.pretty = false;
+ ctx.no_owner = false; /* owner DEFAULT true -> no_owner = false */
+ ctx.no_tablespace = false; /* tablespace DEFAULT true -> no_tablespace = false */
+ ctx.schema_qualified = true;
+ ctx.only_kinds = NULL;
+ ctx.except_kinds = NULL;
+
+ /* Override defaults with any explicitly supplied values. */
+ if (!PG_ARGISNULL(1))
+ ctx.pretty = PG_GETARG_BOOL(1);
+ if (!PG_ARGISNULL(2))
+ ctx.no_owner = !PG_GETARG_BOOL(2);
+ if (!PG_ARGISNULL(3))
+ ctx.no_tablespace = !PG_GETARG_BOOL(3);
+ if (!PG_ARGISNULL(4))
+ ctx.schema_qualified = PG_GETARG_BOOL(4);
+ if (!PG_ARGISNULL(5))
+ ctx.only_kinds = parse_kind_array("only_kinds",
+ PG_GETARG_ARRAYTYPE_P(5));
+ if (!PG_ARGISNULL(6))
+ ctx.except_kinds = parse_kind_array("except_kinds",
+ PG_GETARG_ARRAYTYPE_P(6));
+
+ statements = pg_get_table_ddl_internal(&ctx);
+ funcctx->user_fctx = statements;
+ funcctx->max_calls = list_length(statements);
+
+ MemoryContextSwitchTo(oldcontext);
+ }
+
+ funcctx = SRF_PERCALL_SETUP();
+ statements = (List *) funcctx->user_fctx;
+
+ if (funcctx->call_cntr < funcctx->max_calls)
+ {
+ char *stmt;
+
+ stmt = list_nth(statements, funcctx->call_cntr);
+
+ SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+ }
+ else
+ {
+ list_free_deep(statements);
+ SRF_RETURN_DONE(funcctx);
+ }
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 88de5c0481c..7186cf83e1d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -23,6 +23,7 @@
#include "access/htup_details.h"
#include "access/relation.h"
#include "access/table.h"
+#include "catalog/namespace.h"
#include "catalog/pg_aggregate.h"
#include "catalog/pg_am.h"
#include "catalog/pg_authid.h"
@@ -370,7 +371,7 @@ static void make_propgraphdef_elements(StringInfo buf, Oid pgrelid, char pgekind
static void make_propgraphdef_labels(StringInfo buf, Oid elid, const char *elalias, Oid elrelid);
static void make_propgraphdef_properties(StringInfo buf, Oid ellabelid, Oid elrelid);
static char *pg_get_statisticsobj_worker(Oid statextid, bool columns_only,
- bool missing_ok);
+ bool missing_ok, int prettyFlags);
static char *pg_get_partkeydef_worker(Oid relid, int prettyFlags,
bool attrsOnly, bool missing_ok);
static char *pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
@@ -603,6 +604,19 @@ pg_get_ruledef_ext(PG_FUNCTION_ARGS)
PG_RETURN_TEXT_P(string_to_text(res));
}
+/*
+ * pg_get_ruledef_ddl
+ * Like pg_get_ruledef but uses the active search_path to decide
+ * whether to schema-qualify the rule's target relation name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_ruledef_ddl(Oid ruleoid)
+{
+ return pg_get_ruledef_worker(ruleoid, PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA);
+}
+
static char *
pg_get_ruledef_worker(Oid ruleoid, int prettyFlags)
@@ -1241,6 +1255,22 @@ pg_get_indexdef_string(Oid indexrelid)
0, false);
}
+/*
+ * pg_get_indexdef_ddl
+ * Like pg_get_indexdef_string but uses the active search_path to
+ * decide whether to schema-qualify the indexed relation name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_indexdef_ddl(Oid indexrelid)
+{
+ return pg_get_indexdef_worker(indexrelid, 0, NULL,
+ false, false,
+ true, true,
+ PRETTYFLAG_SCHEMA, false);
+}
+
/* Internal version that just reports the key-column definitions */
char *
pg_get_indexdef_columns(Oid indexrelid, bool pretty)
@@ -1969,7 +1999,7 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS)
Oid statextid = PG_GETARG_OID(0);
char *res;
- res = pg_get_statisticsobj_worker(statextid, false, true);
+ res = pg_get_statisticsobj_worker(statextid, false, true, 0);
if (res == NULL)
PG_RETURN_NULL();
@@ -1984,7 +2014,20 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS)
char *
pg_get_statisticsobjdef_string(Oid statextid)
{
- return pg_get_statisticsobj_worker(statextid, false, false);
+ return pg_get_statisticsobj_worker(statextid, false, false, 0);
+}
+
+/*
+ * pg_get_statisticsobjdef_ddl
+ * Like pg_get_statisticsobjdef_string but uses the active search_path
+ * to decide whether to schema-qualify the statistics object name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_statisticsobjdef_ddl(Oid statextid)
+{
+ return pg_get_statisticsobj_worker(statextid, false, false, PRETTYFLAG_SCHEMA);
}
/*
@@ -1997,7 +2040,7 @@ pg_get_statisticsobjdef_columns(PG_FUNCTION_ARGS)
Oid statextid = PG_GETARG_OID(0);
char *res;
- res = pg_get_statisticsobj_worker(statextid, true, true);
+ res = pg_get_statisticsobj_worker(statextid, true, true, 0);
if (res == NULL)
PG_RETURN_NULL();
@@ -2009,7 +2052,8 @@ pg_get_statisticsobjdef_columns(PG_FUNCTION_ARGS)
* Internal workhorse to decompile an extended statistics object.
*/
static char *
-pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
+pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok,
+ int prettyFlags)
{
Form_pg_statistic_ext statextrec;
HeapTuple statexttup;
@@ -2069,10 +2113,18 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
if (!columns_only)
{
- nsp = get_namespace_name_or_temp(statextrec->stxnamespace);
- appendStringInfo(&buf, "CREATE STATISTICS %s",
- quote_qualified_identifier(nsp,
- NameStr(statextrec->stxname)));
+ if ((prettyFlags & PRETTYFLAG_SCHEMA) &&
+ StatisticsObjIsVisible(statextid))
+ appendStringInfo(&buf, "CREATE STATISTICS %s",
+ quote_identifier(NameStr(statextrec->stxname)));
+ else
+ {
+ nsp = get_namespace_name_or_temp(statextrec->stxnamespace);
+ appendStringInfo(&buf, "CREATE STATISTICS %s",
+ quote_qualified_identifier(nsp,
+ NameStr(statextrec->stxname)));
+ pfree(nsp);
+ }
/*
* Decode the stxkind column so that we know which stats types to
@@ -2163,10 +2215,9 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
{
Node *expr = (Node *) lfirst(lc);
char *str;
- int prettyFlags = PRETTYFLAG_PAREN;
str = deparse_expression_pretty(expr, context, false, false,
- prettyFlags, 0);
+ PRETTYFLAG_PAREN, 0);
if (colno > 0)
appendStringInfoString(&buf, ", ");
@@ -2544,6 +2595,20 @@ pg_get_constraintdef_command(Oid constraintId)
return pg_get_constraintdef_worker(constraintId, true, 0, false);
}
+/*
+ * pg_get_constraintdef_body
+ * Returns the constraint definition body without the
+ * "ALTER TABLE name ADD CONSTRAINT cname" prefix.
+ * FK target relation names in the body already use
+ * generate_relation_name (search_path-aware). For use by
+ * pg_get_table_ddl which builds the ALTER TABLE prefix itself.
+ */
+char *
+pg_get_constraintdef_body(Oid constraintId)
+{
+ return pg_get_constraintdef_worker(constraintId, false, 0, false);
+}
+
/*
* As of 9.4, we now use an MVCC snapshot for this.
*/
diff --git a/src/include/catalog/pg_inherits.h b/src/include/catalog/pg_inherits.h
index cc874abaabb..1e463b0106d 100644
--- a/src/include/catalog/pg_inherits.h
+++ b/src/include/catalog/pg_inherits.h
@@ -55,6 +55,7 @@ DECLARE_INDEX(pg_inherits_parent_index, 2187, InheritsParentIndexId, pg_inherits
extern List *find_inheritance_children(Oid parentrelId, LOCKMODE lockmode);
extern List *find_inheritance_children_extended(Oid parentrelId, bool omit_detached,
LOCKMODE lockmode, bool *detached_exist, TransactionId *detached_xmin);
+extern List *find_inheritance_parents(Oid relid, LOCKMODE lockmode);
extern List *find_all_inheritors(Oid parentrelId, LOCKMODE lockmode,
List **numparents);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..d86af56ba78 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8634,6 +8634,14 @@
proargtypes => 'regdatabase bool bool bool',
proargnames => '{database,pretty,owner,tablespace}',
proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' },
+{ oid => '8215', descr => 'get DDL to recreate a table',
+ proname => 'pg_get_table_ddl', prorows => '50',
+ proisstrict => 'f', proretset => 't', provolatile => 's', proparallel => 'r',
+ pronargdefaults => '6', prorettype => 'text',
+ proargtypes => 'regclass bool bool bool bool _text _text',
+ proargnames => '{relation,pretty,owner,tablespace,schema_qualified,only_kinds,except_kinds}',
+ proargdefaults => '{false,true,true,true,NULL,NULL}',
+ prosrc => 'pg_get_table_ddl' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index c3d8518cb62..d33563fdead 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -92,6 +92,7 @@ extern void check_of_type(HeapTuple typetuple);
extern void register_on_commit_action(Oid relid, OnCommitAction action);
extern void remove_on_commit_action(Oid relid);
+extern OnCommitAction get_on_commit_action(Oid relid);
extern void PreCommit_on_commit_actions(void);
extern void AtEOXact_on_commit_actions(bool isCommit);
@@ -108,4 +109,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
List *partConstraint);
+extern const char *storage_name(char c);
+
#endif /* TABLECMDS_H */
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 25c05e2f649..c3b3dc65c93 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -25,6 +25,7 @@ typedef struct PlannedStmt PlannedStmt;
#define RULE_INDEXDEF_KEYS_ONLY 0x02 /* ignore included attributes */
extern char *pg_get_indexdef_string(Oid indexrelid);
+extern char *pg_get_indexdef_ddl(Oid indexrelid);
extern char *pg_get_indexdef_columns(Oid indexrelid, bool pretty);
extern char *pg_get_indexdef_columns_extended(Oid indexrelid,
uint16 flags);
@@ -34,6 +35,7 @@ extern char *pg_get_partkeydef_columns(Oid relid, bool pretty);
extern char *pg_get_partconstrdef_string(Oid partitionId, char *aliasname);
extern char *pg_get_constraintdef_command(Oid constraintId);
+extern char *pg_get_constraintdef_body(Oid constraintId);
extern char *deparse_expression(Node *expr, List *dpcontext,
bool forceprefix, bool showimplicit);
extern List *deparse_context_for(const char *aliasname, Oid relid);
@@ -54,5 +56,7 @@ extern char *get_range_partbound_string(List *bound_datums);
extern void get_reloptions(StringInfo buf, Datum reloptions);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern char *pg_get_statisticsobjdef_ddl(Oid statextid);
+extern char *pg_get_ruledef_ddl(Oid ruleoid);
#endif /* RULEUTILS_H */
diff --git a/src/test/regress/expected/pg_get_table_ddl.out b/src/test/regress/expected/pg_get_table_ddl.out
new file mode 100644
index 00000000000..64163982903
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,1316 @@
+--
+-- pg_get_table_ddl
+--
+-- All tests pass owner=>false so the ALTER TABLE OWNER TO line is not
+-- emitted, keeping output stable across test runners (which may run under
+-- different role names).
+--
+CREATE SCHEMA pgtbl_ddl_test;
+SET search_path = pgtbl_ddl_test;
+-- Basic table with PRIMARY KEY, NOT NULL, DEFAULT, COLLATE.
+CREATE TABLE basic (
+ id int PRIMARY KEY,
+ name text NOT NULL DEFAULT 'anon' COLLATE "C"
+);
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.basic (id integer NOT NULL, name text COLLATE "C" DEFAULT 'anon'::text NOT NULL);
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- Identity columns (with default and custom sequence options).
+CREATE TABLE id_cols (
+ id_always int GENERATED ALWAYS AS IDENTITY,
+ id_default int GENERATED BY DEFAULT AS IDENTITY
+);
+SELECT * FROM pg_get_table_ddl('id_cols'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.id_cols (id_always integer GENERATED ALWAYS AS IDENTITY NOT NULL, id_default integer GENERATED BY DEFAULT AS IDENTITY NOT NULL);
+(1 row)
+
+CREATE TABLE id_custom (
+ v int GENERATED ALWAYS AS IDENTITY (
+ SEQUENCE NAME id_custom_v_seq
+ START WITH 100 INCREMENT BY 5
+ MINVALUE 50 MAXVALUE 1000
+ CACHE 10 CYCLE
+ )
+);
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.id_custom (v integer GENERATED ALWAYS AS IDENTITY (START WITH 100 INCREMENT BY 5 MINVALUE 50 MAXVALUE 1000 CACHE 10 CYCLE) NOT NULL);
+(1 row)
+
+-- Generated stored column.
+CREATE TABLE gen_cols (
+ cents int,
+ dollars numeric GENERATED ALWAYS AS (cents / 100.0) STORED
+);
+SELECT * FROM pg_get_table_ddl('gen_cols'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.gen_cols (cents integer, dollars numeric GENERATED ALWAYS AS (((cents)::numeric / 100.0)) STORED);
+(1 row)
+
+-- Generated virtual column.
+CREATE TABLE gen_virtual (
+ base int,
+ derived int GENERATED ALWAYS AS (base * 2) VIRTUAL
+);
+SELECT * FROM pg_get_table_ddl('gen_virtual'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.gen_virtual (base integer, derived integer GENERATED ALWAYS AS ((base * 2)) VIRTUAL);
+(1 row)
+
+-- STORAGE and COMPRESSION (only emitted when non-default for the type).
+CREATE TABLE storage_cols (
+ a text STORAGE EXTERNAL,
+ b text STORAGE MAIN,
+ c text COMPRESSION pglz
+);
+SELECT * FROM pg_get_table_ddl('storage_cols'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.storage_cols (a text STORAGE EXTERNAL, b text STORAGE MAIN, c text COMPRESSION pglz);
+(1 row)
+
+-- Constraints: CHECK, UNIQUE, FOREIGN KEY (DEFERRABLE).
+CREATE TABLE refd (id int PRIMARY KEY);
+CREATE TABLE cons (
+ a int CHECK (a > 0),
+ b int UNIQUE,
+ c int REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED
+);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(3 rows)
+
+-- UNIQUE NULLS NOT DISTINCT (PG 15): null values are treated as equal for
+-- uniqueness purposes, so the NULLS NOT DISTINCT clause must be emitted.
+CREATE TABLE nulls_nd (
+ a int,
+ b int,
+ UNIQUE NULLS NOT DISTINCT (a, b)
+);
+SELECT * FROM pg_get_table_ddl('nulls_nd'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.nulls_nd (a integer, b integer);
+ ALTER TABLE pgtbl_ddl_test.nulls_nd ADD CONSTRAINT nulls_nd_a_b_key UNIQUE NULLS NOT DISTINCT (a, b);
+(2 rows)
+
+-- INCLUDE columns on a constraint-backed UNIQUE index (PG 11): the
+-- covering columns appear in an INCLUDE (...) clause on the ALTER TABLE
+-- ADD CONSTRAINT statement, not in the key column list.
+CREATE TABLE idx_inc (
+ id int PRIMARY KEY,
+ name text,
+ extra text,
+ UNIQUE (name) INCLUDE (extra)
+);
+SELECT * FROM pg_get_table_ddl('idx_inc'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idx_inc (id integer NOT NULL, name text, extra text);
+ ALTER TABLE pgtbl_ddl_test.idx_inc ADD CONSTRAINT idx_inc_name_extra_key UNIQUE (name) INCLUDE (extra);
+ ALTER TABLE pgtbl_ddl_test.idx_inc ADD CONSTRAINT idx_inc_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- FOREIGN KEY with ON DELETE / ON UPDATE referential actions and MATCH
+-- clause. Each variant must appear verbatim in the reconstructed DDL.
+CREATE TABLE fk_acts_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_acts (
+ a int,
+ b int,
+ c int,
+ d int,
+ CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES fk_acts_tgt(id)
+ ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET NULL,
+ CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET DEFAULT,
+ CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES fk_acts_tgt(id)
+ MATCH FULL
+);
+SELECT * FROM pg_get_table_ddl('fk_acts'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.fk_acts (a integer, b integer, c integer, d integer);
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON UPDATE RESTRICT ON DELETE CASCADE;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) MATCH FULL;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON DELETE SET DEFAULT;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON DELETE SET NULL;
+(5 rows)
+
+-- NOT ENFORCED foreign key (PG 17): the constraint is recorded in the
+-- catalog but not validated by the engine; NOT ENFORCED must appear in
+-- the reconstructed DDL.
+CREATE TABLE notenf_tgt (id int PRIMARY KEY);
+CREATE TABLE notenf (
+ a int,
+ CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES notenf_tgt(id)
+ NOT ENFORCED
+);
+SELECT * FROM pg_get_table_ddl('notenf'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.notenf (a integer);
+ ALTER TABLE pgtbl_ddl_test.notenf ADD CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.notenf_tgt(id) NOT ENFORCED;
+(2 rows)
+
+-- Self-referencing FK: the PK must appear before the FK that references it.
+-- The catalog scan returns constraints in name order; without the fix,
+-- "self_ref_parent_id_fkey" sorts before "self_ref_pkey" and the FK ADD
+-- CONSTRAINT fails with "there is no unique constraint matching given keys".
+CREATE TABLE self_ref (
+ id int PRIMARY KEY,
+ parent_id int REFERENCES self_ref(id)
+);
+SELECT * FROM pg_get_table_ddl('self_ref'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.self_ref (id integer NOT NULL, parent_id integer);
+ ALTER TABLE pgtbl_ddl_test.self_ref ADD CONSTRAINT self_ref_pkey PRIMARY KEY (id);
+ ALTER TABLE pgtbl_ddl_test.self_ref ADD CONSTRAINT self_ref_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES pgtbl_ddl_test.self_ref(id);
+(3 rows)
+
+-- WITHOUT OVERLAPS on a PRIMARY KEY (PG 17): the range-type period
+-- column forms a temporal primary key; WITHOUT OVERLAPS must appear
+-- inside the key column list in the reconstructed ALTER TABLE.
+-- Both columns use range types with native GiST support so no
+-- btree_gist extension is needed.
+CREATE TABLE temporal_pk (
+ grp int4range,
+ during daterange,
+ PRIMARY KEY (grp, during WITHOUT OVERLAPS)
+);
+SELECT * FROM pg_get_table_ddl('temporal_pk'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.temporal_pk (grp int4range NOT NULL, during daterange NOT NULL);
+ ALTER TABLE pgtbl_ddl_test.temporal_pk ADD CONSTRAINT temporal_pk_pkey PRIMARY KEY (grp, during WITHOUT OVERLAPS);
+(2 rows)
+
+-- Indexes: functional and partial. Constraint-backing indexes are
+-- suppressed (they are emitted by the constraint loop).
+CREATE TABLE idxd (id int PRIMARY KEY, name text);
+CREATE INDEX idxd_lower ON idxd (lower(name));
+CREATE INDEX idxd_partial ON idxd (id) WHERE id > 100;
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- Inheritance, including a child DEFAULT override on an inherited column.
+CREATE TABLE par (a int DEFAULT 1, b text);
+CREATE TABLE ch (c int) INHERITS (par);
+ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ch (c integer) INHERITS (pgtbl_ddl_test.par);
+ ALTER TABLE pgtbl_ddl_test.ch ALTER COLUMN a SET DEFAULT 999;
+(2 rows)
+
+-- Per-column attoptions: emitted as ALTER COLUMN SET (...).
+CREATE TABLE attopt (a int, b text);
+ALTER TABLE attopt ALTER COLUMN a SET (n_distinct = 100);
+SELECT * FROM pg_get_table_ddl('attopt'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.attopt (a integer, b text);
+ ALTER TABLE pgtbl_ddl_test.attopt ALTER COLUMN a SET (n_distinct='100');
+(2 rows)
+
+-- Partitioned table parents (RANGE, HASH, and LIST).
+CREATE TABLE parted_range (id int, k int) PARTITION BY RANGE (id);
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range (id integer, k integer) PARTITION BY RANGE (id);
+(1 row)
+
+CREATE TABLE parted_hash (id int) PARTITION BY HASH (id);
+SELECT * FROM pg_get_table_ddl('parted_hash'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash (id integer) PARTITION BY HASH (id);
+(1 row)
+
+CREATE TABLE parted_list (id int, region text) PARTITION BY LIST (region);
+SELECT * FROM pg_get_table_ddl('parted_list'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_list (id integer, region text) PARTITION BY LIST (region);
+(1 row)
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0);
+(1 row)
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(1 row)
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO pgtbl_ddl_test.rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM pgtbl_ddl_test.stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx (a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(3 rows)
+
+-- UNLOGGED + reloptions.
+CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70);
+SELECT * FROM pg_get_table_ddl('uno'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE UNLOGGED TABLE pgtbl_ddl_test.uno (id integer) WITH (fillfactor='70');
+(1 row)
+
+-- Typed table (CREATE TABLE OF type_name). Columns inherited from the
+-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come
+-- out through a single "(col WITH OPTIONS ...)" list.
+CREATE TYPE typed_t AS (a int, b text);
+CREATE TABLE typed_plain OF typed_t;
+SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_plain OF pgtbl_ddl_test.typed_t;
+(1 row)
+
+CREATE TABLE typed_over OF typed_t (
+ a WITH OPTIONS DEFAULT 7 NOT NULL,
+ b WITH OPTIONS NOT NULL,
+ CONSTRAINT b_nonempty CHECK (length(b) > 0)
+);
+SELECT * FROM pg_get_table_ddl('typed_over'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_over OF pgtbl_ddl_test.typed_t (a WITH OPTIONS DEFAULT 7 NOT NULL, b WITH OPTIONS NOT NULL, CONSTRAINT b_nonempty CHECK ((length(b) > 0)));
+(1 row)
+
+-- Temporary tables + ON COMMIT. Temp tables must never be emitted with
+-- the session-private pg_temp_NN schema prefix -- the TEMPORARY keyword
+-- already places the table in pg_temp and the prefix is non-replayable.
+-- ON COMMIT DROP only fires at commit, so the queries that need to see
+-- the catalog entry must run in the same transaction as the CREATE.
+BEGIN;
+CREATE TEMP TABLE temp_default (id int);
+CREATE TEMP TABLE temp_delete (id int) ON COMMIT DELETE ROWS;
+CREATE TEMP TABLE temp_drop (id int) ON COMMIT DROP;
+SELECT line FROM pg_get_table_ddl('temp_default'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------
+ CREATE TEMPORARY TABLE temp_default (id integer);
+(1 row)
+
+SELECT line FROM pg_get_table_ddl('temp_delete'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE temp_delete (id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT line FROM pg_get_table_ddl('temp_drop'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------
+ CREATE TEMPORARY TABLE temp_drop (id integer) ON COMMIT DROP;
+(1 row)
+
+ROLLBACK;
+-- Pretty mode.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, pretty => true);
+ pg_get_table_ddl
+------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.basic ( +
+ id integer NOT NULL, +
+ name text COLLATE "C" DEFAULT 'anon'::text NOT NULL +
+ );
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- only_kinds / except_kinds gating: each emits either only the listed kinds
+-- (only_kinds) or every kind except the listed ones (except_kinds). The two
+-- are mutually exclusive. Kind vocabulary: table, index,
+-- primary_key, unique, check, foreign_key, exclusion, rule,
+-- statistics, trigger, policy, rls, replica_identity, partition.
+-- NOT NULL is not in the vocabulary - always emitted to prevent
+-- producing schemas that silently accept NULLs the source would
+-- have rejected.
+--
+-- except_kinds=index hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['index']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- except_kinds=primary_key,unique,check,foreign_key,exclusion hides every
+-- table-level constraint. Inline CHECK in the CREATE TABLE body is
+-- suppressed too.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['primary_key','unique','check','foreign_key','exclusion']);
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=foreign_key suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- first pass of the two-pass FK-clone workflow: emit tables without
+-- cross-table FKs, load data, then re-run with only_kinds=foreign_key
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+(2 rows)
+
+-- only_kinds=foreign_key is the second pass of the two-pass FK-clone
+-- workflow: emit only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN
+-- KEY rows; the CREATE TABLE and every non-FK sub-object are
+-- suppressed (the table already exists from the first pass).
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(1 row)
+
+-- A table with no foreign keys produces no rows under
+-- only_kinds=foreign_key.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- only_kinds=table emits only the bare CREATE TABLE (with any inline
+-- column-level NOT NULL and CHECK) - no indexes, no PRIMARY KEY ALTER,
+-- no other sub-objects. Useful for capturing a table's shape without
+-- pulling in everything attached to it.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table']);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+(1 row)
+
+-- only_kinds=check on a non-partition table. The constraint loop must emit
+-- column-level and table-level CHECK constraints via ALTER TABLE here,
+-- because the inline path in CREATE TABLE never runs (KIND_TABLE is
+-- not in the filter).
+CREATE TABLE chk_only (
+ id int PRIMARY KEY,
+ qty int CHECK (qty > 0),
+ CONSTRAINT chk_only_id_pos CHECK (id > 0)
+);
+SELECT * FROM pg_get_table_ddl('chk_only'::regclass, owner => false, only_kinds => ARRAY['check']);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.chk_only ADD CONSTRAINT chk_only_id_pos CHECK ((id > 0));
+ ALTER TABLE pgtbl_ddl_test.chk_only ADD CONSTRAINT chk_only_qty_check CHECK ((qty > 0));
+(2 rows)
+
+DROP TABLE chk_only;
+-- only_kinds=partition on a partitioned-table parent: each child's full DDL
+-- is emitted (CREATE TABLE ... PARTITION OF ...). The "partition"
+-- keyword is a gate at the parent level; it is stripped from the
+-- propagated filter so the child does not exclude its own
+-- CREATE TABLE.
+CREATE TABLE p_only (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE p_only_a PARTITION OF p_only FOR VALUES FROM (0) TO (100);
+CREATE TABLE p_only_b PARTITION OF p_only FOR VALUES FROM (100) TO (200);
+SELECT * FROM pg_get_table_ddl('p_only'::regclass, owner => false, only_kinds => ARRAY['partition']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.p_only_a PARTITION OF pgtbl_ddl_test.p_only FOR VALUES FROM (0) TO (100);
+ CREATE TABLE pgtbl_ddl_test.p_only_b PARTITION OF pgtbl_ddl_test.p_only FOR VALUES FROM (100) TO (200);
+(2 rows)
+
+DROP TABLE p_only;
+-- only_kinds and except_kinds are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false,
+ only_kinds => ARRAY['foreign_key'],
+ except_kinds => ARRAY['foreign_key']);
+ERROR: "only_kinds" and "except_kinds" parameters are mutually exclusive
+-- Pub/sub schema clone: keep the table and its primary key, drop the
+-- rest of the constraints so the subscriber can replicate without
+-- pulling cross-table dependencies along.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique','check','foreign_key','exclusion']);
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=rule hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, except_kinds => ARRAY['rule']);
+ pg_get_table_ddl
+----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+(1 row)
+
+-- except_kinds=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, except_kinds => ARRAY['statistics']);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, except_kinds => ARRAY['rls']);
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+(1 row)
+
+-- except_kinds=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, except_kinds => ARRAY['replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+(1 row)
+
+-- Partition children are emitted by default; except_kinds=partition
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false, except_kinds => ARRAY['partition']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range (id integer, k integer) PARTITION BY RANGE (id);
+(1 row)
+
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY[' INDEX ',' RULE ']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- schema_qualified=false strips the schema from the target table itself
+-- and from same-schema sibling references (inheritance/partition
+-- parents, identity sequences in the same schema), while cross-schema
+-- references are kept qualified for correctness.
+--
+-- Bare CREATE TABLE for an ordinary table.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE basic (id integer NOT NULL, name text COLLATE "C" DEFAULT 'anon'::text NOT NULL);
+ ALTER TABLE basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- INHERITS parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------
+ CREATE TABLE ch (c integer) INHERITS (par);
+ ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+(2 rows)
+
+-- Partition parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE parted_range_1 PARTITION OF parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+-- Cross-schema FK target keeps qualification. The 'refd' table is in
+-- pgtbl_ddl_test; place an FK target in a different schema and verify it
+-- stays qualified even with schema_qualified=false.
+CREATE SCHEMA pgtbl_ddl_other;
+CREATE TABLE pgtbl_ddl_other.parent (id int PRIMARY KEY);
+CREATE TABLE xschema_fk (
+ id int PRIMARY KEY,
+ pid int REFERENCES pgtbl_ddl_other.parent
+);
+SELECT * FROM pg_get_table_ddl('xschema_fk'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE xschema_fk (id integer NOT NULL, pid integer);
+ ALTER TABLE xschema_fk ADD CONSTRAINT xschema_fk_pkey PRIMARY KEY (id);
+ ALTER TABLE xschema_fk ADD CONSTRAINT xschema_fk_pid_fkey FOREIGN KEY (pid) REFERENCES pgtbl_ddl_other.parent(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
+-- Cross-schema partition child with schema_qualified=false: the child must
+-- carry its own schema prefix so the output is unambiguous regardless of the
+-- caller's search_path. Before the fix, the child was emitted as the bare
+-- name "pt_c" (landing in whichever schema is first in search_path) while
+-- the parent reference inside the CREATE TABLE came out as the fully-qualified
+-- "pgtbl_ddl_part_s.pt", making the DDL inconsistent and unreplayable.
+CREATE SCHEMA pgtbl_ddl_part_s;
+CREATE SCHEMA pgtbl_ddl_part_other;
+CREATE TABLE pgtbl_ddl_part_s.pt (id int, val int) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_part_other.pt_c
+ PARTITION OF pgtbl_ddl_part_s.pt
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_part_s.pt'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------
+ CREATE TABLE pt (id integer, val integer) PARTITION BY RANGE (id);
+ CREATE TABLE pgtbl_ddl_part_other.pt_c PARTITION OF pgtbl_ddl_part_s.pt FOR VALUES FROM (0) TO (100);
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_part_s CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_part_s.pt
+DROP SCHEMA pgtbl_ddl_part_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);
+ 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. String
+-- literal text is output verbatim by the deparser and is unaffected by
+-- search_path narrowing.
+CREATE SCHEMA pgtbl_ddl_str;
+CREATE TABLE pgtbl_ddl_str.p (id int, note text) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_str.pc PARTITION OF pgtbl_ddl_str.p
+ (CONSTRAINT chk CHECK (note <> 'pgtbl_ddl_str.secret'))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_str.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK ((note <> 'pgtbl_ddl_str.secret'::text));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_str CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_str.p
+-- schema_qualified=false must NOT corrupt double-quoted identifiers
+-- whose contents happen to contain the schema name followed by a dot
+-- (for example a column literally named "<schema>.weird" inside that
+-- schema). The mechanism is purely search_path narrowing: the deparser
+-- outputs attribute names verbatim from pg_attribute, so the quoted
+-- identifier content is never examined for schema prefixes. This holds
+-- even when the schema name is a single letter.
+CREATE SCHEMA pgtbl_ddl_qid;
+CREATE TABLE pgtbl_ddl_qid.p (id int, "pgtbl_ddl_qid.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_qid.pc PARTITION OF pgtbl_ddl_qid.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_qid.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_qid.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK (("pgtbl_ddl_qid.weird" > 0));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_qid CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_qid.p
+-- Same with a short (single-letter) schema name to confirm there is no
+-- minimum-length assumption in the stripping path.
+CREATE SCHEMA pgtbl_ddl_q1;
+CREATE TABLE pgtbl_ddl_q1.p (id int, "pgtbl_ddl_q1.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_q1.pc PARTITION OF pgtbl_ddl_q1.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_q1.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_q1.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK (("pgtbl_ddl_q1.weird" > 0));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_q1 CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_q1.p
+-- schema_qualified=false when the base schema name itself requires
+-- quoting (its prefix starts with "). Both forms of the prefix -
+-- bare-lowercase and quoted - must be stripped from outer
+-- ALTER TABLE / ON / CREATE INDEX prefixes, while inner quoted
+-- identifiers that happen to begin the same way must be preserved.
+CREATE SCHEMA "PgTbl-Ddl-Q";
+CREATE TABLE "PgTbl-Ddl-Q"."T" (id int, val int,
+ CONSTRAINT chk CHECK (val > 0));
+CREATE INDEX "T_val_idx" ON "PgTbl-Ddl-Q"."T" (val);
+SELECT * FROM pg_get_table_ddl('"PgTbl-Ddl-Q"."T"'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE TABLE "T" (id integer, val integer, CONSTRAINT chk CHECK ((val > 0)));
+ CREATE INDEX "T_val_idx" ON "T" USING btree (val);
+(2 rows)
+
+DROP SCHEMA "PgTbl-Ddl-Q" CASCADE;
+NOTICE: drop cascades to table "PgTbl-Ddl-Q"."T"
+-- Error: not an ordinary or partitioned table.
+CREATE VIEW v AS SELECT 1 AS x;
+SELECT * FROM pg_get_table_ddl('v'::regclass);
+ERROR: "v" is not an ordinary or partitioned table
+CREATE SEQUENCE s;
+SELECT * FROM pg_get_table_ddl('s'::regclass);
+ERROR: "s" is not an ordinary or partitioned table
+-- NULL argument returns no rows.
+SELECT * FROM pg_get_table_ddl(NULL);
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- REPLICA IDENTITY USING INDEX validation: if the referenced index
+-- would not be emitted (because its source kind is suppressed) but
+-- replica_identity itself would be, the function raises an error so
+-- the emitted DDL never dangles. Exercise all three constraint-backed
+-- forms (PK, UNIQUE, EXCLUSION) and a bare CREATE UNIQUE INDEX, in
+-- both the except-list and only-list shapes.
+--
+-- PK-backed: except_kinds=primary_key without except_kinds=replica_identity.
+CREATE TABLE ri_pk_excl (a int PRIMARY KEY);
+ALTER TABLE ri_pk_excl REPLICA IDENTITY USING INDEX ri_pk_excl_pkey;
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+ERROR: REPLICA IDENTITY for table "ri_pk_excl" requires kind "primary_key" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "primary_key" kind, which is not in the active filter.
+HINT: Either add "primary_key" to the filter or remove "replica_identity" from it.
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_pk_excl (a integer NOT NULL);
+(1 row)
+
+DROP TABLE ri_pk_excl;
+-- UNIQUE-constraint-backed replica identity index.
+CREATE TABLE ri_uniq_excl (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_uniq_excl REPLICA IDENTITY USING INDEX ri_uniq_excl_a_key;
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique']);
+ERROR: REPLICA IDENTITY for table "ri_uniq_excl" requires kind "unique" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "unique" kind, which is not in the active filter.
+HINT: Either add "unique" to the filter or remove "replica_identity" from it.
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique','replica_identity']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_uniq_excl (a integer NOT NULL, b integer);
+(1 row)
+
+DROP TABLE ri_uniq_excl;
+-- Bare CREATE UNIQUE INDEX (no backing constraint) used as REPLICA
+-- IDENTITY: the index is emitted by the "index" kind, so excluding
+-- "index" without also excluding "replica_identity" must error.
+CREATE TABLE ri_idx_excl (a int NOT NULL, b int);
+CREATE UNIQUE INDEX ri_idx_excl_a_uniq ON ri_idx_excl (a);
+ALTER TABLE ri_idx_excl REPLICA IDENTITY USING INDEX ri_idx_excl_a_uniq;
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index']);
+ERROR: REPLICA IDENTITY for table "ri_idx_excl" requires kind "index" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "index" kind, which is not in the active filter.
+HINT: Either add "index" to the filter or remove "replica_identity" from it.
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx_excl (a integer NOT NULL, b integer);
+(1 row)
+
+DROP TABLE ri_idx_excl;
+-- Include-side analog: only_kinds=replica_identity must include the kind
+-- that emits the index, or the function errors.
+CREATE TABLE ri_only (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ERROR: REPLICA IDENTITY for table "ri_only" requires kind "unique" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "unique" kind, which is not in the active filter.
+HINT: Either add "unique" to the filter or remove "replica_identity" from it.
+-- Including both kinds succeeds.
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['unique','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.ri_only ADD CONSTRAINT ri_only_a_key UNIQUE (a);
+ ALTER TABLE pgtbl_ddl_test.ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+(2 rows)
+
+DROP TABLE ri_only;
+-- Per-kind only_kinds / except_kinds coverage. For each kind in the vocabulary,
+-- verify that only_kinds=K emits just that kind's output and that except_kinds=K
+-- omits just that kind's output. Kinds with no fixture above (trigger,
+-- policy) are still recognized but currently emit nothing; only_kinds=K
+-- returns zero rows for them. Fixtures used:
+-- idxd: PK + two secondary CREATE INDEX
+-- cons: PK + UNIQUE + CHECK + FK (constraint-backed)
+-- rt: CREATE RULE
+-- stx: CREATE STATISTICS
+-- rls: ENABLE/FORCE ROW LEVEL SECURITY
+-- ri_full: REPLICA IDENTITY FULL (no source-kind dependency)
+CREATE TABLE excl_fix (rng int4range,
+ EXCLUDE USING gist (rng WITH &&));
+-- only_kinds=K: emit only kind K's statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['unique']);
+ pg_get_table_ddl
+-----------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.excl_fix ADD CONSTRAINT excl_fix_rng_excl EXCLUDE USING gist (rng WITH &&);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, only_kinds => ARRAY['rule']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO pgtbl_ddl_test.rt_log (id)+
+ VALUES (new.id);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM pgtbl_ddl_test.stx;
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, only_kinds => ARRAY['rls']);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(1 row)
+
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['policy']);
+-- except_kinds=K: emit everything except kind K's statements. PK/UNIQUE/
+-- INDEX are exercised on tables without REPLICA IDENTITY USING INDEX
+-- so the validation does not trip.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['check']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, except_kinds => ARRAY['exclusion']);
+ pg_get_table_ddl
+-------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.excl_fix (rng int4range);
+(1 row)
+
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['policy']);
+-- except_kinds=table suppresses the CREATE TABLE + OWNER + child-default
+-- SET DEFAULT + attoptions passes, leaving only the sub-object passes.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['table']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- Multi-kind compositions.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table','index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['primary_key','foreign_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(1 row)
+
+-- Duplicate entries in the list are silently de-duplicated by the
+-- Bitmapset.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index','index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+DROP TABLE excl_fix;
+-- only_kinds=K on a table that does not have any K of that kind is a valid
+-- filter and returns zero rows. Tested previously with foreign_key
+-- on 'refd'; here also rule, statistics, rls, replica_identity, and
+-- exclusion on 'basic' (which has only PK + columns).
+SELECT count(*) AS rule_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rule']);
+ rule_rows
+-----------
+ 0
+(1 row)
+
+SELECT count(*) AS stats_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+ stats_rows
+------------
+ 0
+(1 row)
+
+SELECT count(*) AS rls_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rls']);
+ rls_rows
+----------
+ 0
+(1 row)
+
+SELECT count(*) AS ri_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ ri_rows
+---------
+ 0
+(1 row)
+
+SELECT count(*) AS excl_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+ excl_rows
+-----------
+ 0
+(1 row)
+
+-- Input-validation edge cases for the kind array. All
+-- of these raise an error; the function never reaches relation open.
+-- Empty array:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[]::text[]);
+ERROR: parameter "only_kinds" must specify at least one kind
+-- NULL element:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[NULL::text]);
+ERROR: parameter "only_kinds" must not contain NULL elements
+-- Unrecognized kind:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY['no_such_kind']);
+ERROR: unrecognized kind "no_such_kind" in parameter "only_kinds"
+-- Same kind specified through both parameters is still mutex-rejected.
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ only_kinds => ARRAY['index'],
+ except_kinds => ARRAY['rule']);
+ERROR: "only_kinds" and "except_kinds" parameters are mutually exclusive
+-- Round-trip verification. For every test table, capture the DDL the
+-- function emits, drop the schema, replay each table's DDL in
+-- dependency order, and confirm that pg_get_table_ddl on the recreated
+-- relation matches the original line-for-line. The final SELECT must
+-- return zero rows.
+CREATE TEMP TABLE pgtbl_ddl_rt_orig (name text, ord int, line text);
+INSERT INTO pgtbl_ddl_rt_orig
+SELECT t.name, o.ord, o.line
+FROM (VALUES
+ ('basic'), ('id_cols'), ('id_custom'), ('gen_cols'), ('gen_virtual'), ('storage_cols'),
+ ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'),
+ ('fk_acts_tgt'), ('fk_acts'), ('notenf_tgt'), ('notenf'), ('self_ref'), ('temporal_pk'),
+ ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('parted_list'),
+ ('rt_log'), ('rt'),
+ ('stx'), ('rls'), ('ri_full'), ('ri_idx'), ('uno')
+ ) AS t(name),
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord);
+DO $$
+DECLARE
+ tables CONSTANT text[] := ARRAY[
+ 'basic', 'id_cols', 'id_custom', 'gen_cols', 'gen_virtual', 'storage_cols',
+ 'refd', 'cons', 'nulls_nd', 'idx_inc',
+ 'fk_acts_tgt', 'fk_acts', 'notenf_tgt', 'notenf', 'self_ref', 'temporal_pk',
+ 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ 'parted_list',
+ 'rt_log', 'rt',
+ 'stx', 'rls', 'ri_full', 'ri_idx', 'uno'
+ ];
+ t text;
+ stmt text;
+BEGIN
+ DROP SCHEMA pgtbl_ddl_test CASCADE;
+ CREATE SCHEMA pgtbl_ddl_test;
+ FOREACH t IN ARRAY tables LOOP
+ FOR stmt IN
+ SELECT line FROM pgtbl_ddl_rt_orig WHERE name = t ORDER BY ord
+ LOOP
+ BEGIN
+ EXECUTE stmt;
+ EXCEPTION WHEN duplicate_table THEN
+ NULL; -- partition child already created by its parent's DDL
+ END;
+ END LOOP;
+ END LOOP;
+END $$;
+NOTICE: drop cascades to 35 other objects
+DETAIL: drop cascades to table basic
+drop cascades to table id_cols
+drop cascades to table id_custom
+drop cascades to table gen_cols
+drop cascades to table gen_virtual
+drop cascades to table storage_cols
+drop cascades to table refd
+drop cascades to table cons
+drop cascades to table nulls_nd
+drop cascades to table idx_inc
+drop cascades to table fk_acts_tgt
+drop cascades to table fk_acts
+drop cascades to table notenf_tgt
+drop cascades to table notenf
+drop cascades to table self_ref
+drop cascades to table temporal_pk
+drop cascades to table idxd
+drop cascades to table par
+drop cascades to table ch
+drop cascades to table attopt
+drop cascades to table parted_range
+drop cascades to table parted_hash
+drop cascades to table parted_list
+drop cascades to table rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 30 other objects
+DETAIL: drop cascades to table basic
+drop cascades to table id_cols
+drop cascades to table id_custom
+drop cascades to table gen_cols
+drop cascades to table gen_virtual
+drop cascades to table storage_cols
+drop cascades to table refd
+drop cascades to table cons
+drop cascades to table nulls_nd
+drop cascades to table idx_inc
+drop cascades to table fk_acts_tgt
+drop cascades to table fk_acts
+drop cascades to table notenf_tgt
+drop cascades to table notenf
+drop cascades to table self_ref
+drop cascades to table temporal_pk
+drop cascades to table idxd
+drop cascades to table par
+drop cascades to table ch
+drop cascades to table attopt
+drop cascades to table parted_range
+drop cascades to table parted_hash
+drop cascades to table parted_list
+drop cascades to table rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..18baa1637ed
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,768 @@
+--
+-- pg_get_table_ddl
+--
+-- All tests pass owner=>false so the ALTER TABLE OWNER TO line is not
+-- emitted, keeping output stable across test runners (which may run under
+-- different role names).
+--
+CREATE SCHEMA pgtbl_ddl_test;
+SET search_path = pgtbl_ddl_test;
+
+-- Basic table with PRIMARY KEY, NOT NULL, DEFAULT, COLLATE.
+CREATE TABLE basic (
+ id int PRIMARY KEY,
+ name text NOT NULL DEFAULT 'anon' COLLATE "C"
+);
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false);
+
+-- Identity columns (with default and custom sequence options).
+CREATE TABLE id_cols (
+ id_always int GENERATED ALWAYS AS IDENTITY,
+ id_default int GENERATED BY DEFAULT AS IDENTITY
+);
+SELECT * FROM pg_get_table_ddl('id_cols'::regclass, owner => false);
+
+CREATE TABLE id_custom (
+ v int GENERATED ALWAYS AS IDENTITY (
+ SEQUENCE NAME id_custom_v_seq
+ START WITH 100 INCREMENT BY 5
+ MINVALUE 50 MAXVALUE 1000
+ CACHE 10 CYCLE
+ )
+);
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false);
+
+-- Generated stored column.
+CREATE TABLE gen_cols (
+ cents int,
+ dollars numeric GENERATED ALWAYS AS (cents / 100.0) STORED
+);
+SELECT * FROM pg_get_table_ddl('gen_cols'::regclass, owner => false);
+
+-- Generated virtual column.
+CREATE TABLE gen_virtual (
+ base int,
+ derived int GENERATED ALWAYS AS (base * 2) VIRTUAL
+);
+SELECT * FROM pg_get_table_ddl('gen_virtual'::regclass, owner => false);
+
+-- STORAGE and COMPRESSION (only emitted when non-default for the type).
+CREATE TABLE storage_cols (
+ a text STORAGE EXTERNAL,
+ b text STORAGE MAIN,
+ c text COMPRESSION pglz
+);
+SELECT * FROM pg_get_table_ddl('storage_cols'::regclass, owner => false);
+
+-- Constraints: CHECK, UNIQUE, FOREIGN KEY (DEFERRABLE).
+CREATE TABLE refd (id int PRIMARY KEY);
+CREATE TABLE cons (
+ a int CHECK (a > 0),
+ b int UNIQUE,
+ c int REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED
+);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false);
+
+-- UNIQUE NULLS NOT DISTINCT (PG 15): null values are treated as equal for
+-- uniqueness purposes, so the NULLS NOT DISTINCT clause must be emitted.
+CREATE TABLE nulls_nd (
+ a int,
+ b int,
+ UNIQUE NULLS NOT DISTINCT (a, b)
+);
+SELECT * FROM pg_get_table_ddl('nulls_nd'::regclass, owner => false);
+
+-- INCLUDE columns on a constraint-backed UNIQUE index (PG 11): the
+-- covering columns appear in an INCLUDE (...) clause on the ALTER TABLE
+-- ADD CONSTRAINT statement, not in the key column list.
+CREATE TABLE idx_inc (
+ id int PRIMARY KEY,
+ name text,
+ extra text,
+ UNIQUE (name) INCLUDE (extra)
+);
+SELECT * FROM pg_get_table_ddl('idx_inc'::regclass, owner => false);
+
+-- FOREIGN KEY with ON DELETE / ON UPDATE referential actions and MATCH
+-- clause. Each variant must appear verbatim in the reconstructed DDL.
+CREATE TABLE fk_acts_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_acts (
+ a int,
+ b int,
+ c int,
+ d int,
+ CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES fk_acts_tgt(id)
+ ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET NULL,
+ CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET DEFAULT,
+ CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES fk_acts_tgt(id)
+ MATCH FULL
+);
+SELECT * FROM pg_get_table_ddl('fk_acts'::regclass, owner => false);
+
+-- NOT ENFORCED foreign key (PG 17): the constraint is recorded in the
+-- catalog but not validated by the engine; NOT ENFORCED must appear in
+-- the reconstructed DDL.
+CREATE TABLE notenf_tgt (id int PRIMARY KEY);
+CREATE TABLE notenf (
+ a int,
+ CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES notenf_tgt(id)
+ NOT ENFORCED
+);
+SELECT * FROM pg_get_table_ddl('notenf'::regclass, owner => false);
+
+-- Self-referencing FK: the PK must appear before the FK that references it.
+-- The catalog scan returns constraints in name order; without the fix,
+-- "self_ref_parent_id_fkey" sorts before "self_ref_pkey" and the FK ADD
+-- CONSTRAINT fails with "there is no unique constraint matching given keys".
+CREATE TABLE self_ref (
+ id int PRIMARY KEY,
+ parent_id int REFERENCES self_ref(id)
+);
+SELECT * FROM pg_get_table_ddl('self_ref'::regclass, owner => false);
+
+-- WITHOUT OVERLAPS on a PRIMARY KEY (PG 17): the range-type period
+-- column forms a temporal primary key; WITHOUT OVERLAPS must appear
+-- inside the key column list in the reconstructed ALTER TABLE.
+-- Both columns use range types with native GiST support so no
+-- btree_gist extension is needed.
+CREATE TABLE temporal_pk (
+ grp int4range,
+ during daterange,
+ PRIMARY KEY (grp, during WITHOUT OVERLAPS)
+);
+SELECT * FROM pg_get_table_ddl('temporal_pk'::regclass, owner => false);
+
+-- Indexes: functional and partial. Constraint-backing indexes are
+-- suppressed (they are emitted by the constraint loop).
+CREATE TABLE idxd (id int PRIMARY KEY, name text);
+CREATE INDEX idxd_lower ON idxd (lower(name));
+CREATE INDEX idxd_partial ON idxd (id) WHERE id > 100;
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false);
+
+-- Inheritance, including a child DEFAULT override on an inherited column.
+CREATE TABLE par (a int DEFAULT 1, b text);
+CREATE TABLE ch (c int) INHERITS (par);
+ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false);
+
+-- Per-column attoptions: emitted as ALTER COLUMN SET (...).
+CREATE TABLE attopt (a int, b text);
+ALTER TABLE attopt ALTER COLUMN a SET (n_distinct = 100);
+SELECT * FROM pg_get_table_ddl('attopt'::regclass, owner => false);
+
+-- Partitioned table parents (RANGE, HASH, and LIST).
+CREATE TABLE parted_range (id int, k int) PARTITION BY RANGE (id);
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false);
+
+CREATE TABLE parted_hash (id int) PARTITION BY HASH (id);
+SELECT * FROM pg_get_table_ddl('parted_hash'::regclass, owner => false);
+
+CREATE TABLE parted_list (id int, region text) PARTITION BY LIST (region);
+SELECT * FROM pg_get_table_ddl('parted_list'::regclass, owner => false);
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false);
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, owner => false);
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, owner => false);
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false);
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false);
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false);
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false);
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, owner => false);
+
+-- UNLOGGED + reloptions.
+CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70);
+SELECT * FROM pg_get_table_ddl('uno'::regclass, owner => false);
+
+-- Typed table (CREATE TABLE OF type_name). Columns inherited from the
+-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come
+-- out through a single "(col WITH OPTIONS ...)" list.
+CREATE TYPE typed_t AS (a int, b text);
+CREATE TABLE typed_plain OF typed_t;
+SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, owner => false);
+
+CREATE TABLE typed_over OF typed_t (
+ a WITH OPTIONS DEFAULT 7 NOT NULL,
+ b WITH OPTIONS NOT NULL,
+ CONSTRAINT b_nonempty CHECK (length(b) > 0)
+);
+SELECT * FROM pg_get_table_ddl('typed_over'::regclass, owner => false);
+
+-- Temporary tables + ON COMMIT. Temp tables must never be emitted with
+-- the session-private pg_temp_NN schema prefix -- the TEMPORARY keyword
+-- already places the table in pg_temp and the prefix is non-replayable.
+-- ON COMMIT DROP only fires at commit, so the queries that need to see
+-- the catalog entry must run in the same transaction as the CREATE.
+BEGIN;
+CREATE TEMP TABLE temp_default (id int);
+CREATE TEMP TABLE temp_delete (id int) ON COMMIT DELETE ROWS;
+CREATE TEMP TABLE temp_drop (id int) ON COMMIT DROP;
+
+SELECT line FROM pg_get_table_ddl('temp_default'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT line FROM pg_get_table_ddl('temp_delete'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT line FROM pg_get_table_ddl('temp_drop'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ROLLBACK;
+
+-- Pretty mode.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, pretty => true);
+
+-- only_kinds / except_kinds gating: each emits either only the listed kinds
+-- (only_kinds) or every kind except the listed ones (except_kinds). The two
+-- are mutually exclusive. Kind vocabulary: table, index,
+-- primary_key, unique, check, foreign_key, exclusion, rule,
+-- statistics, trigger, policy, rls, replica_identity, partition.
+-- NOT NULL is not in the vocabulary - always emitted to prevent
+-- producing schemas that silently accept NULLs the source would
+-- have rejected.
+--
+-- except_kinds=index hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['index']);
+
+-- except_kinds=primary_key,unique,check,foreign_key,exclusion hides every
+-- table-level constraint. Inline CHECK in the CREATE TABLE body is
+-- suppressed too.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['primary_key','unique','check','foreign_key','exclusion']);
+
+-- except_kinds=foreign_key suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- first pass of the two-pass FK-clone workflow: emit tables without
+-- cross-table FKs, load data, then re-run with only_kinds=foreign_key
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['foreign_key']);
+
+-- only_kinds=foreign_key is the second pass of the two-pass FK-clone
+-- workflow: emit only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN
+-- KEY rows; the CREATE TABLE and every non-FK sub-object are
+-- suppressed (the table already exists from the first pass).
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+
+-- A table with no foreign keys produces no rows under
+-- only_kinds=foreign_key.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+
+-- only_kinds=table emits only the bare CREATE TABLE (with any inline
+-- column-level NOT NULL and CHECK) - no indexes, no PRIMARY KEY ALTER,
+-- no other sub-objects. Useful for capturing a table's shape without
+-- pulling in everything attached to it.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table']);
+
+-- only_kinds=check on a non-partition table. The constraint loop must emit
+-- column-level and table-level CHECK constraints via ALTER TABLE here,
+-- because the inline path in CREATE TABLE never runs (KIND_TABLE is
+-- not in the filter).
+CREATE TABLE chk_only (
+ id int PRIMARY KEY,
+ qty int CHECK (qty > 0),
+ CONSTRAINT chk_only_id_pos CHECK (id > 0)
+);
+SELECT * FROM pg_get_table_ddl('chk_only'::regclass, owner => false, only_kinds => ARRAY['check']);
+DROP TABLE chk_only;
+
+-- only_kinds=partition on a partitioned-table parent: each child's full DDL
+-- is emitted (CREATE TABLE ... PARTITION OF ...). The "partition"
+-- keyword is a gate at the parent level; it is stripped from the
+-- propagated filter so the child does not exclude its own
+-- CREATE TABLE.
+CREATE TABLE p_only (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE p_only_a PARTITION OF p_only FOR VALUES FROM (0) TO (100);
+CREATE TABLE p_only_b PARTITION OF p_only FOR VALUES FROM (100) TO (200);
+SELECT * FROM pg_get_table_ddl('p_only'::regclass, owner => false, only_kinds => ARRAY['partition']);
+DROP TABLE p_only;
+
+-- only_kinds and except_kinds are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false,
+ only_kinds => ARRAY['foreign_key'],
+ except_kinds => ARRAY['foreign_key']);
+
+-- Pub/sub schema clone: keep the table and its primary key, drop the
+-- rest of the constraints so the subscriber can replicate without
+-- pulling cross-table dependencies along.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique','check','foreign_key','exclusion']);
+
+-- except_kinds=rule hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, except_kinds => ARRAY['rule']);
+
+-- except_kinds=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, except_kinds => ARRAY['statistics']);
+
+-- except_kinds=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, except_kinds => ARRAY['rls']);
+
+-- except_kinds=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, except_kinds => ARRAY['replica_identity']);
+
+-- Partition children are emitted by default; except_kinds=partition
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false, except_kinds => ARRAY['partition']);
+
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY[' INDEX ',' RULE ']);
+
+-- schema_qualified=false strips the schema from the target table itself
+-- and from same-schema sibling references (inheritance/partition
+-- parents, identity sequences in the same schema), while cross-schema
+-- references are kept qualified for correctness.
+--
+-- Bare CREATE TABLE for an ordinary table.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false,
+ schema_qualified => false);
+
+-- INHERITS parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Partition parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Cross-schema FK target keeps qualification. The 'refd' table is in
+-- pgtbl_ddl_test; place an FK target in a different schema and verify it
+-- stays qualified even with schema_qualified=false.
+CREATE SCHEMA pgtbl_ddl_other;
+CREATE TABLE pgtbl_ddl_other.parent (id int PRIMARY KEY);
+CREATE TABLE xschema_fk (
+ id int PRIMARY KEY,
+ pid int REFERENCES pgtbl_ddl_other.parent
+);
+SELECT * FROM pg_get_table_ddl('xschema_fk'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Cross-schema FK whose target schema name ends with the base schema name
+-- (e.g. "xpgtbl_ddl_test" ends with "pgtbl_ddl_test"). A post-hoc text
+-- strip would mangle "xpgtbl_ddl_test.reftbl" into "xreftbl"; deciding
+-- qualification at generation time avoids the problem entirely.
+CREATE SCHEMA xpgtbl_ddl_test;
+CREATE TABLE xpgtbl_ddl_test.reftbl (id int PRIMARY KEY);
+CREATE TABLE suffix_schema_fk (
+ id int PRIMARY KEY,
+ ref int REFERENCES xpgtbl_ddl_test.reftbl
+);
+SELECT * FROM pg_get_table_ddl('suffix_schema_fk'::regclass, owner => false,
+ schema_qualified => false);
+DROP TABLE suffix_schema_fk;
+DROP SCHEMA xpgtbl_ddl_test CASCADE;
+
+-- Identity column with a custom sequence name in the same schema is
+-- emitted unqualified.
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Round-trip: with schema_qualified=false, after SET search_path the
+-- DDL can be replayed into a different target schema.
+CREATE SCHEMA pgtbl_ddl_replay;
+DO $$
+DECLARE
+ stmt text;
+BEGIN
+ SET LOCAL search_path = pgtbl_ddl_replay;
+ FOR stmt IN
+ SELECT line FROM pg_get_table_ddl('pgtbl_ddl_test.basic'::regclass,
+ owner => false,
+ schema_qualified => false) AS line
+ LOOP
+ EXECUTE stmt;
+ END LOOP;
+END $$;
+SELECT relnamespace::regnamespace::text, relname
+FROM pg_class WHERE oid = 'pgtbl_ddl_replay.basic'::regclass;
+DROP SCHEMA pgtbl_ddl_replay CASCADE;
+DROP TABLE xschema_fk;
+DROP SCHEMA pgtbl_ddl_other CASCADE;
+
+-- Cross-schema partition child with schema_qualified=false: the child must
+-- carry its own schema prefix so the output is unambiguous regardless of the
+-- caller's search_path. Before the fix, the child was emitted as the bare
+-- name "pt_c" (landing in whichever schema is first in search_path) while
+-- the parent reference inside the CREATE TABLE came out as the fully-qualified
+-- "pgtbl_ddl_part_s.pt", making the DDL inconsistent and unreplayable.
+CREATE SCHEMA pgtbl_ddl_part_s;
+CREATE SCHEMA pgtbl_ddl_part_other;
+CREATE TABLE pgtbl_ddl_part_s.pt (id int, val int) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_part_other.pt_c
+ PARTITION OF pgtbl_ddl_part_s.pt
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_part_s.pt'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_part_s CASCADE;
+DROP SCHEMA pgtbl_ddl_part_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. String
+-- literal text is output verbatim by the deparser and is unaffected by
+-- search_path narrowing.
+CREATE SCHEMA pgtbl_ddl_str;
+CREATE TABLE pgtbl_ddl_str.p (id int, note text) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_str.pc PARTITION OF pgtbl_ddl_str.p
+ (CONSTRAINT chk CHECK (note <> 'pgtbl_ddl_str.secret'))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_str.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_str CASCADE;
+
+-- schema_qualified=false must NOT corrupt double-quoted identifiers
+-- whose contents happen to contain the schema name followed by a dot
+-- (for example a column literally named "<schema>.weird" inside that
+-- schema). The mechanism is purely search_path narrowing: the deparser
+-- outputs attribute names verbatim from pg_attribute, so the quoted
+-- identifier content is never examined for schema prefixes. This holds
+-- even when the schema name is a single letter.
+CREATE SCHEMA pgtbl_ddl_qid;
+CREATE TABLE pgtbl_ddl_qid.p (id int, "pgtbl_ddl_qid.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_qid.pc PARTITION OF pgtbl_ddl_qid.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_qid.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_qid.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_qid CASCADE;
+-- Same with a short (single-letter) schema name to confirm there is no
+-- minimum-length assumption in the stripping path.
+CREATE SCHEMA pgtbl_ddl_q1;
+CREATE TABLE pgtbl_ddl_q1.p (id int, "pgtbl_ddl_q1.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_q1.pc PARTITION OF pgtbl_ddl_q1.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_q1.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_q1.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_q1 CASCADE;
+
+-- schema_qualified=false when the base schema name itself requires
+-- quoting (its prefix starts with "). Both forms of the prefix -
+-- bare-lowercase and quoted - must be stripped from outer
+-- ALTER TABLE / ON / CREATE INDEX prefixes, while inner quoted
+-- identifiers that happen to begin the same way must be preserved.
+CREATE SCHEMA "PgTbl-Ddl-Q";
+CREATE TABLE "PgTbl-Ddl-Q"."T" (id int, val int,
+ CONSTRAINT chk CHECK (val > 0));
+CREATE INDEX "T_val_idx" ON "PgTbl-Ddl-Q"."T" (val);
+SELECT * FROM pg_get_table_ddl('"PgTbl-Ddl-Q"."T"'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA "PgTbl-Ddl-Q" CASCADE;
+
+-- Error: not an ordinary or partitioned table.
+CREATE VIEW v AS SELECT 1 AS x;
+SELECT * FROM pg_get_table_ddl('v'::regclass);
+
+CREATE SEQUENCE s;
+SELECT * FROM pg_get_table_ddl('s'::regclass);
+
+-- NULL argument returns no rows.
+SELECT * FROM pg_get_table_ddl(NULL);
+
+-- REPLICA IDENTITY USING INDEX validation: if the referenced index
+-- would not be emitted (because its source kind is suppressed) but
+-- replica_identity itself would be, the function raises an error so
+-- the emitted DDL never dangles. Exercise all three constraint-backed
+-- forms (PK, UNIQUE, EXCLUSION) and a bare CREATE UNIQUE INDEX, in
+-- both the except-list and only-list shapes.
+--
+-- PK-backed: except_kinds=primary_key without except_kinds=replica_identity.
+CREATE TABLE ri_pk_excl (a int PRIMARY KEY);
+ALTER TABLE ri_pk_excl REPLICA IDENTITY USING INDEX ri_pk_excl_pkey;
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key','replica_identity']);
+DROP TABLE ri_pk_excl;
+
+-- UNIQUE-constraint-backed replica identity index.
+CREATE TABLE ri_uniq_excl (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_uniq_excl REPLICA IDENTITY USING INDEX ri_uniq_excl_a_key;
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique','replica_identity']);
+DROP TABLE ri_uniq_excl;
+
+-- Bare CREATE UNIQUE INDEX (no backing constraint) used as REPLICA
+-- IDENTITY: the index is emitted by the "index" kind, so excluding
+-- "index" without also excluding "replica_identity" must error.
+CREATE TABLE ri_idx_excl (a int NOT NULL, b int);
+CREATE UNIQUE INDEX ri_idx_excl_a_uniq ON ri_idx_excl (a);
+ALTER TABLE ri_idx_excl REPLICA IDENTITY USING INDEX ri_idx_excl_a_uniq;
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index']);
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index','replica_identity']);
+DROP TABLE ri_idx_excl;
+
+-- Include-side analog: only_kinds=replica_identity must include the kind
+-- that emits the index, or the function errors.
+CREATE TABLE ri_only (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+-- Including both kinds succeeds.
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['unique','replica_identity']);
+DROP TABLE ri_only;
+
+-- Per-kind only_kinds / except_kinds coverage. For each kind in the vocabulary,
+-- verify that only_kinds=K emits just that kind's output and that except_kinds=K
+-- omits just that kind's output. Kinds with no fixture above (trigger,
+-- policy) are still recognized but currently emit nothing; only_kinds=K
+-- returns zero rows for them. Fixtures used:
+-- idxd: PK + two secondary CREATE INDEX
+-- cons: PK + UNIQUE + CHECK + FK (constraint-backed)
+-- rt: CREATE RULE
+-- stx: CREATE STATISTICS
+-- rls: ENABLE/FORCE ROW LEVEL SECURITY
+-- ri_full: REPLICA IDENTITY FULL (no source-kind dependency)
+CREATE TABLE excl_fix (rng int4range,
+ EXCLUDE USING gist (rng WITH &&));
+
+-- only_kinds=K: emit only kind K's statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index']);
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['primary_key']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, only_kinds => ARRAY['rule']);
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, only_kinds => ARRAY['rls']);
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['policy']);
+
+-- except_kinds=K: emit everything except kind K's statements. PK/UNIQUE/
+-- INDEX are exercised on tables without REPLICA IDENTITY USING INDEX
+-- so the validation does not trip.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['check']);
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, except_kinds => ARRAY['exclusion']);
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['policy']);
+-- except_kinds=table suppresses the CREATE TABLE + OWNER + child-default
+-- SET DEFAULT + attoptions passes, leaving only the sub-object passes.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['table']);
+
+-- Multi-kind compositions.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table','index']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['primary_key','foreign_key']);
+-- Duplicate entries in the list are silently de-duplicated by the
+-- Bitmapset.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index','index']);
+
+DROP TABLE excl_fix;
+
+-- only_kinds=K on a table that does not have any K of that kind is a valid
+-- filter and returns zero rows. Tested previously with foreign_key
+-- on 'refd'; here also rule, statistics, rls, replica_identity, and
+-- exclusion on 'basic' (which has only PK + columns).
+SELECT count(*) AS rule_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rule']);
+SELECT count(*) AS stats_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+SELECT count(*) AS rls_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rls']);
+SELECT count(*) AS ri_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+SELECT count(*) AS excl_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+
+-- Input-validation edge cases for the kind array. All
+-- of these raise an error; the function never reaches relation open.
+-- Empty array:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[]::text[]);
+-- NULL element:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[NULL::text]);
+-- Unrecognized kind:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY['no_such_kind']);
+-- Same kind specified through both parameters is still mutex-rejected.
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ only_kinds => ARRAY['index'],
+ except_kinds => ARRAY['rule']);
+
+-- Round-trip verification. For every test table, capture the DDL the
+-- function emits, drop the schema, replay each table's DDL in
+-- dependency order, and confirm that pg_get_table_ddl on the recreated
+-- relation matches the original line-for-line. The final SELECT must
+-- return zero rows.
+CREATE TEMP TABLE pgtbl_ddl_rt_orig (name text, ord int, line text);
+INSERT INTO pgtbl_ddl_rt_orig
+SELECT t.name, o.ord, o.line
+FROM (VALUES
+ ('basic'), ('id_cols'), ('id_custom'), ('gen_cols'), ('gen_virtual'), ('storage_cols'),
+ ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'),
+ ('fk_acts_tgt'), ('fk_acts'), ('notenf_tgt'), ('notenf'), ('self_ref'), ('temporal_pk'),
+ ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('parted_list'),
+ ('rt_log'), ('rt'),
+ ('stx'), ('rls'), ('ri_full'), ('ri_idx'), ('uno')
+ ) AS t(name),
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord);
+
+DO $$
+DECLARE
+ tables CONSTANT text[] := ARRAY[
+ 'basic', 'id_cols', 'id_custom', 'gen_cols', 'gen_virtual', 'storage_cols',
+ 'refd', 'cons', 'nulls_nd', 'idx_inc',
+ 'fk_acts_tgt', 'fk_acts', 'notenf_tgt', 'notenf', 'self_ref', 'temporal_pk',
+ 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ 'parted_list',
+ 'rt_log', 'rt',
+ 'stx', 'rls', 'ri_full', 'ri_idx', 'uno'
+ ];
+ t text;
+ stmt text;
+BEGIN
+ DROP SCHEMA pgtbl_ddl_test CASCADE;
+ CREATE SCHEMA pgtbl_ddl_test;
+ FOREACH t IN ARRAY tables LOOP
+ FOR stmt IN
+ SELECT line FROM pgtbl_ddl_rt_orig WHERE name = t ORDER BY ord
+ LOOP
+ BEGIN
+ EXECUTE stmt;
+ EXCEPTION WHEN duplicate_table THEN
+ NULL; -- partition child already created by its parent's DDL
+ END;
+ END LOOP;
+ END LOOP;
+END $$;
+
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..eb1189762a0 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1648,6 +1648,7 @@ ListenerEntry
LoInfo
LoadStmt
LocalBufferLookupEnt
+LocalNotNullEntry
LocalPgBackendStatus
LocalTransactionId
Location
@@ -3152,6 +3153,7 @@ TableFuncScan
TableFuncScanState
TableFuncType
TableInfo
+TableDdlContext
TableLikeClause
TableSampleClause
TableScanDesc
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-02 16:35 Marcos Pegoraro <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 0 replies; 54+ messages in thread
From: Marcos Pegoraro @ 2026-07-02 16:35 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
Em qui., 2 de jul. de 2026 às 04:49, Akshay Joshi <
[email protected]> escreveu:
> *owner* is the one case where it could work, but to make it consistent
> with how owner behaves in pg_get_tablespace_ddl and pg_get_database_ddl, we
> should not add it.
>
> *tablespace* doesn't map to a kind at all. It controls the inline
> TABLESPACE clause within the CREATE TABLE statement body it's a sub-clause,
> not a separate statement. If we added tablespace as a kind, except_kinds =>
> '{table,tablespace}' would be wrong (if you're skipping the table
> statement, there's no inline clause to suppress), and except_kinds =>
> '{tablespace}' would imply skipping a standalone statement that doesn't
> exist.
>
> *pretty* and *schema_qualified* are rendering/formatting options, not
> statement filters. They affect how every statement is rendered —
> indentation, name qualification — not which statements are emitted. Putting
> them in except_kinds conflates two orthogonal axes: filtering (what to
> emit) and formatting (how to emit it).
>
>>
Well, I thought you could continue with the same variable structure, just
assigning them beforehand if those values were used in the kind list.
So, TableDdlContext continues the same, you just add
TABLE_DDL_KIND_NO_OWNER and others to TableDdlKind
And before anything you see if TableDdlKind->TABLE_DDL_KIND_NO_OWNER is set
then TableDdlContext->no_owner receives that value, just that.
regards
Marcos
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-02 22:34 Zsolt Parragi <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Zsolt Parragi @ 2026-07-02 22:34 UTC (permalink / raw)
To: [email protected]
Hello!
I can confirm the previous issues fixed, however I also found one more
with unique indexes on partitioned tables:
CREATE SCHEMA s;
CREATE TABLE s.p (id int, region text) PARTITION BY LIST (region);
CREATE TABLE s.p_a PARTITION OF s.p FOR VALUES IN ('a');
CREATE UNIQUE INDEX p_uidx ON s.p (id, region);
SELECT pg_get_table_ddl('s.p', owner => false);
-- CREATE TABLE s.p (id integer, region text) PARTITION BY LIST (region);
-- CREATE UNIQUE INDEX p_uidx ON s.p USING btree (id, region);
-- CREATE TABLE s.p_a PARTITION OF s.p FOR VALUES IN ('a');
-- CREATE UNIQUE INDEX p_a_id_region_idx ON s.p_a USING btree (id,
region); -- fails because index already exists
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-03 08:47 Akshay Joshi <[email protected]>
parent: Zsolt Parragi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-07-03 08:47 UTC (permalink / raw)
To: Zsolt Parragi <[email protected]>; +Cc: [email protected]
Thanks Zsolt for the review. I have tried to fix most of the *edge cases. *I
added 12 new test cases.
The v16 patch is ready for review.
*Note: Windows-MinGW-Meson *builds are failing but not because of this
patch.
On Fri, Jul 3, 2026 at 4:04 AM Zsolt Parragi <[email protected]>
wrote:
> Hello!
>
> I can confirm the previous issues fixed, however I also found one more
> with unique indexes on partitioned tables:
>
> CREATE SCHEMA s;
> CREATE TABLE s.p (id int, region text) PARTITION BY LIST (region);
> CREATE TABLE s.p_a PARTITION OF s.p FOR VALUES IN ('a');
> CREATE UNIQUE INDEX p_uidx ON s.p (id, region);
>
> SELECT pg_get_table_ddl('s.p', owner => false);
> -- CREATE TABLE s.p (id integer, region text) PARTITION BY LIST (region);
> -- CREATE UNIQUE INDEX p_uidx ON s.p USING btree (id, region);
> -- CREATE TABLE s.p_a PARTITION OF s.p FOR VALUES IN ('a');
> -- CREATE UNIQUE INDEX p_a_id_region_idx ON s.p_a USING btree (id,
> region); -- fails because index already exists
>
>
>
Attachments:
[application/octet-stream] v16-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (219.0K, ../../CANxoLDfY0BucA49t_1n+AAkKZfxmQPfihjP8YMObf=8=-3XfUQ@mail.gmail.com/3-v16-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From bc6baad03083707069014954e17d0d845b6ef67f Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v16] Add pg_get_table_ddl() to reconstruct CREATE TABLE
statements
The function reconstructs the CREATE TABLE statement for an ordinary or
partitioned table, followed by the ALTER TABLE / CREATE INDEX /
CREATE RULE / CREATE STATISTICS statements needed to restore its full
definition. Each statement is returned as a separate row.
Supported per-column features: data type with type modifiers, COLLATE,
STORAGE, COMPRESSION (pglz / lz4), GENERATED ALWAYS AS (expr)
STORED/VIRTUAL, GENERATED ALWAYS|BY DEFAULT AS IDENTITY (with sequence
options), DEFAULT, NOT NULL (including named NOT NULL constraints), and
per-column attoptions emitted as ALTER COLUMN SET (...).
Supported table-level features: UNLOGGED, INHERITS, PARTITION BY (RANGE
/ LIST / HASH parents), PARTITION OF parent FOR VALUES (FROM/TO, WITH
modulus/remainder, DEFAULT), USING table access method, WITH
(reloptions), TABLESPACE, and inline CHECK constraints in the CREATE
TABLE body.
Supported sub-objects (re-using existing deparse helpers from
ruleutils.c): indexes (including partial and functional) via
pg_get_indexdef_ddl; constraints (PRIMARY KEY with WITHOUT OVERLAPS for
temporal keys, UNIQUE with NULLS NOT DISTINCT and INCLUDE columns,
FOREIGN KEY with ON DELETE/UPDATE referential actions and MATCH clause,
NOT ENFORCED foreign keys, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_body; rules via pg_get_ruledef_ddl; extended
statistics via pg_get_statisticsobjdef_ddl; REPLICA IDENTITY
NOTHING/FULL/USING INDEX; ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY;
and child-local DEFAULT overrides on inheritance/partition children.
DDL for partition children of a partitioned-table parent is appended
after the parent by default.
The function signature follows the named-parameter convention
established by pg_get_role_ddl(), pg_get_tablespace_ddl(), and
pg_get_database_ddl():
pg_get_table_ddl(relation regclass,
pretty boolean DEFAULT false,
owner boolean DEFAULT true,
tablespace boolean DEFAULT true,
schema_qualified boolean DEFAULT true,
only_kinds text[] DEFAULT NULL,
except_kinds text[] DEFAULT NULL)
pretty controls pretty-printed output. owner controls emission of the
ALTER TABLE ... OWNER TO statement. tablespace controls the TABLESPACE
clause on CREATE TABLE. schema_qualified controls whether object names
are emitted with their schema prefix: when true (the default) the
active search_path is temporarily narrowed to pg_catalog so every
deparse helper produces fully-qualified names; when false it is narrowed
to the target table's own schema so same-schema references come out
unqualified while cross-schema references remain qualified for
correctness. Temporary tables are never schema-qualified regardless of
this setting: the TEMPORARY keyword already places them in pg_temp, and
emitting pg_temp_NN.relname would produce non-replayable DDL.
Object-class filtering uses two mutually-exclusive text-array
parameters, only_kinds and except_kinds. When only_kinds is set, only
the listed kinds are emitted; when except_kinds is set, every kind
except the listed ones is emitted; when neither is set every kind is
emitted. Each array element is matched case-insensitively with
leading/trailing whitespace trimmed; an unrecognized name raises an
error. The kind vocabulary is:
table, index, primary_key, unique, check, foreign_key, exclusion,
rule, statistics, rls, replica_identity, partition
trigger and policy are accepted in the vocabulary but currently produce
no output; they are reserved for when standalone pg_get_trigger_ddl()
and pg_get_policy_ddl() helpers become available.
NOT NULL is not part of the vocabulary: it is always emitted to prevent
producing schemas that silently accept NULLs the source would reject.
When replica_identity is in the active filter and the table's REPLICA
IDENTITY USING INDEX references an index that the filter would suppress,
the function raises an error before emitting any output, so the
generated DDL never references an index it did not produce.
Default omission convention: every optional clause is omitted when its
value matches what the server would reapply on round-trip, including
type-default COLLATE, per-type STORAGE, the auto-generated identity
sequence name and parameter defaults, heap access method, default
REPLICA IDENTITY, disabled RLS toggles, empty reloptions, and the
default tablespace.
Author: Akshay Joshi <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kyotaro Horiguchi <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Rui Zhao <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 146 ++
src/backend/catalog/pg_inherits.c | 93 +
src/backend/commands/tablecmds.c | 30 +-
src/backend/utils/adt/ddlutils.c | 2001 ++++++++++++++++-
src/backend/utils/adt/ruleutils.c | 87 +-
src/include/catalog/pg_inherits.h | 1 +
src/include/catalog/pg_proc.dat | 8 +
src/include/commands/tablecmds.h | 3 +
src/include/utils/ruleutils.h | 4 +
.../regress/expected/pg_get_table_ddl.out | 1518 +++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 879 ++++++++
src/tools/pgindent/typedefs.list | 2 +
13 files changed, 4757 insertions(+), 17 deletions(-)
create mode 100644 src/test/regress/expected/pg_get_table_ddl.out
create mode 100644 src/test/regress/sql/pg_get_table_ddl.sql
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 69ef3857cfa..55c6b873240 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3960,6 +3960,152 @@ 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>relation</parameter> <type>regclass</type>
+ <optional>, <parameter>pretty</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> false</optional>
+ <optional>, <parameter>owner</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>tablespace</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>schema_qualified</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>only_kinds</parameter> <type>text[]</type>
+ <literal>DEFAULT</literal> NULL</optional>
+ <optional>, <parameter>except_kinds</parameter> <type>text[]</type>
+ <literal>DEFAULT</literal> NULL</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.
+ Foreign-key constraints are always emitted after all other
+ constraints so that self-referencing foreign keys (where the
+ referenced primary or unique key is on the same table) can be
+ added once that key already exists.
+ When <parameter>pretty</parameter> is true, the output is
+ pretty-printed. When <parameter>owner</parameter> is false, the
+ <command>ALTER TABLE ... OWNER TO</command> statement is omitted.
+ When <parameter>tablespace</parameter> is false, the
+ <literal>TABLESPACE</literal> clause is omitted from the
+ <command>CREATE TABLE</command> statement. The
+ <parameter>only_kinds</parameter> and <parameter>except_kinds</parameter>
+ parameters described below control which object classes are emitted.
+ </para>
+ <para>
+ The <parameter>only_kinds</parameter> and <parameter>except_kinds</parameter>
+ parameters each take a text array of object-class kind names
+ and are mutually exclusive (specifying both raises an
+ error). When <parameter>only_kinds</parameter> is set, only the
+ listed kinds are emitted; when <parameter>except_kinds</parameter> is
+ set, every kind <emphasis>except</emphasis> the listed ones is
+ emitted. When neither is set (the default), every kind is
+ emitted. Leading and trailing whitespace in each array element
+ is ignored and matching is case-insensitive; an unrecognized
+ kind name raises an error.
+ The kind vocabulary is
+ <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).
+ Note that <literal>trigger</literal> and <literal>policy</literal>
+ are accepted in the vocabulary but currently produce no output;
+ they are reserved for future use when standalone
+ <function>pg_get_trigger_ddl</function> and
+ <function>pg_get_policy_ddl</function> helpers become available. 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_kinds => ARRAY['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_kinds => ARRAY['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 <parameter>schema_qualified</parameter> parameter (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. Partition children that reside in a different
+ schema than the target table are always emitted with full schema
+ qualification regardless of this parameter, because the output
+ would otherwise place the child in the wrong schema when
+ replayed.
+ </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 95abaf4890c..06afa6811ec 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);
@@ -2524,7 +2523,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)
@@ -19837,6 +19836,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..c74c39a125f 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -5,7 +5,7 @@
*
* This file contains the pg_get_*_ddl family of functions that generate
* DDL statements to recreate database objects such as roles, tablespaces,
- * and databases, along with common infrastructure for option parsing and
+ * databases, and tables, along with common infrastructure for
* pretty-printing.
*
* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
@@ -21,30 +21,89 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.h"
+#include "catalog/namespace.h"
+#include "catalog/partition.h"
+#include "catalog/pg_am.h"
#include "catalog/pg_auth_members.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_collation.h"
+#include "catalog/pg_constraint.h"
#include "catalog/pg_database.h"
#include "catalog/pg_db_role_setting.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_policy.h"
+#include "catalog/pg_sequence.h"
+#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_tablespace.h"
+#include "catalog/pg_trigger.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
#include "commands/tablespace.h"
-#include "common/relpath.h"
#include "funcapi.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "utils/acl.h"
+#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/datetime.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
-#include "utils/pg_locale.h"
#include "utils/rel.h"
#include "utils/ruleutils.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
#include "utils/varlena.h"
+/*
+ * Object-class kinds that the only / except options on
+ * pg_get_table_ddl can filter on. Members are stored as integers in a
+ * Bitmapset on TableDdlContext. Keep table_ddl_kind_names[] in sync
+ * with the order of additions here.
+ */
+typedef enum TableDdlKind
+{
+ TABLE_DDL_KIND_TABLE,
+ TABLE_DDL_KIND_INDEX,
+ TABLE_DDL_KIND_PRIMARY_KEY,
+ TABLE_DDL_KIND_UNIQUE,
+ TABLE_DDL_KIND_CHECK,
+ TABLE_DDL_KIND_FOREIGN_KEY,
+ TABLE_DDL_KIND_EXCLUSION,
+ TABLE_DDL_KIND_RULE,
+ TABLE_DDL_KIND_STATISTICS,
+ TABLE_DDL_KIND_TRIGGER,
+ TABLE_DDL_KIND_POLICY,
+ TABLE_DDL_KIND_RLS,
+ TABLE_DDL_KIND_REPLICA_IDENTITY,
+ TABLE_DDL_KIND_PARTITION,
+} TableDdlKind;
+
+static const struct
+{
+ const char *name;
+ TableDdlKind kind;
+} table_ddl_kind_names[] =
+{
+ {"table", TABLE_DDL_KIND_TABLE},
+ {"index", TABLE_DDL_KIND_INDEX},
+ {"primary_key", TABLE_DDL_KIND_PRIMARY_KEY},
+ {"unique", TABLE_DDL_KIND_UNIQUE},
+ {"check", TABLE_DDL_KIND_CHECK},
+ {"foreign_key", TABLE_DDL_KIND_FOREIGN_KEY},
+ {"exclusion", TABLE_DDL_KIND_EXCLUSION},
+ {"rule", TABLE_DDL_KIND_RULE},
+ {"statistics", TABLE_DDL_KIND_STATISTICS},
+ {"trigger", TABLE_DDL_KIND_TRIGGER},
+ {"policy", TABLE_DDL_KIND_POLICY},
+ {"rls", TABLE_DDL_KIND_RLS},
+ {"replica_identity", TABLE_DDL_KIND_REPLICA_IDENTITY},
+ {"partition", TABLE_DDL_KIND_PARTITION},
+};
+
+static Bitmapset *parse_kind_array(const char *paramname, ArrayType *arr);
static void append_ddl_option(StringInfo buf, bool pretty, int indent,
const char *fmt, ...)
pg_attribute_printf(4, 5);
@@ -57,6 +116,183 @@ static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid);
static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
bool no_owner, bool no_tablespace);
+/*
+ * Per-column cache of locally-declared NOT NULL constraints. Built once
+ * by collect_local_not_null() and consulted by the column-emit helpers
+ * and the post-CREATE constraint loop. Entries with conoid == InvalidOid
+ * mean the column has no local NOT NULL constraint row in pg_constraint
+ * (the column may still have attnotnull=true on a pre-PG-18-upgraded
+ * catalog, in which case emit plain inline NOT NULL).
+ */
+typedef struct LocalNotNullEntry
+{
+ Oid conoid;
+ char *name;
+ bool is_auto; /* matches "<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_kind_array
+ * Parse a text[] of object-class kind names into a Bitmapset of
+ * TableDdlKind values.
+ *
+ * Each element is matched case-insensitively; surrounding whitespace is
+ * stripped. NULL elements are rejected. Unknown kind names raise an error
+ * citing the supplied parameter name. An empty array is also rejected.
+ * Duplicate entries are silently de-duplicated by the Bitmapset.
+ */
+static Bitmapset *
+parse_kind_array(const char *paramname, ArrayType *arr)
+{
+ Datum *elems;
+ bool *nulls;
+ int nelems;
+ Bitmapset *result = NULL;
+
+ deconstruct_array_builtin(arr, TEXTOID, &elems, &nulls, &nelems);
+
+ if (nelems == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" must specify at least one kind",
+ paramname)));
+
+ for (int i = 0; i < nelems; i++)
+ {
+ char *raw;
+ char *token;
+ char *end;
+ bool found = false;
+
+ if (nulls[i])
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" must not contain NULL elements",
+ paramname)));
+
+ raw = text_to_cstring(DatumGetTextPP(elems[i]));
+
+ /* Trim leading whitespace. */
+ token = raw;
+ while (*token == ' ' || *token == '\t')
+ token++;
+
+ /* Trim trailing whitespace. */
+ end = token + strlen(token);
+ while (end > token && (end[-1] == ' ' || end[-1] == '\t'))
+ end--;
+ *end = '\0';
+
+ for (size_t j = 0; j < lengthof(table_ddl_kind_names); j++)
+ {
+ if (pg_strcasecmp(token, table_ddl_kind_names[j].name) == 0)
+ {
+ result = bms_add_member(result,
+ (int) table_ddl_kind_names[j].kind);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("unrecognized kind \"%s\" in parameter \"%s\"",
+ token, paramname)));
+
+ pfree(raw);
+ }
+
+ return result;
+}
+
+/*
+ * is_kind_included
+ * Determine whether DDL for the given object-class kind should be
+ * emitted under the current options.
+ */
+static bool
+is_kind_included(const TableDdlContext *ctx, TableDdlKind kind)
+{
+ if (ctx->only_kinds != NULL)
+ return bms_is_member((int) kind, ctx->only_kinds);
+ if (ctx->except_kinds != NULL)
+ return !bms_is_member((int) kind, ctx->except_kinds);
+ return true;
+}
/*
* Helper to append a formatted string with optional pretty-printing.
@@ -975,3 +1211,1762 @@ 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;
+
+ /*
+ * Skip indexes that were inherited from a parent partitioned index.
+ * They are created automatically when the parent index DDL is
+ * replayed and the partition is attached, so emitting them separately
+ * would produce a "relation already exists" error on replay.
+ */
+ if (get_rel_relispartition(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;
+ List *fk_stmts = NIL;
+
+ 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);
+
+ /*
+ * Defer FK statements so they are emitted after all other
+ * constraints. A self-referencing FK (REFERENCES same_table)
+ * requires the PK/UNIQUE it targets to exist first, and because
+ * the catalog scan returns constraints in name order, an FK whose
+ * name sorts before the PK name would otherwise be emitted first
+ * and fail with "there is no unique constraint matching given
+ * keys".
+ */
+ if (con->contype == CONSTRAINT_FOREIGN)
+ fk_stmts = lappend(fk_stmts, pstrdup(ctx->buf.data));
+ else
+ append_stmt(ctx);
+
+ pfree(conbody);
+ }
+ }
+ systable_endscan(conScan);
+ table_close(conRel, AccessShareLock);
+
+ /* Append deferred FK statements after all other constraints. */
+ ctx->statements = list_concat(ctx->statements, fk_stmts);
+}
+
+/*
+ * 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;
+
+ /*
+ * When schema_qualified is false, the contract is that the output
+ * is replayable with the parent's schema in search_path. That
+ * contract cannot hold for a child that lives in a different schema:
+ * its own name would be ambiguous (landing in whatever schema is
+ * first in search_path) and references back to the parent would have
+ * to be schema-qualified anyway. Force full qualification so the
+ * child's DDL is unambiguous regardless of the caller's search_path.
+ */
+ if (!childctx.schema_qualified &&
+ get_rel_namespace(childoid) != ctx->base_namespace)
+ childctx.schema_qualified = true;
+
+ if (childctx.only_kinds != NULL &&
+ bms_is_member((int) TABLE_DDL_KIND_PARTITION,
+ childctx.only_kinds))
+ {
+ Bitmapset *child_only = bms_copy(childctx.only_kinds);
+
+ child_only = bms_del_member(child_only,
+ (int) TABLE_DDL_KIND_PARTITION);
+ if (bms_is_empty(child_only))
+ {
+ bms_free(child_only);
+ childctx.only_kinds = NULL;
+ }
+ else
+ childctx.only_kinds = child_only;
+ }
+
+ childstmts = pg_get_table_ddl_internal(&childctx);
+ ctx->statements = list_concat(ctx->statements, childstmts);
+ }
+ list_free(children);
+}
+
+/*
+ * pg_get_table_ddl_internal
+ * Generate DDL statements to recreate a regular or partitioned table.
+ *
+ * The caller initializes *ctx with the user-supplied option fields
+ * (relid, pretty, no_owner, no_tablespace, schema_qualified,
+ * only_kinds, except_kinds). This function opens the relation,
+ * validates access, populates the derived fields (rel, qualname,
+ * nn_entries, ...), runs the emission passes, and returns the
+ * accumulated statement list.
+ *
+ * Each emission helper consults is_kind_included() to decide whether
+ * it should run. The table-proper passes (CREATE TABLE / OWNER /
+ * ALTER COLUMN ... SET DEFAULT / ALTER COLUMN ... SET (...)) are
+ * grouped under KIND_TABLE so the FK-only / sub-object-only workflow
+ * is expressible as a single only or except list.
+ *
+ * Trigger and policy emission are scaffolded but currently disabled
+ * (#if 0) - they will become a single helper call once the standalone
+ * pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+ */
+static List *
+pg_get_table_ddl_internal(TableDdlContext *ctx)
+{
+ Relation rel;
+ char relkind;
+ AclResult aclresult;
+
+ rel = table_open(ctx->relid, AccessShareLock);
+
+ relkind = rel->rd_rel->relkind;
+
+ /*
+ * The initial cut only supports ordinary and partitioned tables. Views,
+ * matviews, foreign tables, sequences, indexes, composite types, and
+ * TOAST tables are out of scope for now.
+ */
+ if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ char *relname = pstrdup(RelationGetRelationName(rel));
+
+ table_close(rel, AccessShareLock);
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not an ordinary or partitioned table",
+ relname)));
+ }
+
+ /* Caller needs SELECT on the table to read its definition. */
+ aclresult = pg_class_aclcheck(ctx->relid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLE,
+ RelationGetRelationName(rel));
+
+ /*
+ * Validation: if the table has REPLICA IDENTITY USING INDEX and the
+ * referenced index would not be emitted under the active filter, the
+ * emitted REPLICA IDENTITY clause would reference an index the same DDL
+ * never produced. Determine which kind would emit the index (one of
+ * primary_key / unique / exclusion for constraint-backed indexes,
+ * otherwise the generic "index" kind) and require it to be in scope
+ * whenever "replica_identity" is. The check uses is_kind_included so
+ * it covers both forms naturally: an "except" list that omits the
+ * source kind, and an "only" list that omits it.
+ */
+ if (rel->rd_rel->relreplident == REPLICA_IDENTITY_INDEX &&
+ is_kind_included(ctx, TABLE_DDL_KIND_REPLICA_IDENTITY))
+ {
+ Oid replidx = RelationGetReplicaIndex(rel);
+
+ if (OidIsValid(replidx))
+ {
+ TableDdlKind idx_kind = TABLE_DDL_KIND_INDEX;
+ Oid conoid = get_index_constraint(replidx);
+
+ if (OidIsValid(conoid))
+ {
+ HeapTuple conTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conoid));
+
+ if (HeapTupleIsValid(conTup))
+ {
+ Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
+
+ switch (con->contype)
+ {
+ case CONSTRAINT_PRIMARY:
+ idx_kind = TABLE_DDL_KIND_PRIMARY_KEY;
+ break;
+ case CONSTRAINT_UNIQUE:
+ idx_kind = TABLE_DDL_KIND_UNIQUE;
+ break;
+ case CONSTRAINT_EXCLUSION:
+ idx_kind = TABLE_DDL_KIND_EXCLUSION;
+ break;
+ default:
+ break;
+ }
+ ReleaseSysCache(conTup);
+ }
+ }
+
+ if (!is_kind_included(ctx, idx_kind))
+ {
+ char *relname = pstrdup(RelationGetRelationName(rel));
+ const char *idx_name = table_ddl_kind_names[(int) idx_kind].name;
+
+ table_close(rel, AccessShareLock);
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("REPLICA IDENTITY for table \"%s\" requires kind \"%s\" to be emitted",
+ relname, idx_name),
+ errdetail("The table's REPLICA IDENTITY USING INDEX references an index produced by the \"%s\" kind, which is not in the active filter.",
+ idx_name),
+ errhint("Either add \"%s\" to the filter or remove \"replica_identity\" from it.",
+ idx_name)));
+ }
+ }
+ }
+
+ /*
+ * Populate derived fields now that the relation is open and validated.
+ * The remaining derived fields (nn_entries, skip_notnull_oids, buf,
+ * statements) start zeroed via the caller's `TableDdlContext ctx = {0}`
+ * initializer; the NOT NULL cache is populated by collect_local_not_null,
+ * the buffer is initStringInfo'd, and each emit pass appends to
+ * statements via lappend.
+ */
+ ctx->rel = rel;
+ ctx->base_namespace = RelationGetNamespace(rel);
+ ctx->save_nestlevel = -1;
+ ctx->qualname = lookup_relname_for_emit(ctx->relid, ctx->schema_qualified,
+ ctx->base_namespace);
+
+ /*
+ * Temporarily override search_path so that the ruleutils helpers
+ * (pg_get_indexdef_ddl, pg_get_constraintdef_body,
+ * pg_get_statisticsobjdef_ddl, pg_get_ruledef_ddl, etc.) produce names
+ * that match the schema_qualified flag. Those helpers decide whether to
+ * qualify a name by calling RelationIsVisible(), which checks whether the
+ * object's schema appears in the active search_path.
+ *
+ * schema_qualified = false: narrow to the base schema so that same-schema
+ * references in DEFAULT expressions, FK targets, indexes, rules, and
+ * statistics come out unqualified automatically. Cross-schema references
+ * stay qualified, which is the correctness requirement.
+ *
+ * schema_qualified = true: narrow to pg_catalog only so that objects in
+ * the base schema (or any user schema the caller placed on search_path)
+ * are not reachable without qualification, forcing fully-qualified output
+ * from every helper regardless of the caller's session search_path.
+ *
+ * AtEOXact_GUC cleans up at xact end if anything throws between here and
+ * the explicit restore below; on the normal path we restore right before
+ * returning.
+ */
+ if (!ctx->schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx->base_namespace);
+
+ if (nspname != NULL)
+ {
+ const char *qnsp = quote_identifier(nspname);
+
+ ctx->save_nestlevel = NewGUCNestLevel();
+ (void) set_config_option("search_path", qnsp,
+ PGC_USERSET, PGC_S_SESSION,
+ GUC_ACTION_SAVE, true, 0, false);
+ if (qnsp != nspname)
+ pfree((char *) qnsp);
+ pfree(nspname);
+ }
+ }
+ else
+ {
+ ctx->save_nestlevel = NewGUCNestLevel();
+ (void) set_config_option("search_path", "pg_catalog",
+ PGC_USERSET, PGC_S_SESSION,
+ GUC_ACTION_SAVE, true, 0, false);
+ }
+
+ /*
+ * Cache locally-declared NOT NULL constraint metadata so the column- emit
+ * helpers can produce "CONSTRAINT name NOT NULL" inline for user-named
+ * constraints, and so the constraint loop can avoid double-emitting them.
+ */
+ ctx->nn_entries = collect_local_not_null(rel, &ctx->skip_notnull_oids);
+
+ initStringInfo(&ctx->buf);
+
+ /*
+ * Emission passes. Order is significant: CREATE TABLE first; OWNER and
+ * the per-column ALTER COLUMN passes before sub-object emission;
+ * sub-objects in dependency-friendly order (indexes before constraints,
+ * since constraint-backed indexes are emitted out-of-line by the
+ * constraint loop); partition children last so the parent already exists
+ * at replay time. Each helper gates itself on is_kind_included for the
+ * relevant TABLE_DDL_KIND_*; the four "table itself" passes are grouped
+ * here under KIND_TABLE so all of CREATE TABLE / OWNER / SET DEFAULT /
+ * SET (...) drop out together when the user asks for only sub-objects
+ * (e.g. only => 'foreign_key' for the second pass of a two-pass FK
+ * clone).
+ */
+ if (is_kind_included(ctx, TABLE_DDL_KIND_TABLE))
+ {
+ emit_create_table_stmt(ctx);
+ emit_owner_stmt(ctx);
+ emit_child_default_overrides(ctx);
+ emit_attoptions(ctx);
+ }
+ emit_indexes(ctx);
+ emit_local_constraints(ctx);
+ emit_rules(ctx);
+ emit_statistics(ctx);
+ emit_replica_identity(ctx);
+ emit_rls_toggles(ctx);
+
+ /*
+ * Triggers and row-level security policies - disabled until the
+ * standalone pg_get_trigger_ddl() and pg_get_policy_ddl() helpers land.
+ * The scan + lock + filter scaffolding below is preserved (inside #if 0)
+ * so wiring up each emission becomes a one-liner in the loop body once
+ * those helpers are available.
+ */
+#if 0
+ if (is_kind_included(ctx, TABLE_DDL_KIND_TRIGGER))
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(ctx->relid));
+ trigScan = systable_beginscan(trigRel, TriggerRelidNameIndexId,
+ true, NULL, 1, &trigKey);
+ while (HeapTupleIsValid(trigTup = systable_getnext(trigScan)))
+ {
+ Form_pg_trigger trg = (Form_pg_trigger) GETSTRUCT(trigTup);
+
+ if (trg->tgisinternal)
+ continue;
+
+ /* TODO: append pg_get_trigger_ddl(trg->oid) output here. */
+ }
+ systable_endscan(trigScan);
+ table_close(trigRel, AccessShareLock);
+ }
+
+ if (is_kind_included(ctx, TABLE_DDL_KIND_POLICY))
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(ctx->relid));
+ polScan = systable_beginscan(polRel, PolicyPolrelidPolnameIndexId,
+ true, NULL, 1, &polKey);
+ while (HeapTupleIsValid(polTup = systable_getnext(polScan)))
+ {
+ /* TODO: append pg_get_policy_ddl(relid, polname) output here. */
+ }
+ systable_endscan(polScan);
+ table_close(polRel, AccessShareLock);
+ }
+#endif
+
+ emit_partition_children(ctx);
+
+ {
+ int natts = RelationGetDescr(rel)->natts;
+
+ for (int i = 1; i <= natts; i++)
+ if (ctx->nn_entries[i].name != NULL)
+ pfree(ctx->nn_entries[i].name);
+ }
+ pfree(ctx->nn_entries);
+
+ table_close(rel, AccessShareLock);
+ pfree(ctx->buf.data);
+ pfree(ctx->qualname);
+ list_free(ctx->skip_notnull_oids);
+
+ /*
+ * Pop the narrowed search_path now that all helpers have run. Errors
+ * thrown earlier are cleaned up by AtEOXact_GUC at xact end.
+ */
+ if (ctx->save_nestlevel >= 0)
+ AtEOXact_GUC(true, ctx->save_nestlevel);
+
+ return ctx->statements;
+}
+
+/*
+ * pg_get_table_ddl
+ * Return DDL to recreate a table as a set of text rows.
+ */
+Datum
+pg_get_table_ddl(PG_FUNCTION_ARGS)
+{
+ FuncCallContext *funcctx;
+ List *statements;
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcontext;
+ TableDdlContext ctx = {0};
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ if (!PG_ARGISNULL(5) && !PG_ARGISNULL(6))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"only_kinds\" and \"except_kinds\" parameters are mutually exclusive")));
+
+ /* Option defaults (match proargdefaults in pg_proc.dat). */
+ ctx.relid = PG_GETARG_OID(0);
+ ctx.pretty = false;
+ ctx.no_owner = false; /* owner DEFAULT true -> no_owner = false */
+ ctx.no_tablespace = false; /* tablespace DEFAULT true -> no_tablespace = false */
+ ctx.schema_qualified = true;
+ ctx.only_kinds = NULL;
+ ctx.except_kinds = NULL;
+
+ /* Override defaults with any explicitly supplied values. */
+ if (!PG_ARGISNULL(1))
+ ctx.pretty = PG_GETARG_BOOL(1);
+ if (!PG_ARGISNULL(2))
+ ctx.no_owner = !PG_GETARG_BOOL(2);
+ if (!PG_ARGISNULL(3))
+ ctx.no_tablespace = !PG_GETARG_BOOL(3);
+ if (!PG_ARGISNULL(4))
+ ctx.schema_qualified = PG_GETARG_BOOL(4);
+ if (!PG_ARGISNULL(5))
+ ctx.only_kinds = parse_kind_array("only_kinds",
+ PG_GETARG_ARRAYTYPE_P(5));
+ if (!PG_ARGISNULL(6))
+ ctx.except_kinds = parse_kind_array("except_kinds",
+ PG_GETARG_ARRAYTYPE_P(6));
+
+ statements = pg_get_table_ddl_internal(&ctx);
+ funcctx->user_fctx = statements;
+ funcctx->max_calls = list_length(statements);
+
+ MemoryContextSwitchTo(oldcontext);
+ }
+
+ funcctx = SRF_PERCALL_SETUP();
+ statements = (List *) funcctx->user_fctx;
+
+ if (funcctx->call_cntr < funcctx->max_calls)
+ {
+ char *stmt;
+
+ stmt = list_nth(statements, funcctx->call_cntr);
+
+ SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+ }
+ else
+ {
+ list_free_deep(statements);
+ SRF_RETURN_DONE(funcctx);
+ }
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 88de5c0481c..7186cf83e1d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -23,6 +23,7 @@
#include "access/htup_details.h"
#include "access/relation.h"
#include "access/table.h"
+#include "catalog/namespace.h"
#include "catalog/pg_aggregate.h"
#include "catalog/pg_am.h"
#include "catalog/pg_authid.h"
@@ -370,7 +371,7 @@ static void make_propgraphdef_elements(StringInfo buf, Oid pgrelid, char pgekind
static void make_propgraphdef_labels(StringInfo buf, Oid elid, const char *elalias, Oid elrelid);
static void make_propgraphdef_properties(StringInfo buf, Oid ellabelid, Oid elrelid);
static char *pg_get_statisticsobj_worker(Oid statextid, bool columns_only,
- bool missing_ok);
+ bool missing_ok, int prettyFlags);
static char *pg_get_partkeydef_worker(Oid relid, int prettyFlags,
bool attrsOnly, bool missing_ok);
static char *pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
@@ -603,6 +604,19 @@ pg_get_ruledef_ext(PG_FUNCTION_ARGS)
PG_RETURN_TEXT_P(string_to_text(res));
}
+/*
+ * pg_get_ruledef_ddl
+ * Like pg_get_ruledef but uses the active search_path to decide
+ * whether to schema-qualify the rule's target relation name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_ruledef_ddl(Oid ruleoid)
+{
+ return pg_get_ruledef_worker(ruleoid, PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA);
+}
+
static char *
pg_get_ruledef_worker(Oid ruleoid, int prettyFlags)
@@ -1241,6 +1255,22 @@ pg_get_indexdef_string(Oid indexrelid)
0, false);
}
+/*
+ * pg_get_indexdef_ddl
+ * Like pg_get_indexdef_string but uses the active search_path to
+ * decide whether to schema-qualify the indexed relation name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_indexdef_ddl(Oid indexrelid)
+{
+ return pg_get_indexdef_worker(indexrelid, 0, NULL,
+ false, false,
+ true, true,
+ PRETTYFLAG_SCHEMA, false);
+}
+
/* Internal version that just reports the key-column definitions */
char *
pg_get_indexdef_columns(Oid indexrelid, bool pretty)
@@ -1969,7 +1999,7 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS)
Oid statextid = PG_GETARG_OID(0);
char *res;
- res = pg_get_statisticsobj_worker(statextid, false, true);
+ res = pg_get_statisticsobj_worker(statextid, false, true, 0);
if (res == NULL)
PG_RETURN_NULL();
@@ -1984,7 +2014,20 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS)
char *
pg_get_statisticsobjdef_string(Oid statextid)
{
- return pg_get_statisticsobj_worker(statextid, false, false);
+ return pg_get_statisticsobj_worker(statextid, false, false, 0);
+}
+
+/*
+ * pg_get_statisticsobjdef_ddl
+ * Like pg_get_statisticsobjdef_string but uses the active search_path
+ * to decide whether to schema-qualify the statistics object name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_statisticsobjdef_ddl(Oid statextid)
+{
+ return pg_get_statisticsobj_worker(statextid, false, false, PRETTYFLAG_SCHEMA);
}
/*
@@ -1997,7 +2040,7 @@ pg_get_statisticsobjdef_columns(PG_FUNCTION_ARGS)
Oid statextid = PG_GETARG_OID(0);
char *res;
- res = pg_get_statisticsobj_worker(statextid, true, true);
+ res = pg_get_statisticsobj_worker(statextid, true, true, 0);
if (res == NULL)
PG_RETURN_NULL();
@@ -2009,7 +2052,8 @@ pg_get_statisticsobjdef_columns(PG_FUNCTION_ARGS)
* Internal workhorse to decompile an extended statistics object.
*/
static char *
-pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
+pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok,
+ int prettyFlags)
{
Form_pg_statistic_ext statextrec;
HeapTuple statexttup;
@@ -2069,10 +2113,18 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
if (!columns_only)
{
- nsp = get_namespace_name_or_temp(statextrec->stxnamespace);
- appendStringInfo(&buf, "CREATE STATISTICS %s",
- quote_qualified_identifier(nsp,
- NameStr(statextrec->stxname)));
+ if ((prettyFlags & PRETTYFLAG_SCHEMA) &&
+ StatisticsObjIsVisible(statextid))
+ appendStringInfo(&buf, "CREATE STATISTICS %s",
+ quote_identifier(NameStr(statextrec->stxname)));
+ else
+ {
+ nsp = get_namespace_name_or_temp(statextrec->stxnamespace);
+ appendStringInfo(&buf, "CREATE STATISTICS %s",
+ quote_qualified_identifier(nsp,
+ NameStr(statextrec->stxname)));
+ pfree(nsp);
+ }
/*
* Decode the stxkind column so that we know which stats types to
@@ -2163,10 +2215,9 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
{
Node *expr = (Node *) lfirst(lc);
char *str;
- int prettyFlags = PRETTYFLAG_PAREN;
str = deparse_expression_pretty(expr, context, false, false,
- prettyFlags, 0);
+ PRETTYFLAG_PAREN, 0);
if (colno > 0)
appendStringInfoString(&buf, ", ");
@@ -2544,6 +2595,20 @@ pg_get_constraintdef_command(Oid constraintId)
return pg_get_constraintdef_worker(constraintId, true, 0, false);
}
+/*
+ * pg_get_constraintdef_body
+ * Returns the constraint definition body without the
+ * "ALTER TABLE name ADD CONSTRAINT cname" prefix.
+ * FK target relation names in the body already use
+ * generate_relation_name (search_path-aware). For use by
+ * pg_get_table_ddl which builds the ALTER TABLE prefix itself.
+ */
+char *
+pg_get_constraintdef_body(Oid constraintId)
+{
+ return pg_get_constraintdef_worker(constraintId, false, 0, false);
+}
+
/*
* As of 9.4, we now use an MVCC snapshot for this.
*/
diff --git a/src/include/catalog/pg_inherits.h b/src/include/catalog/pg_inherits.h
index cc874abaabb..1e463b0106d 100644
--- a/src/include/catalog/pg_inherits.h
+++ b/src/include/catalog/pg_inherits.h
@@ -55,6 +55,7 @@ DECLARE_INDEX(pg_inherits_parent_index, 2187, InheritsParentIndexId, pg_inherits
extern List *find_inheritance_children(Oid parentrelId, LOCKMODE lockmode);
extern List *find_inheritance_children_extended(Oid parentrelId, bool omit_detached,
LOCKMODE lockmode, bool *detached_exist, TransactionId *detached_xmin);
+extern List *find_inheritance_parents(Oid relid, LOCKMODE lockmode);
extern List *find_all_inheritors(Oid parentrelId, LOCKMODE lockmode,
List **numparents);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..d86af56ba78 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8634,6 +8634,14 @@
proargtypes => 'regdatabase bool bool bool',
proargnames => '{database,pretty,owner,tablespace}',
proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' },
+{ oid => '8215', descr => 'get DDL to recreate a table',
+ proname => 'pg_get_table_ddl', prorows => '50',
+ proisstrict => 'f', proretset => 't', provolatile => 's', proparallel => 'r',
+ pronargdefaults => '6', prorettype => 'text',
+ proargtypes => 'regclass bool bool bool bool _text _text',
+ proargnames => '{relation,pretty,owner,tablespace,schema_qualified,only_kinds,except_kinds}',
+ proargdefaults => '{false,true,true,true,NULL,NULL}',
+ prosrc => 'pg_get_table_ddl' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index c3d8518cb62..d33563fdead 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -92,6 +92,7 @@ extern void check_of_type(HeapTuple typetuple);
extern void register_on_commit_action(Oid relid, OnCommitAction action);
extern void remove_on_commit_action(Oid relid);
+extern OnCommitAction get_on_commit_action(Oid relid);
extern void PreCommit_on_commit_actions(void);
extern void AtEOXact_on_commit_actions(bool isCommit);
@@ -108,4 +109,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
List *partConstraint);
+extern const char *storage_name(char c);
+
#endif /* TABLECMDS_H */
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 25c05e2f649..c3b3dc65c93 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -25,6 +25,7 @@ typedef struct PlannedStmt PlannedStmt;
#define RULE_INDEXDEF_KEYS_ONLY 0x02 /* ignore included attributes */
extern char *pg_get_indexdef_string(Oid indexrelid);
+extern char *pg_get_indexdef_ddl(Oid indexrelid);
extern char *pg_get_indexdef_columns(Oid indexrelid, bool pretty);
extern char *pg_get_indexdef_columns_extended(Oid indexrelid,
uint16 flags);
@@ -34,6 +35,7 @@ extern char *pg_get_partkeydef_columns(Oid relid, bool pretty);
extern char *pg_get_partconstrdef_string(Oid partitionId, char *aliasname);
extern char *pg_get_constraintdef_command(Oid constraintId);
+extern char *pg_get_constraintdef_body(Oid constraintId);
extern char *deparse_expression(Node *expr, List *dpcontext,
bool forceprefix, bool showimplicit);
extern List *deparse_context_for(const char *aliasname, Oid relid);
@@ -54,5 +56,7 @@ extern char *get_range_partbound_string(List *bound_datums);
extern void get_reloptions(StringInfo buf, Datum reloptions);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern char *pg_get_statisticsobjdef_ddl(Oid statextid);
+extern char *pg_get_ruledef_ddl(Oid ruleoid);
#endif /* RULEUTILS_H */
diff --git a/src/test/regress/expected/pg_get_table_ddl.out b/src/test/regress/expected/pg_get_table_ddl.out
new file mode 100644
index 00000000000..bb28491b95d
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,1518 @@
+--
+-- pg_get_table_ddl
+--
+-- All tests pass owner=>false so the ALTER TABLE OWNER TO line is not
+-- emitted, keeping output stable across test runners (which may run under
+-- different role names).
+--
+CREATE SCHEMA pgtbl_ddl_test;
+SET search_path = pgtbl_ddl_test;
+-- Basic table with PRIMARY KEY, NOT NULL, DEFAULT, COLLATE.
+CREATE TABLE basic (
+ id int PRIMARY KEY,
+ name text NOT NULL DEFAULT 'anon' COLLATE "C"
+);
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.basic (id integer NOT NULL, name text COLLATE "C" DEFAULT 'anon'::text NOT NULL);
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- Identity columns (with default and custom sequence options).
+CREATE TABLE id_cols (
+ id_always int GENERATED ALWAYS AS IDENTITY,
+ id_default int GENERATED BY DEFAULT AS IDENTITY
+);
+SELECT * FROM pg_get_table_ddl('id_cols'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.id_cols (id_always integer GENERATED ALWAYS AS IDENTITY NOT NULL, id_default integer GENERATED BY DEFAULT AS IDENTITY NOT NULL);
+(1 row)
+
+CREATE TABLE id_custom (
+ v int GENERATED ALWAYS AS IDENTITY (
+ SEQUENCE NAME id_custom_v_seq
+ START WITH 100 INCREMENT BY 5
+ MINVALUE 50 MAXVALUE 1000
+ CACHE 10 CYCLE
+ )
+);
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.id_custom (v integer GENERATED ALWAYS AS IDENTITY (START WITH 100 INCREMENT BY 5 MINVALUE 50 MAXVALUE 1000 CACHE 10 CYCLE) NOT NULL);
+(1 row)
+
+-- Generated stored column.
+CREATE TABLE gen_cols (
+ cents int,
+ dollars numeric GENERATED ALWAYS AS (cents / 100.0) STORED
+);
+SELECT * FROM pg_get_table_ddl('gen_cols'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.gen_cols (cents integer, dollars numeric GENERATED ALWAYS AS (((cents)::numeric / 100.0)) STORED);
+(1 row)
+
+-- Generated virtual column.
+CREATE TABLE gen_virtual (
+ base int,
+ derived int GENERATED ALWAYS AS (base * 2) VIRTUAL
+);
+SELECT * FROM pg_get_table_ddl('gen_virtual'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.gen_virtual (base integer, derived integer GENERATED ALWAYS AS ((base * 2)) VIRTUAL);
+(1 row)
+
+-- STORAGE and COMPRESSION (only emitted when non-default for the type).
+CREATE TABLE storage_cols (
+ a text STORAGE EXTERNAL,
+ b text STORAGE MAIN,
+ c text COMPRESSION pglz
+);
+SELECT * FROM pg_get_table_ddl('storage_cols'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.storage_cols (a text STORAGE EXTERNAL, b text STORAGE MAIN, c text COMPRESSION pglz);
+(1 row)
+
+-- Constraints: CHECK, UNIQUE, FOREIGN KEY (DEFERRABLE).
+CREATE TABLE refd (id int PRIMARY KEY);
+CREATE TABLE cons (
+ a int CHECK (a > 0),
+ b int UNIQUE,
+ c int REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED
+);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(3 rows)
+
+-- UNIQUE NULLS NOT DISTINCT (PG 15): null values are treated as equal for
+-- uniqueness purposes, so the NULLS NOT DISTINCT clause must be emitted.
+CREATE TABLE nulls_nd (
+ a int,
+ b int,
+ UNIQUE NULLS NOT DISTINCT (a, b)
+);
+SELECT * FROM pg_get_table_ddl('nulls_nd'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.nulls_nd (a integer, b integer);
+ ALTER TABLE pgtbl_ddl_test.nulls_nd ADD CONSTRAINT nulls_nd_a_b_key UNIQUE NULLS NOT DISTINCT (a, b);
+(2 rows)
+
+-- INCLUDE columns on a constraint-backed UNIQUE index (PG 11): the
+-- covering columns appear in an INCLUDE (...) clause on the ALTER TABLE
+-- ADD CONSTRAINT statement, not in the key column list.
+CREATE TABLE idx_inc (
+ id int PRIMARY KEY,
+ name text,
+ extra text,
+ UNIQUE (name) INCLUDE (extra)
+);
+SELECT * FROM pg_get_table_ddl('idx_inc'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idx_inc (id integer NOT NULL, name text, extra text);
+ ALTER TABLE pgtbl_ddl_test.idx_inc ADD CONSTRAINT idx_inc_name_extra_key UNIQUE (name) INCLUDE (extra);
+ ALTER TABLE pgtbl_ddl_test.idx_inc ADD CONSTRAINT idx_inc_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- FOREIGN KEY with ON DELETE / ON UPDATE referential actions and MATCH
+-- clause. Each variant must appear verbatim in the reconstructed DDL.
+CREATE TABLE fk_acts_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_acts (
+ a int,
+ b int,
+ c int,
+ d int,
+ CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES fk_acts_tgt(id)
+ ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET NULL,
+ CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET DEFAULT,
+ CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES fk_acts_tgt(id)
+ MATCH FULL
+);
+SELECT * FROM pg_get_table_ddl('fk_acts'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.fk_acts (a integer, b integer, c integer, d integer);
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON UPDATE RESTRICT ON DELETE CASCADE;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) MATCH FULL;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON DELETE SET DEFAULT;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON DELETE SET NULL;
+(5 rows)
+
+-- NOT ENFORCED foreign key (PG 17): the constraint is recorded in the
+-- catalog but not validated by the engine; NOT ENFORCED must appear in
+-- the reconstructed DDL.
+CREATE TABLE notenf_tgt (id int PRIMARY KEY);
+CREATE TABLE notenf (
+ a int,
+ CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES notenf_tgt(id)
+ NOT ENFORCED
+);
+SELECT * FROM pg_get_table_ddl('notenf'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.notenf (a integer);
+ ALTER TABLE pgtbl_ddl_test.notenf ADD CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.notenf_tgt(id) NOT ENFORCED;
+(2 rows)
+
+-- Self-referencing FK: the PK must appear before the FK that references it.
+-- The catalog scan returns constraints in name order; without the fix,
+-- "self_ref_parent_id_fkey" sorts before "self_ref_pkey" and the FK ADD
+-- CONSTRAINT fails with "there is no unique constraint matching given keys".
+CREATE TABLE self_ref (
+ id int PRIMARY KEY,
+ parent_id int REFERENCES self_ref(id)
+);
+SELECT * FROM pg_get_table_ddl('self_ref'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.self_ref (id integer NOT NULL, parent_id integer);
+ ALTER TABLE pgtbl_ddl_test.self_ref ADD CONSTRAINT self_ref_pkey PRIMARY KEY (id);
+ ALTER TABLE pgtbl_ddl_test.self_ref ADD CONSTRAINT self_ref_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES pgtbl_ddl_test.self_ref(id);
+(3 rows)
+
+-- WITHOUT OVERLAPS on a PRIMARY KEY (PG 17): the range-type period
+-- column forms a temporal primary key; WITHOUT OVERLAPS must appear
+-- inside the key column list in the reconstructed ALTER TABLE.
+-- Both columns use range types with native GiST support so no
+-- btree_gist extension is needed.
+CREATE TABLE temporal_pk (
+ grp int4range,
+ during daterange,
+ PRIMARY KEY (grp, during WITHOUT OVERLAPS)
+);
+SELECT * FROM pg_get_table_ddl('temporal_pk'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.temporal_pk (grp int4range NOT NULL, during daterange NOT NULL);
+ ALTER TABLE pgtbl_ddl_test.temporal_pk ADD CONSTRAINT temporal_pk_pkey PRIMARY KEY (grp, during WITHOUT OVERLAPS);
+(2 rows)
+
+-- Indexes: functional and partial. Constraint-backing indexes are
+-- suppressed (they are emitted by the constraint loop).
+CREATE TABLE idxd (id int PRIMARY KEY, name text);
+CREATE INDEX idxd_lower ON idxd (lower(name));
+CREATE INDEX idxd_partial ON idxd (id) WHERE id > 100;
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- Inheritance, including a child DEFAULT override on an inherited column.
+CREATE TABLE par (a int DEFAULT 1, b text);
+CREATE TABLE ch (c int) INHERITS (par);
+ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ch (c integer) INHERITS (pgtbl_ddl_test.par);
+ ALTER TABLE pgtbl_ddl_test.ch ALTER COLUMN a SET DEFAULT 999;
+(2 rows)
+
+-- Per-column attoptions: emitted as ALTER COLUMN SET (...).
+CREATE TABLE attopt (a int, b text);
+ALTER TABLE attopt ALTER COLUMN a SET (n_distinct = 100);
+SELECT * FROM pg_get_table_ddl('attopt'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.attopt (a integer, b text);
+ ALTER TABLE pgtbl_ddl_test.attopt ALTER COLUMN a SET (n_distinct='100');
+(2 rows)
+
+-- Partitioned table parents (RANGE, HASH, and LIST).
+CREATE TABLE parted_range (id int, k int) PARTITION BY RANGE (id);
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range (id integer, k integer) PARTITION BY RANGE (id);
+(1 row)
+
+CREATE TABLE parted_hash (id int) PARTITION BY HASH (id);
+SELECT * FROM pg_get_table_ddl('parted_hash'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash (id integer) PARTITION BY HASH (id);
+(1 row)
+
+CREATE TABLE parted_list (id int, region text) PARTITION BY LIST (region);
+SELECT * FROM pg_get_table_ddl('parted_list'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_list (id integer, region text) PARTITION BY LIST (region);
+(1 row)
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_1 PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pgtbl_ddl_test.parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_hash_0 PARTITION OF pgtbl_ddl_test.parted_hash FOR VALUES WITH (modulus 2, remainder 0);
+(1 row)
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range_def PARTITION OF pgtbl_ddl_test.parted_range DEFAULT;
+(1 row)
+
+-- Non-constraint-backed index on a partitioned table: the parent's index DDL
+-- must appear once; the auto-created child partition indexes (relispartition=t)
+-- must be suppressed so the DDL round-trips without "relation already exists".
+CREATE TABLE parted_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_idx_a PARTITION OF parted_idx FOR VALUES IN ('a');
+CREATE UNIQUE INDEX parted_idx_uidx ON parted_idx (id, region);
+SELECT * FROM pg_get_table_ddl('parted_idx'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_idx (id integer, region text) PARTITION BY LIST (region);
+ CREATE UNIQUE INDEX parted_idx_uidx ON pgtbl_ddl_test.parted_idx USING btree (id, region);
+ CREATE TABLE pgtbl_ddl_test.parted_idx_a PARTITION OF pgtbl_ddl_test.parted_idx FOR VALUES IN ('a');
+(3 rows)
+
+-- Plain (non-unique) CREATE INDEX on partitioned table: the auto-created child
+-- index (relispartition=true) is suppressed, just like the UNIQUE case.
+CREATE TABLE parted_plain (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE parted_plain_a PARTITION OF parted_plain FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_plain_id_idx ON parted_plain (id);
+SELECT * FROM pg_get_table_ddl('parted_plain'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_plain (id integer, val text) PARTITION BY RANGE (id);
+ CREATE INDEX parted_plain_id_idx ON pgtbl_ddl_test.parted_plain USING btree (id);
+ CREATE TABLE pgtbl_ddl_test.parted_plain_a PARTITION OF pgtbl_ddl_test.parted_plain FOR VALUES FROM (0) TO (100);
+(3 rows)
+
+-- Constraint-backed PRIMARY KEY on partitioned table: the child's inherited
+-- constraint (conislocal=false) and its backing index are both suppressed.
+CREATE TABLE parted_pk (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_pk_a PARTITION OF parted_pk FOR VALUES IN ('a');
+ALTER TABLE parted_pk ADD PRIMARY KEY (id, region);
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_pk (id integer NOT NULL, region text NOT NULL) PARTITION BY LIST (region);
+ ALTER TABLE pgtbl_ddl_test.parted_pk ADD CONSTRAINT parted_pk_pkey PRIMARY KEY (id, region);
+ CREATE TABLE pgtbl_ddl_test.parted_pk_a PARTITION OF pgtbl_ddl_test.parted_pk FOR VALUES IN ('a');
+(3 rows)
+
+-- Multi-level (sub-partitioned) table with a CREATE INDEX on the root.
+-- The mid-level partitioned index (relispartition=true, relkind='I') and the
+-- leaf index (relispartition=true, relkind='i') are both suppressed.
+CREATE TABLE parted_sub (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_sub_a PARTITION OF parted_sub
+ FOR VALUES IN ('a') PARTITION BY RANGE (id);
+CREATE TABLE parted_sub_a1 PARTITION OF parted_sub_a FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_sub_id_idx ON parted_sub (id);
+SELECT * FROM pg_get_table_ddl('parted_sub'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_sub (id integer, region text) PARTITION BY LIST (region);
+ CREATE INDEX parted_sub_id_idx ON pgtbl_ddl_test.parted_sub USING btree (id);
+ CREATE TABLE pgtbl_ddl_test.parted_sub_a PARTITION OF pgtbl_ddl_test.parted_sub FOR VALUES IN ('a') PARTITION BY RANGE (id);
+ CREATE TABLE pgtbl_ddl_test.parted_sub_a1 PARTITION OF pgtbl_ddl_test.parted_sub_a FOR VALUES FROM (0) TO (100);
+(4 rows)
+
+-- Partition child with its own locally-defined index (relispartition=false):
+-- this is NOT inherited from any parent partitioned index and must be emitted.
+CREATE TABLE parted_child_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_child_idx_a PARTITION OF parted_child_idx FOR VALUES IN ('a');
+CREATE INDEX parted_child_idx_a_id ON parted_child_idx_a (id);
+SELECT * FROM pg_get_table_ddl('parted_child_idx'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_child_idx (id integer, region text) PARTITION BY LIST (region);
+ CREATE TABLE pgtbl_ddl_test.parted_child_idx_a PARTITION OF pgtbl_ddl_test.parted_child_idx FOR VALUES IN ('a');
+ CREATE INDEX parted_child_idx_a_id ON pgtbl_ddl_test.parted_child_idx_a USING btree (id);
+(3 rows)
+
+-- Mix: parent has a CREATE INDEX (auto-propagated to child, relispartition=true,
+-- suppressed) AND the child has its own local index (relispartition=false, emitted).
+CREATE TABLE parted_mixed (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_mixed_a PARTITION OF parted_mixed FOR VALUES IN ('a');
+CREATE INDEX parted_mixed_id ON parted_mixed (id);
+CREATE INDEX parted_mixed_a_region ON parted_mixed_a (region);
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_mixed (id integer, region text) PARTITION BY LIST (region);
+ CREATE INDEX parted_mixed_id ON pgtbl_ddl_test.parted_mixed USING btree (id);
+ CREATE TABLE pgtbl_ddl_test.parted_mixed_a PARTITION OF pgtbl_ddl_test.parted_mixed FOR VALUES IN ('a');
+ CREATE INDEX parted_mixed_a_region ON pgtbl_ddl_test.parted_mixed_a USING btree (region);
+(4 rows)
+
+-- FK on a partitioned table: declared on the parent; partition children inherit
+-- the constraint with conislocal=false and must not re-emit it.
+CREATE TABLE parted_fk_tgt (id int PRIMARY KEY);
+CREATE TABLE parted_fk (id int, tgt_id int REFERENCES parted_fk_tgt(id))
+ PARTITION BY RANGE (id);
+CREATE TABLE parted_fk_a PARTITION OF parted_fk FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('parted_fk'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_fk (id integer, tgt_id integer) PARTITION BY RANGE (id);
+ ALTER TABLE pgtbl_ddl_test.parted_fk ADD CONSTRAINT parted_fk_tgt_id_fkey FOREIGN KEY (tgt_id) REFERENCES pgtbl_ddl_test.parted_fk_tgt(id);
+ CREATE TABLE pgtbl_ddl_test.parted_fk_a PARTITION OF pgtbl_ddl_test.parted_fk FOR VALUES FROM (0) TO (100);
+(3 rows)
+
+-- Statistics declared explicitly on a partition child (not auto-propagated):
+-- the child's statistics object appears inside the child's DDL block.
+CREATE TABLE parted_stx (id int, region text, val float) PARTITION BY LIST (region);
+CREATE TABLE parted_stx_a PARTITION OF parted_stx FOR VALUES IN ('a');
+CREATE STATISTICS parted_stx_stat (ndistinct) ON id, region FROM parted_stx;
+CREATE STATISTICS parted_stx_a_stat (ndistinct) ON id, val FROM parted_stx_a;
+SELECT * FROM pg_get_table_ddl('parted_stx'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_stx (id integer, region text, val double precision) PARTITION BY LIST (region);
+ CREATE STATISTICS pgtbl_ddl_test.parted_stx_stat (ndistinct) ON id, region FROM pgtbl_ddl_test.parted_stx;
+ CREATE TABLE pgtbl_ddl_test.parted_stx_a PARTITION OF pgtbl_ddl_test.parted_stx FOR VALUES IN ('a');
+ CREATE STATISTICS pgtbl_ddl_test.parted_stx_a_stat (ndistinct) ON id, val FROM pgtbl_ddl_test.parted_stx_a;
+(4 rows)
+
+DROP TABLE parted_stx;
+-- NO INHERIT CHECK constraint: the NO INHERIT flag must appear verbatim in the
+-- inline constraint definition inside the CREATE TABLE body.
+CREATE TABLE noinhchk (id int, val int,
+ CONSTRAINT val_pos CHECK (val > 0) NO INHERIT);
+SELECT * FROM pg_get_table_ddl('noinhchk'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.noinhchk (id integer, val integer, CONSTRAINT val_pos CHECK ((val > 0)) NO INHERIT);
+(1 row)
+
+DROP TABLE noinhchk;
+-- Multiple inheritance parents: the INHERITS clause must list both parents in
+-- declaration order; only the locally-defined column appears in the column list.
+CREATE TABLE mi_p1 (a int DEFAULT 1);
+CREATE TABLE mi_p2 (b text DEFAULT 'x');
+CREATE TABLE mi_child (c float) INHERITS (mi_p1, mi_p2);
+SELECT * FROM pg_get_table_ddl('mi_child'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.mi_child (c double precision) INHERITS (pgtbl_ddl_test.mi_p1, pgtbl_ddl_test.mi_p2);
+ ALTER TABLE pgtbl_ddl_test.mi_child ALTER COLUMN a SET DEFAULT 1;
+ ALTER TABLE pgtbl_ddl_test.mi_child ALTER COLUMN b SET DEFAULT 'x'::text;
+(3 rows)
+
+DROP TABLE mi_p1, mi_p2 CASCADE;
+NOTICE: drop cascades to table mi_child
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO pgtbl_ddl_test.rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM pgtbl_ddl_test.stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx (a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(3 rows)
+
+-- UNLOGGED + reloptions.
+CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70);
+SELECT * FROM pg_get_table_ddl('uno'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE UNLOGGED TABLE pgtbl_ddl_test.uno (id integer) WITH (fillfactor='70');
+(1 row)
+
+-- Typed table (CREATE TABLE OF type_name). Columns inherited from the
+-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come
+-- out through a single "(col WITH OPTIONS ...)" list.
+CREATE TYPE typed_t AS (a int, b text);
+CREATE TABLE typed_plain OF typed_t;
+SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_plain OF pgtbl_ddl_test.typed_t;
+(1 row)
+
+CREATE TABLE typed_over OF typed_t (
+ a WITH OPTIONS DEFAULT 7 NOT NULL,
+ b WITH OPTIONS NOT NULL,
+ CONSTRAINT b_nonempty CHECK (length(b) > 0)
+);
+SELECT * FROM pg_get_table_ddl('typed_over'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_over OF pgtbl_ddl_test.typed_t (a WITH OPTIONS DEFAULT 7 NOT NULL, b WITH OPTIONS NOT NULL, CONSTRAINT b_nonempty CHECK ((length(b) > 0)));
+(1 row)
+
+-- Temporary tables + ON COMMIT. Temp tables must never be emitted with
+-- the session-private pg_temp_NN schema prefix -- the TEMPORARY keyword
+-- already places the table in pg_temp and the prefix is non-replayable.
+-- ON COMMIT DROP only fires at commit, so the queries that need to see
+-- the catalog entry must run in the same transaction as the CREATE.
+BEGIN;
+CREATE TEMP TABLE temp_default (id int);
+CREATE TEMP TABLE temp_delete (id int) ON COMMIT DELETE ROWS;
+CREATE TEMP TABLE temp_drop (id int) ON COMMIT DROP;
+SELECT line FROM pg_get_table_ddl('temp_default'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------
+ CREATE TEMPORARY TABLE temp_default (id integer);
+(1 row)
+
+SELECT line FROM pg_get_table_ddl('temp_delete'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE temp_delete (id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT line FROM pg_get_table_ddl('temp_drop'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------
+ CREATE TEMPORARY TABLE temp_drop (id integer) ON COMMIT DROP;
+(1 row)
+
+ROLLBACK;
+-- Pretty mode.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, pretty => true);
+ pg_get_table_ddl
+------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.basic ( +
+ id integer NOT NULL, +
+ name text COLLATE "C" DEFAULT 'anon'::text NOT NULL +
+ );
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- only_kinds / except_kinds gating: each emits either only the listed kinds
+-- (only_kinds) or every kind except the listed ones (except_kinds). The two
+-- are mutually exclusive. Kind vocabulary: table, index,
+-- primary_key, unique, check, foreign_key, exclusion, rule,
+-- statistics, trigger, policy, rls, replica_identity, partition.
+-- NOT NULL is not in the vocabulary - always emitted to prevent
+-- producing schemas that silently accept NULLs the source would
+-- have rejected.
+--
+-- except_kinds=index hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['index']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- except_kinds=primary_key,unique,check,foreign_key,exclusion hides every
+-- table-level constraint. Inline CHECK in the CREATE TABLE body is
+-- suppressed too.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['primary_key','unique','check','foreign_key','exclusion']);
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=foreign_key suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- first pass of the two-pass FK-clone workflow: emit tables without
+-- cross-table FKs, load data, then re-run with only_kinds=foreign_key
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+(2 rows)
+
+-- only_kinds=foreign_key is the second pass of the two-pass FK-clone
+-- workflow: emit only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN
+-- KEY rows; the CREATE TABLE and every non-FK sub-object are
+-- suppressed (the table already exists from the first pass).
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(1 row)
+
+-- A table with no foreign keys produces no rows under
+-- only_kinds=foreign_key.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- only_kinds=table emits only the bare CREATE TABLE (with any inline
+-- column-level NOT NULL and CHECK) - no indexes, no PRIMARY KEY ALTER,
+-- no other sub-objects. Useful for capturing a table's shape without
+-- pulling in everything attached to it.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table']);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+(1 row)
+
+-- only_kinds=check on a non-partition table. The constraint loop must emit
+-- column-level and table-level CHECK constraints via ALTER TABLE here,
+-- because the inline path in CREATE TABLE never runs (KIND_TABLE is
+-- not in the filter).
+CREATE TABLE chk_only (
+ id int PRIMARY KEY,
+ qty int CHECK (qty > 0),
+ CONSTRAINT chk_only_id_pos CHECK (id > 0)
+);
+SELECT * FROM pg_get_table_ddl('chk_only'::regclass, owner => false, only_kinds => ARRAY['check']);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.chk_only ADD CONSTRAINT chk_only_id_pos CHECK ((id > 0));
+ ALTER TABLE pgtbl_ddl_test.chk_only ADD CONSTRAINT chk_only_qty_check CHECK ((qty > 0));
+(2 rows)
+
+DROP TABLE chk_only;
+-- only_kinds=partition on a partitioned-table parent: each child's full DDL
+-- is emitted (CREATE TABLE ... PARTITION OF ...). The "partition"
+-- keyword is a gate at the parent level; it is stripped from the
+-- propagated filter so the child does not exclude its own
+-- CREATE TABLE.
+CREATE TABLE p_only (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE p_only_a PARTITION OF p_only FOR VALUES FROM (0) TO (100);
+CREATE TABLE p_only_b PARTITION OF p_only FOR VALUES FROM (100) TO (200);
+SELECT * FROM pg_get_table_ddl('p_only'::regclass, owner => false, only_kinds => ARRAY['partition']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.p_only_a PARTITION OF pgtbl_ddl_test.p_only FOR VALUES FROM (0) TO (100);
+ CREATE TABLE pgtbl_ddl_test.p_only_b PARTITION OF pgtbl_ddl_test.p_only FOR VALUES FROM (100) TO (200);
+(2 rows)
+
+DROP TABLE p_only;
+-- only_kinds and except_kinds are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false,
+ only_kinds => ARRAY['foreign_key'],
+ except_kinds => ARRAY['foreign_key']);
+ERROR: "only_kinds" and "except_kinds" parameters are mutually exclusive
+-- Pub/sub schema clone: keep the table and its primary key, drop the
+-- rest of the constraints so the subscriber can replicate without
+-- pulling cross-table dependencies along.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique','check','foreign_key','exclusion']);
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=rule hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, except_kinds => ARRAY['rule']);
+ pg_get_table_ddl
+----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+(1 row)
+
+-- except_kinds=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, except_kinds => ARRAY['statistics']);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, except_kinds => ARRAY['rls']);
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+(1 row)
+
+-- except_kinds=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, except_kinds => ARRAY['replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+(1 row)
+
+-- Partition children are emitted by default; except_kinds=partition
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false, except_kinds => ARRAY['partition']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range (id integer, k integer) PARTITION BY RANGE (id);
+(1 row)
+
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY[' INDEX ',' RULE ']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- schema_qualified=false strips the schema from the target table itself
+-- and from same-schema sibling references (inheritance/partition
+-- parents, identity sequences in the same schema), while cross-schema
+-- references are kept qualified for correctness.
+--
+-- Bare CREATE TABLE for an ordinary table.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE basic (id integer NOT NULL, name text COLLATE "C" DEFAULT 'anon'::text NOT NULL);
+ ALTER TABLE basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- INHERITS parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------
+ CREATE TABLE ch (c integer) INHERITS (par);
+ ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+(2 rows)
+
+-- Partition parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE parted_range_1 PARTITION OF parted_range FOR VALUES FROM (0) TO (100);
+ ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+(2 rows)
+
+-- Cross-schema FK target keeps qualification. The 'refd' table is in
+-- pgtbl_ddl_test; place an FK target in a different schema and verify it
+-- stays qualified even with schema_qualified=false.
+CREATE SCHEMA pgtbl_ddl_other;
+CREATE TABLE pgtbl_ddl_other.parent (id int PRIMARY KEY);
+CREATE TABLE xschema_fk (
+ id int PRIMARY KEY,
+ pid int REFERENCES pgtbl_ddl_other.parent
+);
+SELECT * FROM pg_get_table_ddl('xschema_fk'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE xschema_fk (id integer NOT NULL, pid integer);
+ ALTER TABLE xschema_fk ADD CONSTRAINT xschema_fk_pkey PRIMARY KEY (id);
+ ALTER TABLE xschema_fk ADD CONSTRAINT xschema_fk_pid_fkey FOREIGN KEY (pid) REFERENCES pgtbl_ddl_other.parent(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
+-- Cross-schema partition child with schema_qualified=false: the child must
+-- carry its own schema prefix so the output is unambiguous regardless of the
+-- caller's search_path. Before the fix, the child was emitted as the bare
+-- name "pt_c" (landing in whichever schema is first in search_path) while
+-- the parent reference inside the CREATE TABLE came out as the fully-qualified
+-- "pgtbl_ddl_part_s.pt", making the DDL inconsistent and unreplayable.
+CREATE SCHEMA pgtbl_ddl_part_s;
+CREATE SCHEMA pgtbl_ddl_part_other;
+CREATE TABLE pgtbl_ddl_part_s.pt (id int, val int) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_part_other.pt_c
+ PARTITION OF pgtbl_ddl_part_s.pt
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_part_s.pt'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------
+ CREATE TABLE pt (id integer, val integer) PARTITION BY RANGE (id);
+ CREATE TABLE pgtbl_ddl_part_other.pt_c PARTITION OF pgtbl_ddl_part_s.pt FOR VALUES FROM (0) TO (100);
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_part_s CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_part_s.pt
+DROP SCHEMA pgtbl_ddl_part_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);
+ 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. String
+-- literal text is output verbatim by the deparser and is unaffected by
+-- search_path narrowing.
+CREATE SCHEMA pgtbl_ddl_str;
+CREATE TABLE pgtbl_ddl_str.p (id int, note text) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_str.pc PARTITION OF pgtbl_ddl_str.p
+ (CONSTRAINT chk CHECK (note <> 'pgtbl_ddl_str.secret'))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_str.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK ((note <> 'pgtbl_ddl_str.secret'::text));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_str CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_str.p
+-- schema_qualified=false must NOT corrupt double-quoted identifiers
+-- whose contents happen to contain the schema name followed by a dot
+-- (for example a column literally named "<schema>.weird" inside that
+-- schema). The mechanism is purely search_path narrowing: the deparser
+-- outputs attribute names verbatim from pg_attribute, so the quoted
+-- identifier content is never examined for schema prefixes. This holds
+-- even when the schema name is a single letter.
+CREATE SCHEMA pgtbl_ddl_qid;
+CREATE TABLE pgtbl_ddl_qid.p (id int, "pgtbl_ddl_qid.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_qid.pc PARTITION OF pgtbl_ddl_qid.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_qid.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_qid.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK (("pgtbl_ddl_qid.weird" > 0));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_qid CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_qid.p
+-- Same with a short (single-letter) schema name to confirm there is no
+-- minimum-length assumption in the stripping path.
+CREATE SCHEMA pgtbl_ddl_q1;
+CREATE TABLE pgtbl_ddl_q1.p (id int, "pgtbl_ddl_q1.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_q1.pc PARTITION OF pgtbl_ddl_q1.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_q1.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_q1.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK (("pgtbl_ddl_q1.weird" > 0));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_q1 CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_q1.p
+-- schema_qualified=false when the base schema name itself requires
+-- quoting (its prefix starts with "). Both forms of the prefix -
+-- bare-lowercase and quoted - must be stripped from outer
+-- ALTER TABLE / ON / CREATE INDEX prefixes, while inner quoted
+-- identifiers that happen to begin the same way must be preserved.
+CREATE SCHEMA "PgTbl-Ddl-Q";
+CREATE TABLE "PgTbl-Ddl-Q"."T" (id int, val int,
+ CONSTRAINT chk CHECK (val > 0));
+CREATE INDEX "T_val_idx" ON "PgTbl-Ddl-Q"."T" (val);
+SELECT * FROM pg_get_table_ddl('"PgTbl-Ddl-Q"."T"'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE TABLE "T" (id integer, val integer, CONSTRAINT chk CHECK ((val > 0)));
+ CREATE INDEX "T_val_idx" ON "T" USING btree (val);
+(2 rows)
+
+DROP SCHEMA "PgTbl-Ddl-Q" CASCADE;
+NOTICE: drop cascades to table "PgTbl-Ddl-Q"."T"
+-- Error: not an ordinary or partitioned table.
+CREATE VIEW v AS SELECT 1 AS x;
+SELECT * FROM pg_get_table_ddl('v'::regclass);
+ERROR: "v" is not an ordinary or partitioned table
+CREATE SEQUENCE s;
+SELECT * FROM pg_get_table_ddl('s'::regclass);
+ERROR: "s" is not an ordinary or partitioned table
+-- NULL argument returns no rows.
+SELECT * FROM pg_get_table_ddl(NULL);
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- REPLICA IDENTITY USING INDEX validation: if the referenced index
+-- would not be emitted (because its source kind is suppressed) but
+-- replica_identity itself would be, the function raises an error so
+-- the emitted DDL never dangles. Exercise all three constraint-backed
+-- forms (PK, UNIQUE, EXCLUSION) and a bare CREATE UNIQUE INDEX, in
+-- both the except-list and only-list shapes.
+--
+-- PK-backed: except_kinds=primary_key without except_kinds=replica_identity.
+CREATE TABLE ri_pk_excl (a int PRIMARY KEY);
+ALTER TABLE ri_pk_excl REPLICA IDENTITY USING INDEX ri_pk_excl_pkey;
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+ERROR: REPLICA IDENTITY for table "ri_pk_excl" requires kind "primary_key" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "primary_key" kind, which is not in the active filter.
+HINT: Either add "primary_key" to the filter or remove "replica_identity" from it.
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_pk_excl (a integer NOT NULL);
+(1 row)
+
+DROP TABLE ri_pk_excl;
+-- UNIQUE-constraint-backed replica identity index.
+CREATE TABLE ri_uniq_excl (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_uniq_excl REPLICA IDENTITY USING INDEX ri_uniq_excl_a_key;
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique']);
+ERROR: REPLICA IDENTITY for table "ri_uniq_excl" requires kind "unique" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "unique" kind, which is not in the active filter.
+HINT: Either add "unique" to the filter or remove "replica_identity" from it.
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique','replica_identity']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_uniq_excl (a integer NOT NULL, b integer);
+(1 row)
+
+DROP TABLE ri_uniq_excl;
+-- Bare CREATE UNIQUE INDEX (no backing constraint) used as REPLICA
+-- IDENTITY: the index is emitted by the "index" kind, so excluding
+-- "index" without also excluding "replica_identity" must error.
+CREATE TABLE ri_idx_excl (a int NOT NULL, b int);
+CREATE UNIQUE INDEX ri_idx_excl_a_uniq ON ri_idx_excl (a);
+ALTER TABLE ri_idx_excl REPLICA IDENTITY USING INDEX ri_idx_excl_a_uniq;
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index']);
+ERROR: REPLICA IDENTITY for table "ri_idx_excl" requires kind "index" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "index" kind, which is not in the active filter.
+HINT: Either add "index" to the filter or remove "replica_identity" from it.
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx_excl (a integer NOT NULL, b integer);
+(1 row)
+
+DROP TABLE ri_idx_excl;
+-- Include-side analog: only_kinds=replica_identity must include the kind
+-- that emits the index, or the function errors.
+CREATE TABLE ri_only (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ERROR: REPLICA IDENTITY for table "ri_only" requires kind "unique" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "unique" kind, which is not in the active filter.
+HINT: Either add "unique" to the filter or remove "replica_identity" from it.
+-- Including both kinds succeeds.
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['unique','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.ri_only ADD CONSTRAINT ri_only_a_key UNIQUE (a);
+ ALTER TABLE pgtbl_ddl_test.ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+(2 rows)
+
+DROP TABLE ri_only;
+-- Per-kind only_kinds / except_kinds coverage. For each kind in the vocabulary,
+-- verify that only_kinds=K emits just that kind's output and that except_kinds=K
+-- omits just that kind's output. Kinds with no fixture above (trigger,
+-- policy) are still recognized but currently emit nothing; only_kinds=K
+-- returns zero rows for them. Fixtures used:
+-- idxd: PK + two secondary CREATE INDEX
+-- cons: PK + UNIQUE + CHECK + FK (constraint-backed)
+-- rt: CREATE RULE
+-- stx: CREATE STATISTICS
+-- rls: ENABLE/FORCE ROW LEVEL SECURITY
+-- ri_full: REPLICA IDENTITY FULL (no source-kind dependency)
+CREATE TABLE excl_fix (rng int4range,
+ EXCLUDE USING gist (rng WITH &&));
+-- only_kinds=K: emit only kind K's statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['unique']);
+ pg_get_table_ddl
+-----------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.excl_fix ADD CONSTRAINT excl_fix_rng_excl EXCLUDE USING gist (rng WITH &&);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, only_kinds => ARRAY['rule']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO pgtbl_ddl_test.rt_log (id)+
+ VALUES (new.id);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM pgtbl_ddl_test.stx;
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, only_kinds => ARRAY['rls']);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(1 row)
+
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['policy']);
+-- except_kinds=K: emit everything except kind K's statements. PK/UNIQUE/
+-- INDEX are exercised on tables without REPLICA IDENTITY USING INDEX
+-- so the validation does not trip.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['check']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, except_kinds => ARRAY['exclusion']);
+ pg_get_table_ddl
+-------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.excl_fix (rng int4range);
+(1 row)
+
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['policy']);
+-- except_kinds=table suppresses the CREATE TABLE + OWNER + child-default
+-- SET DEFAULT + attoptions passes, leaving only the sub-object passes.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['table']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- Multi-kind compositions.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table','index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['primary_key','foreign_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(1 row)
+
+-- Duplicate entries in the list are silently de-duplicated by the
+-- Bitmapset.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index','index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+DROP TABLE excl_fix;
+-- Kind-filter tests on partitioned tables with indexes.
+--
+-- only_kinds=index on a partitioned table: only the root CREATE INDEX is
+-- emitted; no CREATE TABLE, no partition children (KIND_PARTITION absent).
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ only_kinds => ARRAY['index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE INDEX parted_mixed_id ON pgtbl_ddl_test.parted_mixed USING btree (id);
+(1 row)
+
+-- except_kinds=index on a partitioned table: CREATE TABLE for the parent
+-- and all children are emitted; no index statements.
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ except_kinds => ARRAY['index']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_mixed (id integer, region text) PARTITION BY LIST (region);
+ CREATE TABLE pgtbl_ddl_test.parted_mixed_a PARTITION OF pgtbl_ddl_test.parted_mixed FOR VALUES IN ('a');
+(2 rows)
+
+-- only_kinds=primary_key on a partitioned table: only the ALTER TABLE ADD
+-- CONSTRAINT PK on the parent; the child's inherited PK (conislocal=false)
+-- is skipped; no CREATE TABLE, no partition children.
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false,
+ only_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.parted_pk ADD CONSTRAINT parted_pk_pkey PRIMARY KEY (id, region);
+(1 row)
+
+-- only_kinds=K on a table that does not have any K of that kind is a valid
+-- filter and returns zero rows. Tested previously with foreign_key
+-- on 'refd'; here also rule, statistics, rls, replica_identity, and
+-- exclusion on 'basic' (which has only PK + columns).
+SELECT count(*) AS rule_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rule']);
+ rule_rows
+-----------
+ 0
+(1 row)
+
+SELECT count(*) AS stats_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+ stats_rows
+------------
+ 0
+(1 row)
+
+SELECT count(*) AS rls_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rls']);
+ rls_rows
+----------
+ 0
+(1 row)
+
+SELECT count(*) AS ri_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ ri_rows
+---------
+ 0
+(1 row)
+
+SELECT count(*) AS excl_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+ excl_rows
+-----------
+ 0
+(1 row)
+
+-- Input-validation edge cases for the kind array. All
+-- of these raise an error; the function never reaches relation open.
+-- Empty array:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[]::text[]);
+ERROR: parameter "only_kinds" must specify at least one kind
+-- NULL element:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[NULL::text]);
+ERROR: parameter "only_kinds" must not contain NULL elements
+-- Unrecognized kind:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY['no_such_kind']);
+ERROR: unrecognized kind "no_such_kind" in parameter "only_kinds"
+-- Same kind specified through both parameters is still mutex-rejected.
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ only_kinds => ARRAY['index'],
+ except_kinds => ARRAY['rule']);
+ERROR: "only_kinds" and "except_kinds" parameters are mutually exclusive
+-- Round-trip verification. For every test table, capture the DDL the
+-- function emits, drop the schema, replay each table's DDL in
+-- dependency order, and confirm that pg_get_table_ddl on the recreated
+-- relation matches the original line-for-line. The final SELECT must
+-- return zero rows.
+CREATE TEMP TABLE pgtbl_ddl_rt_orig (name text, ord int, line text);
+INSERT INTO pgtbl_ddl_rt_orig
+SELECT t.name, o.ord, o.line
+FROM (VALUES
+ ('basic'), ('id_cols'), ('id_custom'), ('gen_cols'), ('gen_virtual'), ('storage_cols'),
+ ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'),
+ ('fk_acts_tgt'), ('fk_acts'), ('notenf_tgt'), ('notenf'), ('self_ref'), ('temporal_pk'),
+ ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('parted_list'),
+ ('parted_idx'), ('parted_idx_a'),
+ ('parted_plain'), ('parted_plain_a'),
+ ('parted_pk'), ('parted_pk_a'),
+ ('parted_sub'), ('parted_sub_a'), ('parted_sub_a1'),
+ ('parted_child_idx'), ('parted_child_idx_a'),
+ ('parted_mixed'), ('parted_mixed_a'),
+ ('parted_fk_tgt'), ('parted_fk'), ('parted_fk_a'),
+ ('rt_log'), ('rt'),
+ ('stx'), ('rls'), ('ri_full'), ('ri_idx'), ('uno')
+ ) AS t(name),
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord);
+DO $$
+DECLARE
+ tables CONSTANT text[] := ARRAY[
+ 'basic', 'id_cols', 'id_custom', 'gen_cols', 'gen_virtual', 'storage_cols',
+ 'refd', 'cons', 'nulls_nd', 'idx_inc',
+ 'fk_acts_tgt', 'fk_acts', 'notenf_tgt', 'notenf', 'self_ref', 'temporal_pk',
+ 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ 'parted_list',
+ 'parted_idx', 'parted_idx_a',
+ 'parted_plain', 'parted_plain_a',
+ 'parted_pk', 'parted_pk_a',
+ 'parted_sub', 'parted_sub_a', 'parted_sub_a1',
+ 'parted_child_idx', 'parted_child_idx_a',
+ 'parted_mixed', 'parted_mixed_a',
+ 'parted_fk_tgt', 'parted_fk', 'parted_fk_a',
+ 'rt_log', 'rt',
+ 'stx', 'rls', 'ri_full', 'ri_idx', 'uno'
+ ];
+ t text;
+ stmt text;
+BEGIN
+ DROP SCHEMA pgtbl_ddl_test CASCADE;
+ CREATE SCHEMA pgtbl_ddl_test;
+ FOREACH t IN ARRAY tables LOOP
+ FOR stmt IN
+ SELECT line FROM pgtbl_ddl_rt_orig WHERE name = t ORDER BY ord
+ LOOP
+ BEGIN
+ EXECUTE stmt;
+ EXCEPTION WHEN duplicate_table THEN
+ NULL; -- partition child already created by its parent's DDL
+ END;
+ END LOOP;
+ END LOOP;
+END $$;
+NOTICE: drop cascades to 43 other objects
+DETAIL: drop cascades to table basic
+drop cascades to table id_cols
+drop cascades to table id_custom
+drop cascades to table gen_cols
+drop cascades to table gen_virtual
+drop cascades to table storage_cols
+drop cascades to table refd
+drop cascades to table cons
+drop cascades to table nulls_nd
+drop cascades to table idx_inc
+drop cascades to table fk_acts_tgt
+drop cascades to table fk_acts
+drop cascades to table notenf_tgt
+drop cascades to table notenf
+drop cascades to table self_ref
+drop cascades to table temporal_pk
+drop cascades to table idxd
+drop cascades to table par
+drop cascades to table ch
+drop cascades to table attopt
+drop cascades to table parted_range
+drop cascades to table parted_hash
+drop cascades to table parted_list
+drop cascades to table parted_idx
+drop cascades to table parted_plain
+drop cascades to table parted_pk
+drop cascades to table parted_sub
+drop cascades to table parted_child_idx
+drop cascades to table parted_mixed
+drop cascades to table parted_fk_tgt
+drop cascades to table parted_fk
+drop cascades to table rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 38 other objects
+DETAIL: drop cascades to table basic
+drop cascades to table id_cols
+drop cascades to table id_custom
+drop cascades to table gen_cols
+drop cascades to table gen_virtual
+drop cascades to table storage_cols
+drop cascades to table refd
+drop cascades to table cons
+drop cascades to table nulls_nd
+drop cascades to table idx_inc
+drop cascades to table fk_acts_tgt
+drop cascades to table fk_acts
+drop cascades to table notenf_tgt
+drop cascades to table notenf
+drop cascades to table self_ref
+drop cascades to table temporal_pk
+drop cascades to table idxd
+drop cascades to table par
+drop cascades to table ch
+drop cascades to table attopt
+drop cascades to table parted_range
+drop cascades to table parted_hash
+drop cascades to table parted_list
+drop cascades to table parted_idx
+drop cascades to table parted_plain
+drop cascades to table parted_pk
+drop cascades to table parted_sub
+drop cascades to table parted_child_idx
+drop cascades to table parted_mixed
+drop cascades to table parted_fk_tgt
+drop cascades to table parted_fk
+drop cascades to table rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..badc41ff7eb
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,879 @@
+--
+-- pg_get_table_ddl
+--
+-- All tests pass owner=>false so the ALTER TABLE OWNER TO line is not
+-- emitted, keeping output stable across test runners (which may run under
+-- different role names).
+--
+CREATE SCHEMA pgtbl_ddl_test;
+SET search_path = pgtbl_ddl_test;
+
+-- Basic table with PRIMARY KEY, NOT NULL, DEFAULT, COLLATE.
+CREATE TABLE basic (
+ id int PRIMARY KEY,
+ name text NOT NULL DEFAULT 'anon' COLLATE "C"
+);
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false);
+
+-- Identity columns (with default and custom sequence options).
+CREATE TABLE id_cols (
+ id_always int GENERATED ALWAYS AS IDENTITY,
+ id_default int GENERATED BY DEFAULT AS IDENTITY
+);
+SELECT * FROM pg_get_table_ddl('id_cols'::regclass, owner => false);
+
+CREATE TABLE id_custom (
+ v int GENERATED ALWAYS AS IDENTITY (
+ SEQUENCE NAME id_custom_v_seq
+ START WITH 100 INCREMENT BY 5
+ MINVALUE 50 MAXVALUE 1000
+ CACHE 10 CYCLE
+ )
+);
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false);
+
+-- Generated stored column.
+CREATE TABLE gen_cols (
+ cents int,
+ dollars numeric GENERATED ALWAYS AS (cents / 100.0) STORED
+);
+SELECT * FROM pg_get_table_ddl('gen_cols'::regclass, owner => false);
+
+-- Generated virtual column.
+CREATE TABLE gen_virtual (
+ base int,
+ derived int GENERATED ALWAYS AS (base * 2) VIRTUAL
+);
+SELECT * FROM pg_get_table_ddl('gen_virtual'::regclass, owner => false);
+
+-- STORAGE and COMPRESSION (only emitted when non-default for the type).
+CREATE TABLE storage_cols (
+ a text STORAGE EXTERNAL,
+ b text STORAGE MAIN,
+ c text COMPRESSION pglz
+);
+SELECT * FROM pg_get_table_ddl('storage_cols'::regclass, owner => false);
+
+-- Constraints: CHECK, UNIQUE, FOREIGN KEY (DEFERRABLE).
+CREATE TABLE refd (id int PRIMARY KEY);
+CREATE TABLE cons (
+ a int CHECK (a > 0),
+ b int UNIQUE,
+ c int REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED
+);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false);
+
+-- UNIQUE NULLS NOT DISTINCT (PG 15): null values are treated as equal for
+-- uniqueness purposes, so the NULLS NOT DISTINCT clause must be emitted.
+CREATE TABLE nulls_nd (
+ a int,
+ b int,
+ UNIQUE NULLS NOT DISTINCT (a, b)
+);
+SELECT * FROM pg_get_table_ddl('nulls_nd'::regclass, owner => false);
+
+-- INCLUDE columns on a constraint-backed UNIQUE index (PG 11): the
+-- covering columns appear in an INCLUDE (...) clause on the ALTER TABLE
+-- ADD CONSTRAINT statement, not in the key column list.
+CREATE TABLE idx_inc (
+ id int PRIMARY KEY,
+ name text,
+ extra text,
+ UNIQUE (name) INCLUDE (extra)
+);
+SELECT * FROM pg_get_table_ddl('idx_inc'::regclass, owner => false);
+
+-- FOREIGN KEY with ON DELETE / ON UPDATE referential actions and MATCH
+-- clause. Each variant must appear verbatim in the reconstructed DDL.
+CREATE TABLE fk_acts_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_acts (
+ a int,
+ b int,
+ c int,
+ d int,
+ CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES fk_acts_tgt(id)
+ ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET NULL,
+ CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET DEFAULT,
+ CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES fk_acts_tgt(id)
+ MATCH FULL
+);
+SELECT * FROM pg_get_table_ddl('fk_acts'::regclass, owner => false);
+
+-- NOT ENFORCED foreign key (PG 17): the constraint is recorded in the
+-- catalog but not validated by the engine; NOT ENFORCED must appear in
+-- the reconstructed DDL.
+CREATE TABLE notenf_tgt (id int PRIMARY KEY);
+CREATE TABLE notenf (
+ a int,
+ CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES notenf_tgt(id)
+ NOT ENFORCED
+);
+SELECT * FROM pg_get_table_ddl('notenf'::regclass, owner => false);
+
+-- Self-referencing FK: the PK must appear before the FK that references it.
+-- The catalog scan returns constraints in name order; without the fix,
+-- "self_ref_parent_id_fkey" sorts before "self_ref_pkey" and the FK ADD
+-- CONSTRAINT fails with "there is no unique constraint matching given keys".
+CREATE TABLE self_ref (
+ id int PRIMARY KEY,
+ parent_id int REFERENCES self_ref(id)
+);
+SELECT * FROM pg_get_table_ddl('self_ref'::regclass, owner => false);
+
+-- WITHOUT OVERLAPS on a PRIMARY KEY (PG 17): the range-type period
+-- column forms a temporal primary key; WITHOUT OVERLAPS must appear
+-- inside the key column list in the reconstructed ALTER TABLE.
+-- Both columns use range types with native GiST support so no
+-- btree_gist extension is needed.
+CREATE TABLE temporal_pk (
+ grp int4range,
+ during daterange,
+ PRIMARY KEY (grp, during WITHOUT OVERLAPS)
+);
+SELECT * FROM pg_get_table_ddl('temporal_pk'::regclass, owner => false);
+
+-- Indexes: functional and partial. Constraint-backing indexes are
+-- suppressed (they are emitted by the constraint loop).
+CREATE TABLE idxd (id int PRIMARY KEY, name text);
+CREATE INDEX idxd_lower ON idxd (lower(name));
+CREATE INDEX idxd_partial ON idxd (id) WHERE id > 100;
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false);
+
+-- Inheritance, including a child DEFAULT override on an inherited column.
+CREATE TABLE par (a int DEFAULT 1, b text);
+CREATE TABLE ch (c int) INHERITS (par);
+ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false);
+
+-- Per-column attoptions: emitted as ALTER COLUMN SET (...).
+CREATE TABLE attopt (a int, b text);
+ALTER TABLE attopt ALTER COLUMN a SET (n_distinct = 100);
+SELECT * FROM pg_get_table_ddl('attopt'::regclass, owner => false);
+
+-- Partitioned table parents (RANGE, HASH, and LIST).
+CREATE TABLE parted_range (id int, k int) PARTITION BY RANGE (id);
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false);
+
+CREATE TABLE parted_hash (id int) PARTITION BY HASH (id);
+SELECT * FROM pg_get_table_ddl('parted_hash'::regclass, owner => false);
+
+CREATE TABLE parted_list (id int, region text) PARTITION BY LIST (region);
+SELECT * FROM pg_get_table_ddl('parted_list'::regclass, owner => false);
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false);
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, owner => false);
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, owner => false);
+
+-- Non-constraint-backed index on a partitioned table: the parent's index DDL
+-- must appear once; the auto-created child partition indexes (relispartition=t)
+-- must be suppressed so the DDL round-trips without "relation already exists".
+CREATE TABLE parted_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_idx_a PARTITION OF parted_idx FOR VALUES IN ('a');
+CREATE UNIQUE INDEX parted_idx_uidx ON parted_idx (id, region);
+SELECT * FROM pg_get_table_ddl('parted_idx'::regclass, owner => false);
+
+-- Plain (non-unique) CREATE INDEX on partitioned table: the auto-created child
+-- index (relispartition=true) is suppressed, just like the UNIQUE case.
+CREATE TABLE parted_plain (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE parted_plain_a PARTITION OF parted_plain FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_plain_id_idx ON parted_plain (id);
+SELECT * FROM pg_get_table_ddl('parted_plain'::regclass, owner => false);
+
+-- Constraint-backed PRIMARY KEY on partitioned table: the child's inherited
+-- constraint (conislocal=false) and its backing index are both suppressed.
+CREATE TABLE parted_pk (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_pk_a PARTITION OF parted_pk FOR VALUES IN ('a');
+ALTER TABLE parted_pk ADD PRIMARY KEY (id, region);
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false);
+
+-- Multi-level (sub-partitioned) table with a CREATE INDEX on the root.
+-- The mid-level partitioned index (relispartition=true, relkind='I') and the
+-- leaf index (relispartition=true, relkind='i') are both suppressed.
+CREATE TABLE parted_sub (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_sub_a PARTITION OF parted_sub
+ FOR VALUES IN ('a') PARTITION BY RANGE (id);
+CREATE TABLE parted_sub_a1 PARTITION OF parted_sub_a FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_sub_id_idx ON parted_sub (id);
+SELECT * FROM pg_get_table_ddl('parted_sub'::regclass, owner => false);
+
+-- Partition child with its own locally-defined index (relispartition=false):
+-- this is NOT inherited from any parent partitioned index and must be emitted.
+CREATE TABLE parted_child_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_child_idx_a PARTITION OF parted_child_idx FOR VALUES IN ('a');
+CREATE INDEX parted_child_idx_a_id ON parted_child_idx_a (id);
+SELECT * FROM pg_get_table_ddl('parted_child_idx'::regclass, owner => false);
+
+-- Mix: parent has a CREATE INDEX (auto-propagated to child, relispartition=true,
+-- suppressed) AND the child has its own local index (relispartition=false, emitted).
+CREATE TABLE parted_mixed (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_mixed_a PARTITION OF parted_mixed FOR VALUES IN ('a');
+CREATE INDEX parted_mixed_id ON parted_mixed (id);
+CREATE INDEX parted_mixed_a_region ON parted_mixed_a (region);
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false);
+
+-- FK on a partitioned table: declared on the parent; partition children inherit
+-- the constraint with conislocal=false and must not re-emit it.
+CREATE TABLE parted_fk_tgt (id int PRIMARY KEY);
+CREATE TABLE parted_fk (id int, tgt_id int REFERENCES parted_fk_tgt(id))
+ PARTITION BY RANGE (id);
+CREATE TABLE parted_fk_a PARTITION OF parted_fk FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('parted_fk'::regclass, owner => false);
+
+-- Statistics declared explicitly on a partition child (not auto-propagated):
+-- the child's statistics object appears inside the child's DDL block.
+CREATE TABLE parted_stx (id int, region text, val float) PARTITION BY LIST (region);
+CREATE TABLE parted_stx_a PARTITION OF parted_stx FOR VALUES IN ('a');
+CREATE STATISTICS parted_stx_stat (ndistinct) ON id, region FROM parted_stx;
+CREATE STATISTICS parted_stx_a_stat (ndistinct) ON id, val FROM parted_stx_a;
+SELECT * FROM pg_get_table_ddl('parted_stx'::regclass, owner => false);
+DROP TABLE parted_stx;
+
+-- NO INHERIT CHECK constraint: the NO INHERIT flag must appear verbatim in the
+-- inline constraint definition inside the CREATE TABLE body.
+CREATE TABLE noinhchk (id int, val int,
+ CONSTRAINT val_pos CHECK (val > 0) NO INHERIT);
+SELECT * FROM pg_get_table_ddl('noinhchk'::regclass, owner => false);
+DROP TABLE noinhchk;
+
+-- Multiple inheritance parents: the INHERITS clause must list both parents in
+-- declaration order; only the locally-defined column appears in the column list.
+CREATE TABLE mi_p1 (a int DEFAULT 1);
+CREATE TABLE mi_p2 (b text DEFAULT 'x');
+CREATE TABLE mi_child (c float) INHERITS (mi_p1, mi_p2);
+SELECT * FROM pg_get_table_ddl('mi_child'::regclass, owner => false);
+DROP TABLE mi_p1, mi_p2 CASCADE;
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false);
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false);
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false);
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false);
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, owner => false);
+
+-- UNLOGGED + reloptions.
+CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70);
+SELECT * FROM pg_get_table_ddl('uno'::regclass, owner => false);
+
+-- Typed table (CREATE TABLE OF type_name). Columns inherited from the
+-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come
+-- out through a single "(col WITH OPTIONS ...)" list.
+CREATE TYPE typed_t AS (a int, b text);
+CREATE TABLE typed_plain OF typed_t;
+SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, owner => false);
+
+CREATE TABLE typed_over OF typed_t (
+ a WITH OPTIONS DEFAULT 7 NOT NULL,
+ b WITH OPTIONS NOT NULL,
+ CONSTRAINT b_nonempty CHECK (length(b) > 0)
+);
+SELECT * FROM pg_get_table_ddl('typed_over'::regclass, owner => false);
+
+-- Temporary tables + ON COMMIT. Temp tables must never be emitted with
+-- the session-private pg_temp_NN schema prefix -- the TEMPORARY keyword
+-- already places the table in pg_temp and the prefix is non-replayable.
+-- ON COMMIT DROP only fires at commit, so the queries that need to see
+-- the catalog entry must run in the same transaction as the CREATE.
+BEGIN;
+CREATE TEMP TABLE temp_default (id int);
+CREATE TEMP TABLE temp_delete (id int) ON COMMIT DELETE ROWS;
+CREATE TEMP TABLE temp_drop (id int) ON COMMIT DROP;
+
+SELECT line FROM pg_get_table_ddl('temp_default'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT line FROM pg_get_table_ddl('temp_delete'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT line FROM pg_get_table_ddl('temp_drop'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ROLLBACK;
+
+-- Pretty mode.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, pretty => true);
+
+-- only_kinds / except_kinds gating: each emits either only the listed kinds
+-- (only_kinds) or every kind except the listed ones (except_kinds). The two
+-- are mutually exclusive. Kind vocabulary: table, index,
+-- primary_key, unique, check, foreign_key, exclusion, rule,
+-- statistics, trigger, policy, rls, replica_identity, partition.
+-- NOT NULL is not in the vocabulary - always emitted to prevent
+-- producing schemas that silently accept NULLs the source would
+-- have rejected.
+--
+-- except_kinds=index hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['index']);
+
+-- except_kinds=primary_key,unique,check,foreign_key,exclusion hides every
+-- table-level constraint. Inline CHECK in the CREATE TABLE body is
+-- suppressed too.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['primary_key','unique','check','foreign_key','exclusion']);
+
+-- except_kinds=foreign_key suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- first pass of the two-pass FK-clone workflow: emit tables without
+-- cross-table FKs, load data, then re-run with only_kinds=foreign_key
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['foreign_key']);
+
+-- only_kinds=foreign_key is the second pass of the two-pass FK-clone
+-- workflow: emit only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN
+-- KEY rows; the CREATE TABLE and every non-FK sub-object are
+-- suppressed (the table already exists from the first pass).
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+
+-- A table with no foreign keys produces no rows under
+-- only_kinds=foreign_key.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+
+-- only_kinds=table emits only the bare CREATE TABLE (with any inline
+-- column-level NOT NULL and CHECK) - no indexes, no PRIMARY KEY ALTER,
+-- no other sub-objects. Useful for capturing a table's shape without
+-- pulling in everything attached to it.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table']);
+
+-- only_kinds=check on a non-partition table. The constraint loop must emit
+-- column-level and table-level CHECK constraints via ALTER TABLE here,
+-- because the inline path in CREATE TABLE never runs (KIND_TABLE is
+-- not in the filter).
+CREATE TABLE chk_only (
+ id int PRIMARY KEY,
+ qty int CHECK (qty > 0),
+ CONSTRAINT chk_only_id_pos CHECK (id > 0)
+);
+SELECT * FROM pg_get_table_ddl('chk_only'::regclass, owner => false, only_kinds => ARRAY['check']);
+DROP TABLE chk_only;
+
+-- only_kinds=partition on a partitioned-table parent: each child's full DDL
+-- is emitted (CREATE TABLE ... PARTITION OF ...). The "partition"
+-- keyword is a gate at the parent level; it is stripped from the
+-- propagated filter so the child does not exclude its own
+-- CREATE TABLE.
+CREATE TABLE p_only (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE p_only_a PARTITION OF p_only FOR VALUES FROM (0) TO (100);
+CREATE TABLE p_only_b PARTITION OF p_only FOR VALUES FROM (100) TO (200);
+SELECT * FROM pg_get_table_ddl('p_only'::regclass, owner => false, only_kinds => ARRAY['partition']);
+DROP TABLE p_only;
+
+-- only_kinds and except_kinds are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false,
+ only_kinds => ARRAY['foreign_key'],
+ except_kinds => ARRAY['foreign_key']);
+
+-- Pub/sub schema clone: keep the table and its primary key, drop the
+-- rest of the constraints so the subscriber can replicate without
+-- pulling cross-table dependencies along.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique','check','foreign_key','exclusion']);
+
+-- except_kinds=rule hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, except_kinds => ARRAY['rule']);
+
+-- except_kinds=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, except_kinds => ARRAY['statistics']);
+
+-- except_kinds=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, except_kinds => ARRAY['rls']);
+
+-- except_kinds=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, except_kinds => ARRAY['replica_identity']);
+
+-- Partition children are emitted by default; except_kinds=partition
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false, except_kinds => ARRAY['partition']);
+
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY[' INDEX ',' RULE ']);
+
+-- schema_qualified=false strips the schema from the target table itself
+-- and from same-schema sibling references (inheritance/partition
+-- parents, identity sequences in the same schema), while cross-schema
+-- references are kept qualified for correctness.
+--
+-- Bare CREATE TABLE for an ordinary table.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false,
+ schema_qualified => false);
+
+-- INHERITS parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Partition parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Cross-schema FK target keeps qualification. The 'refd' table is in
+-- pgtbl_ddl_test; place an FK target in a different schema and verify it
+-- stays qualified even with schema_qualified=false.
+CREATE SCHEMA pgtbl_ddl_other;
+CREATE TABLE pgtbl_ddl_other.parent (id int PRIMARY KEY);
+CREATE TABLE xschema_fk (
+ id int PRIMARY KEY,
+ pid int REFERENCES pgtbl_ddl_other.parent
+);
+SELECT * FROM pg_get_table_ddl('xschema_fk'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Cross-schema FK whose target schema name ends with the base schema name
+-- (e.g. "xpgtbl_ddl_test" ends with "pgtbl_ddl_test"). A post-hoc text
+-- strip would mangle "xpgtbl_ddl_test.reftbl" into "xreftbl"; deciding
+-- qualification at generation time avoids the problem entirely.
+CREATE SCHEMA xpgtbl_ddl_test;
+CREATE TABLE xpgtbl_ddl_test.reftbl (id int PRIMARY KEY);
+CREATE TABLE suffix_schema_fk (
+ id int PRIMARY KEY,
+ ref int REFERENCES xpgtbl_ddl_test.reftbl
+);
+SELECT * FROM pg_get_table_ddl('suffix_schema_fk'::regclass, owner => false,
+ schema_qualified => false);
+DROP TABLE suffix_schema_fk;
+DROP SCHEMA xpgtbl_ddl_test CASCADE;
+
+-- Identity column with a custom sequence name in the same schema is
+-- emitted unqualified.
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Round-trip: with schema_qualified=false, after SET search_path the
+-- DDL can be replayed into a different target schema.
+CREATE SCHEMA pgtbl_ddl_replay;
+DO $$
+DECLARE
+ stmt text;
+BEGIN
+ SET LOCAL search_path = pgtbl_ddl_replay;
+ FOR stmt IN
+ SELECT line FROM pg_get_table_ddl('pgtbl_ddl_test.basic'::regclass,
+ owner => false,
+ schema_qualified => false) AS line
+ LOOP
+ EXECUTE stmt;
+ END LOOP;
+END $$;
+SELECT relnamespace::regnamespace::text, relname
+FROM pg_class WHERE oid = 'pgtbl_ddl_replay.basic'::regclass;
+DROP SCHEMA pgtbl_ddl_replay CASCADE;
+DROP TABLE xschema_fk;
+DROP SCHEMA pgtbl_ddl_other CASCADE;
+
+-- Cross-schema partition child with schema_qualified=false: the child must
+-- carry its own schema prefix so the output is unambiguous regardless of the
+-- caller's search_path. Before the fix, the child was emitted as the bare
+-- name "pt_c" (landing in whichever schema is first in search_path) while
+-- the parent reference inside the CREATE TABLE came out as the fully-qualified
+-- "pgtbl_ddl_part_s.pt", making the DDL inconsistent and unreplayable.
+CREATE SCHEMA pgtbl_ddl_part_s;
+CREATE SCHEMA pgtbl_ddl_part_other;
+CREATE TABLE pgtbl_ddl_part_s.pt (id int, val int) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_part_other.pt_c
+ PARTITION OF pgtbl_ddl_part_s.pt
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_part_s.pt'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_part_s CASCADE;
+DROP SCHEMA pgtbl_ddl_part_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. String
+-- literal text is output verbatim by the deparser and is unaffected by
+-- search_path narrowing.
+CREATE SCHEMA pgtbl_ddl_str;
+CREATE TABLE pgtbl_ddl_str.p (id int, note text) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_str.pc PARTITION OF pgtbl_ddl_str.p
+ (CONSTRAINT chk CHECK (note <> 'pgtbl_ddl_str.secret'))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_str.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_str CASCADE;
+
+-- schema_qualified=false must NOT corrupt double-quoted identifiers
+-- whose contents happen to contain the schema name followed by a dot
+-- (for example a column literally named "<schema>.weird" inside that
+-- schema). The mechanism is purely search_path narrowing: the deparser
+-- outputs attribute names verbatim from pg_attribute, so the quoted
+-- identifier content is never examined for schema prefixes. This holds
+-- even when the schema name is a single letter.
+CREATE SCHEMA pgtbl_ddl_qid;
+CREATE TABLE pgtbl_ddl_qid.p (id int, "pgtbl_ddl_qid.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_qid.pc PARTITION OF pgtbl_ddl_qid.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_qid.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_qid.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_qid CASCADE;
+-- Same with a short (single-letter) schema name to confirm there is no
+-- minimum-length assumption in the stripping path.
+CREATE SCHEMA pgtbl_ddl_q1;
+CREATE TABLE pgtbl_ddl_q1.p (id int, "pgtbl_ddl_q1.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_q1.pc PARTITION OF pgtbl_ddl_q1.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_q1.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_q1.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_q1 CASCADE;
+
+-- schema_qualified=false when the base schema name itself requires
+-- quoting (its prefix starts with "). Both forms of the prefix -
+-- bare-lowercase and quoted - must be stripped from outer
+-- ALTER TABLE / ON / CREATE INDEX prefixes, while inner quoted
+-- identifiers that happen to begin the same way must be preserved.
+CREATE SCHEMA "PgTbl-Ddl-Q";
+CREATE TABLE "PgTbl-Ddl-Q"."T" (id int, val int,
+ CONSTRAINT chk CHECK (val > 0));
+CREATE INDEX "T_val_idx" ON "PgTbl-Ddl-Q"."T" (val);
+SELECT * FROM pg_get_table_ddl('"PgTbl-Ddl-Q"."T"'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA "PgTbl-Ddl-Q" CASCADE;
+
+-- Error: not an ordinary or partitioned table.
+CREATE VIEW v AS SELECT 1 AS x;
+SELECT * FROM pg_get_table_ddl('v'::regclass);
+
+CREATE SEQUENCE s;
+SELECT * FROM pg_get_table_ddl('s'::regclass);
+
+-- NULL argument returns no rows.
+SELECT * FROM pg_get_table_ddl(NULL);
+
+-- REPLICA IDENTITY USING INDEX validation: if the referenced index
+-- would not be emitted (because its source kind is suppressed) but
+-- replica_identity itself would be, the function raises an error so
+-- the emitted DDL never dangles. Exercise all three constraint-backed
+-- forms (PK, UNIQUE, EXCLUSION) and a bare CREATE UNIQUE INDEX, in
+-- both the except-list and only-list shapes.
+--
+-- PK-backed: except_kinds=primary_key without except_kinds=replica_identity.
+CREATE TABLE ri_pk_excl (a int PRIMARY KEY);
+ALTER TABLE ri_pk_excl REPLICA IDENTITY USING INDEX ri_pk_excl_pkey;
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key','replica_identity']);
+DROP TABLE ri_pk_excl;
+
+-- UNIQUE-constraint-backed replica identity index.
+CREATE TABLE ri_uniq_excl (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_uniq_excl REPLICA IDENTITY USING INDEX ri_uniq_excl_a_key;
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique','replica_identity']);
+DROP TABLE ri_uniq_excl;
+
+-- Bare CREATE UNIQUE INDEX (no backing constraint) used as REPLICA
+-- IDENTITY: the index is emitted by the "index" kind, so excluding
+-- "index" without also excluding "replica_identity" must error.
+CREATE TABLE ri_idx_excl (a int NOT NULL, b int);
+CREATE UNIQUE INDEX ri_idx_excl_a_uniq ON ri_idx_excl (a);
+ALTER TABLE ri_idx_excl REPLICA IDENTITY USING INDEX ri_idx_excl_a_uniq;
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index']);
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index','replica_identity']);
+DROP TABLE ri_idx_excl;
+
+-- Include-side analog: only_kinds=replica_identity must include the kind
+-- that emits the index, or the function errors.
+CREATE TABLE ri_only (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+-- Including both kinds succeeds.
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['unique','replica_identity']);
+DROP TABLE ri_only;
+
+-- Per-kind only_kinds / except_kinds coverage. For each kind in the vocabulary,
+-- verify that only_kinds=K emits just that kind's output and that except_kinds=K
+-- omits just that kind's output. Kinds with no fixture above (trigger,
+-- policy) are still recognized but currently emit nothing; only_kinds=K
+-- returns zero rows for them. Fixtures used:
+-- idxd: PK + two secondary CREATE INDEX
+-- cons: PK + UNIQUE + CHECK + FK (constraint-backed)
+-- rt: CREATE RULE
+-- stx: CREATE STATISTICS
+-- rls: ENABLE/FORCE ROW LEVEL SECURITY
+-- ri_full: REPLICA IDENTITY FULL (no source-kind dependency)
+CREATE TABLE excl_fix (rng int4range,
+ EXCLUDE USING gist (rng WITH &&));
+
+-- only_kinds=K: emit only kind K's statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index']);
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['primary_key']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, only_kinds => ARRAY['rule']);
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, only_kinds => ARRAY['rls']);
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['policy']);
+
+-- except_kinds=K: emit everything except kind K's statements. PK/UNIQUE/
+-- INDEX are exercised on tables without REPLICA IDENTITY USING INDEX
+-- so the validation does not trip.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['check']);
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, except_kinds => ARRAY['exclusion']);
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['policy']);
+-- except_kinds=table suppresses the CREATE TABLE + OWNER + child-default
+-- SET DEFAULT + attoptions passes, leaving only the sub-object passes.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['table']);
+
+-- Multi-kind compositions.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table','index']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['primary_key','foreign_key']);
+-- Duplicate entries in the list are silently de-duplicated by the
+-- Bitmapset.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index','index']);
+
+DROP TABLE excl_fix;
+
+-- Kind-filter tests on partitioned tables with indexes.
+--
+-- only_kinds=index on a partitioned table: only the root CREATE INDEX is
+-- emitted; no CREATE TABLE, no partition children (KIND_PARTITION absent).
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ only_kinds => ARRAY['index']);
+
+-- except_kinds=index on a partitioned table: CREATE TABLE for the parent
+-- and all children are emitted; no index statements.
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ except_kinds => ARRAY['index']);
+
+-- only_kinds=primary_key on a partitioned table: only the ALTER TABLE ADD
+-- CONSTRAINT PK on the parent; the child's inherited PK (conislocal=false)
+-- is skipped; no CREATE TABLE, no partition children.
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false,
+ only_kinds => ARRAY['primary_key']);
+
+-- only_kinds=K on a table that does not have any K of that kind is a valid
+-- filter and returns zero rows. Tested previously with foreign_key
+-- on 'refd'; here also rule, statistics, rls, replica_identity, and
+-- exclusion on 'basic' (which has only PK + columns).
+SELECT count(*) AS rule_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rule']);
+SELECT count(*) AS stats_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+SELECT count(*) AS rls_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rls']);
+SELECT count(*) AS ri_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+SELECT count(*) AS excl_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+
+-- Input-validation edge cases for the kind array. All
+-- of these raise an error; the function never reaches relation open.
+-- Empty array:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[]::text[]);
+-- NULL element:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[NULL::text]);
+-- Unrecognized kind:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY['no_such_kind']);
+-- Same kind specified through both parameters is still mutex-rejected.
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ only_kinds => ARRAY['index'],
+ except_kinds => ARRAY['rule']);
+
+-- Round-trip verification. For every test table, capture the DDL the
+-- function emits, drop the schema, replay each table's DDL in
+-- dependency order, and confirm that pg_get_table_ddl on the recreated
+-- relation matches the original line-for-line. The final SELECT must
+-- return zero rows.
+CREATE TEMP TABLE pgtbl_ddl_rt_orig (name text, ord int, line text);
+INSERT INTO pgtbl_ddl_rt_orig
+SELECT t.name, o.ord, o.line
+FROM (VALUES
+ ('basic'), ('id_cols'), ('id_custom'), ('gen_cols'), ('gen_virtual'), ('storage_cols'),
+ ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'),
+ ('fk_acts_tgt'), ('fk_acts'), ('notenf_tgt'), ('notenf'), ('self_ref'), ('temporal_pk'),
+ ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('parted_list'),
+ ('parted_idx'), ('parted_idx_a'),
+ ('parted_plain'), ('parted_plain_a'),
+ ('parted_pk'), ('parted_pk_a'),
+ ('parted_sub'), ('parted_sub_a'), ('parted_sub_a1'),
+ ('parted_child_idx'), ('parted_child_idx_a'),
+ ('parted_mixed'), ('parted_mixed_a'),
+ ('parted_fk_tgt'), ('parted_fk'), ('parted_fk_a'),
+ ('rt_log'), ('rt'),
+ ('stx'), ('rls'), ('ri_full'), ('ri_idx'), ('uno')
+ ) AS t(name),
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord);
+
+DO $$
+DECLARE
+ tables CONSTANT text[] := ARRAY[
+ 'basic', 'id_cols', 'id_custom', 'gen_cols', 'gen_virtual', 'storage_cols',
+ 'refd', 'cons', 'nulls_nd', 'idx_inc',
+ 'fk_acts_tgt', 'fk_acts', 'notenf_tgt', 'notenf', 'self_ref', 'temporal_pk',
+ 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ 'parted_list',
+ 'parted_idx', 'parted_idx_a',
+ 'parted_plain', 'parted_plain_a',
+ 'parted_pk', 'parted_pk_a',
+ 'parted_sub', 'parted_sub_a', 'parted_sub_a1',
+ 'parted_child_idx', 'parted_child_idx_a',
+ 'parted_mixed', 'parted_mixed_a',
+ 'parted_fk_tgt', 'parted_fk', 'parted_fk_a',
+ 'rt_log', 'rt',
+ 'stx', 'rls', 'ri_full', 'ri_idx', 'uno'
+ ];
+ t text;
+ stmt text;
+BEGIN
+ DROP SCHEMA pgtbl_ddl_test CASCADE;
+ CREATE SCHEMA pgtbl_ddl_test;
+ FOREACH t IN ARRAY tables LOOP
+ FOR stmt IN
+ SELECT line FROM pgtbl_ddl_rt_orig WHERE name = t ORDER BY ord
+ LOOP
+ BEGIN
+ EXECUTE stmt;
+ EXCEPTION WHEN duplicate_table THEN
+ NULL; -- partition child already created by its parent's DDL
+ END;
+ END LOOP;
+ END LOOP;
+END $$;
+
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..eb1189762a0 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1648,6 +1648,7 @@ ListenerEntry
LoInfo
LoadStmt
LocalBufferLookupEnt
+LocalNotNullEntry
LocalPgBackendStatus
LocalTransactionId
Location
@@ -3152,6 +3153,7 @@ TableFuncScan
TableFuncScanState
TableFuncType
TableInfo
+TableDdlContext
TableLikeClause
TableSampleClause
TableScanDesc
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-04 16:35 Rui Zhao <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Rui Zhao @ 2026-07-04 16:35 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
Hi Akshay,
Re-tested v16 on current master -- builds clean and the recent fixes hold up
(self-ref FK after PK, no duplicate partition-child index, cross-schema
partition child qualified, schema_qualified => true consistent). A few
issues, in severity order; several are places where pg_dump already does the
right thing.
1. Clause order: TABLESPACE is emitted before ON COMMIT (ddlutils.c:2042 vs
2054), but the grammar is "... OptWith OnCommitOption OptTableSpace" -- ON
COMMIT must come first, so a temp table with both clauses is non-replayable:
CREATE TABLESPACE ts1 LOCATION '/path/to/dir';
CREATE TEMP TABLE tt (a int) ON COMMIT DROP TABLESPACE ts1;
SELECT d FROM pg_get_table_ddl('tt'::regclass, owner => false) d;
-- CREATE TEMPORARY TABLE tt (a integer) TABLESPACE ts1 ON COMMIT DROP;
CREATE TEMPORARY TABLE tt (a integer) TABLESPACE ts1 ON COMMIT DROP;
-- ERROR: syntax error at or near "ON"
Swapping the two blocks so ON COMMIT precedes TABLESPACE fixes it.
2. Child-default override recurses (missing ONLY). emit_child_default_overrides
emits the inherited-column default without ONLY (ddlutils.c:2118), so SET
DEFAULT recurses into the table's children and reconstructing one table
silently rewrites another:
CREATE TABLE dpar (x int);
CREATE TABLE dch () INHERITS (dpar);
CREATE TABLE dgc () INHERITS (dch);
ALTER TABLE ONLY dch ALTER COLUMN x SET DEFAULT 5;
ALTER TABLE ONLY dgc ALTER COLUMN x SET DEFAULT 10;
SELECT d FROM pg_get_table_ddl('dch'::regclass, owner => false) d;
-- ALTER TABLE dch ALTER COLUMN x SET DEFAULT 5; -- no ONLY
ALTER TABLE dch ALTER COLUMN x SET DEFAULT 5; -- replay this line
-- => dgc's default is now 5, not 10
pg_dump uses ALTER TABLE ONLY here for exactly this reason.
Partitioned tables hit this especially easily -- any partitioned table with a
column default emits a redundant, ONLY-less SET DEFAULT for every partition
that merely inherits it:
CREATE TABLE p (id int, amt int DEFAULT 5) PARTITION BY LIST (id);
CREATE TABLE p_a PARTITION OF p FOR VALUES IN (1);
SELECT d FROM pg_get_table_ddl('p'::regclass, owner => false) d;
-- CREATE TABLE public.p (id integer, amt integer DEFAULT 5)
PARTITION BY LIST (id);
-- CREATE TABLE public.p_a PARTITION OF public.p FOR VALUES IN (1);
-- ALTER TABLE public.p_a ALTER COLUMN amt SET DEFAULT 5; --
redundant, no ONLY
p_a already inherits amt's default from the PARTITION OF, so the third line is
redundant; pg_dump instead keeps the default inline on the child and attaches
with ALTER TABLE ONLY ... ATTACH PARTITION. A full-hierarchy replay
self-corrects (each child's own SET DEFAULT runs last), but a partial replay or
a lone emitted statement does not. The commit message lists "child-local
DEFAULT overrides on inheritance/partition children" as supported, so this is
in scope. (More generally the patch never emits ONLY anywhere; ADD CONSTRAINT,
where CHECK / NOT NULL also recurse to children, is worth the same audit.)
3. Inherited-only NOT NULL emitted as local. When a child redeclares an
inherited column (attislocal) without restating NOT NULL, the constraint is
inherited-only (conislocal = false); collect_local_not_null skips it, but
append_column_defs keys off att->attnotnull and emits a bare NOT NULL anyway:
CREATE TABLE par (a int NOT NULL);
CREATE TABLE chld (a int) INHERITS (par); -- 'a' redeclared, no NOT NULL
SELECT d FROM pg_get_table_ddl('chld'::regclass, owner => false) d;
-- CREATE TABLE public.chld (a integer NOT NULL) INHERITS (public.par);
On replay the child now owns the constraint (conislocal flips false -> true,
name regenerates par_a_not_null -> chld_a_not_null), so a later
"ALTER TABLE par ALTER a DROP NOT NULL" cascades to the original child but not
the reconstructed one. pg_dump emits "a integer" with no NOT NULL here,
suppressing it via notnull_islocal (pg_dump.c ~9916). The docs describe this as
the intended behavior -- "Inherited columns and constraints ... are not
duplicated on inheritance children or partitions" -- so it reads as a
documented contract the code doesn't quite meet. The inline CHECK path in this
patch already filters on conislocal; the NOT NULL path could do the same.
4. Typed-table STORAGE / COMPRESSION not emitted -- intended? append_column_defs
emits per-column STORAGE for ordinary tables, but the typed-table path
(append_typed_column_overrides) only handles DEFAULT / NOT NULL / CHECK, so a
storage override on a typed table is not reproduced:
CREATE TYPE mytype AS (a int, b text);
CREATE TABLE typed_t OF mytype;
ALTER TABLE typed_t ALTER COLUMN b SET STORAGE external;
SELECT d FROM pg_get_table_ddl('typed_t'::regclass, owner => false) d;
-- CREATE TABLE public.typed_t OF public.mytype; -- STORAGE
not emitted
The docs scope the typed-table form to overrides for "defaults, NOT NULL, and
CHECK", so this may well be deliberate. But STORAGE is listed in the general
per-column coverage, and pg_dump does emit it (ALTER TABLE ONLY ... ALTER COLUMN
b SET STORAGE EXTERNAL) -- so it seems worth confirming the omission is
intentional rather than an oversight.
5. Minor: is_auto (ddlutils.c:1381) and the identity SEQUENCE NAME check (1681)
rebuild the expected auto-name with snprintf, but the backend uses
makeObjectName(), which truncates name1/name2 to fit NAMEDATALEN and never the
label -- so for long names the "_not_null" / "_seq" suffix is dropped and the
checks misfire, emitting a name a short-named table would omit:
CREATE TABLE aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(bbbbbbbbbbbbbbbbbbbb int GENERATED ALWAYS AS IDENTITY);
SELECT d FROM pg_get_table_ddl(
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'::regclass,
owner => false) d;
-- CREATE TABLE public.aaaa...(50) (bbbb...(20) integer
-- GENERATED ALWAYS AS IDENTITY (SEQUENCE NAME public.aaaa..._seq)
-- CONSTRAINT aaaa..._not_null NOT NULL);
The default-omission convention in the commit message lists "the auto-generated
identity sequence name" among the clauses meant to be dropped, which is exactly
what misfires here for long names. It still replays (the names are real), so
this one is cosmetic; comparing against makeObjectName(relname, colname,
"not_null" / "seq") makes both sides agree.
Everything else looks good.
Thanks,
Rui
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-06 10:57 Akshay Joshi <[email protected]>
parent: Rui Zhao <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-07-06 10:57 UTC (permalink / raw)
To: Rui Zhao <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
Thanks for the review, Rui. I’ve addressed all the issues you raised.
The v17 patch is now ready for your review.
On Sat, Jul 4, 2026 at 10:05 PM Rui Zhao <[email protected]> wrote:
> Hi Akshay,
>
> Re-tested v16 on current master -- builds clean and the recent fixes hold
> up
> (self-ref FK after PK, no duplicate partition-child index, cross-schema
> partition child qualified, schema_qualified => true consistent). A few
> issues, in severity order; several are places where pg_dump already does
> the
> right thing.
>
> 1. Clause order: TABLESPACE is emitted before ON COMMIT (ddlutils.c:2042 vs
> 2054), but the grammar is "... OptWith OnCommitOption OptTableSpace" -- ON
> COMMIT must come first, so a temp table with both clauses is
> non-replayable:
>
> CREATE TABLESPACE ts1 LOCATION '/path/to/dir';
> CREATE TEMP TABLE tt (a int) ON COMMIT DROP TABLESPACE ts1;
> SELECT d FROM pg_get_table_ddl('tt'::regclass, owner => false) d;
> -- CREATE TEMPORARY TABLE tt (a integer) TABLESPACE ts1 ON COMMIT
> DROP;
>
> CREATE TEMPORARY TABLE tt (a integer) TABLESPACE ts1 ON COMMIT DROP;
> -- ERROR: syntax error at or near "ON"
>
> Swapping the two blocks so ON COMMIT precedes TABLESPACE fixes it.
>
> 2. Child-default override recurses (missing ONLY).
> emit_child_default_overrides
> emits the inherited-column default without ONLY (ddlutils.c:2118), so SET
> DEFAULT recurses into the table's children and reconstructing one table
> silently rewrites another:
>
> CREATE TABLE dpar (x int);
> CREATE TABLE dch () INHERITS (dpar);
> CREATE TABLE dgc () INHERITS (dch);
> ALTER TABLE ONLY dch ALTER COLUMN x SET DEFAULT 5;
> ALTER TABLE ONLY dgc ALTER COLUMN x SET DEFAULT 10;
>
> SELECT d FROM pg_get_table_ddl('dch'::regclass, owner => false) d;
> -- ALTER TABLE dch ALTER COLUMN x SET DEFAULT 5; -- no ONLY
>
> ALTER TABLE dch ALTER COLUMN x SET DEFAULT 5; -- replay this
> line
> -- => dgc's default is now 5, not 10
>
> pg_dump uses ALTER TABLE ONLY here for exactly this reason.
>
> Partitioned tables hit this especially easily -- any partitioned table
> with a
> column default emits a redundant, ONLY-less SET DEFAULT for every partition
> that merely inherits it:
>
> CREATE TABLE p (id int, amt int DEFAULT 5) PARTITION BY LIST (id);
> CREATE TABLE p_a PARTITION OF p FOR VALUES IN (1);
>
> SELECT d FROM pg_get_table_ddl('p'::regclass, owner => false) d;
> -- CREATE TABLE public.p (id integer, amt integer DEFAULT 5)
> PARTITION BY LIST (id);
> -- CREATE TABLE public.p_a PARTITION OF public.p FOR VALUES IN (1);
> -- ALTER TABLE public.p_a ALTER COLUMN amt SET DEFAULT 5; --
> redundant, no ONLY
>
> p_a already inherits amt's default from the PARTITION OF, so the third
> line is
> redundant; pg_dump instead keeps the default inline on the child and
> attaches
> with ALTER TABLE ONLY ... ATTACH PARTITION. A full-hierarchy replay
> self-corrects (each child's own SET DEFAULT runs last), but a partial
> replay or
> a lone emitted statement does not. The commit message lists "child-local
> DEFAULT overrides on inheritance/partition children" as supported, so this
> is
> in scope. (More generally the patch never emits ONLY anywhere; ADD
> CONSTRAINT,
> where CHECK / NOT NULL also recurse to children, is worth the same audit.)
>
> 3. Inherited-only NOT NULL emitted as local. When a child redeclares an
> inherited column (attislocal) without restating NOT NULL, the constraint is
> inherited-only (conislocal = false); collect_local_not_null skips it, but
> append_column_defs keys off att->attnotnull and emits a bare NOT NULL
> anyway:
>
> CREATE TABLE par (a int NOT NULL);
> CREATE TABLE chld (a int) INHERITS (par); -- 'a' redeclared, no NOT
> NULL
>
> SELECT d FROM pg_get_table_ddl('chld'::regclass, owner => false) d;
> -- CREATE TABLE public.chld (a integer NOT NULL) INHERITS
> (public.par);
>
> On replay the child now owns the constraint (conislocal flips false ->
> true,
> name regenerates par_a_not_null -> chld_a_not_null), so a later
> "ALTER TABLE par ALTER a DROP NOT NULL" cascades to the original child but
> not
> the reconstructed one. pg_dump emits "a integer" with no NOT NULL here,
> suppressing it via notnull_islocal (pg_dump.c ~9916). The docs describe
> this as
> the intended behavior -- "Inherited columns and constraints ... are not
> duplicated on inheritance children or partitions" -- so it reads as a
> documented contract the code doesn't quite meet. The inline CHECK path in
> this
> patch already filters on conislocal; the NOT NULL path could do the same.
>
> 4. Typed-table STORAGE / COMPRESSION not emitted -- intended?
> append_column_defs
> emits per-column STORAGE for ordinary tables, but the typed-table path
> (append_typed_column_overrides) only handles DEFAULT / NOT NULL / CHECK,
> so a
> storage override on a typed table is not reproduced:
>
> CREATE TYPE mytype AS (a int, b text);
> CREATE TABLE typed_t OF mytype;
> ALTER TABLE typed_t ALTER COLUMN b SET STORAGE external;
>
> SELECT d FROM pg_get_table_ddl('typed_t'::regclass, owner => false) d;
> -- CREATE TABLE public.typed_t OF public.mytype; -- STORAGE
> not emitted
>
> The docs scope the typed-table form to overrides for "defaults, NOT NULL,
> and
> CHECK", so this may well be deliberate. But STORAGE is listed in the
> general
> per-column coverage, and pg_dump does emit it (ALTER TABLE ONLY ... ALTER
> COLUMN
> b SET STORAGE EXTERNAL) -- so it seems worth confirming the omission is
> intentional rather than an oversight.
>
> 5. Minor: is_auto (ddlutils.c:1381) and the identity SEQUENCE NAME check
> (1681)
> rebuild the expected auto-name with snprintf, but the backend uses
> makeObjectName(), which truncates name1/name2 to fit NAMEDATALEN and never
> the
> label -- so for long names the "_not_null" / "_seq" suffix is dropped and
> the
> checks misfire, emitting a name a short-named table would omit:
>
> CREATE TABLE aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
> (bbbbbbbbbbbbbbbbbbbb int GENERATED ALWAYS AS IDENTITY);
>
> SELECT d FROM pg_get_table_ddl(
> 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'::regclass,
> owner => false) d;
> -- CREATE TABLE public.aaaa...(50) (bbbb...(20) integer
> -- GENERATED ALWAYS AS IDENTITY (SEQUENCE NAME public.aaaa..._seq)
> -- CONSTRAINT aaaa..._not_null NOT NULL);
>
> The default-omission convention in the commit message lists "the
> auto-generated
> identity sequence name" among the clauses meant to be dropped, which is
> exactly
> what misfires here for long names. It still replays (the names are real),
> so
> this one is cosmetic; comparing against makeObjectName(relname, colname,
> "not_null" / "seq") makes both sides agree.
>
> Everything else looks good.
>
> Thanks,
> Rui
>
Attachments:
[application/octet-stream] v17-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (224.5K, ../../CANxoLDdH-yWa_HT2hW_q5-5BPnfPiB=wmw+3x3BSvTRN3S-Y5Q@mail.gmail.com/3-v17-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From f72834694eee07d49262e9ba859c567e864c71b5 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v17] Add pg_get_table_ddl() to reconstruct CREATE TABLE
statements
The function reconstructs the CREATE TABLE statement for an ordinary or
partitioned table, followed by the ALTER TABLE / CREATE INDEX /
CREATE RULE / CREATE STATISTICS statements needed to restore its full
definition. Each statement is returned as a separate row.
Supported per-column features: data type with type modifiers, COLLATE,
STORAGE, COMPRESSION (pglz / lz4), GENERATED ALWAYS AS (expr)
STORED/VIRTUAL, GENERATED ALWAYS|BY DEFAULT AS IDENTITY (with sequence
options), DEFAULT, NOT NULL (including named NOT NULL constraints), and
per-column attoptions emitted as ALTER COLUMN SET (...).
Supported table-level features: UNLOGGED, INHERITS, PARTITION BY (RANGE
/ LIST / HASH parents), PARTITION OF parent FOR VALUES (FROM/TO, WITH
modulus/remainder, DEFAULT), USING table access method, WITH
(reloptions), TABLESPACE, and inline CHECK constraints in the CREATE
TABLE body.
Supported sub-objects (re-using existing deparse helpers from
ruleutils.c): indexes (including partial and functional) via
pg_get_indexdef_ddl; constraints (PRIMARY KEY with WITHOUT OVERLAPS for
temporal keys, UNIQUE with NULLS NOT DISTINCT and INCLUDE columns,
FOREIGN KEY with ON DELETE/UPDATE referential actions and MATCH clause,
NOT ENFORCED foreign keys, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_body; rules via pg_get_ruledef_ddl; extended
statistics via pg_get_statisticsobjdef_ddl; REPLICA IDENTITY
NOTHING/FULL/USING INDEX; ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY;
and child-local DEFAULT overrides on inheritance/partition children.
DDL for partition children of a partitioned-table parent is appended
after the parent by default.
The function signature follows the named-parameter convention
established by pg_get_role_ddl(), pg_get_tablespace_ddl(), and
pg_get_database_ddl():
pg_get_table_ddl(relation regclass,
pretty boolean DEFAULT false,
owner boolean DEFAULT true,
tablespace boolean DEFAULT true,
schema_qualified boolean DEFAULT true,
only_kinds text[] DEFAULT NULL,
except_kinds text[] DEFAULT NULL)
pretty controls pretty-printed output. owner controls emission of the
ALTER TABLE ... OWNER TO statement. tablespace controls the TABLESPACE
clause on CREATE TABLE. schema_qualified controls whether object names
are emitted with their schema prefix: when true (the default) the
active search_path is temporarily narrowed to pg_catalog so every
deparse helper produces fully-qualified names; when false it is narrowed
to the target table's own schema so same-schema references come out
unqualified while cross-schema references remain qualified for
correctness. Temporary tables are never schema-qualified regardless of
this setting: the TEMPORARY keyword already places them in pg_temp, and
emitting pg_temp_NN.relname would produce non-replayable DDL.
Object-class filtering uses two mutually-exclusive text-array
parameters, only_kinds and except_kinds. When only_kinds is set, only
the listed kinds are emitted; when except_kinds is set, every kind
except the listed ones is emitted; when neither is set every kind is
emitted. Each array element is matched case-insensitively with
leading/trailing whitespace trimmed; an unrecognized name raises an
error. The kind vocabulary is:
table, index, primary_key, unique, check, foreign_key, exclusion,
rule, statistics, rls, replica_identity, partition
trigger and policy are accepted in the vocabulary but currently produce
no output; they are reserved for when standalone pg_get_trigger_ddl()
and pg_get_policy_ddl() helpers become available.
NOT NULL is not part of the vocabulary: it is always emitted to prevent
producing schemas that silently accept NULLs the source would reject.
When replica_identity is in the active filter and the table's REPLICA
IDENTITY USING INDEX references an index that the filter would suppress,
the function raises an error before emitting any output, so the
generated DDL never references an index it did not produce.
Default omission convention: every optional clause is omitted when its
value matches what the server would reapply on round-trip, including
type-default COLLATE, per-type STORAGE, the auto-generated identity
sequence name and parameter defaults, heap access method, default
REPLICA IDENTITY, disabled RLS toggles, empty reloptions, and the
default tablespace.
Author: Akshay Joshi <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kyotaro Horiguchi <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Rui Zhao <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 146 ++
src/backend/catalog/pg_inherits.c | 93 +
src/backend/commands/tablecmds.c | 30 +-
src/backend/utils/adt/ddlutils.c | 2182 ++++++++++++++++-
src/backend/utils/adt/ruleutils.c | 87 +-
src/include/catalog/pg_inherits.h | 1 +
src/include/catalog/pg_proc.dat | 8 +
src/include/commands/tablecmds.h | 3 +
src/include/utils/ruleutils.h | 4 +
.../regress/expected/pg_get_table_ddl.out | 1516 ++++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 879 +++++++
src/tools/pgindent/typedefs.list | 2 +
13 files changed, 4936 insertions(+), 17 deletions(-)
create mode 100644 src/test/regress/expected/pg_get_table_ddl.out
create mode 100644 src/test/regress/sql/pg_get_table_ddl.sql
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 69ef3857cfa..55c6b873240 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3960,6 +3960,152 @@ 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>relation</parameter> <type>regclass</type>
+ <optional>, <parameter>pretty</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> false</optional>
+ <optional>, <parameter>owner</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>tablespace</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>schema_qualified</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>only_kinds</parameter> <type>text[]</type>
+ <literal>DEFAULT</literal> NULL</optional>
+ <optional>, <parameter>except_kinds</parameter> <type>text[]</type>
+ <literal>DEFAULT</literal> NULL</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.
+ Foreign-key constraints are always emitted after all other
+ constraints so that self-referencing foreign keys (where the
+ referenced primary or unique key is on the same table) can be
+ added once that key already exists.
+ When <parameter>pretty</parameter> is true, the output is
+ pretty-printed. When <parameter>owner</parameter> is false, the
+ <command>ALTER TABLE ... OWNER TO</command> statement is omitted.
+ When <parameter>tablespace</parameter> is false, the
+ <literal>TABLESPACE</literal> clause is omitted from the
+ <command>CREATE TABLE</command> statement. The
+ <parameter>only_kinds</parameter> and <parameter>except_kinds</parameter>
+ parameters described below control which object classes are emitted.
+ </para>
+ <para>
+ The <parameter>only_kinds</parameter> and <parameter>except_kinds</parameter>
+ parameters each take a text array of object-class kind names
+ and are mutually exclusive (specifying both raises an
+ error). When <parameter>only_kinds</parameter> is set, only the
+ listed kinds are emitted; when <parameter>except_kinds</parameter> is
+ set, every kind <emphasis>except</emphasis> the listed ones is
+ emitted. When neither is set (the default), every kind is
+ emitted. Leading and trailing whitespace in each array element
+ is ignored and matching is case-insensitive; an unrecognized
+ kind name raises an error.
+ The kind vocabulary is
+ <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).
+ Note that <literal>trigger</literal> and <literal>policy</literal>
+ are accepted in the vocabulary but currently produce no output;
+ they are reserved for future use when standalone
+ <function>pg_get_trigger_ddl</function> and
+ <function>pg_get_policy_ddl</function> helpers become available. 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_kinds => ARRAY['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_kinds => ARRAY['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 <parameter>schema_qualified</parameter> parameter (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. Partition children that reside in a different
+ schema than the target table are always emitted with full schema
+ qualification regardless of this parameter, because the output
+ would otherwise place the child in the wrong schema when
+ replayed.
+ </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 95abaf4890c..06afa6811ec 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);
@@ -2524,7 +2523,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)
@@ -19837,6 +19836,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..07b4257acf2 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -5,7 +5,7 @@
*
* This file contains the pg_get_*_ddl family of functions that generate
* DDL statements to recreate database objects such as roles, tablespaces,
- * and databases, along with common infrastructure for option parsing and
+ * databases, and tables, along with common infrastructure for
* pretty-printing.
*
* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
@@ -21,30 +21,89 @@
#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
+#include "access/toast_compression.h"
+#include "catalog/namespace.h"
+#include "catalog/partition.h"
+#include "catalog/pg_am.h"
#include "catalog/pg_auth_members.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_collation.h"
+#include "catalog/pg_constraint.h"
#include "catalog/pg_database.h"
#include "catalog/pg_db_role_setting.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_policy.h"
+#include "catalog/pg_sequence.h"
+#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_tablespace.h"
+#include "catalog/pg_trigger.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
#include "commands/tablespace.h"
-#include "common/relpath.h"
#include "funcapi.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "utils/acl.h"
+#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/datetime.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
-#include "utils/pg_locale.h"
#include "utils/rel.h"
#include "utils/ruleutils.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
#include "utils/varlena.h"
+/*
+ * Object-class kinds that the only / except options on
+ * pg_get_table_ddl can filter on. Members are stored as integers in a
+ * Bitmapset on TableDdlContext. Keep table_ddl_kind_names[] in sync
+ * with the order of additions here.
+ */
+typedef enum TableDdlKind
+{
+ TABLE_DDL_KIND_TABLE,
+ TABLE_DDL_KIND_INDEX,
+ TABLE_DDL_KIND_PRIMARY_KEY,
+ TABLE_DDL_KIND_UNIQUE,
+ TABLE_DDL_KIND_CHECK,
+ TABLE_DDL_KIND_FOREIGN_KEY,
+ TABLE_DDL_KIND_EXCLUSION,
+ TABLE_DDL_KIND_RULE,
+ TABLE_DDL_KIND_STATISTICS,
+ TABLE_DDL_KIND_TRIGGER,
+ TABLE_DDL_KIND_POLICY,
+ TABLE_DDL_KIND_RLS,
+ TABLE_DDL_KIND_REPLICA_IDENTITY,
+ TABLE_DDL_KIND_PARTITION,
+} TableDdlKind;
+
+static const struct
+{
+ const char *name;
+ TableDdlKind kind;
+} table_ddl_kind_names[] =
+{
+ {"table", TABLE_DDL_KIND_TABLE},
+ {"index", TABLE_DDL_KIND_INDEX},
+ {"primary_key", TABLE_DDL_KIND_PRIMARY_KEY},
+ {"unique", TABLE_DDL_KIND_UNIQUE},
+ {"check", TABLE_DDL_KIND_CHECK},
+ {"foreign_key", TABLE_DDL_KIND_FOREIGN_KEY},
+ {"exclusion", TABLE_DDL_KIND_EXCLUSION},
+ {"rule", TABLE_DDL_KIND_RULE},
+ {"statistics", TABLE_DDL_KIND_STATISTICS},
+ {"trigger", TABLE_DDL_KIND_TRIGGER},
+ {"policy", TABLE_DDL_KIND_POLICY},
+ {"rls", TABLE_DDL_KIND_RLS},
+ {"replica_identity", TABLE_DDL_KIND_REPLICA_IDENTITY},
+ {"partition", TABLE_DDL_KIND_PARTITION},
+};
+
+static Bitmapset *parse_kind_array(const char *paramname, ArrayType *arr);
static void append_ddl_option(StringInfo buf, bool pretty, int indent,
const char *fmt, ...)
pg_attribute_printf(4, 5);
@@ -57,6 +116,194 @@ 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).
+ *
+ * inherited_notnull is set when a NOT NULL constraint row exists in
+ * pg_constraint but conislocal=false (purely inherited). In that case
+ * the NOT NULL must not be emitted in the column list: the INHERITS /
+ * PARTITION OF clause already propagates it, and emitting it here would
+ * flip conislocal to true on replay.
+ */
+typedef struct LocalNotNullEntry
+{
+ Oid conoid;
+ char *name;
+ bool is_auto; /* matches "<table>_<col>_not_null" */
+ bool no_inherit;
+ bool inherited_notnull; /* row exists but conislocal=false */
+} 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 bool child_default_inherited_from_parent(Oid childOid,
+ const char *attname,
+ const char *child_adbin);
+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_typed_column_storage(TableDdlContext *ctx);
+static void emit_indexes(TableDdlContext *ctx);
+static void emit_local_constraints(TableDdlContext *ctx);
+static void emit_rules(TableDdlContext *ctx);
+static void emit_statistics(TableDdlContext *ctx);
+static void emit_replica_identity(TableDdlContext *ctx);
+static void emit_rls_toggles(TableDdlContext *ctx);
+static void emit_partition_children(TableDdlContext *ctx);
+
+
+/*
+ * parse_kind_array
+ * Parse a text[] of object-class kind names into a Bitmapset of
+ * TableDdlKind values.
+ *
+ * Each element is matched case-insensitively; surrounding whitespace is
+ * stripped. NULL elements are rejected. Unknown kind names raise an error
+ * citing the supplied parameter name. An empty array is also rejected.
+ * Duplicate entries are silently de-duplicated by the Bitmapset.
+ */
+static Bitmapset *
+parse_kind_array(const char *paramname, ArrayType *arr)
+{
+ Datum *elems;
+ bool *nulls;
+ int nelems;
+ Bitmapset *result = NULL;
+
+ deconstruct_array_builtin(arr, TEXTOID, &elems, &nulls, &nelems);
+
+ if (nelems == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" must specify at least one kind",
+ paramname)));
+
+ for (int i = 0; i < nelems; i++)
+ {
+ char *raw;
+ char *token;
+ char *end;
+ bool found = false;
+
+ if (nulls[i])
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" must not contain NULL elements",
+ paramname)));
+
+ raw = text_to_cstring(DatumGetTextPP(elems[i]));
+
+ /* Trim leading whitespace. */
+ token = raw;
+ while (*token == ' ' || *token == '\t')
+ token++;
+
+ /* Trim trailing whitespace. */
+ end = token + strlen(token);
+ while (end > token && (end[-1] == ' ' || end[-1] == '\t'))
+ end--;
+ *end = '\0';
+
+ for (size_t j = 0; j < lengthof(table_ddl_kind_names); j++)
+ {
+ if (pg_strcasecmp(token, table_ddl_kind_names[j].name) == 0)
+ {
+ result = bms_add_member(result,
+ (int) table_ddl_kind_names[j].kind);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("unrecognized kind \"%s\" in parameter \"%s\"",
+ token, paramname)));
+
+ pfree(raw);
+ }
+
+ return result;
+}
+
+/*
+ * is_kind_included
+ * Determine whether DDL for the given object-class kind should be
+ * emitted under the current options.
+ */
+static bool
+is_kind_included(const TableDdlContext *ctx, TableDdlKind kind)
+{
+ if (ctx->only_kinds != NULL)
+ return bms_is_member((int) kind, ctx->only_kinds);
+ if (ctx->except_kinds != NULL)
+ return !bms_is_member((int) kind, ctx->except_kinds);
+ return true;
+}
/*
* Helper to append a formatted string with optional pretty-printing.
@@ -975,3 +1222,1932 @@ 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;
+
+ if (con->contype != CONSTRAINT_NOTNULL)
+ 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;
+
+ /*
+ * Inherited-only constraint: record the fact so column-emit helpers
+ * can suppress inline NOT NULL (the parent's INHERITS clause already
+ * propagates it; re-emitting it would flip conislocal to true on
+ * replay).
+ */
+ if (!con->conislocal)
+ {
+ entries[attnum].inherited_notnull = true;
+ continue;
+ }
+
+ att = TupleDescAttr(tupdesc, attnum - 1);
+ autoname = makeObjectName(relname, NameStr(att->attname), "not_null");
+
+ 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;
+ pfree(autoname);
+
+ /*
+ * 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;
+
+ 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);
+
+ autoname = makeObjectName(RelationGetRelationName(rel),
+ NameStr(att->attname), "seq");
+ 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);
+ }
+ pfree(autoname);
+ 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];
+
+ /*
+ * Suppress NOT NULL when the constraint is inherited-only
+ * (conislocal=false). The INHERITS clause already propagates it;
+ * emitting it here would make conislocal true on replay. When
+ * there is no pg_constraint row at all (pre-PG18 catalog with
+ * attnotnull=true), inherited_notnull is false so we fall through
+ * and emit plain NOT NULL as before.
+ */
+ if (!nn->inherited_notnull)
+ {
+ 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);
+ /* inherited-only NOT NULL is not an override; treat as absent */
+ has_notnull = att->attnotnull &&
+ !nn_entries[att->attnum].inherited_notnull;
+
+ 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 (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");
+ }
+
+ 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);
+ }
+ }
+
+ 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);
+}
+
+/*
+ * child_default_inherited_from_parent
+ * Return true if child_adbin is identical to the adbin stored for
+ * attname on any immediate parent of childOid.
+ *
+ * PostgreSQL copies the adbin expression verbatim from parent to child
+ * when a partition is created, so a byte-for-byte match means the child
+ * did not override the default locally. For regular inheritance the
+ * parent typically has no default at all (atthasdef=false), so the
+ * function returns false and the override is correctly emitted.
+ */
+static bool
+child_default_inherited_from_parent(Oid childOid, const char *attname,
+ const char *child_adbin)
+{
+ List *parents;
+ ListCell *lc;
+ bool result = false;
+
+ parents = find_inheritance_parents(childOid, NoLock);
+
+ foreach(lc, parents)
+ {
+ Oid parentOid = lfirst_oid(lc);
+ Relation parentRel;
+ AttrNumber parentAttnum;
+ TupleConstr *constr;
+
+ parentRel = try_table_open(parentOid, AccessShareLock);
+ if (parentRel == NULL)
+ continue;
+
+ parentAttnum = get_attnum(parentOid, attname);
+ if (parentAttnum != InvalidAttrNumber)
+ {
+ Form_pg_attribute parentAtt =
+ TupleDescAttr(RelationGetDescr(parentRel), parentAttnum - 1);
+
+ if (parentAtt->atthasdef)
+ {
+ constr = RelationGetDescr(parentRel)->constr;
+ if (constr != NULL)
+ {
+ for (int j = 0; j < constr->num_defval; j++)
+ {
+ if (constr->defval[j].adnum == parentAttnum &&
+ strcmp(constr->defval[j].adbin, child_adbin) == 0)
+ {
+ result = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ table_close(parentRel, AccessShareLock);
+ if (result)
+ break;
+ }
+
+ list_free(parents);
+ return result;
+}
+
+/*
+ * emit_child_default_overrides
+ * ALTER TABLE ONLY qualname ALTER COLUMN col SET DEFAULT expr - one per
+ * inherited (attislocal=false) non-generated column that carries a
+ * default which differs from every immediate parent's default. Using
+ * ONLY prevents the statement from cascading into grandchildren and
+ * overwriting their own defaults. Defaults that are simply inherited
+ * unchanged from a parent are skipped to avoid redundant output.
+ * 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);
+ TupleConstr *constr = tupdesc->constr;
+ List *dpcontext = NIL;
+
+ for (int i = 0; i < tupdesc->natts; i++)
+ {
+ Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+ const char *adbin = NULL;
+ char *defstr;
+
+ if (att->attisdropped || att->attislocal || !att->atthasdef)
+ continue;
+ if (att->attgenerated != '\0')
+ continue;
+
+ /* Locate the raw adbin for parent-comparison and for deparse. */
+ if (constr != NULL)
+ {
+ for (int j = 0; j < constr->num_defval; j++)
+ {
+ if (constr->defval[j].adnum == att->attnum)
+ {
+ adbin = constr->defval[j].adbin;
+ break;
+ }
+ }
+ }
+ if (adbin == NULL)
+ continue;
+
+ /*
+ * Skip columns whose default expression is identical to a parent's;
+ * the child merely inherited it (typical for partition children).
+ * An explicit local override will have a different adbin.
+ */
+ if (child_default_inherited_from_parent(ctx->relid,
+ NameStr(att->attname),
+ adbin))
+ continue;
+
+ defstr = find_attrdef_text(ctx->rel, att->attnum, &dpcontext);
+ if (defstr == NULL)
+ continue;
+
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE ONLY %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_typed_column_storage
+ * For typed tables (CREATE TABLE ... OF type_name), emit per-column
+ * STORAGE and COMPRESSION overrides as ALTER TABLE statements.
+ *
+ * The typed-table grammar allows only DEFAULT / NOT NULL / CHECK in the
+ * CREATE TABLE column-options list (columnOptions -> ColQualList), so
+ * STORAGE and COMPRESSION cannot appear inline there. They must be
+ * emitted post-CREATE, matching what pg_dump produces.
+ */
+static void
+emit_typed_column_storage(TableDdlContext *ctx)
+{
+ TupleDesc tupdesc;
+
+ if (!OidIsValid(ctx->rel->rd_rel->reloftype))
+ return;
+
+ tupdesc = RelationGetDescr(ctx->rel);
+
+ for (int i = 0; i < tupdesc->natts; i++)
+ {
+ Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+
+ if (att->attisdropped)
+ continue;
+
+ if (att->attstorage != get_typstorage(att->atttypid))
+ {
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE %s ALTER COLUMN %s SET STORAGE %s;",
+ ctx->qualname,
+ quote_identifier(NameStr(att->attname)),
+ storage_name(att->attstorage));
+ append_stmt(ctx);
+ }
+
+ if (CompressionMethodIsValid(att->attcompression))
+ {
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE %s ALTER COLUMN %s SET COMPRESSION %s;",
+ ctx->qualname,
+ quote_identifier(NameStr(att->attname)),
+ GetCompressionMethodName(att->attcompression));
+ append_stmt(ctx);
+ }
+ }
+}
+
+/*
+ * 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;
+
+ /*
+ * Skip indexes that were inherited from a parent partitioned index.
+ * They are created automatically when the parent index DDL is
+ * replayed and the partition is attached, so emitting them separately
+ * would produce a "relation already exists" error on replay.
+ */
+ if (get_rel_relispartition(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;
+ List *fk_stmts = NIL;
+
+ 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);
+
+ /*
+ * Defer FK statements so they are emitted after all other
+ * constraints. A self-referencing FK (REFERENCES same_table)
+ * requires the PK/UNIQUE it targets to exist first, and because
+ * the catalog scan returns constraints in name order, an FK whose
+ * name sorts before the PK name would otherwise be emitted first
+ * and fail with "there is no unique constraint matching given
+ * keys".
+ */
+ if (con->contype == CONSTRAINT_FOREIGN)
+ fk_stmts = lappend(fk_stmts, pstrdup(ctx->buf.data));
+ else
+ append_stmt(ctx);
+
+ pfree(conbody);
+ }
+ }
+ systable_endscan(conScan);
+ table_close(conRel, AccessShareLock);
+
+ /* Append deferred FK statements after all other constraints. */
+ ctx->statements = list_concat(ctx->statements, fk_stmts);
+}
+
+/*
+ * 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;
+
+ /*
+ * When schema_qualified is false, the contract is that the output
+ * is replayable with the parent's schema in search_path. That
+ * contract cannot hold for a child that lives in a different schema:
+ * its own name would be ambiguous (landing in whatever schema is
+ * first in search_path) and references back to the parent would have
+ * to be schema-qualified anyway. Force full qualification so the
+ * child's DDL is unambiguous regardless of the caller's search_path.
+ */
+ if (!childctx.schema_qualified &&
+ get_rel_namespace(childoid) != ctx->base_namespace)
+ childctx.schema_qualified = true;
+
+ if (childctx.only_kinds != NULL &&
+ bms_is_member((int) TABLE_DDL_KIND_PARTITION,
+ childctx.only_kinds))
+ {
+ Bitmapset *child_only = bms_copy(childctx.only_kinds);
+
+ child_only = bms_del_member(child_only,
+ (int) TABLE_DDL_KIND_PARTITION);
+ if (bms_is_empty(child_only))
+ {
+ bms_free(child_only);
+ childctx.only_kinds = NULL;
+ }
+ else
+ childctx.only_kinds = child_only;
+ }
+
+ childstmts = pg_get_table_ddl_internal(&childctx);
+ ctx->statements = list_concat(ctx->statements, childstmts);
+ }
+ list_free(children);
+}
+
+/*
+ * pg_get_table_ddl_internal
+ * Generate DDL statements to recreate a regular or partitioned table.
+ *
+ * The caller initializes *ctx with the user-supplied option fields
+ * (relid, pretty, no_owner, no_tablespace, schema_qualified,
+ * only_kinds, except_kinds). This function opens the relation,
+ * validates access, populates the derived fields (rel, qualname,
+ * nn_entries, ...), runs the emission passes, and returns the
+ * accumulated statement list.
+ *
+ * Each emission helper consults is_kind_included() to decide whether
+ * it should run. The table-proper passes (CREATE TABLE / OWNER /
+ * ALTER COLUMN ... SET DEFAULT / ALTER COLUMN ... SET (...)) are
+ * grouped under KIND_TABLE so the FK-only / sub-object-only workflow
+ * is expressible as a single only or except list.
+ *
+ * Trigger and policy emission are scaffolded but currently disabled
+ * (#if 0) - they will become a single helper call once the standalone
+ * pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+ */
+static List *
+pg_get_table_ddl_internal(TableDdlContext *ctx)
+{
+ Relation rel;
+ char relkind;
+ AclResult aclresult;
+
+ rel = table_open(ctx->relid, AccessShareLock);
+
+ relkind = rel->rd_rel->relkind;
+
+ /*
+ * The initial cut only supports ordinary and partitioned tables. Views,
+ * matviews, foreign tables, sequences, indexes, composite types, and
+ * TOAST tables are out of scope for now.
+ */
+ if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ char *relname = pstrdup(RelationGetRelationName(rel));
+
+ table_close(rel, AccessShareLock);
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not an ordinary or partitioned table",
+ relname)));
+ }
+
+ /* Caller needs SELECT on the table to read its definition. */
+ aclresult = pg_class_aclcheck(ctx->relid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLE,
+ RelationGetRelationName(rel));
+
+ /*
+ * Validation: if the table has REPLICA IDENTITY USING INDEX and the
+ * referenced index would not be emitted under the active filter, the
+ * emitted REPLICA IDENTITY clause would reference an index the same DDL
+ * never produced. Determine which kind would emit the index (one of
+ * primary_key / unique / exclusion for constraint-backed indexes,
+ * otherwise the generic "index" kind) and require it to be in scope
+ * whenever "replica_identity" is. The check uses is_kind_included so
+ * it covers both forms naturally: an "except" list that omits the
+ * source kind, and an "only" list that omits it.
+ */
+ if (rel->rd_rel->relreplident == REPLICA_IDENTITY_INDEX &&
+ is_kind_included(ctx, TABLE_DDL_KIND_REPLICA_IDENTITY))
+ {
+ Oid replidx = RelationGetReplicaIndex(rel);
+
+ if (OidIsValid(replidx))
+ {
+ TableDdlKind idx_kind = TABLE_DDL_KIND_INDEX;
+ Oid conoid = get_index_constraint(replidx);
+
+ if (OidIsValid(conoid))
+ {
+ HeapTuple conTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conoid));
+
+ if (HeapTupleIsValid(conTup))
+ {
+ Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
+
+ switch (con->contype)
+ {
+ case CONSTRAINT_PRIMARY:
+ idx_kind = TABLE_DDL_KIND_PRIMARY_KEY;
+ break;
+ case CONSTRAINT_UNIQUE:
+ idx_kind = TABLE_DDL_KIND_UNIQUE;
+ break;
+ case CONSTRAINT_EXCLUSION:
+ idx_kind = TABLE_DDL_KIND_EXCLUSION;
+ break;
+ default:
+ break;
+ }
+ ReleaseSysCache(conTup);
+ }
+ }
+
+ if (!is_kind_included(ctx, idx_kind))
+ {
+ char *relname = pstrdup(RelationGetRelationName(rel));
+ const char *idx_name = table_ddl_kind_names[(int) idx_kind].name;
+
+ table_close(rel, AccessShareLock);
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("REPLICA IDENTITY for table \"%s\" requires kind \"%s\" to be emitted",
+ relname, idx_name),
+ errdetail("The table's REPLICA IDENTITY USING INDEX references an index produced by the \"%s\" kind, which is not in the active filter.",
+ idx_name),
+ errhint("Either add \"%s\" to the filter or remove \"replica_identity\" from it.",
+ idx_name)));
+ }
+ }
+ }
+
+ /*
+ * Populate derived fields now that the relation is open and validated.
+ * The remaining derived fields (nn_entries, skip_notnull_oids, buf,
+ * statements) start zeroed via the caller's `TableDdlContext ctx = {0}`
+ * initializer; the NOT NULL cache is populated by collect_local_not_null,
+ * the buffer is initStringInfo'd, and each emit pass appends to
+ * statements via lappend.
+ */
+ ctx->rel = rel;
+ ctx->base_namespace = RelationGetNamespace(rel);
+ ctx->save_nestlevel = -1;
+ ctx->qualname = lookup_relname_for_emit(ctx->relid, ctx->schema_qualified,
+ ctx->base_namespace);
+
+ /*
+ * Temporarily override search_path so that the ruleutils helpers
+ * (pg_get_indexdef_ddl, pg_get_constraintdef_body,
+ * pg_get_statisticsobjdef_ddl, pg_get_ruledef_ddl, etc.) produce names
+ * that match the schema_qualified flag. Those helpers decide whether to
+ * qualify a name by calling RelationIsVisible(), which checks whether the
+ * object's schema appears in the active search_path.
+ *
+ * schema_qualified = false: narrow to the base schema so that same-schema
+ * references in DEFAULT expressions, FK targets, indexes, rules, and
+ * statistics come out unqualified automatically. Cross-schema references
+ * stay qualified, which is the correctness requirement.
+ *
+ * schema_qualified = true: narrow to pg_catalog only so that objects in
+ * the base schema (or any user schema the caller placed on search_path)
+ * are not reachable without qualification, forcing fully-qualified output
+ * from every helper regardless of the caller's session search_path.
+ *
+ * AtEOXact_GUC cleans up at xact end if anything throws between here and
+ * the explicit restore below; on the normal path we restore right before
+ * returning.
+ */
+ if (!ctx->schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx->base_namespace);
+
+ if (nspname != NULL)
+ {
+ const char *qnsp = quote_identifier(nspname);
+
+ ctx->save_nestlevel = NewGUCNestLevel();
+ (void) set_config_option("search_path", qnsp,
+ PGC_USERSET, PGC_S_SESSION,
+ GUC_ACTION_SAVE, true, 0, false);
+ if (qnsp != nspname)
+ pfree((char *) qnsp);
+ pfree(nspname);
+ }
+ }
+ else
+ {
+ ctx->save_nestlevel = NewGUCNestLevel();
+ (void) set_config_option("search_path", "pg_catalog",
+ PGC_USERSET, PGC_S_SESSION,
+ GUC_ACTION_SAVE, true, 0, false);
+ }
+
+ /*
+ * Cache locally-declared NOT NULL constraint metadata so the column- emit
+ * helpers can produce "CONSTRAINT name NOT NULL" inline for user-named
+ * constraints, and so the constraint loop can avoid double-emitting them.
+ */
+ ctx->nn_entries = collect_local_not_null(rel, &ctx->skip_notnull_oids);
+
+ initStringInfo(&ctx->buf);
+
+ /*
+ * Emission passes. Order is significant: CREATE TABLE first; OWNER and
+ * the per-column ALTER COLUMN passes before sub-object emission;
+ * sub-objects in dependency-friendly order (indexes before constraints,
+ * since constraint-backed indexes are emitted out-of-line by the
+ * constraint loop); partition children last so the parent already exists
+ * at replay time. Each helper gates itself on is_kind_included for the
+ * relevant TABLE_DDL_KIND_*; the four "table itself" passes are grouped
+ * here under KIND_TABLE so all of CREATE TABLE / OWNER / SET DEFAULT /
+ * SET (...) drop out together when the user asks for only sub-objects
+ * (e.g. only => 'foreign_key' for the second pass of a two-pass FK
+ * clone).
+ */
+ if (is_kind_included(ctx, TABLE_DDL_KIND_TABLE))
+ {
+ emit_create_table_stmt(ctx);
+ emit_owner_stmt(ctx);
+ emit_child_default_overrides(ctx);
+ emit_attoptions(ctx);
+ emit_typed_column_storage(ctx);
+ }
+ emit_indexes(ctx);
+ emit_local_constraints(ctx);
+ emit_rules(ctx);
+ emit_statistics(ctx);
+ emit_replica_identity(ctx);
+ emit_rls_toggles(ctx);
+
+ /*
+ * Triggers and row-level security policies - disabled until the
+ * standalone pg_get_trigger_ddl() and pg_get_policy_ddl() helpers land.
+ * The scan + lock + filter scaffolding below is preserved (inside #if 0)
+ * so wiring up each emission becomes a one-liner in the loop body once
+ * those helpers are available.
+ */
+#if 0
+ if (is_kind_included(ctx, TABLE_DDL_KIND_TRIGGER))
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(ctx->relid));
+ trigScan = systable_beginscan(trigRel, TriggerRelidNameIndexId,
+ true, NULL, 1, &trigKey);
+ while (HeapTupleIsValid(trigTup = systable_getnext(trigScan)))
+ {
+ Form_pg_trigger trg = (Form_pg_trigger) GETSTRUCT(trigTup);
+
+ if (trg->tgisinternal)
+ continue;
+
+ /* TODO: append pg_get_trigger_ddl(trg->oid) output here. */
+ }
+ systable_endscan(trigScan);
+ table_close(trigRel, AccessShareLock);
+ }
+
+ if (is_kind_included(ctx, TABLE_DDL_KIND_POLICY))
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(ctx->relid));
+ polScan = systable_beginscan(polRel, PolicyPolrelidPolnameIndexId,
+ true, NULL, 1, &polKey);
+ while (HeapTupleIsValid(polTup = systable_getnext(polScan)))
+ {
+ /* TODO: append pg_get_policy_ddl(relid, polname) output here. */
+ }
+ systable_endscan(polScan);
+ table_close(polRel, AccessShareLock);
+ }
+#endif
+
+ emit_partition_children(ctx);
+
+ {
+ int natts = RelationGetDescr(rel)->natts;
+
+ for (int i = 1; i <= natts; i++)
+ if (ctx->nn_entries[i].name != NULL)
+ pfree(ctx->nn_entries[i].name);
+ }
+ pfree(ctx->nn_entries);
+
+ table_close(rel, AccessShareLock);
+ pfree(ctx->buf.data);
+ pfree(ctx->qualname);
+ list_free(ctx->skip_notnull_oids);
+
+ /*
+ * Pop the narrowed search_path now that all helpers have run. Errors
+ * thrown earlier are cleaned up by AtEOXact_GUC at xact end.
+ */
+ if (ctx->save_nestlevel >= 0)
+ AtEOXact_GUC(true, ctx->save_nestlevel);
+
+ return ctx->statements;
+}
+
+/*
+ * pg_get_table_ddl
+ * Return DDL to recreate a table as a set of text rows.
+ */
+Datum
+pg_get_table_ddl(PG_FUNCTION_ARGS)
+{
+ FuncCallContext *funcctx;
+ List *statements;
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcontext;
+ TableDdlContext ctx = {0};
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ if (!PG_ARGISNULL(5) && !PG_ARGISNULL(6))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"only_kinds\" and \"except_kinds\" parameters are mutually exclusive")));
+
+ /* Option defaults (match proargdefaults in pg_proc.dat). */
+ ctx.relid = PG_GETARG_OID(0);
+ ctx.pretty = false;
+ ctx.no_owner = false; /* owner DEFAULT true -> no_owner = false */
+ ctx.no_tablespace = false; /* tablespace DEFAULT true -> no_tablespace = false */
+ ctx.schema_qualified = true;
+ ctx.only_kinds = NULL;
+ ctx.except_kinds = NULL;
+
+ /* Override defaults with any explicitly supplied values. */
+ if (!PG_ARGISNULL(1))
+ ctx.pretty = PG_GETARG_BOOL(1);
+ if (!PG_ARGISNULL(2))
+ ctx.no_owner = !PG_GETARG_BOOL(2);
+ if (!PG_ARGISNULL(3))
+ ctx.no_tablespace = !PG_GETARG_BOOL(3);
+ if (!PG_ARGISNULL(4))
+ ctx.schema_qualified = PG_GETARG_BOOL(4);
+ if (!PG_ARGISNULL(5))
+ ctx.only_kinds = parse_kind_array("only_kinds",
+ PG_GETARG_ARRAYTYPE_P(5));
+ if (!PG_ARGISNULL(6))
+ ctx.except_kinds = parse_kind_array("except_kinds",
+ PG_GETARG_ARRAYTYPE_P(6));
+
+ statements = pg_get_table_ddl_internal(&ctx);
+ funcctx->user_fctx = statements;
+ funcctx->max_calls = list_length(statements);
+
+ MemoryContextSwitchTo(oldcontext);
+ }
+
+ funcctx = SRF_PERCALL_SETUP();
+ statements = (List *) funcctx->user_fctx;
+
+ if (funcctx->call_cntr < funcctx->max_calls)
+ {
+ char *stmt;
+
+ stmt = list_nth(statements, funcctx->call_cntr);
+
+ SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+ }
+ else
+ {
+ list_free_deep(statements);
+ SRF_RETURN_DONE(funcctx);
+ }
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 88de5c0481c..7186cf83e1d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -23,6 +23,7 @@
#include "access/htup_details.h"
#include "access/relation.h"
#include "access/table.h"
+#include "catalog/namespace.h"
#include "catalog/pg_aggregate.h"
#include "catalog/pg_am.h"
#include "catalog/pg_authid.h"
@@ -370,7 +371,7 @@ static void make_propgraphdef_elements(StringInfo buf, Oid pgrelid, char pgekind
static void make_propgraphdef_labels(StringInfo buf, Oid elid, const char *elalias, Oid elrelid);
static void make_propgraphdef_properties(StringInfo buf, Oid ellabelid, Oid elrelid);
static char *pg_get_statisticsobj_worker(Oid statextid, bool columns_only,
- bool missing_ok);
+ bool missing_ok, int prettyFlags);
static char *pg_get_partkeydef_worker(Oid relid, int prettyFlags,
bool attrsOnly, bool missing_ok);
static char *pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
@@ -603,6 +604,19 @@ pg_get_ruledef_ext(PG_FUNCTION_ARGS)
PG_RETURN_TEXT_P(string_to_text(res));
}
+/*
+ * pg_get_ruledef_ddl
+ * Like pg_get_ruledef but uses the active search_path to decide
+ * whether to schema-qualify the rule's target relation name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_ruledef_ddl(Oid ruleoid)
+{
+ return pg_get_ruledef_worker(ruleoid, PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA);
+}
+
static char *
pg_get_ruledef_worker(Oid ruleoid, int prettyFlags)
@@ -1241,6 +1255,22 @@ pg_get_indexdef_string(Oid indexrelid)
0, false);
}
+/*
+ * pg_get_indexdef_ddl
+ * Like pg_get_indexdef_string but uses the active search_path to
+ * decide whether to schema-qualify the indexed relation name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_indexdef_ddl(Oid indexrelid)
+{
+ return pg_get_indexdef_worker(indexrelid, 0, NULL,
+ false, false,
+ true, true,
+ PRETTYFLAG_SCHEMA, false);
+}
+
/* Internal version that just reports the key-column definitions */
char *
pg_get_indexdef_columns(Oid indexrelid, bool pretty)
@@ -1969,7 +1999,7 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS)
Oid statextid = PG_GETARG_OID(0);
char *res;
- res = pg_get_statisticsobj_worker(statextid, false, true);
+ res = pg_get_statisticsobj_worker(statextid, false, true, 0);
if (res == NULL)
PG_RETURN_NULL();
@@ -1984,7 +2014,20 @@ pg_get_statisticsobjdef(PG_FUNCTION_ARGS)
char *
pg_get_statisticsobjdef_string(Oid statextid)
{
- return pg_get_statisticsobj_worker(statextid, false, false);
+ return pg_get_statisticsobj_worker(statextid, false, false, 0);
+}
+
+/*
+ * pg_get_statisticsobjdef_ddl
+ * Like pg_get_statisticsobjdef_string but uses the active search_path
+ * to decide whether to schema-qualify the statistics object name.
+ * For use by pg_get_table_ddl which has already set search_path
+ * to the base schema when schema_qualified=false.
+ */
+char *
+pg_get_statisticsobjdef_ddl(Oid statextid)
+{
+ return pg_get_statisticsobj_worker(statextid, false, false, PRETTYFLAG_SCHEMA);
}
/*
@@ -1997,7 +2040,7 @@ pg_get_statisticsobjdef_columns(PG_FUNCTION_ARGS)
Oid statextid = PG_GETARG_OID(0);
char *res;
- res = pg_get_statisticsobj_worker(statextid, true, true);
+ res = pg_get_statisticsobj_worker(statextid, true, true, 0);
if (res == NULL)
PG_RETURN_NULL();
@@ -2009,7 +2052,8 @@ pg_get_statisticsobjdef_columns(PG_FUNCTION_ARGS)
* Internal workhorse to decompile an extended statistics object.
*/
static char *
-pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
+pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok,
+ int prettyFlags)
{
Form_pg_statistic_ext statextrec;
HeapTuple statexttup;
@@ -2069,10 +2113,18 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
if (!columns_only)
{
- nsp = get_namespace_name_or_temp(statextrec->stxnamespace);
- appendStringInfo(&buf, "CREATE STATISTICS %s",
- quote_qualified_identifier(nsp,
- NameStr(statextrec->stxname)));
+ if ((prettyFlags & PRETTYFLAG_SCHEMA) &&
+ StatisticsObjIsVisible(statextid))
+ appendStringInfo(&buf, "CREATE STATISTICS %s",
+ quote_identifier(NameStr(statextrec->stxname)));
+ else
+ {
+ nsp = get_namespace_name_or_temp(statextrec->stxnamespace);
+ appendStringInfo(&buf, "CREATE STATISTICS %s",
+ quote_qualified_identifier(nsp,
+ NameStr(statextrec->stxname)));
+ pfree(nsp);
+ }
/*
* Decode the stxkind column so that we know which stats types to
@@ -2163,10 +2215,9 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
{
Node *expr = (Node *) lfirst(lc);
char *str;
- int prettyFlags = PRETTYFLAG_PAREN;
str = deparse_expression_pretty(expr, context, false, false,
- prettyFlags, 0);
+ PRETTYFLAG_PAREN, 0);
if (colno > 0)
appendStringInfoString(&buf, ", ");
@@ -2544,6 +2595,20 @@ pg_get_constraintdef_command(Oid constraintId)
return pg_get_constraintdef_worker(constraintId, true, 0, false);
}
+/*
+ * pg_get_constraintdef_body
+ * Returns the constraint definition body without the
+ * "ALTER TABLE name ADD CONSTRAINT cname" prefix.
+ * FK target relation names in the body already use
+ * generate_relation_name (search_path-aware). For use by
+ * pg_get_table_ddl which builds the ALTER TABLE prefix itself.
+ */
+char *
+pg_get_constraintdef_body(Oid constraintId)
+{
+ return pg_get_constraintdef_worker(constraintId, false, 0, false);
+}
+
/*
* As of 9.4, we now use an MVCC snapshot for this.
*/
diff --git a/src/include/catalog/pg_inherits.h b/src/include/catalog/pg_inherits.h
index cc874abaabb..1e463b0106d 100644
--- a/src/include/catalog/pg_inherits.h
+++ b/src/include/catalog/pg_inherits.h
@@ -55,6 +55,7 @@ DECLARE_INDEX(pg_inherits_parent_index, 2187, InheritsParentIndexId, pg_inherits
extern List *find_inheritance_children(Oid parentrelId, LOCKMODE lockmode);
extern List *find_inheritance_children_extended(Oid parentrelId, bool omit_detached,
LOCKMODE lockmode, bool *detached_exist, TransactionId *detached_xmin);
+extern List *find_inheritance_parents(Oid relid, LOCKMODE lockmode);
extern List *find_all_inheritors(Oid parentrelId, LOCKMODE lockmode,
List **numparents);
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..d86af56ba78 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8634,6 +8634,14 @@
proargtypes => 'regdatabase bool bool bool',
proargnames => '{database,pretty,owner,tablespace}',
proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' },
+{ oid => '8215', descr => 'get DDL to recreate a table',
+ proname => 'pg_get_table_ddl', prorows => '50',
+ proisstrict => 'f', proretset => 't', provolatile => 's', proparallel => 'r',
+ pronargdefaults => '6', prorettype => 'text',
+ proargtypes => 'regclass bool bool bool bool _text _text',
+ proargnames => '{relation,pretty,owner,tablespace,schema_qualified,only_kinds,except_kinds}',
+ proargdefaults => '{false,true,true,true,NULL,NULL}',
+ prosrc => 'pg_get_table_ddl' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index c3d8518cb62..d33563fdead 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -92,6 +92,7 @@ extern void check_of_type(HeapTuple typetuple);
extern void register_on_commit_action(Oid relid, OnCommitAction action);
extern void remove_on_commit_action(Oid relid);
+extern OnCommitAction get_on_commit_action(Oid relid);
extern void PreCommit_on_commit_actions(void);
extern void AtEOXact_on_commit_actions(bool isCommit);
@@ -108,4 +109,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
List *partConstraint);
+extern const char *storage_name(char c);
+
#endif /* TABLECMDS_H */
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 25c05e2f649..c3b3dc65c93 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -25,6 +25,7 @@ typedef struct PlannedStmt PlannedStmt;
#define RULE_INDEXDEF_KEYS_ONLY 0x02 /* ignore included attributes */
extern char *pg_get_indexdef_string(Oid indexrelid);
+extern char *pg_get_indexdef_ddl(Oid indexrelid);
extern char *pg_get_indexdef_columns(Oid indexrelid, bool pretty);
extern char *pg_get_indexdef_columns_extended(Oid indexrelid,
uint16 flags);
@@ -34,6 +35,7 @@ extern char *pg_get_partkeydef_columns(Oid relid, bool pretty);
extern char *pg_get_partconstrdef_string(Oid partitionId, char *aliasname);
extern char *pg_get_constraintdef_command(Oid constraintId);
+extern char *pg_get_constraintdef_body(Oid constraintId);
extern char *deparse_expression(Node *expr, List *dpcontext,
bool forceprefix, bool showimplicit);
extern List *deparse_context_for(const char *aliasname, Oid relid);
@@ -54,5 +56,7 @@ extern char *get_range_partbound_string(List *bound_datums);
extern void get_reloptions(StringInfo buf, Datum reloptions);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern char *pg_get_statisticsobjdef_ddl(Oid statextid);
+extern char *pg_get_ruledef_ddl(Oid ruleoid);
#endif /* RULEUTILS_H */
diff --git a/src/test/regress/expected/pg_get_table_ddl.out b/src/test/regress/expected/pg_get_table_ddl.out
new file mode 100644
index 00000000000..db1b90cc0af
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,1516 @@
+--
+-- pg_get_table_ddl
+--
+-- All tests pass owner=>false so the ALTER TABLE OWNER TO line is not
+-- emitted, keeping output stable across test runners (which may run under
+-- different role names).
+--
+CREATE SCHEMA pgtbl_ddl_test;
+SET search_path = pgtbl_ddl_test;
+-- Basic table with PRIMARY KEY, NOT NULL, DEFAULT, COLLATE.
+CREATE TABLE basic (
+ id int PRIMARY KEY,
+ name text NOT NULL DEFAULT 'anon' COLLATE "C"
+);
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.basic (id integer NOT NULL, name text COLLATE "C" DEFAULT 'anon'::text NOT NULL);
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- Identity columns (with default and custom sequence options).
+CREATE TABLE id_cols (
+ id_always int GENERATED ALWAYS AS IDENTITY,
+ id_default int GENERATED BY DEFAULT AS IDENTITY
+);
+SELECT * FROM pg_get_table_ddl('id_cols'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.id_cols (id_always integer GENERATED ALWAYS AS IDENTITY NOT NULL, id_default integer GENERATED BY DEFAULT AS IDENTITY NOT NULL);
+(1 row)
+
+CREATE TABLE id_custom (
+ v int GENERATED ALWAYS AS IDENTITY (
+ SEQUENCE NAME id_custom_v_seq
+ START WITH 100 INCREMENT BY 5
+ MINVALUE 50 MAXVALUE 1000
+ CACHE 10 CYCLE
+ )
+);
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.id_custom (v integer GENERATED ALWAYS AS IDENTITY (START WITH 100 INCREMENT BY 5 MINVALUE 50 MAXVALUE 1000 CACHE 10 CYCLE) NOT NULL);
+(1 row)
+
+-- Generated stored column.
+CREATE TABLE gen_cols (
+ cents int,
+ dollars numeric GENERATED ALWAYS AS (cents / 100.0) STORED
+);
+SELECT * FROM pg_get_table_ddl('gen_cols'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.gen_cols (cents integer, dollars numeric GENERATED ALWAYS AS (((cents)::numeric / 100.0)) STORED);
+(1 row)
+
+-- Generated virtual column.
+CREATE TABLE gen_virtual (
+ base int,
+ derived int GENERATED ALWAYS AS (base * 2) VIRTUAL
+);
+SELECT * FROM pg_get_table_ddl('gen_virtual'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.gen_virtual (base integer, derived integer GENERATED ALWAYS AS ((base * 2)) VIRTUAL);
+(1 row)
+
+-- STORAGE and COMPRESSION (only emitted when non-default for the type).
+CREATE TABLE storage_cols (
+ a text STORAGE EXTERNAL,
+ b text STORAGE MAIN,
+ c text COMPRESSION pglz
+);
+SELECT * FROM pg_get_table_ddl('storage_cols'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.storage_cols (a text STORAGE EXTERNAL, b text STORAGE MAIN, c text COMPRESSION pglz);
+(1 row)
+
+-- Constraints: CHECK, UNIQUE, FOREIGN KEY (DEFERRABLE).
+CREATE TABLE refd (id int PRIMARY KEY);
+CREATE TABLE cons (
+ a int CHECK (a > 0),
+ b int UNIQUE,
+ c int REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED
+);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(3 rows)
+
+-- UNIQUE NULLS NOT DISTINCT (PG 15): null values are treated as equal for
+-- uniqueness purposes, so the NULLS NOT DISTINCT clause must be emitted.
+CREATE TABLE nulls_nd (
+ a int,
+ b int,
+ UNIQUE NULLS NOT DISTINCT (a, b)
+);
+SELECT * FROM pg_get_table_ddl('nulls_nd'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.nulls_nd (a integer, b integer);
+ ALTER TABLE pgtbl_ddl_test.nulls_nd ADD CONSTRAINT nulls_nd_a_b_key UNIQUE NULLS NOT DISTINCT (a, b);
+(2 rows)
+
+-- INCLUDE columns on a constraint-backed UNIQUE index (PG 11): the
+-- covering columns appear in an INCLUDE (...) clause on the ALTER TABLE
+-- ADD CONSTRAINT statement, not in the key column list.
+CREATE TABLE idx_inc (
+ id int PRIMARY KEY,
+ name text,
+ extra text,
+ UNIQUE (name) INCLUDE (extra)
+);
+SELECT * FROM pg_get_table_ddl('idx_inc'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idx_inc (id integer NOT NULL, name text, extra text);
+ ALTER TABLE pgtbl_ddl_test.idx_inc ADD CONSTRAINT idx_inc_name_extra_key UNIQUE (name) INCLUDE (extra);
+ ALTER TABLE pgtbl_ddl_test.idx_inc ADD CONSTRAINT idx_inc_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- FOREIGN KEY with ON DELETE / ON UPDATE referential actions and MATCH
+-- clause. Each variant must appear verbatim in the reconstructed DDL.
+CREATE TABLE fk_acts_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_acts (
+ a int,
+ b int,
+ c int,
+ d int,
+ CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES fk_acts_tgt(id)
+ ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET NULL,
+ CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET DEFAULT,
+ CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES fk_acts_tgt(id)
+ MATCH FULL
+);
+SELECT * FROM pg_get_table_ddl('fk_acts'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.fk_acts (a integer, b integer, c integer, d integer);
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON UPDATE RESTRICT ON DELETE CASCADE;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) MATCH FULL;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON DELETE SET DEFAULT;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON DELETE SET NULL;
+(5 rows)
+
+-- NOT ENFORCED foreign key (PG 17): the constraint is recorded in the
+-- catalog but not validated by the engine; NOT ENFORCED must appear in
+-- the reconstructed DDL.
+CREATE TABLE notenf_tgt (id int PRIMARY KEY);
+CREATE TABLE notenf (
+ a int,
+ CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES notenf_tgt(id)
+ NOT ENFORCED
+);
+SELECT * FROM pg_get_table_ddl('notenf'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.notenf (a integer);
+ ALTER TABLE pgtbl_ddl_test.notenf ADD CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.notenf_tgt(id) NOT ENFORCED;
+(2 rows)
+
+-- Self-referencing FK: the PK must appear before the FK that references it.
+-- The catalog scan returns constraints in name order; without the fix,
+-- "self_ref_parent_id_fkey" sorts before "self_ref_pkey" and the FK ADD
+-- CONSTRAINT fails with "there is no unique constraint matching given keys".
+CREATE TABLE self_ref (
+ id int PRIMARY KEY,
+ parent_id int REFERENCES self_ref(id)
+);
+SELECT * FROM pg_get_table_ddl('self_ref'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.self_ref (id integer NOT NULL, parent_id integer);
+ ALTER TABLE pgtbl_ddl_test.self_ref ADD CONSTRAINT self_ref_pkey PRIMARY KEY (id);
+ ALTER TABLE pgtbl_ddl_test.self_ref ADD CONSTRAINT self_ref_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES pgtbl_ddl_test.self_ref(id);
+(3 rows)
+
+-- WITHOUT OVERLAPS on a PRIMARY KEY (PG 17): the range-type period
+-- column forms a temporal primary key; WITHOUT OVERLAPS must appear
+-- inside the key column list in the reconstructed ALTER TABLE.
+-- Both columns use range types with native GiST support so no
+-- btree_gist extension is needed.
+CREATE TABLE temporal_pk (
+ grp int4range,
+ during daterange,
+ PRIMARY KEY (grp, during WITHOUT OVERLAPS)
+);
+SELECT * FROM pg_get_table_ddl('temporal_pk'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.temporal_pk (grp int4range NOT NULL, during daterange NOT NULL);
+ ALTER TABLE pgtbl_ddl_test.temporal_pk ADD CONSTRAINT temporal_pk_pkey PRIMARY KEY (grp, during WITHOUT OVERLAPS);
+(2 rows)
+
+-- Indexes: functional and partial. Constraint-backing indexes are
+-- suppressed (they are emitted by the constraint loop).
+CREATE TABLE idxd (id int PRIMARY KEY, name text);
+CREATE INDEX idxd_lower ON idxd (lower(name));
+CREATE INDEX idxd_partial ON idxd (id) WHERE id > 100;
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- Inheritance, including a child DEFAULT override on an inherited column.
+CREATE TABLE par (a int DEFAULT 1, b text);
+CREATE TABLE ch (c int) INHERITS (par);
+ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ch (c integer) INHERITS (pgtbl_ddl_test.par);
+ ALTER TABLE ONLY 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 ONLY 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)
+
+-- Non-constraint-backed index on a partitioned table: the parent's index DDL
+-- must appear once; the auto-created child partition indexes (relispartition=t)
+-- must be suppressed so the DDL round-trips without "relation already exists".
+CREATE TABLE parted_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_idx_a PARTITION OF parted_idx FOR VALUES IN ('a');
+CREATE UNIQUE INDEX parted_idx_uidx ON parted_idx (id, region);
+SELECT * FROM pg_get_table_ddl('parted_idx'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_idx (id integer, region text) PARTITION BY LIST (region);
+ CREATE UNIQUE INDEX parted_idx_uidx ON pgtbl_ddl_test.parted_idx USING btree (id, region);
+ CREATE TABLE pgtbl_ddl_test.parted_idx_a PARTITION OF pgtbl_ddl_test.parted_idx FOR VALUES IN ('a');
+(3 rows)
+
+-- Plain (non-unique) CREATE INDEX on partitioned table: the auto-created child
+-- index (relispartition=true) is suppressed, just like the UNIQUE case.
+CREATE TABLE parted_plain (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE parted_plain_a PARTITION OF parted_plain FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_plain_id_idx ON parted_plain (id);
+SELECT * FROM pg_get_table_ddl('parted_plain'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_plain (id integer, val text) PARTITION BY RANGE (id);
+ CREATE INDEX parted_plain_id_idx ON pgtbl_ddl_test.parted_plain USING btree (id);
+ CREATE TABLE pgtbl_ddl_test.parted_plain_a PARTITION OF pgtbl_ddl_test.parted_plain FOR VALUES FROM (0) TO (100);
+(3 rows)
+
+-- Constraint-backed PRIMARY KEY on partitioned table: the child's inherited
+-- constraint (conislocal=false) and its backing index are both suppressed.
+CREATE TABLE parted_pk (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_pk_a PARTITION OF parted_pk FOR VALUES IN ('a');
+ALTER TABLE parted_pk ADD PRIMARY KEY (id, region);
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_pk (id integer NOT NULL, region text NOT NULL) PARTITION BY LIST (region);
+ ALTER TABLE pgtbl_ddl_test.parted_pk ADD CONSTRAINT parted_pk_pkey PRIMARY KEY (id, region);
+ CREATE TABLE pgtbl_ddl_test.parted_pk_a PARTITION OF pgtbl_ddl_test.parted_pk FOR VALUES IN ('a');
+(3 rows)
+
+-- Multi-level (sub-partitioned) table with a CREATE INDEX on the root.
+-- The mid-level partitioned index (relispartition=true, relkind='I') and the
+-- leaf index (relispartition=true, relkind='i') are both suppressed.
+CREATE TABLE parted_sub (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_sub_a PARTITION OF parted_sub
+ FOR VALUES IN ('a') PARTITION BY RANGE (id);
+CREATE TABLE parted_sub_a1 PARTITION OF parted_sub_a FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_sub_id_idx ON parted_sub (id);
+SELECT * FROM pg_get_table_ddl('parted_sub'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_sub (id integer, region text) PARTITION BY LIST (region);
+ CREATE INDEX parted_sub_id_idx ON pgtbl_ddl_test.parted_sub USING btree (id);
+ CREATE TABLE pgtbl_ddl_test.parted_sub_a PARTITION OF pgtbl_ddl_test.parted_sub FOR VALUES IN ('a') PARTITION BY RANGE (id);
+ CREATE TABLE pgtbl_ddl_test.parted_sub_a1 PARTITION OF pgtbl_ddl_test.parted_sub_a FOR VALUES FROM (0) TO (100);
+(4 rows)
+
+-- Partition child with its own locally-defined index (relispartition=false):
+-- this is NOT inherited from any parent partitioned index and must be emitted.
+CREATE TABLE parted_child_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_child_idx_a PARTITION OF parted_child_idx FOR VALUES IN ('a');
+CREATE INDEX parted_child_idx_a_id ON parted_child_idx_a (id);
+SELECT * FROM pg_get_table_ddl('parted_child_idx'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_child_idx (id integer, region text) PARTITION BY LIST (region);
+ CREATE TABLE pgtbl_ddl_test.parted_child_idx_a PARTITION OF pgtbl_ddl_test.parted_child_idx FOR VALUES IN ('a');
+ CREATE INDEX parted_child_idx_a_id ON pgtbl_ddl_test.parted_child_idx_a USING btree (id);
+(3 rows)
+
+-- Mix: parent has a CREATE INDEX (auto-propagated to child, relispartition=true,
+-- suppressed) AND the child has its own local index (relispartition=false, emitted).
+CREATE TABLE parted_mixed (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_mixed_a PARTITION OF parted_mixed FOR VALUES IN ('a');
+CREATE INDEX parted_mixed_id ON parted_mixed (id);
+CREATE INDEX parted_mixed_a_region ON parted_mixed_a (region);
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_mixed (id integer, region text) PARTITION BY LIST (region);
+ CREATE INDEX parted_mixed_id ON pgtbl_ddl_test.parted_mixed USING btree (id);
+ CREATE TABLE pgtbl_ddl_test.parted_mixed_a PARTITION OF pgtbl_ddl_test.parted_mixed FOR VALUES IN ('a');
+ CREATE INDEX parted_mixed_a_region ON pgtbl_ddl_test.parted_mixed_a USING btree (region);
+(4 rows)
+
+-- FK on a partitioned table: declared on the parent; partition children inherit
+-- the constraint with conislocal=false and must not re-emit it.
+CREATE TABLE parted_fk_tgt (id int PRIMARY KEY);
+CREATE TABLE parted_fk (id int, tgt_id int REFERENCES parted_fk_tgt(id))
+ PARTITION BY RANGE (id);
+CREATE TABLE parted_fk_a PARTITION OF parted_fk FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('parted_fk'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_fk (id integer, tgt_id integer) PARTITION BY RANGE (id);
+ ALTER TABLE pgtbl_ddl_test.parted_fk ADD CONSTRAINT parted_fk_tgt_id_fkey FOREIGN KEY (tgt_id) REFERENCES pgtbl_ddl_test.parted_fk_tgt(id);
+ CREATE TABLE pgtbl_ddl_test.parted_fk_a PARTITION OF pgtbl_ddl_test.parted_fk FOR VALUES FROM (0) TO (100);
+(3 rows)
+
+-- Statistics declared explicitly on a partition child (not auto-propagated):
+-- the child's statistics object appears inside the child's DDL block.
+CREATE TABLE parted_stx (id int, region text, val float) PARTITION BY LIST (region);
+CREATE TABLE parted_stx_a PARTITION OF parted_stx FOR VALUES IN ('a');
+CREATE STATISTICS parted_stx_stat (ndistinct) ON id, region FROM parted_stx;
+CREATE STATISTICS parted_stx_a_stat (ndistinct) ON id, val FROM parted_stx_a;
+SELECT * FROM pg_get_table_ddl('parted_stx'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_stx (id integer, region text, val double precision) PARTITION BY LIST (region);
+ CREATE STATISTICS pgtbl_ddl_test.parted_stx_stat (ndistinct) ON id, region FROM pgtbl_ddl_test.parted_stx;
+ CREATE TABLE pgtbl_ddl_test.parted_stx_a PARTITION OF pgtbl_ddl_test.parted_stx FOR VALUES IN ('a');
+ CREATE STATISTICS pgtbl_ddl_test.parted_stx_a_stat (ndistinct) ON id, val FROM pgtbl_ddl_test.parted_stx_a;
+(4 rows)
+
+DROP TABLE parted_stx;
+-- NO INHERIT CHECK constraint: the NO INHERIT flag must appear verbatim in the
+-- inline constraint definition inside the CREATE TABLE body.
+CREATE TABLE noinhchk (id int, val int,
+ CONSTRAINT val_pos CHECK (val > 0) NO INHERIT);
+SELECT * FROM pg_get_table_ddl('noinhchk'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.noinhchk (id integer, val integer, CONSTRAINT val_pos CHECK ((val > 0)) NO INHERIT);
+(1 row)
+
+DROP TABLE noinhchk;
+-- Multiple inheritance parents: the INHERITS clause must list both parents in
+-- declaration order; only the locally-defined column appears in the column list.
+CREATE TABLE mi_p1 (a int DEFAULT 1);
+CREATE TABLE mi_p2 (b text DEFAULT 'x');
+CREATE TABLE mi_child (c float) INHERITS (mi_p1, mi_p2);
+SELECT * FROM pg_get_table_ddl('mi_child'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.mi_child (c double precision) INHERITS (pgtbl_ddl_test.mi_p1, pgtbl_ddl_test.mi_p2);
+(1 row)
+
+DROP TABLE mi_p1, mi_p2 CASCADE;
+NOTICE: drop cascades to table mi_child
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO pgtbl_ddl_test.rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM pgtbl_ddl_test.stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx (a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(3 rows)
+
+-- UNLOGGED + reloptions.
+CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70);
+SELECT * FROM pg_get_table_ddl('uno'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE UNLOGGED TABLE pgtbl_ddl_test.uno (id integer) WITH (fillfactor='70');
+(1 row)
+
+-- Typed table (CREATE TABLE OF type_name). Columns inherited from the
+-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come
+-- out through a single "(col WITH OPTIONS ...)" list.
+CREATE TYPE typed_t AS (a int, b text);
+CREATE TABLE typed_plain OF typed_t;
+SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_plain OF pgtbl_ddl_test.typed_t;
+(1 row)
+
+CREATE TABLE typed_over OF typed_t (
+ a WITH OPTIONS DEFAULT 7 NOT NULL,
+ b WITH OPTIONS NOT NULL,
+ CONSTRAINT b_nonempty CHECK (length(b) > 0)
+);
+SELECT * FROM pg_get_table_ddl('typed_over'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_over OF pgtbl_ddl_test.typed_t (a WITH OPTIONS DEFAULT 7 NOT NULL, b WITH OPTIONS NOT NULL, CONSTRAINT b_nonempty CHECK ((length(b) > 0)));
+(1 row)
+
+-- Temporary tables + ON COMMIT. Temp tables must never be emitted with
+-- the session-private pg_temp_NN schema prefix -- the TEMPORARY keyword
+-- already places the table in pg_temp and the prefix is non-replayable.
+-- ON COMMIT DROP only fires at commit, so the queries that need to see
+-- the catalog entry must run in the same transaction as the CREATE.
+BEGIN;
+CREATE TEMP TABLE temp_default (id int);
+CREATE TEMP TABLE temp_delete (id int) ON COMMIT DELETE ROWS;
+CREATE TEMP TABLE temp_drop (id int) ON COMMIT DROP;
+SELECT line FROM pg_get_table_ddl('temp_default'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------
+ CREATE TEMPORARY TABLE temp_default (id integer);
+(1 row)
+
+SELECT line FROM pg_get_table_ddl('temp_delete'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE temp_delete (id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT line FROM pg_get_table_ddl('temp_drop'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------
+ CREATE TEMPORARY TABLE temp_drop (id integer) ON COMMIT DROP;
+(1 row)
+
+ROLLBACK;
+-- Pretty mode.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, pretty => true);
+ pg_get_table_ddl
+------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.basic ( +
+ id integer NOT NULL, +
+ name text COLLATE "C" DEFAULT 'anon'::text NOT NULL +
+ );
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- only_kinds / except_kinds gating: each emits either only the listed kinds
+-- (only_kinds) or every kind except the listed ones (except_kinds). The two
+-- are mutually exclusive. Kind vocabulary: table, index,
+-- primary_key, unique, check, foreign_key, exclusion, rule,
+-- statistics, trigger, policy, rls, replica_identity, partition.
+-- NOT NULL is not in the vocabulary - always emitted to prevent
+-- producing schemas that silently accept NULLs the source would
+-- have rejected.
+--
+-- except_kinds=index hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['index']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- except_kinds=primary_key,unique,check,foreign_key,exclusion hides every
+-- table-level constraint. Inline CHECK in the CREATE TABLE body is
+-- suppressed too.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['primary_key','unique','check','foreign_key','exclusion']);
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=foreign_key suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- first pass of the two-pass FK-clone workflow: emit tables without
+-- cross-table FKs, load data, then re-run with only_kinds=foreign_key
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+(2 rows)
+
+-- only_kinds=foreign_key is the second pass of the two-pass FK-clone
+-- workflow: emit only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN
+-- KEY rows; the CREATE TABLE and every non-FK sub-object are
+-- suppressed (the table already exists from the first pass).
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(1 row)
+
+-- A table with no foreign keys produces no rows under
+-- only_kinds=foreign_key.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- only_kinds=table emits only the bare CREATE TABLE (with any inline
+-- column-level NOT NULL and CHECK) - no indexes, no PRIMARY KEY ALTER,
+-- no other sub-objects. Useful for capturing a table's shape without
+-- pulling in everything attached to it.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table']);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+(1 row)
+
+-- only_kinds=check on a non-partition table. The constraint loop must emit
+-- column-level and table-level CHECK constraints via ALTER TABLE here,
+-- because the inline path in CREATE TABLE never runs (KIND_TABLE is
+-- not in the filter).
+CREATE TABLE chk_only (
+ id int PRIMARY KEY,
+ qty int CHECK (qty > 0),
+ CONSTRAINT chk_only_id_pos CHECK (id > 0)
+);
+SELECT * FROM pg_get_table_ddl('chk_only'::regclass, owner => false, only_kinds => ARRAY['check']);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.chk_only ADD CONSTRAINT chk_only_id_pos CHECK ((id > 0));
+ ALTER TABLE pgtbl_ddl_test.chk_only ADD CONSTRAINT chk_only_qty_check CHECK ((qty > 0));
+(2 rows)
+
+DROP TABLE chk_only;
+-- only_kinds=partition on a partitioned-table parent: each child's full DDL
+-- is emitted (CREATE TABLE ... PARTITION OF ...). The "partition"
+-- keyword is a gate at the parent level; it is stripped from the
+-- propagated filter so the child does not exclude its own
+-- CREATE TABLE.
+CREATE TABLE p_only (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE p_only_a PARTITION OF p_only FOR VALUES FROM (0) TO (100);
+CREATE TABLE p_only_b PARTITION OF p_only FOR VALUES FROM (100) TO (200);
+SELECT * FROM pg_get_table_ddl('p_only'::regclass, owner => false, only_kinds => ARRAY['partition']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.p_only_a PARTITION OF pgtbl_ddl_test.p_only FOR VALUES FROM (0) TO (100);
+ CREATE TABLE pgtbl_ddl_test.p_only_b PARTITION OF pgtbl_ddl_test.p_only FOR VALUES FROM (100) TO (200);
+(2 rows)
+
+DROP TABLE p_only;
+-- only_kinds and except_kinds are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false,
+ only_kinds => ARRAY['foreign_key'],
+ except_kinds => ARRAY['foreign_key']);
+ERROR: "only_kinds" and "except_kinds" parameters are mutually exclusive
+-- Pub/sub schema clone: keep the table and its primary key, drop the
+-- rest of the constraints so the subscriber can replicate without
+-- pulling cross-table dependencies along.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique','check','foreign_key','exclusion']);
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=rule hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, except_kinds => ARRAY['rule']);
+ pg_get_table_ddl
+----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+(1 row)
+
+-- except_kinds=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, except_kinds => ARRAY['statistics']);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, except_kinds => ARRAY['rls']);
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+(1 row)
+
+-- except_kinds=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, except_kinds => ARRAY['replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+(1 row)
+
+-- Partition children are emitted by default; except_kinds=partition
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false, except_kinds => ARRAY['partition']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range (id integer, k integer) PARTITION BY RANGE (id);
+(1 row)
+
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY[' INDEX ',' RULE ']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- schema_qualified=false strips the schema from the target table itself
+-- and from same-schema sibling references (inheritance/partition
+-- parents, identity sequences in the same schema), while cross-schema
+-- references are kept qualified for correctness.
+--
+-- Bare CREATE TABLE for an ordinary table.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE basic (id integer NOT NULL, name text COLLATE "C" DEFAULT 'anon'::text NOT NULL);
+ ALTER TABLE basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- INHERITS parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------
+ CREATE TABLE ch (c integer) INHERITS (par);
+ ALTER TABLE ONLY 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 ONLY 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_pkey PRIMARY KEY (id);
+ ALTER TABLE xschema_fk ADD CONSTRAINT xschema_fk_pid_fkey FOREIGN KEY (pid) REFERENCES pgtbl_ddl_other.parent(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
+-- Cross-schema partition child with schema_qualified=false: the child must
+-- carry its own schema prefix so the output is unambiguous regardless of the
+-- caller's search_path. Before the fix, the child was emitted as the bare
+-- name "pt_c" (landing in whichever schema is first in search_path) while
+-- the parent reference inside the CREATE TABLE came out as the fully-qualified
+-- "pgtbl_ddl_part_s.pt", making the DDL inconsistent and unreplayable.
+CREATE SCHEMA pgtbl_ddl_part_s;
+CREATE SCHEMA pgtbl_ddl_part_other;
+CREATE TABLE pgtbl_ddl_part_s.pt (id int, val int) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_part_other.pt_c
+ PARTITION OF pgtbl_ddl_part_s.pt
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_part_s.pt'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------
+ CREATE TABLE pt (id integer, val integer) PARTITION BY RANGE (id);
+ CREATE TABLE pgtbl_ddl_part_other.pt_c PARTITION OF pgtbl_ddl_part_s.pt FOR VALUES FROM (0) TO (100);
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_part_s CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_part_s.pt
+DROP SCHEMA pgtbl_ddl_part_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);
+ 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. String
+-- literal text is output verbatim by the deparser and is unaffected by
+-- search_path narrowing.
+CREATE SCHEMA pgtbl_ddl_str;
+CREATE TABLE pgtbl_ddl_str.p (id int, note text) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_str.pc PARTITION OF pgtbl_ddl_str.p
+ (CONSTRAINT chk CHECK (note <> 'pgtbl_ddl_str.secret'))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_str.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK ((note <> 'pgtbl_ddl_str.secret'::text));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_str CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_str.p
+-- schema_qualified=false must NOT corrupt double-quoted identifiers
+-- whose contents happen to contain the schema name followed by a dot
+-- (for example a column literally named "<schema>.weird" inside that
+-- schema). The mechanism is purely search_path narrowing: the deparser
+-- outputs attribute names verbatim from pg_attribute, so the quoted
+-- identifier content is never examined for schema prefixes. This holds
+-- even when the schema name is a single letter.
+CREATE SCHEMA pgtbl_ddl_qid;
+CREATE TABLE pgtbl_ddl_qid.p (id int, "pgtbl_ddl_qid.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_qid.pc PARTITION OF pgtbl_ddl_qid.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_qid.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_qid.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK (("pgtbl_ddl_qid.weird" > 0));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_qid CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_qid.p
+-- Same with a short (single-letter) schema name to confirm there is no
+-- minimum-length assumption in the stripping path.
+CREATE SCHEMA pgtbl_ddl_q1;
+CREATE TABLE pgtbl_ddl_q1.p (id int, "pgtbl_ddl_q1.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_q1.pc PARTITION OF pgtbl_ddl_q1.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_q1.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_q1.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK (("pgtbl_ddl_q1.weird" > 0));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_q1 CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_q1.p
+-- schema_qualified=false when the base schema name itself requires
+-- quoting (its prefix starts with "). Both forms of the prefix -
+-- bare-lowercase and quoted - must be stripped from outer
+-- ALTER TABLE / ON / CREATE INDEX prefixes, while inner quoted
+-- identifiers that happen to begin the same way must be preserved.
+CREATE SCHEMA "PgTbl-Ddl-Q";
+CREATE TABLE "PgTbl-Ddl-Q"."T" (id int, val int,
+ CONSTRAINT chk CHECK (val > 0));
+CREATE INDEX "T_val_idx" ON "PgTbl-Ddl-Q"."T" (val);
+SELECT * FROM pg_get_table_ddl('"PgTbl-Ddl-Q"."T"'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE TABLE "T" (id integer, val integer, CONSTRAINT chk CHECK ((val > 0)));
+ CREATE INDEX "T_val_idx" ON "T" USING btree (val);
+(2 rows)
+
+DROP SCHEMA "PgTbl-Ddl-Q" CASCADE;
+NOTICE: drop cascades to table "PgTbl-Ddl-Q"."T"
+-- Error: not an ordinary or partitioned table.
+CREATE VIEW v AS SELECT 1 AS x;
+SELECT * FROM pg_get_table_ddl('v'::regclass);
+ERROR: "v" is not an ordinary or partitioned table
+CREATE SEQUENCE s;
+SELECT * FROM pg_get_table_ddl('s'::regclass);
+ERROR: "s" is not an ordinary or partitioned table
+-- NULL argument returns no rows.
+SELECT * FROM pg_get_table_ddl(NULL);
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- REPLICA IDENTITY USING INDEX validation: if the referenced index
+-- would not be emitted (because its source kind is suppressed) but
+-- replica_identity itself would be, the function raises an error so
+-- the emitted DDL never dangles. Exercise all three constraint-backed
+-- forms (PK, UNIQUE, EXCLUSION) and a bare CREATE UNIQUE INDEX, in
+-- both the except-list and only-list shapes.
+--
+-- PK-backed: except_kinds=primary_key without except_kinds=replica_identity.
+CREATE TABLE ri_pk_excl (a int PRIMARY KEY);
+ALTER TABLE ri_pk_excl REPLICA IDENTITY USING INDEX ri_pk_excl_pkey;
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+ERROR: REPLICA IDENTITY for table "ri_pk_excl" requires kind "primary_key" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "primary_key" kind, which is not in the active filter.
+HINT: Either add "primary_key" to the filter or remove "replica_identity" from it.
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_pk_excl (a integer NOT NULL);
+(1 row)
+
+DROP TABLE ri_pk_excl;
+-- UNIQUE-constraint-backed replica identity index.
+CREATE TABLE ri_uniq_excl (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_uniq_excl REPLICA IDENTITY USING INDEX ri_uniq_excl_a_key;
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique']);
+ERROR: REPLICA IDENTITY for table "ri_uniq_excl" requires kind "unique" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "unique" kind, which is not in the active filter.
+HINT: Either add "unique" to the filter or remove "replica_identity" from it.
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique','replica_identity']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_uniq_excl (a integer NOT NULL, b integer);
+(1 row)
+
+DROP TABLE ri_uniq_excl;
+-- Bare CREATE UNIQUE INDEX (no backing constraint) used as REPLICA
+-- IDENTITY: the index is emitted by the "index" kind, so excluding
+-- "index" without also excluding "replica_identity" must error.
+CREATE TABLE ri_idx_excl (a int NOT NULL, b int);
+CREATE UNIQUE INDEX ri_idx_excl_a_uniq ON ri_idx_excl (a);
+ALTER TABLE ri_idx_excl REPLICA IDENTITY USING INDEX ri_idx_excl_a_uniq;
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index']);
+ERROR: REPLICA IDENTITY for table "ri_idx_excl" requires kind "index" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "index" kind, which is not in the active filter.
+HINT: Either add "index" to the filter or remove "replica_identity" from it.
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx_excl (a integer NOT NULL, b integer);
+(1 row)
+
+DROP TABLE ri_idx_excl;
+-- Include-side analog: only_kinds=replica_identity must include the kind
+-- that emits the index, or the function errors.
+CREATE TABLE ri_only (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ERROR: REPLICA IDENTITY for table "ri_only" requires kind "unique" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "unique" kind, which is not in the active filter.
+HINT: Either add "unique" to the filter or remove "replica_identity" from it.
+-- Including both kinds succeeds.
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['unique','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.ri_only ADD CONSTRAINT ri_only_a_key UNIQUE (a);
+ ALTER TABLE pgtbl_ddl_test.ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+(2 rows)
+
+DROP TABLE ri_only;
+-- Per-kind only_kinds / except_kinds coverage. For each kind in the vocabulary,
+-- verify that only_kinds=K emits just that kind's output and that except_kinds=K
+-- omits just that kind's output. Kinds with no fixture above (trigger,
+-- policy) are still recognized but currently emit nothing; only_kinds=K
+-- returns zero rows for them. Fixtures used:
+-- idxd: PK + two secondary CREATE INDEX
+-- cons: PK + UNIQUE + CHECK + FK (constraint-backed)
+-- rt: CREATE RULE
+-- stx: CREATE STATISTICS
+-- rls: ENABLE/FORCE ROW LEVEL SECURITY
+-- ri_full: REPLICA IDENTITY FULL (no source-kind dependency)
+CREATE TABLE excl_fix (rng int4range,
+ EXCLUDE USING gist (rng WITH &&));
+-- only_kinds=K: emit only kind K's statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['unique']);
+ pg_get_table_ddl
+-----------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.excl_fix ADD CONSTRAINT excl_fix_rng_excl EXCLUDE USING gist (rng WITH &&);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, only_kinds => ARRAY['rule']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO pgtbl_ddl_test.rt_log (id)+
+ VALUES (new.id);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM pgtbl_ddl_test.stx;
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, only_kinds => ARRAY['rls']);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(1 row)
+
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['policy']);
+-- except_kinds=K: emit everything except kind K's statements. PK/UNIQUE/
+-- INDEX are exercised on tables without REPLICA IDENTITY USING INDEX
+-- so the validation does not trip.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['check']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, except_kinds => ARRAY['exclusion']);
+ pg_get_table_ddl
+-------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.excl_fix (rng int4range);
+(1 row)
+
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['policy']);
+-- except_kinds=table suppresses the CREATE TABLE + OWNER + child-default
+-- SET DEFAULT + attoptions passes, leaving only the sub-object passes.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['table']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- Multi-kind compositions.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table','index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['primary_key','foreign_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(1 row)
+
+-- Duplicate entries in the list are silently de-duplicated by the
+-- Bitmapset.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index','index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+DROP TABLE excl_fix;
+-- Kind-filter tests on partitioned tables with indexes.
+--
+-- only_kinds=index on a partitioned table: only the root CREATE INDEX is
+-- emitted; no CREATE TABLE, no partition children (KIND_PARTITION absent).
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ only_kinds => ARRAY['index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE INDEX parted_mixed_id ON pgtbl_ddl_test.parted_mixed USING btree (id);
+(1 row)
+
+-- except_kinds=index on a partitioned table: CREATE TABLE for the parent
+-- and all children are emitted; no index statements.
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ except_kinds => ARRAY['index']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_mixed (id integer, region text) PARTITION BY LIST (region);
+ CREATE TABLE pgtbl_ddl_test.parted_mixed_a PARTITION OF pgtbl_ddl_test.parted_mixed FOR VALUES IN ('a');
+(2 rows)
+
+-- only_kinds=primary_key on a partitioned table: only the ALTER TABLE ADD
+-- CONSTRAINT PK on the parent; the child's inherited PK (conislocal=false)
+-- is skipped; no CREATE TABLE, no partition children.
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false,
+ only_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.parted_pk ADD CONSTRAINT parted_pk_pkey PRIMARY KEY (id, region);
+(1 row)
+
+-- only_kinds=K on a table that does not have any K of that kind is a valid
+-- filter and returns zero rows. Tested previously with foreign_key
+-- on 'refd'; here also rule, statistics, rls, replica_identity, and
+-- exclusion on 'basic' (which has only PK + columns).
+SELECT count(*) AS rule_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rule']);
+ rule_rows
+-----------
+ 0
+(1 row)
+
+SELECT count(*) AS stats_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+ stats_rows
+------------
+ 0
+(1 row)
+
+SELECT count(*) AS rls_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rls']);
+ rls_rows
+----------
+ 0
+(1 row)
+
+SELECT count(*) AS ri_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ ri_rows
+---------
+ 0
+(1 row)
+
+SELECT count(*) AS excl_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+ excl_rows
+-----------
+ 0
+(1 row)
+
+-- Input-validation edge cases for the kind array. All
+-- of these raise an error; the function never reaches relation open.
+-- Empty array:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[]::text[]);
+ERROR: parameter "only_kinds" must specify at least one kind
+-- NULL element:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[NULL::text]);
+ERROR: parameter "only_kinds" must not contain NULL elements
+-- Unrecognized kind:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY['no_such_kind']);
+ERROR: unrecognized kind "no_such_kind" in parameter "only_kinds"
+-- Same kind specified through both parameters is still mutex-rejected.
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ only_kinds => ARRAY['index'],
+ except_kinds => ARRAY['rule']);
+ERROR: "only_kinds" and "except_kinds" parameters are mutually exclusive
+-- Round-trip verification. For every test table, capture the DDL the
+-- function emits, drop the schema, replay each table's DDL in
+-- dependency order, and confirm that pg_get_table_ddl on the recreated
+-- relation matches the original line-for-line. The final SELECT must
+-- return zero rows.
+CREATE TEMP TABLE pgtbl_ddl_rt_orig (name text, ord int, line text);
+INSERT INTO pgtbl_ddl_rt_orig
+SELECT t.name, o.ord, o.line
+FROM (VALUES
+ ('basic'), ('id_cols'), ('id_custom'), ('gen_cols'), ('gen_virtual'), ('storage_cols'),
+ ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'),
+ ('fk_acts_tgt'), ('fk_acts'), ('notenf_tgt'), ('notenf'), ('self_ref'), ('temporal_pk'),
+ ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('parted_list'),
+ ('parted_idx'), ('parted_idx_a'),
+ ('parted_plain'), ('parted_plain_a'),
+ ('parted_pk'), ('parted_pk_a'),
+ ('parted_sub'), ('parted_sub_a'), ('parted_sub_a1'),
+ ('parted_child_idx'), ('parted_child_idx_a'),
+ ('parted_mixed'), ('parted_mixed_a'),
+ ('parted_fk_tgt'), ('parted_fk'), ('parted_fk_a'),
+ ('rt_log'), ('rt'),
+ ('stx'), ('rls'), ('ri_full'), ('ri_idx'), ('uno')
+ ) AS t(name),
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord);
+DO $$
+DECLARE
+ tables CONSTANT text[] := ARRAY[
+ 'basic', 'id_cols', 'id_custom', 'gen_cols', 'gen_virtual', 'storage_cols',
+ 'refd', 'cons', 'nulls_nd', 'idx_inc',
+ 'fk_acts_tgt', 'fk_acts', 'notenf_tgt', 'notenf', 'self_ref', 'temporal_pk',
+ 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ 'parted_list',
+ 'parted_idx', 'parted_idx_a',
+ 'parted_plain', 'parted_plain_a',
+ 'parted_pk', 'parted_pk_a',
+ 'parted_sub', 'parted_sub_a', 'parted_sub_a1',
+ 'parted_child_idx', 'parted_child_idx_a',
+ 'parted_mixed', 'parted_mixed_a',
+ 'parted_fk_tgt', 'parted_fk', 'parted_fk_a',
+ 'rt_log', 'rt',
+ 'stx', 'rls', 'ri_full', 'ri_idx', 'uno'
+ ];
+ t text;
+ stmt text;
+BEGIN
+ DROP SCHEMA pgtbl_ddl_test CASCADE;
+ CREATE SCHEMA pgtbl_ddl_test;
+ FOREACH t IN ARRAY tables LOOP
+ FOR stmt IN
+ SELECT line FROM pgtbl_ddl_rt_orig WHERE name = t ORDER BY ord
+ LOOP
+ BEGIN
+ EXECUTE stmt;
+ EXCEPTION WHEN duplicate_table THEN
+ NULL; -- partition child already created by its parent's DDL
+ END;
+ END LOOP;
+ END LOOP;
+END $$;
+NOTICE: drop cascades to 43 other objects
+DETAIL: drop cascades to table basic
+drop cascades to table id_cols
+drop cascades to table id_custom
+drop cascades to table gen_cols
+drop cascades to table gen_virtual
+drop cascades to table storage_cols
+drop cascades to table refd
+drop cascades to table cons
+drop cascades to table nulls_nd
+drop cascades to table idx_inc
+drop cascades to table fk_acts_tgt
+drop cascades to table fk_acts
+drop cascades to table notenf_tgt
+drop cascades to table notenf
+drop cascades to table self_ref
+drop cascades to table temporal_pk
+drop cascades to table idxd
+drop cascades to table par
+drop cascades to table ch
+drop cascades to table attopt
+drop cascades to table parted_range
+drop cascades to table parted_hash
+drop cascades to table parted_list
+drop cascades to table parted_idx
+drop cascades to table parted_plain
+drop cascades to table parted_pk
+drop cascades to table parted_sub
+drop cascades to table parted_child_idx
+drop cascades to table parted_mixed
+drop cascades to table parted_fk_tgt
+drop cascades to table parted_fk
+drop cascades to table rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 38 other objects
+DETAIL: drop cascades to table basic
+drop cascades to table id_cols
+drop cascades to table id_custom
+drop cascades to table gen_cols
+drop cascades to table gen_virtual
+drop cascades to table storage_cols
+drop cascades to table refd
+drop cascades to table cons
+drop cascades to table nulls_nd
+drop cascades to table idx_inc
+drop cascades to table fk_acts_tgt
+drop cascades to table fk_acts
+drop cascades to table notenf_tgt
+drop cascades to table notenf
+drop cascades to table self_ref
+drop cascades to table temporal_pk
+drop cascades to table idxd
+drop cascades to table par
+drop cascades to table ch
+drop cascades to table attopt
+drop cascades to table parted_range
+drop cascades to table parted_hash
+drop cascades to table parted_list
+drop cascades to table parted_idx
+drop cascades to table parted_plain
+drop cascades to table parted_pk
+drop cascades to table parted_sub
+drop cascades to table parted_child_idx
+drop cascades to table parted_mixed
+drop cascades to table parted_fk_tgt
+drop cascades to table parted_fk
+drop cascades to table rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..badc41ff7eb
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,879 @@
+--
+-- pg_get_table_ddl
+--
+-- All tests pass owner=>false so the ALTER TABLE OWNER TO line is not
+-- emitted, keeping output stable across test runners (which may run under
+-- different role names).
+--
+CREATE SCHEMA pgtbl_ddl_test;
+SET search_path = pgtbl_ddl_test;
+
+-- Basic table with PRIMARY KEY, NOT NULL, DEFAULT, COLLATE.
+CREATE TABLE basic (
+ id int PRIMARY KEY,
+ name text NOT NULL DEFAULT 'anon' COLLATE "C"
+);
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false);
+
+-- Identity columns (with default and custom sequence options).
+CREATE TABLE id_cols (
+ id_always int GENERATED ALWAYS AS IDENTITY,
+ id_default int GENERATED BY DEFAULT AS IDENTITY
+);
+SELECT * FROM pg_get_table_ddl('id_cols'::regclass, owner => false);
+
+CREATE TABLE id_custom (
+ v int GENERATED ALWAYS AS IDENTITY (
+ SEQUENCE NAME id_custom_v_seq
+ START WITH 100 INCREMENT BY 5
+ MINVALUE 50 MAXVALUE 1000
+ CACHE 10 CYCLE
+ )
+);
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false);
+
+-- Generated stored column.
+CREATE TABLE gen_cols (
+ cents int,
+ dollars numeric GENERATED ALWAYS AS (cents / 100.0) STORED
+);
+SELECT * FROM pg_get_table_ddl('gen_cols'::regclass, owner => false);
+
+-- Generated virtual column.
+CREATE TABLE gen_virtual (
+ base int,
+ derived int GENERATED ALWAYS AS (base * 2) VIRTUAL
+);
+SELECT * FROM pg_get_table_ddl('gen_virtual'::regclass, owner => false);
+
+-- STORAGE and COMPRESSION (only emitted when non-default for the type).
+CREATE TABLE storage_cols (
+ a text STORAGE EXTERNAL,
+ b text STORAGE MAIN,
+ c text COMPRESSION pglz
+);
+SELECT * FROM pg_get_table_ddl('storage_cols'::regclass, owner => false);
+
+-- Constraints: CHECK, UNIQUE, FOREIGN KEY (DEFERRABLE).
+CREATE TABLE refd (id int PRIMARY KEY);
+CREATE TABLE cons (
+ a int CHECK (a > 0),
+ b int UNIQUE,
+ c int REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED
+);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false);
+
+-- UNIQUE NULLS NOT DISTINCT (PG 15): null values are treated as equal for
+-- uniqueness purposes, so the NULLS NOT DISTINCT clause must be emitted.
+CREATE TABLE nulls_nd (
+ a int,
+ b int,
+ UNIQUE NULLS NOT DISTINCT (a, b)
+);
+SELECT * FROM pg_get_table_ddl('nulls_nd'::regclass, owner => false);
+
+-- INCLUDE columns on a constraint-backed UNIQUE index (PG 11): the
+-- covering columns appear in an INCLUDE (...) clause on the ALTER TABLE
+-- ADD CONSTRAINT statement, not in the key column list.
+CREATE TABLE idx_inc (
+ id int PRIMARY KEY,
+ name text,
+ extra text,
+ UNIQUE (name) INCLUDE (extra)
+);
+SELECT * FROM pg_get_table_ddl('idx_inc'::regclass, owner => false);
+
+-- FOREIGN KEY with ON DELETE / ON UPDATE referential actions and MATCH
+-- clause. Each variant must appear verbatim in the reconstructed DDL.
+CREATE TABLE fk_acts_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_acts (
+ a int,
+ b int,
+ c int,
+ d int,
+ CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES fk_acts_tgt(id)
+ ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET NULL,
+ CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET DEFAULT,
+ CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES fk_acts_tgt(id)
+ MATCH FULL
+);
+SELECT * FROM pg_get_table_ddl('fk_acts'::regclass, owner => false);
+
+-- NOT ENFORCED foreign key (PG 17): the constraint is recorded in the
+-- catalog but not validated by the engine; NOT ENFORCED must appear in
+-- the reconstructed DDL.
+CREATE TABLE notenf_tgt (id int PRIMARY KEY);
+CREATE TABLE notenf (
+ a int,
+ CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES notenf_tgt(id)
+ NOT ENFORCED
+);
+SELECT * FROM pg_get_table_ddl('notenf'::regclass, owner => false);
+
+-- Self-referencing FK: the PK must appear before the FK that references it.
+-- The catalog scan returns constraints in name order; without the fix,
+-- "self_ref_parent_id_fkey" sorts before "self_ref_pkey" and the FK ADD
+-- CONSTRAINT fails with "there is no unique constraint matching given keys".
+CREATE TABLE self_ref (
+ id int PRIMARY KEY,
+ parent_id int REFERENCES self_ref(id)
+);
+SELECT * FROM pg_get_table_ddl('self_ref'::regclass, owner => false);
+
+-- WITHOUT OVERLAPS on a PRIMARY KEY (PG 17): the range-type period
+-- column forms a temporal primary key; WITHOUT OVERLAPS must appear
+-- inside the key column list in the reconstructed ALTER TABLE.
+-- Both columns use range types with native GiST support so no
+-- btree_gist extension is needed.
+CREATE TABLE temporal_pk (
+ grp int4range,
+ during daterange,
+ PRIMARY KEY (grp, during WITHOUT OVERLAPS)
+);
+SELECT * FROM pg_get_table_ddl('temporal_pk'::regclass, owner => false);
+
+-- Indexes: functional and partial. Constraint-backing indexes are
+-- suppressed (they are emitted by the constraint loop).
+CREATE TABLE idxd (id int PRIMARY KEY, name text);
+CREATE INDEX idxd_lower ON idxd (lower(name));
+CREATE INDEX idxd_partial ON idxd (id) WHERE id > 100;
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false);
+
+-- Inheritance, including a child DEFAULT override on an inherited column.
+CREATE TABLE par (a int DEFAULT 1, b text);
+CREATE TABLE ch (c int) INHERITS (par);
+ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false);
+
+-- Per-column attoptions: emitted as ALTER COLUMN SET (...).
+CREATE TABLE attopt (a int, b text);
+ALTER TABLE attopt ALTER COLUMN a SET (n_distinct = 100);
+SELECT * FROM pg_get_table_ddl('attopt'::regclass, owner => false);
+
+-- Partitioned table parents (RANGE, HASH, and LIST).
+CREATE TABLE parted_range (id int, k int) PARTITION BY RANGE (id);
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false);
+
+CREATE TABLE parted_hash (id int) PARTITION BY HASH (id);
+SELECT * FROM pg_get_table_ddl('parted_hash'::regclass, owner => false);
+
+CREATE TABLE parted_list (id int, region text) PARTITION BY LIST (region);
+SELECT * FROM pg_get_table_ddl('parted_list'::regclass, owner => false);
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false);
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, owner => false);
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, owner => false);
+
+-- Non-constraint-backed index on a partitioned table: the parent's index DDL
+-- must appear once; the auto-created child partition indexes (relispartition=t)
+-- must be suppressed so the DDL round-trips without "relation already exists".
+CREATE TABLE parted_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_idx_a PARTITION OF parted_idx FOR VALUES IN ('a');
+CREATE UNIQUE INDEX parted_idx_uidx ON parted_idx (id, region);
+SELECT * FROM pg_get_table_ddl('parted_idx'::regclass, owner => false);
+
+-- Plain (non-unique) CREATE INDEX on partitioned table: the auto-created child
+-- index (relispartition=true) is suppressed, just like the UNIQUE case.
+CREATE TABLE parted_plain (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE parted_plain_a PARTITION OF parted_plain FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_plain_id_idx ON parted_plain (id);
+SELECT * FROM pg_get_table_ddl('parted_plain'::regclass, owner => false);
+
+-- Constraint-backed PRIMARY KEY on partitioned table: the child's inherited
+-- constraint (conislocal=false) and its backing index are both suppressed.
+CREATE TABLE parted_pk (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_pk_a PARTITION OF parted_pk FOR VALUES IN ('a');
+ALTER TABLE parted_pk ADD PRIMARY KEY (id, region);
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false);
+
+-- Multi-level (sub-partitioned) table with a CREATE INDEX on the root.
+-- The mid-level partitioned index (relispartition=true, relkind='I') and the
+-- leaf index (relispartition=true, relkind='i') are both suppressed.
+CREATE TABLE parted_sub (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_sub_a PARTITION OF parted_sub
+ FOR VALUES IN ('a') PARTITION BY RANGE (id);
+CREATE TABLE parted_sub_a1 PARTITION OF parted_sub_a FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_sub_id_idx ON parted_sub (id);
+SELECT * FROM pg_get_table_ddl('parted_sub'::regclass, owner => false);
+
+-- Partition child with its own locally-defined index (relispartition=false):
+-- this is NOT inherited from any parent partitioned index and must be emitted.
+CREATE TABLE parted_child_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_child_idx_a PARTITION OF parted_child_idx FOR VALUES IN ('a');
+CREATE INDEX parted_child_idx_a_id ON parted_child_idx_a (id);
+SELECT * FROM pg_get_table_ddl('parted_child_idx'::regclass, owner => false);
+
+-- Mix: parent has a CREATE INDEX (auto-propagated to child, relispartition=true,
+-- suppressed) AND the child has its own local index (relispartition=false, emitted).
+CREATE TABLE parted_mixed (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_mixed_a PARTITION OF parted_mixed FOR VALUES IN ('a');
+CREATE INDEX parted_mixed_id ON parted_mixed (id);
+CREATE INDEX parted_mixed_a_region ON parted_mixed_a (region);
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false);
+
+-- FK on a partitioned table: declared on the parent; partition children inherit
+-- the constraint with conislocal=false and must not re-emit it.
+CREATE TABLE parted_fk_tgt (id int PRIMARY KEY);
+CREATE TABLE parted_fk (id int, tgt_id int REFERENCES parted_fk_tgt(id))
+ PARTITION BY RANGE (id);
+CREATE TABLE parted_fk_a PARTITION OF parted_fk FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('parted_fk'::regclass, owner => false);
+
+-- Statistics declared explicitly on a partition child (not auto-propagated):
+-- the child's statistics object appears inside the child's DDL block.
+CREATE TABLE parted_stx (id int, region text, val float) PARTITION BY LIST (region);
+CREATE TABLE parted_stx_a PARTITION OF parted_stx FOR VALUES IN ('a');
+CREATE STATISTICS parted_stx_stat (ndistinct) ON id, region FROM parted_stx;
+CREATE STATISTICS parted_stx_a_stat (ndistinct) ON id, val FROM parted_stx_a;
+SELECT * FROM pg_get_table_ddl('parted_stx'::regclass, owner => false);
+DROP TABLE parted_stx;
+
+-- NO INHERIT CHECK constraint: the NO INHERIT flag must appear verbatim in the
+-- inline constraint definition inside the CREATE TABLE body.
+CREATE TABLE noinhchk (id int, val int,
+ CONSTRAINT val_pos CHECK (val > 0) NO INHERIT);
+SELECT * FROM pg_get_table_ddl('noinhchk'::regclass, owner => false);
+DROP TABLE noinhchk;
+
+-- Multiple inheritance parents: the INHERITS clause must list both parents in
+-- declaration order; only the locally-defined column appears in the column list.
+CREATE TABLE mi_p1 (a int DEFAULT 1);
+CREATE TABLE mi_p2 (b text DEFAULT 'x');
+CREATE TABLE mi_child (c float) INHERITS (mi_p1, mi_p2);
+SELECT * FROM pg_get_table_ddl('mi_child'::regclass, owner => false);
+DROP TABLE mi_p1, mi_p2 CASCADE;
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false);
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false);
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false);
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false);
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, owner => false);
+
+-- UNLOGGED + reloptions.
+CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70);
+SELECT * FROM pg_get_table_ddl('uno'::regclass, owner => false);
+
+-- Typed table (CREATE TABLE OF type_name). Columns inherited from the
+-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come
+-- out through a single "(col WITH OPTIONS ...)" list.
+CREATE TYPE typed_t AS (a int, b text);
+CREATE TABLE typed_plain OF typed_t;
+SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, owner => false);
+
+CREATE TABLE typed_over OF typed_t (
+ a WITH OPTIONS DEFAULT 7 NOT NULL,
+ b WITH OPTIONS NOT NULL,
+ CONSTRAINT b_nonempty CHECK (length(b) > 0)
+);
+SELECT * FROM pg_get_table_ddl('typed_over'::regclass, owner => false);
+
+-- Temporary tables + ON COMMIT. Temp tables must never be emitted with
+-- the session-private pg_temp_NN schema prefix -- the TEMPORARY keyword
+-- already places the table in pg_temp and the prefix is non-replayable.
+-- ON COMMIT DROP only fires at commit, so the queries that need to see
+-- the catalog entry must run in the same transaction as the CREATE.
+BEGIN;
+CREATE TEMP TABLE temp_default (id int);
+CREATE TEMP TABLE temp_delete (id int) ON COMMIT DELETE ROWS;
+CREATE TEMP TABLE temp_drop (id int) ON COMMIT DROP;
+
+SELECT line FROM pg_get_table_ddl('temp_default'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT line FROM pg_get_table_ddl('temp_delete'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT line FROM pg_get_table_ddl('temp_drop'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ROLLBACK;
+
+-- Pretty mode.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, pretty => true);
+
+-- only_kinds / except_kinds gating: each emits either only the listed kinds
+-- (only_kinds) or every kind except the listed ones (except_kinds). The two
+-- are mutually exclusive. Kind vocabulary: table, index,
+-- primary_key, unique, check, foreign_key, exclusion, rule,
+-- statistics, trigger, policy, rls, replica_identity, partition.
+-- NOT NULL is not in the vocabulary - always emitted to prevent
+-- producing schemas that silently accept NULLs the source would
+-- have rejected.
+--
+-- except_kinds=index hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['index']);
+
+-- except_kinds=primary_key,unique,check,foreign_key,exclusion hides every
+-- table-level constraint. Inline CHECK in the CREATE TABLE body is
+-- suppressed too.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['primary_key','unique','check','foreign_key','exclusion']);
+
+-- except_kinds=foreign_key suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- first pass of the two-pass FK-clone workflow: emit tables without
+-- cross-table FKs, load data, then re-run with only_kinds=foreign_key
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['foreign_key']);
+
+-- only_kinds=foreign_key is the second pass of the two-pass FK-clone
+-- workflow: emit only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN
+-- KEY rows; the CREATE TABLE and every non-FK sub-object are
+-- suppressed (the table already exists from the first pass).
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+
+-- A table with no foreign keys produces no rows under
+-- only_kinds=foreign_key.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+
+-- only_kinds=table emits only the bare CREATE TABLE (with any inline
+-- column-level NOT NULL and CHECK) - no indexes, no PRIMARY KEY ALTER,
+-- no other sub-objects. Useful for capturing a table's shape without
+-- pulling in everything attached to it.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table']);
+
+-- only_kinds=check on a non-partition table. The constraint loop must emit
+-- column-level and table-level CHECK constraints via ALTER TABLE here,
+-- because the inline path in CREATE TABLE never runs (KIND_TABLE is
+-- not in the filter).
+CREATE TABLE chk_only (
+ id int PRIMARY KEY,
+ qty int CHECK (qty > 0),
+ CONSTRAINT chk_only_id_pos CHECK (id > 0)
+);
+SELECT * FROM pg_get_table_ddl('chk_only'::regclass, owner => false, only_kinds => ARRAY['check']);
+DROP TABLE chk_only;
+
+-- only_kinds=partition on a partitioned-table parent: each child's full DDL
+-- is emitted (CREATE TABLE ... PARTITION OF ...). The "partition"
+-- keyword is a gate at the parent level; it is stripped from the
+-- propagated filter so the child does not exclude its own
+-- CREATE TABLE.
+CREATE TABLE p_only (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE p_only_a PARTITION OF p_only FOR VALUES FROM (0) TO (100);
+CREATE TABLE p_only_b PARTITION OF p_only FOR VALUES FROM (100) TO (200);
+SELECT * FROM pg_get_table_ddl('p_only'::regclass, owner => false, only_kinds => ARRAY['partition']);
+DROP TABLE p_only;
+
+-- only_kinds and except_kinds are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false,
+ only_kinds => ARRAY['foreign_key'],
+ except_kinds => ARRAY['foreign_key']);
+
+-- Pub/sub schema clone: keep the table and its primary key, drop the
+-- rest of the constraints so the subscriber can replicate without
+-- pulling cross-table dependencies along.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique','check','foreign_key','exclusion']);
+
+-- except_kinds=rule hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, except_kinds => ARRAY['rule']);
+
+-- except_kinds=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, except_kinds => ARRAY['statistics']);
+
+-- except_kinds=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, except_kinds => ARRAY['rls']);
+
+-- except_kinds=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, except_kinds => ARRAY['replica_identity']);
+
+-- Partition children are emitted by default; except_kinds=partition
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false, except_kinds => ARRAY['partition']);
+
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY[' INDEX ',' RULE ']);
+
+-- schema_qualified=false strips the schema from the target table itself
+-- and from same-schema sibling references (inheritance/partition
+-- parents, identity sequences in the same schema), while cross-schema
+-- references are kept qualified for correctness.
+--
+-- Bare CREATE TABLE for an ordinary table.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false,
+ schema_qualified => false);
+
+-- INHERITS parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Partition parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Cross-schema FK target keeps qualification. The 'refd' table is in
+-- pgtbl_ddl_test; place an FK target in a different schema and verify it
+-- stays qualified even with schema_qualified=false.
+CREATE SCHEMA pgtbl_ddl_other;
+CREATE TABLE pgtbl_ddl_other.parent (id int PRIMARY KEY);
+CREATE TABLE xschema_fk (
+ id int PRIMARY KEY,
+ pid int REFERENCES pgtbl_ddl_other.parent
+);
+SELECT * FROM pg_get_table_ddl('xschema_fk'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Cross-schema FK whose target schema name ends with the base schema name
+-- (e.g. "xpgtbl_ddl_test" ends with "pgtbl_ddl_test"). A post-hoc text
+-- strip would mangle "xpgtbl_ddl_test.reftbl" into "xreftbl"; deciding
+-- qualification at generation time avoids the problem entirely.
+CREATE SCHEMA xpgtbl_ddl_test;
+CREATE TABLE xpgtbl_ddl_test.reftbl (id int PRIMARY KEY);
+CREATE TABLE suffix_schema_fk (
+ id int PRIMARY KEY,
+ ref int REFERENCES xpgtbl_ddl_test.reftbl
+);
+SELECT * FROM pg_get_table_ddl('suffix_schema_fk'::regclass, owner => false,
+ schema_qualified => false);
+DROP TABLE suffix_schema_fk;
+DROP SCHEMA xpgtbl_ddl_test CASCADE;
+
+-- Identity column with a custom sequence name in the same schema is
+-- emitted unqualified.
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Round-trip: with schema_qualified=false, after SET search_path the
+-- DDL can be replayed into a different target schema.
+CREATE SCHEMA pgtbl_ddl_replay;
+DO $$
+DECLARE
+ stmt text;
+BEGIN
+ SET LOCAL search_path = pgtbl_ddl_replay;
+ FOR stmt IN
+ SELECT line FROM pg_get_table_ddl('pgtbl_ddl_test.basic'::regclass,
+ owner => false,
+ schema_qualified => false) AS line
+ LOOP
+ EXECUTE stmt;
+ END LOOP;
+END $$;
+SELECT relnamespace::regnamespace::text, relname
+FROM pg_class WHERE oid = 'pgtbl_ddl_replay.basic'::regclass;
+DROP SCHEMA pgtbl_ddl_replay CASCADE;
+DROP TABLE xschema_fk;
+DROP SCHEMA pgtbl_ddl_other CASCADE;
+
+-- Cross-schema partition child with schema_qualified=false: the child must
+-- carry its own schema prefix so the output is unambiguous regardless of the
+-- caller's search_path. Before the fix, the child was emitted as the bare
+-- name "pt_c" (landing in whichever schema is first in search_path) while
+-- the parent reference inside the CREATE TABLE came out as the fully-qualified
+-- "pgtbl_ddl_part_s.pt", making the DDL inconsistent and unreplayable.
+CREATE SCHEMA pgtbl_ddl_part_s;
+CREATE SCHEMA pgtbl_ddl_part_other;
+CREATE TABLE pgtbl_ddl_part_s.pt (id int, val int) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_part_other.pt_c
+ PARTITION OF pgtbl_ddl_part_s.pt
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_part_s.pt'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_part_s CASCADE;
+DROP SCHEMA pgtbl_ddl_part_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. String
+-- literal text is output verbatim by the deparser and is unaffected by
+-- search_path narrowing.
+CREATE SCHEMA pgtbl_ddl_str;
+CREATE TABLE pgtbl_ddl_str.p (id int, note text) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_str.pc PARTITION OF pgtbl_ddl_str.p
+ (CONSTRAINT chk CHECK (note <> 'pgtbl_ddl_str.secret'))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_str.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_str CASCADE;
+
+-- schema_qualified=false must NOT corrupt double-quoted identifiers
+-- whose contents happen to contain the schema name followed by a dot
+-- (for example a column literally named "<schema>.weird" inside that
+-- schema). The mechanism is purely search_path narrowing: the deparser
+-- outputs attribute names verbatim from pg_attribute, so the quoted
+-- identifier content is never examined for schema prefixes. This holds
+-- even when the schema name is a single letter.
+CREATE SCHEMA pgtbl_ddl_qid;
+CREATE TABLE pgtbl_ddl_qid.p (id int, "pgtbl_ddl_qid.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_qid.pc PARTITION OF pgtbl_ddl_qid.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_qid.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_qid.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_qid CASCADE;
+-- Same with a short (single-letter) schema name to confirm there is no
+-- minimum-length assumption in the stripping path.
+CREATE SCHEMA pgtbl_ddl_q1;
+CREATE TABLE pgtbl_ddl_q1.p (id int, "pgtbl_ddl_q1.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_q1.pc PARTITION OF pgtbl_ddl_q1.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_q1.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_q1.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_q1 CASCADE;
+
+-- schema_qualified=false when the base schema name itself requires
+-- quoting (its prefix starts with "). Both forms of the prefix -
+-- bare-lowercase and quoted - must be stripped from outer
+-- ALTER TABLE / ON / CREATE INDEX prefixes, while inner quoted
+-- identifiers that happen to begin the same way must be preserved.
+CREATE SCHEMA "PgTbl-Ddl-Q";
+CREATE TABLE "PgTbl-Ddl-Q"."T" (id int, val int,
+ CONSTRAINT chk CHECK (val > 0));
+CREATE INDEX "T_val_idx" ON "PgTbl-Ddl-Q"."T" (val);
+SELECT * FROM pg_get_table_ddl('"PgTbl-Ddl-Q"."T"'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA "PgTbl-Ddl-Q" CASCADE;
+
+-- Error: not an ordinary or partitioned table.
+CREATE VIEW v AS SELECT 1 AS x;
+SELECT * FROM pg_get_table_ddl('v'::regclass);
+
+CREATE SEQUENCE s;
+SELECT * FROM pg_get_table_ddl('s'::regclass);
+
+-- NULL argument returns no rows.
+SELECT * FROM pg_get_table_ddl(NULL);
+
+-- REPLICA IDENTITY USING INDEX validation: if the referenced index
+-- would not be emitted (because its source kind is suppressed) but
+-- replica_identity itself would be, the function raises an error so
+-- the emitted DDL never dangles. Exercise all three constraint-backed
+-- forms (PK, UNIQUE, EXCLUSION) and a bare CREATE UNIQUE INDEX, in
+-- both the except-list and only-list shapes.
+--
+-- PK-backed: except_kinds=primary_key without except_kinds=replica_identity.
+CREATE TABLE ri_pk_excl (a int PRIMARY KEY);
+ALTER TABLE ri_pk_excl REPLICA IDENTITY USING INDEX ri_pk_excl_pkey;
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key','replica_identity']);
+DROP TABLE ri_pk_excl;
+
+-- UNIQUE-constraint-backed replica identity index.
+CREATE TABLE ri_uniq_excl (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_uniq_excl REPLICA IDENTITY USING INDEX ri_uniq_excl_a_key;
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique','replica_identity']);
+DROP TABLE ri_uniq_excl;
+
+-- Bare CREATE UNIQUE INDEX (no backing constraint) used as REPLICA
+-- IDENTITY: the index is emitted by the "index" kind, so excluding
+-- "index" without also excluding "replica_identity" must error.
+CREATE TABLE ri_idx_excl (a int NOT NULL, b int);
+CREATE UNIQUE INDEX ri_idx_excl_a_uniq ON ri_idx_excl (a);
+ALTER TABLE ri_idx_excl REPLICA IDENTITY USING INDEX ri_idx_excl_a_uniq;
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index']);
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index','replica_identity']);
+DROP TABLE ri_idx_excl;
+
+-- Include-side analog: only_kinds=replica_identity must include the kind
+-- that emits the index, or the function errors.
+CREATE TABLE ri_only (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+-- Including both kinds succeeds.
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['unique','replica_identity']);
+DROP TABLE ri_only;
+
+-- Per-kind only_kinds / except_kinds coverage. For each kind in the vocabulary,
+-- verify that only_kinds=K emits just that kind's output and that except_kinds=K
+-- omits just that kind's output. Kinds with no fixture above (trigger,
+-- policy) are still recognized but currently emit nothing; only_kinds=K
+-- returns zero rows for them. Fixtures used:
+-- idxd: PK + two secondary CREATE INDEX
+-- cons: PK + UNIQUE + CHECK + FK (constraint-backed)
+-- rt: CREATE RULE
+-- stx: CREATE STATISTICS
+-- rls: ENABLE/FORCE ROW LEVEL SECURITY
+-- ri_full: REPLICA IDENTITY FULL (no source-kind dependency)
+CREATE TABLE excl_fix (rng int4range,
+ EXCLUDE USING gist (rng WITH &&));
+
+-- only_kinds=K: emit only kind K's statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index']);
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['primary_key']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, only_kinds => ARRAY['rule']);
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, only_kinds => ARRAY['rls']);
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['policy']);
+
+-- except_kinds=K: emit everything except kind K's statements. PK/UNIQUE/
+-- INDEX are exercised on tables without REPLICA IDENTITY USING INDEX
+-- so the validation does not trip.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['check']);
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, except_kinds => ARRAY['exclusion']);
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['policy']);
+-- except_kinds=table suppresses the CREATE TABLE + OWNER + child-default
+-- SET DEFAULT + attoptions passes, leaving only the sub-object passes.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['table']);
+
+-- Multi-kind compositions.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table','index']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['primary_key','foreign_key']);
+-- Duplicate entries in the list are silently de-duplicated by the
+-- Bitmapset.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index','index']);
+
+DROP TABLE excl_fix;
+
+-- Kind-filter tests on partitioned tables with indexes.
+--
+-- only_kinds=index on a partitioned table: only the root CREATE INDEX is
+-- emitted; no CREATE TABLE, no partition children (KIND_PARTITION absent).
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ only_kinds => ARRAY['index']);
+
+-- except_kinds=index on a partitioned table: CREATE TABLE for the parent
+-- and all children are emitted; no index statements.
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ except_kinds => ARRAY['index']);
+
+-- only_kinds=primary_key on a partitioned table: only the ALTER TABLE ADD
+-- CONSTRAINT PK on the parent; the child's inherited PK (conislocal=false)
+-- is skipped; no CREATE TABLE, no partition children.
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false,
+ only_kinds => ARRAY['primary_key']);
+
+-- only_kinds=K on a table that does not have any K of that kind is a valid
+-- filter and returns zero rows. Tested previously with foreign_key
+-- on 'refd'; here also rule, statistics, rls, replica_identity, and
+-- exclusion on 'basic' (which has only PK + columns).
+SELECT count(*) AS rule_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rule']);
+SELECT count(*) AS stats_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+SELECT count(*) AS rls_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rls']);
+SELECT count(*) AS ri_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+SELECT count(*) AS excl_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+
+-- Input-validation edge cases for the kind array. All
+-- of these raise an error; the function never reaches relation open.
+-- Empty array:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[]::text[]);
+-- NULL element:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[NULL::text]);
+-- Unrecognized kind:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY['no_such_kind']);
+-- Same kind specified through both parameters is still mutex-rejected.
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ only_kinds => ARRAY['index'],
+ except_kinds => ARRAY['rule']);
+
+-- Round-trip verification. For every test table, capture the DDL the
+-- function emits, drop the schema, replay each table's DDL in
+-- dependency order, and confirm that pg_get_table_ddl on the recreated
+-- relation matches the original line-for-line. The final SELECT must
+-- return zero rows.
+CREATE TEMP TABLE pgtbl_ddl_rt_orig (name text, ord int, line text);
+INSERT INTO pgtbl_ddl_rt_orig
+SELECT t.name, o.ord, o.line
+FROM (VALUES
+ ('basic'), ('id_cols'), ('id_custom'), ('gen_cols'), ('gen_virtual'), ('storage_cols'),
+ ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'),
+ ('fk_acts_tgt'), ('fk_acts'), ('notenf_tgt'), ('notenf'), ('self_ref'), ('temporal_pk'),
+ ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('parted_list'),
+ ('parted_idx'), ('parted_idx_a'),
+ ('parted_plain'), ('parted_plain_a'),
+ ('parted_pk'), ('parted_pk_a'),
+ ('parted_sub'), ('parted_sub_a'), ('parted_sub_a1'),
+ ('parted_child_idx'), ('parted_child_idx_a'),
+ ('parted_mixed'), ('parted_mixed_a'),
+ ('parted_fk_tgt'), ('parted_fk'), ('parted_fk_a'),
+ ('rt_log'), ('rt'),
+ ('stx'), ('rls'), ('ri_full'), ('ri_idx'), ('uno')
+ ) AS t(name),
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord);
+
+DO $$
+DECLARE
+ tables CONSTANT text[] := ARRAY[
+ 'basic', 'id_cols', 'id_custom', 'gen_cols', 'gen_virtual', 'storage_cols',
+ 'refd', 'cons', 'nulls_nd', 'idx_inc',
+ 'fk_acts_tgt', 'fk_acts', 'notenf_tgt', 'notenf', 'self_ref', 'temporal_pk',
+ 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ 'parted_list',
+ 'parted_idx', 'parted_idx_a',
+ 'parted_plain', 'parted_plain_a',
+ 'parted_pk', 'parted_pk_a',
+ 'parted_sub', 'parted_sub_a', 'parted_sub_a1',
+ 'parted_child_idx', 'parted_child_idx_a',
+ 'parted_mixed', 'parted_mixed_a',
+ 'parted_fk_tgt', 'parted_fk', 'parted_fk_a',
+ 'rt_log', 'rt',
+ 'stx', 'rls', 'ri_full', 'ri_idx', 'uno'
+ ];
+ t text;
+ stmt text;
+BEGIN
+ DROP SCHEMA pgtbl_ddl_test CASCADE;
+ CREATE SCHEMA pgtbl_ddl_test;
+ FOREACH t IN ARRAY tables LOOP
+ FOR stmt IN
+ SELECT line FROM pgtbl_ddl_rt_orig WHERE name = t ORDER BY ord
+ LOOP
+ BEGIN
+ EXECUTE stmt;
+ EXCEPTION WHEN duplicate_table THEN
+ NULL; -- partition child already created by its parent's DDL
+ END;
+ END LOOP;
+ END LOOP;
+END $$;
+
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ffb413ab612..be539e9ac72 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1648,6 +1648,7 @@ ListenerEntry
LoInfo
LoadStmt
LocalBufferLookupEnt
+LocalNotNullEntry
LocalPgBackendStatus
LocalTransactionId
Location
@@ -3152,6 +3153,7 @@ TableFuncScan
TableFuncScanState
TableFuncType
TableInfo
+TableDdlContext
TableLikeClause
TableSampleClause
TableScanDesc
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-07 16:53 Rui Zhao <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Rui Zhao @ 2026-07-07 16:53 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
Hi Akshay,
Re-tested v17 on current master (73dfe79fd6) -- all five issues are fixed,
and make check passes here.
I then ran a round-trip check against pg_dump: for each table, replay the
pg_get_table_ddl() output into a clone of the database and diff
"pg_dump -t" of both sides (both sides being pg_dump output, any surviving
diff is semantic, not formatting). Corpus: the create_sql scenarios from
002_pg_dump.pl, then the whole regression database. 483 of 560 tables
round-trip identically; setting aside the documented no-output kinds
(trigger/policy), the rest reduce to:
1) serial columns produce non-replayable DDL -- the output references the
sequence but never creates it:
CREATE TABLE ser (id serial, v text);
SELECT d FROM pg_get_table_ddl('ser'::regclass, owner => false) d;
-- CREATE TABLE public.ser (id integer DEFAULT
nextval('public.ser_id_seq'::regclass) NOT NULL, v text);
CREATE TABLE public.ser (id integer DEFAULT
nextval('public.ser_id_seq'::regclass) NOT NULL, v text);
-- ERROR: relation "public.ser_id_seq" does not exist (on an
empty database)
pg_dump emits CREATE SEQUENCE + OWNED BY + SET DEFAULT. This accounts for
14 regression failures, including whole partition trees.
2) Partition/inheritance children that diverged from their parent don't
survive the PARTITION OF / INHERITS rebuild. Three variants:
- a dropped default silently comes back:
CREATE TABLE dp (a int DEFAULT 99) PARTITION BY LIST (a);
CREATE TABLE dp1 PARTITION OF dp FOR VALUES IN (1);
ALTER TABLE ONLY dp1 ALTER COLUMN a DROP DEFAULT;
SELECT d FROM pg_get_table_ddl('dp'::regclass, owner => false) d;
-- CREATE TABLE public.dp (a integer DEFAULT 99) PARTITION BY LIST (a);
-- CREATE TABLE public.dp1 PARTITION OF public.dp FOR VALUES IN (1);
SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef
WHERE adrelid = 'dp1'::regclass;
-- source: (0 rows)
-- replayed: 99 => INSERTs into dp1 now get 99, not NULL
- a child's own NOT NULL constraint name is lost:
CREATE TABLE np (a int NOT NULL) PARTITION BY LIST (a);
CREATE TABLE np1 (a int CONSTRAINT np1_nn NOT NULL);
ALTER TABLE np ATTACH PARTITION np1 FOR VALUES IN (1);
SELECT d FROM pg_get_table_ddl('np'::regclass, owner => false) d;
-- CREATE TABLE public.np (a integer NOT NULL) PARTITION BY LIST (a);
-- CREATE TABLE public.np1 PARTITION OF public.np FOR VALUES IN (1);
SELECT conname FROM pg_constraint
WHERE conrelid = 'np1'::regclass AND contype = 'n';
-- source: np1_nn
-- replayed: np_a_not_null
and when the column itself is inherited, the emitted constraint
errors instead of merging:
CREATE TABLE p5 (a int);
CREATE TABLE c5 () INHERITS (p5);
ALTER TABLE c5 ADD CONSTRAINT c5_nn NOT NULL a;
ALTER TABLE p5 ADD CONSTRAINT p5_nn NOT NULL a;
SELECT d FROM pg_get_table_ddl('c5'::regclass, owner => false) d;
-- CREATE TABLE public.c5 () INHERITS (public.p5);
-- ALTER TABLE public.c5 ADD CONSTRAINT c5_nn NOT NULL a;
ALTER TABLE public.c5 ADD CONSTRAINT c5_nn NOT NULL a; --
replay, p5 recreated first
-- ERROR: cannot create not-null constraint "c5_nn" on
column "a" of table "c5"
-- DETAIL: A not-null constraint named "p5_nn" already
exists for this column.
- a partition attached from a table with different column order is
rebuilt in the parent's order:
CREATE TABLE p (a int, b int, c int) PARTITION BY LIST (a);
CREATE TABLE c1 (c int, b int, a int);
ALTER TABLE p ATTACH PARTITION c1 FOR VALUES IN (1);
SELECT d FROM pg_get_table_ddl('p'::regclass, owner => false) d;
-- CREATE TABLE public.p (a integer, b integer, c integer)
PARTITION BY LIST (a);
-- CREATE TABLE public.c1 PARTITION OF public.p FOR VALUES IN (1);
-- => replayed c1 columns are a, b, c (source: c, b, a), so
-- SELECT * / COPY / positional INSERT all shift
pg_dump handles all three: it emits the named constraint inline in the
child's CREATE body (the merge with the inherited constraint at CREATE
time keeps the local name, and attislocal / conislocal / coninhcount all
survive), and falls back to standalone CREATE + ATTACH PARTITION for
shapes a PARTITION OF / INHERITS clause can't express. The first two
variants have lightweight fixes that keep the PARTITION OF / INHERITS
shape --
CREATE TABLE c5 (CONSTRAINT c5_nn NOT NULL a) INHERITS (p5);
CREATE TABLE np1 PARTITION OF np (CONSTRAINT np1_nn NOT NULL a)
FOR VALUES IN (1);
plus a counter-statement for the divergent-default case (ALTER TABLE
ONLY dp1 ALTER COLUMN a DROP DEFAULT). Only the reordered-column case
really needs the standalone shape. The commit message lists child-local
DEFAULT overrides and named NOT NULL constraints as supported, so I'm
treating these as bugs rather than scope cuts.
3) emit_typed_column_storage() (ddlutils.c:2331, 2342) emits ALTER TABLE
without ONLY; SET STORAGE / SET COMPRESSION recurse:
CREATE TYPE mytype AS (a int, b text);
CREATE TABLE typed_t OF mytype;
ALTER TABLE typed_t ALTER COLUMN b SET STORAGE external;
CREATE TABLE tchild () INHERITS (typed_t);
ALTER TABLE ONLY tchild ALTER COLUMN b SET STORAGE main;
ALTER TABLE public.typed_t ALTER COLUMN b SET STORAGE EXTERNAL;
-- replay v17's output
-- => tchild's attstorage flips m -> e
pg_dump uses ALTER TABLE ONLY here. (The other emitted ALTER TABLEs are
fine: OWNER / REPLICA IDENTITY / RLS / SET (options) don't recurse, and
ADD CONSTRAINT must stay ONLY-less since the dump relies on its recursion
to rebuild the children's suppressed inherited copies.)
4) Some per-table state pg_dump preserves is missing. Index statistics
targets:
CREATE TABLE ist (c1 int);
CREATE INDEX ist_idx ON ist ((c1 + 1));
ALTER INDEX ist_idx ALTER COLUMN 1 SET STATISTICS 400;
SELECT d FROM pg_get_table_ddl('ist'::regclass, owner => false) d;
-- CREATE TABLE public.ist (c1 integer);
-- CREATE INDEX ist_idx ON public.ist USING btree (((c1 + 1)));
-- no SET STATISTICS
Same story for ALTER TABLE ... CLUSTER ON (the index comes back without
the indisclustered marker) and for ALTER TABLE ... DISABLE RULE (the rule
is emitted but comes back enabled, which changes behavior -- and rule is
a covered kind).
5) emit_indexes doesn't check indisvalid, so an invalid index is emitted
as a normal one:
CREATE TABLE inv (x int);
INSERT INTO inv VALUES (1), (1);
CREATE UNIQUE INDEX CONCURRENTLY inv_uidx ON inv (x);
-- ERROR: could not create unique index "inv_uidx" (leaves
indisvalid = false)
SELECT d FROM pg_get_table_ddl('inv'::regclass, owner => false) d;
-- CREATE TABLE public.inv (x integer);
-- CREATE UNIQUE INDEX inv_uidx ON public.inv USING btree (x);
pg_dump skips those (getIndexes: "i.indisvalid OR t2.relkind = 'p'").
6) Minor: COMMENT ON and GRANT/REVOKE are not emitted. If that's
intentional -- like trigger/policy -- worth saying so in the doc.
Thanks,
Rui
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-08 11:56 Akshay Joshi <[email protected]>
parent: Rui Zhao <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Akshay Joshi @ 2026-07-08 11:56 UTC (permalink / raw)
To: Rui Zhao <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; [email protected]
Thank you for the thorough round-trip analysis; this caught real bugs. I
have fixed several of the issues you highlighted, though a few fall outside
this function's current scope.
*Out of Scope (Documented in the Function Description)*
Issue 1 (Serial Columns): Owned sequences are independent catalog objects.
This is the same reason *pg_get_table_ddl* does not emit CREATE TYPE for
composite types used in columns. While the nextval() default is correctly
emitted, the sequence itself requires separate capture. I have added an
explicit note to the documentation to call this out.
Issue 2c (Reordered Partition Columns): You correctly noted that this
scenario requires a standalone CREATE + ATTACH PARTITION fallback.
Detecting column-order divergence and falling back to the non-PARTITION OF
syntax is a larger architecture change, so I have deferred it to a
follow-up task. I have documented this as a known limitation for now.
Issue 6 (COMMENT/GRANT): These are intentionally omitted, consistent with
how we handle triggers and policies. I have added them to the documented
list of exclusions.
The v18 patch is now ready for your review.
On Tue, Jul 7, 2026 at 10:23 PM Rui Zhao <[email protected]> wrote:
> Hi Akshay,
>
> Re-tested v17 on current master (73dfe79fd6) -- all five issues are fixed,
> and make check passes here.
>
> I then ran a round-trip check against pg_dump: for each table, replay the
> pg_get_table_ddl() output into a clone of the database and diff
> "pg_dump -t" of both sides (both sides being pg_dump output, any surviving
> diff is semantic, not formatting). Corpus: the create_sql scenarios from
> 002_pg_dump.pl, then the whole regression database. 483 of 560 tables
> round-trip identically; setting aside the documented no-output kinds
> (trigger/policy), the rest reduce to:
>
> 1) serial columns produce non-replayable DDL -- the output references the
> sequence but never creates it:
>
> CREATE TABLE ser (id serial, v text);
>
> SELECT d FROM pg_get_table_ddl('ser'::regclass, owner => false) d;
> -- CREATE TABLE public.ser (id integer DEFAULT
> nextval('public.ser_id_seq'::regclass) NOT NULL, v text);
>
> CREATE TABLE public.ser (id integer DEFAULT
> nextval('public.ser_id_seq'::regclass) NOT NULL, v text);
> -- ERROR: relation "public.ser_id_seq" does not exist (on an
> empty database)
>
> pg_dump emits CREATE SEQUENCE + OWNED BY + SET DEFAULT. This accounts for
> 14 regression failures, including whole partition trees.
>
> 2) Partition/inheritance children that diverged from their parent don't
> survive the PARTITION OF / INHERITS rebuild. Three variants:
>
> - a dropped default silently comes back:
>
> CREATE TABLE dp (a int DEFAULT 99) PARTITION BY LIST (a);
> CREATE TABLE dp1 PARTITION OF dp FOR VALUES IN (1);
> ALTER TABLE ONLY dp1 ALTER COLUMN a DROP DEFAULT;
>
> SELECT d FROM pg_get_table_ddl('dp'::regclass, owner => false) d;
> -- CREATE TABLE public.dp (a integer DEFAULT 99) PARTITION BY
> LIST (a);
> -- CREATE TABLE public.dp1 PARTITION OF public.dp FOR VALUES IN
> (1);
>
> SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef
> WHERE adrelid = 'dp1'::regclass;
> -- source: (0 rows)
> -- replayed: 99 => INSERTs into dp1 now get 99, not NULL
>
> - a child's own NOT NULL constraint name is lost:
>
> CREATE TABLE np (a int NOT NULL) PARTITION BY LIST (a);
> CREATE TABLE np1 (a int CONSTRAINT np1_nn NOT NULL);
> ALTER TABLE np ATTACH PARTITION np1 FOR VALUES IN (1);
>
> SELECT d FROM pg_get_table_ddl('np'::regclass, owner => false) d;
> -- CREATE TABLE public.np (a integer NOT NULL) PARTITION BY LIST
> (a);
> -- CREATE TABLE public.np1 PARTITION OF public.np FOR VALUES IN
> (1);
>
> SELECT conname FROM pg_constraint
> WHERE conrelid = 'np1'::regclass AND contype = 'n';
> -- source: np1_nn
> -- replayed: np_a_not_null
>
> and when the column itself is inherited, the emitted constraint
> errors instead of merging:
>
> CREATE TABLE p5 (a int);
> CREATE TABLE c5 () INHERITS (p5);
> ALTER TABLE c5 ADD CONSTRAINT c5_nn NOT NULL a;
> ALTER TABLE p5 ADD CONSTRAINT p5_nn NOT NULL a;
>
> SELECT d FROM pg_get_table_ddl('c5'::regclass, owner => false) d;
> -- CREATE TABLE public.c5 () INHERITS (public.p5);
> -- ALTER TABLE public.c5 ADD CONSTRAINT c5_nn NOT NULL a;
>
> ALTER TABLE public.c5 ADD CONSTRAINT c5_nn NOT NULL a; --
> replay, p5 recreated first
> -- ERROR: cannot create not-null constraint "c5_nn" on
> column "a" of table "c5"
> -- DETAIL: A not-null constraint named "p5_nn" already
> exists for this column.
>
> - a partition attached from a table with different column order is
> rebuilt in the parent's order:
>
> CREATE TABLE p (a int, b int, c int) PARTITION BY LIST (a);
> CREATE TABLE c1 (c int, b int, a int);
> ALTER TABLE p ATTACH PARTITION c1 FOR VALUES IN (1);
>
> SELECT d FROM pg_get_table_ddl('p'::regclass, owner => false) d;
> -- CREATE TABLE public.p (a integer, b integer, c integer)
> PARTITION BY LIST (a);
> -- CREATE TABLE public.c1 PARTITION OF public.p FOR VALUES IN (1);
> -- => replayed c1 columns are a, b, c (source: c, b, a), so
> -- SELECT * / COPY / positional INSERT all shift
>
> pg_dump handles all three: it emits the named constraint inline in the
> child's CREATE body (the merge with the inherited constraint at CREATE
> time keeps the local name, and attislocal / conislocal / coninhcount all
> survive), and falls back to standalone CREATE + ATTACH PARTITION for
> shapes a PARTITION OF / INHERITS clause can't express. The first two
> variants have lightweight fixes that keep the PARTITION OF / INHERITS
> shape --
>
> CREATE TABLE c5 (CONSTRAINT c5_nn NOT NULL a) INHERITS (p5);
> CREATE TABLE np1 PARTITION OF np (CONSTRAINT np1_nn NOT NULL a)
> FOR VALUES IN (1);
>
> plus a counter-statement for the divergent-default case (ALTER TABLE
> ONLY dp1 ALTER COLUMN a DROP DEFAULT). Only the reordered-column case
> really needs the standalone shape. The commit message lists child-local
> DEFAULT overrides and named NOT NULL constraints as supported, so I'm
> treating these as bugs rather than scope cuts.
>
> 3) emit_typed_column_storage() (ddlutils.c:2331, 2342) emits ALTER TABLE
> without ONLY; SET STORAGE / SET COMPRESSION recurse:
>
> CREATE TYPE mytype AS (a int, b text);
> CREATE TABLE typed_t OF mytype;
> ALTER TABLE typed_t ALTER COLUMN b SET STORAGE external;
> CREATE TABLE tchild () INHERITS (typed_t);
> ALTER TABLE ONLY tchild ALTER COLUMN b SET STORAGE main;
>
> ALTER TABLE public.typed_t ALTER COLUMN b SET STORAGE EXTERNAL;
> -- replay v17's output
> -- => tchild's attstorage flips m -> e
>
> pg_dump uses ALTER TABLE ONLY here. (The other emitted ALTER TABLEs are
> fine: OWNER / REPLICA IDENTITY / RLS / SET (options) don't recurse, and
> ADD CONSTRAINT must stay ONLY-less since the dump relies on its recursion
> to rebuild the children's suppressed inherited copies.)
>
> 4) Some per-table state pg_dump preserves is missing. Index statistics
> targets:
>
> CREATE TABLE ist (c1 int);
> CREATE INDEX ist_idx ON ist ((c1 + 1));
> ALTER INDEX ist_idx ALTER COLUMN 1 SET STATISTICS 400;
>
> SELECT d FROM pg_get_table_ddl('ist'::regclass, owner => false) d;
> -- CREATE TABLE public.ist (c1 integer);
> -- CREATE INDEX ist_idx ON public.ist USING btree (((c1 + 1)));
> -- no SET STATISTICS
>
> Same story for ALTER TABLE ... CLUSTER ON (the index comes back without
> the indisclustered marker) and for ALTER TABLE ... DISABLE RULE (the rule
> is emitted but comes back enabled, which changes behavior -- and rule is
> a covered kind).
>
> 5) emit_indexes doesn't check indisvalid, so an invalid index is emitted
> as a normal one:
>
> CREATE TABLE inv (x int);
> INSERT INTO inv VALUES (1), (1);
> CREATE UNIQUE INDEX CONCURRENTLY inv_uidx ON inv (x);
> -- ERROR: could not create unique index "inv_uidx" (leaves
> indisvalid = false)
>
> SELECT d FROM pg_get_table_ddl('inv'::regclass, owner => false) d;
> -- CREATE TABLE public.inv (x integer);
> -- CREATE UNIQUE INDEX inv_uidx ON public.inv USING btree (x);
>
> pg_dump skips those (getIndexes: "i.indisvalid OR t2.relkind = 'p'").
>
> 6) Minor: COMMENT ON and GRANT/REVOKE are not emitted. If that's
> intentional -- like trigger/policy -- worth saying so in the doc.
>
> Thanks,
> Rui
>
Attachments:
[application/octet-stream] v18-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (249.7K, ../../CANxoLDfwVcVgFJ7C6W4DUYGt2_r2w81=Q-L8jtiTHE=PiFEN0A@mail.gmail.com/3-v18-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From 3718fdbdaa16a820f5b3047f1fd5211a1faff4a6 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v18] Add pg_get_table_ddl() to reconstruct CREATE TABLE
statements
The function reconstructs the CREATE TABLE statement for an ordinary or
partitioned table, followed by the ALTER TABLE / CREATE INDEX /
CREATE RULE / CREATE STATISTICS statements needed to restore its full
definition. Each statement is returned as a separate row.
Supported per-column features: data type with type modifiers, COLLATE,
STORAGE, COMPRESSION (pglz / lz4), GENERATED ALWAYS AS (expr)
STORED/VIRTUAL, GENERATED ALWAYS|BY DEFAULT AS IDENTITY (with sequence
options), DEFAULT, NOT NULL (including named NOT NULL constraints), and
per-column attoptions emitted as ALTER COLUMN SET (...).
Supported table-level features: UNLOGGED, INHERITS, PARTITION BY (RANGE
/ LIST / HASH parents), PARTITION OF parent FOR VALUES (FROM/TO, WITH
modulus/remainder, DEFAULT), USING table access method, WITH
(reloptions), TABLESPACE, and inline CHECK constraints in the CREATE
TABLE body.
Supported sub-objects (re-using existing deparse helpers from
ruleutils.c): indexes (including partial and functional) via
pg_get_indexdef_ddl; constraints (PRIMARY KEY with WITHOUT OVERLAPS for
temporal keys, UNIQUE with NULLS NOT DISTINCT and INCLUDE columns,
FOREIGN KEY with ON DELETE/UPDATE referential actions and MATCH clause,
NOT ENFORCED foreign keys, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_body; rules via pg_get_ruledef_ddl; extended
statistics via pg_get_statisticsobjdef_ddl; REPLICA IDENTITY
NOTHING/FULL/USING INDEX; ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY;
and child-local DEFAULT overrides on inheritance/partition children.
DDL for partition children of a partitioned-table parent is appended
after the parent by default.
The function signature follows the named-parameter convention
established by pg_get_role_ddl(), pg_get_tablespace_ddl(), and
pg_get_database_ddl():
pg_get_table_ddl(relation regclass,
pretty boolean DEFAULT false,
owner boolean DEFAULT true,
tablespace boolean DEFAULT true,
schema_qualified boolean DEFAULT true,
only_kinds text[] DEFAULT NULL,
except_kinds text[] DEFAULT NULL)
pretty controls pretty-printed output. owner controls emission of the
ALTER TABLE ... OWNER TO statement. tablespace controls the TABLESPACE
clause on CREATE TABLE. schema_qualified controls whether object names
are emitted with their schema prefix: when true (the default) the
active search_path is temporarily narrowed to pg_catalog so every
deparse helper produces fully-qualified names; when false it is narrowed
to the target table's own schema so same-schema references come out
unqualified while cross-schema references remain qualified for
correctness. Temporary tables are never schema-qualified regardless of
this setting: the TEMPORARY keyword already places them in pg_temp, and
emitting pg_temp_NN.relname would produce non-replayable DDL.
Object-class filtering uses two mutually-exclusive text-array
parameters, only_kinds and except_kinds. When only_kinds is set, only
the listed kinds are emitted; when except_kinds is set, every kind
except the listed ones is emitted; when neither is set every kind is
emitted. Each array element is matched case-insensitively with
leading/trailing whitespace trimmed; an unrecognized name raises an
error. The kind vocabulary is:
table, index, primary_key, unique, check, foreign_key, exclusion,
rule, statistics, rls, replica_identity, partition
trigger and policy are accepted in the vocabulary but currently produce
no output; they are reserved for when standalone pg_get_trigger_ddl()
and pg_get_policy_ddl() helpers become available.
NOT NULL is not part of the vocabulary: it is always emitted to prevent
producing schemas that silently accept NULLs the source would reject.
When replica_identity is in the active filter and the table's REPLICA
IDENTITY USING INDEX references an index that the filter would suppress,
the function raises an error before emitting any output, so the
generated DDL never references an index it did not produce.
Default omission convention: every optional clause is omitted when its
value matches what the server would reapply on round-trip, including
type-default COLLATE, per-type STORAGE, the auto-generated identity
sequence name and parameter defaults, heap access method, default
REPLICA IDENTITY, disabled RLS toggles, empty reloptions, and the
default tablespace.
Author: Akshay Joshi <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kyotaro Horiguchi <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Rui Zhao <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 193 ++
src/backend/catalog/pg_inherits.c | 93 +
src/backend/commands/tablecmds.c | 30 +-
src/backend/utils/adt/ddlutils.c | 2660 ++++++++++++++++-
src/backend/utils/adt/ruleutils.c | 87 +-
src/include/catalog/pg_inherits.h | 1 +
src/include/catalog/pg_proc.dat | 8 +
src/include/commands/tablecmds.h | 3 +
src/include/utils/ruleutils.h | 4 +
.../regress/expected/pg_get_table_ddl.out | 1612 ++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 937 ++++++
src/tools/pgindent/typedefs.list | 2 +
13 files changed, 5615 insertions(+), 17 deletions(-)
create mode 100644 src/test/regress/expected/pg_get_table_ddl.out
create mode 100644 src/test/regress/sql/pg_get_table_ddl.sql
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 69ef3857cfa..9dd85b4206e 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3960,6 +3960,199 @@ 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>relation</parameter> <type>regclass</type>
+ <optional>, <parameter>pretty</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> false</optional>
+ <optional>, <parameter>owner</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>tablespace</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>schema_qualified</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>only_kinds</parameter> <type>text[]</type>
+ <literal>DEFAULT</literal> NULL</optional>
+ <optional>, <parameter>except_kinds</parameter> <type>text[]</type>
+ <literal>DEFAULT</literal> NULL</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.
+ This includes per-index statistics targets
+ (<command>ALTER INDEX ... SET STATISTICS</command>),
+ the clustering index (<command>ALTER TABLE ... CLUSTER ON</command>),
+ and non-default rule firing states
+ (<command>ALTER TABLE ... DISABLE/ENABLE RULE</command>).
+ 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.
+ Foreign-key constraints are always emitted after all other
+ constraints so that self-referencing foreign keys (where the
+ referenced primary or unique key is on the same table) can be
+ added once that key already exists.
+ When <parameter>pretty</parameter> is true, the output is
+ pretty-printed. When <parameter>owner</parameter> is false, the
+ <command>ALTER TABLE ... OWNER TO</command> statement is omitted.
+ When <parameter>tablespace</parameter> is false, the
+ <literal>TABLESPACE</literal> clause is omitted from the
+ <command>CREATE TABLE</command> statement. The
+ <parameter>only_kinds</parameter> and <parameter>except_kinds</parameter>
+ parameters described below control which object classes are emitted.
+ </para>
+ <para>
+ The <parameter>only_kinds</parameter> and <parameter>except_kinds</parameter>
+ parameters each take a text array of object-class kind names
+ and are mutually exclusive (specifying both raises an
+ error). When <parameter>only_kinds</parameter> is set, only the
+ listed kinds are emitted; when <parameter>except_kinds</parameter> is
+ set, every kind <emphasis>except</emphasis> the listed ones is
+ emitted. When neither is set (the default), every kind is
+ emitted. Leading and trailing whitespace in each array element
+ is ignored and matching is case-insensitive; an unrecognized
+ kind name raises an error.
+ The kind vocabulary is
+ <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).
+ Note that <literal>trigger</literal> and <literal>policy</literal>
+ are accepted in the vocabulary but currently produce no output;
+ they are reserved for future use when standalone
+ <function>pg_get_trigger_ddl</function> and
+ <function>pg_get_policy_ddl</function> helpers become available. 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_kinds => ARRAY['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_kinds => ARRAY['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 <parameter>schema_qualified</parameter> parameter (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. Partition children that reside in a different
+ schema than the target table are always emitted with full schema
+ qualification regardless of this parameter, because the output
+ would otherwise place the child in the wrong schema when
+ replayed.
+ </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>
+ <para>
+ The following table-related objects are intentionally outside the
+ scope of this function and are not emitted:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <emphasis>Sequences</emphasis> owned by serial columns
+ (<type>serial</type>, <type>bigserial</type>,
+ <type>smallserial</type>) —sequences are independent catalog
+ objects. Use <function>pg_get_sequence_ddl</function> (if
+ available) or <productname>pg_dump</productname> to capture them
+ alongside the table.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <emphasis>Partition children whose column order differs from the
+ parent</emphasis> —the <literal>PARTITION OF</literal> syntax
+ can only reproduce the parent's column order. Such partitions
+ require a standalone <command>CREATE TABLE</command> followed by
+ <command>ALTER TABLE ... ATTACH PARTITION</command>, which
+ <productname>pg_dump</productname> performs automatically.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <emphasis>Triggers and row-level security policies</emphasis> —
+ reserved for future use when standalone
+ <function>pg_get_trigger_ddl</function> and
+ <function>pg_get_policy_ddl</function> helpers are available.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <emphasis><command>COMMENT ON</command> and
+ <command>GRANT</command>/<command>REVOKE</command></emphasis> —
+ these are separate from the table's structural DDL and are not
+ emitted.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </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 95abaf4890c..06afa6811ec 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);
@@ -2524,7 +2523,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)
@@ -19837,6 +19836,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..e65633f6fde 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -5,7 +5,7 @@
*
* This file contains the pg_get_*_ddl family of functions that generate
* DDL statements to recreate database objects such as roles, tablespaces,
- * and databases, along with common infrastructure for option parsing and
+ * databases, and tables, along with common infrastructure for
* pretty-printing.
*
* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
@@ -21,30 +21,92 @@
#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_index.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_policy.h"
+#include "catalog/pg_rewrite.h"
+#include "catalog/pg_sequence.h"
+#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_tablespace.h"
+#include "catalog/pg_trigger.h"
+#include "rewrite/rewriteDefine.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
#include "commands/tablespace.h"
-#include "common/relpath.h"
#include "funcapi.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "utils/acl.h"
+#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/datetime.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
-#include "utils/pg_locale.h"
#include "utils/rel.h"
#include "utils/ruleutils.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
#include "utils/varlena.h"
+/*
+ * Object-class kinds that the only / except options on
+ * pg_get_table_ddl can filter on. Members are stored as integers in a
+ * Bitmapset on TableDdlContext. Keep table_ddl_kind_names[] in sync
+ * with the order of additions here.
+ */
+typedef enum TableDdlKind
+{
+ TABLE_DDL_KIND_TABLE,
+ TABLE_DDL_KIND_INDEX,
+ TABLE_DDL_KIND_PRIMARY_KEY,
+ TABLE_DDL_KIND_UNIQUE,
+ TABLE_DDL_KIND_CHECK,
+ TABLE_DDL_KIND_FOREIGN_KEY,
+ TABLE_DDL_KIND_EXCLUSION,
+ TABLE_DDL_KIND_RULE,
+ TABLE_DDL_KIND_STATISTICS,
+ TABLE_DDL_KIND_TRIGGER,
+ TABLE_DDL_KIND_POLICY,
+ TABLE_DDL_KIND_RLS,
+ TABLE_DDL_KIND_REPLICA_IDENTITY,
+ TABLE_DDL_KIND_PARTITION,
+} TableDdlKind;
+
+static const struct
+{
+ const char *name;
+ TableDdlKind kind;
+} table_ddl_kind_names[] =
+{
+ {"table", TABLE_DDL_KIND_TABLE},
+ {"index", TABLE_DDL_KIND_INDEX},
+ {"primary_key", TABLE_DDL_KIND_PRIMARY_KEY},
+ {"unique", TABLE_DDL_KIND_UNIQUE},
+ {"check", TABLE_DDL_KIND_CHECK},
+ {"foreign_key", TABLE_DDL_KIND_FOREIGN_KEY},
+ {"exclusion", TABLE_DDL_KIND_EXCLUSION},
+ {"rule", TABLE_DDL_KIND_RULE},
+ {"statistics", TABLE_DDL_KIND_STATISTICS},
+ {"trigger", TABLE_DDL_KIND_TRIGGER},
+ {"policy", TABLE_DDL_KIND_POLICY},
+ {"rls", TABLE_DDL_KIND_RLS},
+ {"replica_identity", TABLE_DDL_KIND_REPLICA_IDENTITY},
+ {"partition", TABLE_DDL_KIND_PARTITION},
+};
+
+static Bitmapset *parse_kind_array(const char *paramname, ArrayType *arr);
static void append_ddl_option(StringInfo buf, bool pretty, int indent,
const char *fmt, ...)
pg_attribute_printf(4, 5);
@@ -57,6 +119,196 @@ 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).
+ *
+ * inherited_notnull is set when a NOT NULL constraint row exists in
+ * pg_constraint but conislocal=false (purely inherited). In that case
+ * the NOT NULL must not be emitted in the column list: the INHERITS /
+ * PARTITION OF clause already propagates it, and emitting it here would
+ * flip conislocal to true on replay.
+ */
+typedef struct LocalNotNullEntry
+{
+ Oid conoid;
+ char *name;
+ bool is_auto; /* matches "<table>_<col>_not_null" */
+ bool no_inherit;
+ bool inherited_notnull; /* row exists but conislocal=false */
+} 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 bool child_default_inherited_from_parent(Oid childOid,
+ const char *attname,
+ const char *child_adbin);
+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 bool parent_has_default_for_col(Oid childOid, const char *attname);
+
+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_typed_column_storage(TableDdlContext *ctx);
+static void emit_indexes(TableDdlContext *ctx);
+static void emit_cluster_on(TableDdlContext *ctx);
+static void emit_local_constraints(TableDdlContext *ctx);
+static void emit_rules(TableDdlContext *ctx);
+static void emit_statistics(TableDdlContext *ctx);
+static void emit_replica_identity(TableDdlContext *ctx);
+static void emit_rls_toggles(TableDdlContext *ctx);
+static void emit_partition_children(TableDdlContext *ctx);
+
+
+/*
+ * parse_kind_array
+ * Parse a text[] of object-class kind names into a Bitmapset of
+ * TableDdlKind values.
+ *
+ * Each element is matched case-insensitively; surrounding whitespace is
+ * stripped. NULL elements are rejected. Unknown kind names raise an error
+ * citing the supplied parameter name. An empty array is also rejected.
+ * Duplicate entries are silently de-duplicated by the Bitmapset.
+ */
+static Bitmapset *
+parse_kind_array(const char *paramname, ArrayType *arr)
+{
+ Datum *elems;
+ bool *nulls;
+ int nelems;
+ Bitmapset *result = NULL;
+
+ deconstruct_array_builtin(arr, TEXTOID, &elems, &nulls, &nelems);
+
+ if (nelems == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" must specify at least one kind",
+ paramname)));
+
+ for (int i = 0; i < nelems; i++)
+ {
+ char *raw;
+ char *token;
+ char *end;
+ bool found = false;
+
+ if (nulls[i])
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" must not contain NULL elements",
+ paramname)));
+
+ raw = text_to_cstring(DatumGetTextPP(elems[i]));
+
+ /* Trim leading whitespace. */
+ token = raw;
+ while (*token == ' ' || *token == '\t')
+ token++;
+
+ /* Trim trailing whitespace. */
+ end = token + strlen(token);
+ while (end > token && (end[-1] == ' ' || end[-1] == '\t'))
+ end--;
+ *end = '\0';
+
+ for (size_t j = 0; j < lengthof(table_ddl_kind_names); j++)
+ {
+ if (pg_strcasecmp(token, table_ddl_kind_names[j].name) == 0)
+ {
+ result = bms_add_member(result,
+ (int) table_ddl_kind_names[j].kind);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("unrecognized kind \"%s\" in parameter \"%s\"",
+ token, paramname)));
+
+ pfree(raw);
+ }
+
+ return result;
+}
+
+/*
+ * is_kind_included
+ * Determine whether DDL for the given object-class kind should be
+ * emitted under the current options.
+ */
+static bool
+is_kind_included(const TableDdlContext *ctx, TableDdlKind kind)
+{
+ if (ctx->only_kinds != NULL)
+ return bms_is_member((int) kind, ctx->only_kinds);
+ if (ctx->except_kinds != NULL)
+ return !bms_is_member((int) kind, ctx->except_kinds);
+ return true;
+}
/*
* Helper to append a formatted string with optional pretty-printing.
@@ -975,3 +1227,2405 @@ 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;
+
+ if (con->contype != CONSTRAINT_NOTNULL)
+ 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;
+
+ /*
+ * Inherited-only constraint: record the fact so column-emit helpers
+ * can suppress inline NOT NULL (the parent's INHERITS clause already
+ * propagates it; re-emitting it would flip conislocal to true on
+ * replay).
+ */
+ if (!con->conislocal)
+ {
+ entries[attnum].inherited_notnull = true;
+ continue;
+ }
+
+ att = TupleDescAttr(tupdesc, attnum - 1);
+ autoname = makeObjectName(relname, NameStr(att->attname), "not_null");
+
+ 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;
+ pfree(autoname);
+
+ /*
+ * Track which NOT NULL OIDs must not be re-emitted out-of-line by
+ * emit_local_constraints:
+ *
+ * - For attislocal columns: the inline pass in append_column_defs
+ * materializes the constraint; a second ALTER TABLE would collide.
+ *
+ * - For inherited columns with a user-defined name (!is_auto):
+ * emit_create_table_stmt emits these as table-level CONSTRAINT
+ * clauses in the CREATE TABLE or PARTITION OF body so the name is
+ * preserved when the inherited NOT NULL is established. An
+ * out-of-line ALTER TABLE ADD CONSTRAINT would collide with the
+ * already-propagated inherited NOT NULL.
+ *
+ * - For inherited columns with an auto-name on a partition child:
+ * PARTITION OF re-creates an equivalent constraint (possibly under
+ * the parent-derived name), making the out-of-line ALTER TABLE
+ * redundant and collision-prone. Suppress it; the auto-name
+ * change is acceptable.
+ *
+ * For inherited columns with an auto-name on a plain INHERITS child,
+ * the out-of-line ALTER TABLE is still safe and preserves the name.
+ */
+ if (skip_oids != NULL &&
+ (att->attislocal ||
+ !entries[attnum].is_auto ||
+ rel->rd_rel->relispartition))
+ *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;
+
+ 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);
+
+ autoname = makeObjectName(RelationGetRelationName(rel),
+ NameStr(att->attname), "seq");
+ 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);
+ }
+ pfree(autoname);
+ 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];
+
+ /*
+ * Suppress NOT NULL when the constraint is inherited-only
+ * (conislocal=false). The INHERITS clause already propagates it;
+ * emitting it here would make conislocal true on replay. When
+ * there is no pg_constraint row at all (pre-PG18 catalog with
+ * attnotnull=true), inherited_notnull is false so we fall through
+ * and emit plain NOT NULL as before.
+ */
+ if (!nn->inherited_notnull)
+ {
+ 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);
+ /* inherited-only NOT NULL is not an override; treat as absent */
+ has_notnull = att->attnotnull &&
+ !nn_entries[att->attnum].inherited_notnull;
+
+ 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", parentQual);
+
+ /*
+ * Include NOT NULL constraints in the PARTITION OF column spec
+ * when the child's constraint name differs from what automatic
+ * inheritance would give. When PARTITION OF runs, the parent's
+ * NOT NULL propagates to the child under the parent's constraint
+ * name. If the child carries a different name - whether from a
+ * user-specified CONSTRAINT clause or from a pre-existing table
+ * that was later attached as a partition - we must specify it in
+ * the column spec so the name is preserved on replay.
+ *
+ * We scan pg_constraint directly rather than relying on nn_entries,
+ * because after ATTACH PARTITION the child's constraint can have
+ * conislocal=false (purely inherited) while still carrying the
+ * original local name - a case that collect_local_not_null tracks
+ * only as inherited_notnull without preserving the name.
+ */
+ {
+ Relation conRel;
+ SysScanDesc conScan;
+ ScanKeyData conKey;
+ HeapTuple conTup;
+ StringInfoData colspec;
+ bool hasspecs = false;
+
+ initStringInfo(&colspec);
+
+ 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);
+ Datum conkeyDat;
+ bool conkeyNull;
+ ArrayType *conkeyArr;
+ AttrNumber attnum;
+ char *conname;
+ char *parent_conname;
+ char *attname;
+
+ if (con->contype != CONSTRAINT_NOTNULL)
+ continue;
+
+ conkeyDat = heap_getattr(conTup, Anum_pg_constraint_conkey,
+ RelationGetDescr(conRel), &conkeyNull);
+ if (conkeyNull)
+ continue;
+ conkeyArr = DatumGetArrayTypeP(conkeyDat);
+ if (ARR_NDIM(conkeyArr) != 1 || ARR_DIMS(conkeyArr)[0] < 1 ||
+ ARR_HASNULL(conkeyArr) || ARR_ELEMTYPE(conkeyArr) != INT2OID)
+ continue;
+ attnum = ((int16 *) ARR_DATA_PTR(conkeyArr))[0];
+
+ attname = get_attname(ctx->relid, attnum, true);
+ if (attname == NULL)
+ continue;
+
+ conname = pstrdup(NameStr(con->conname));
+
+ /*
+ * Check if this name is what PARTITION OF would auto-create.
+ * The parent propagates its own NOT NULL name to the child.
+ * Look up the parent's NOT NULL constraint name for this column.
+ */
+ parent_conname = NULL;
+ {
+ AttrNumber parentAttnum = get_attnum(parentOid, attname);
+
+ if (parentAttnum != InvalidAttrNumber)
+ {
+ Relation pconRel =
+ table_open(ConstraintRelationId, AccessShareLock);
+ ScanKeyData pconKey;
+ SysScanDesc pconScan;
+ HeapTuple pconTup;
+
+ ScanKeyInit(&pconKey,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(parentOid));
+ pconScan = systable_beginscan(pconRel,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &pconKey);
+ while (HeapTupleIsValid(pconTup = systable_getnext(pconScan)))
+ {
+ Form_pg_constraint pcon =
+ (Form_pg_constraint) GETSTRUCT(pconTup);
+ Datum pconkeyDat;
+ bool pconkeyNull;
+ ArrayType *pconkeyArr;
+
+ if (pcon->contype != CONSTRAINT_NOTNULL)
+ continue;
+ pconkeyDat = heap_getattr(pconTup,
+ Anum_pg_constraint_conkey,
+ RelationGetDescr(pconRel),
+ &pconkeyNull);
+ if (pconkeyNull)
+ continue;
+ pconkeyArr = DatumGetArrayTypeP(pconkeyDat);
+ if (ARR_NDIM(pconkeyArr) == 1 &&
+ ARR_DIMS(pconkeyArr)[0] >= 1 &&
+ !ARR_HASNULL(pconkeyArr) &&
+ ARR_ELEMTYPE(pconkeyArr) == INT2OID &&
+ ((int16 *) ARR_DATA_PTR(pconkeyArr))[0] ==
+ parentAttnum)
+ {
+ parent_conname = pstrdup(NameStr(pcon->conname));
+ break;
+ }
+ }
+ systable_endscan(pconScan);
+ table_close(pconRel, AccessShareLock);
+ }
+ }
+
+ /*
+ * The child's NOT NULL needs inline specification when its name
+ * differs from the parent's constraint name (which is what
+ * PARTITION OF would automatically propagate). If the parent has
+ * no NOT NULL on this column, the child's constraint was purely
+ * local and also needs to be specified inline.
+ */
+ if (parent_conname == NULL ||
+ strcmp(conname, parent_conname) != 0)
+ {
+ bool no_inherit = con->connoinherit;
+
+ if (hasspecs)
+ appendStringInfoChar(&colspec, ',');
+ if (ctx->pretty)
+ appendStringInfoString(&colspec, "\n ");
+ else if (hasspecs)
+ appendStringInfoChar(&colspec, ' ');
+ hasspecs = true;
+
+ appendStringInfo(&colspec, "CONSTRAINT %s NOT NULL %s",
+ quote_identifier(conname),
+ quote_identifier(attname));
+ if (no_inherit)
+ appendStringInfoString(&colspec, " NO INHERIT");
+
+ /*
+ * Also add to skip_notnull_oids so emit_local_constraints
+ * does not try to emit it again as ALTER TABLE ADD CONSTRAINT.
+ */
+ ctx->skip_notnull_oids =
+ lappend_oid(ctx->skip_notnull_oids, con->oid);
+ }
+
+ pfree(conname);
+ pfree(attname);
+ if (parent_conname)
+ pfree(parent_conname);
+ }
+
+ systable_endscan(conScan);
+ table_close(conRel, AccessShareLock);
+
+ if (hasspecs)
+ {
+ if (ctx->pretty)
+ appendStringInfo(&ctx->buf, " (\n%s\n)", colspec.data);
+ else
+ appendStringInfo(&ctx->buf, " (%s)", colspec.data);
+ }
+ pfree(colspec.data);
+ }
+
+ appendStringInfo(&ctx->buf, " %s", 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;
+ int buf_len_before;
+
+ appendStringInfoString(&ctx->buf, " (");
+
+ buf_len_before = ctx->buf.len;
+ append_column_defs(&ctx->buf, rel, ctx->pretty,
+ is_kind_included(ctx, TABLE_DDL_KIND_CHECK),
+ ctx->schema_qualified, ctx->nn_entries);
+
+ /*
+ * For INHERITS children, inherited columns with a local NOT NULL
+ * carrying a user-defined name need a table-level CONSTRAINT clause
+ * here. Without it, when the parent's NOT NULL is added and
+ * propagates to the child, a subsequent out-of-line ALTER TABLE ADD
+ * CONSTRAINT fails because a NOT NULL already covers the column.
+ * Emitting the constraint inline at CREATE TABLE time causes it to
+ * merge correctly with the parent's propagated constraint.
+ */
+ {
+ TupleDesc tupdesc = RelationGetDescr(rel);
+ bool any_content = (ctx->buf.len > buf_len_before);
+
+ for (int i = 0; i < tupdesc->natts; i++)
+ {
+ Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+ LocalNotNullEntry *nn;
+
+ if (att->attisdropped || att->attislocal)
+ continue;
+
+ nn = &ctx->nn_entries[att->attnum];
+ if (nn->conoid == InvalidOid || nn->is_auto)
+ continue;
+
+ if (any_content)
+ appendStringInfoChar(&ctx->buf, ',');
+ if (ctx->pretty)
+ appendStringInfoString(&ctx->buf, "\n ");
+ else if (any_content)
+ appendStringInfoChar(&ctx->buf, ' ');
+ any_content = true;
+
+ appendStringInfo(&ctx->buf, "CONSTRAINT %s NOT NULL %s",
+ quote_identifier(nn->name),
+ quote_identifier(NameStr(att->attname)));
+ if (nn->no_inherit)
+ appendStringInfoString(&ctx->buf, " NO INHERIT");
+ }
+ }
+
+ 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 (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");
+ }
+
+ 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);
+ }
+ }
+
+ 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);
+}
+
+/*
+ * child_default_inherited_from_parent
+ * Return true if child_adbin is identical to the adbin stored for
+ * attname on any immediate parent of childOid.
+ *
+ * PostgreSQL copies the adbin expression verbatim from parent to child
+ * when a partition is created, so a byte-for-byte match means the child
+ * did not override the default locally. For regular inheritance the
+ * parent typically has no default at all (atthasdef=false), so the
+ * function returns false and the override is correctly emitted.
+ */
+static bool
+child_default_inherited_from_parent(Oid childOid, const char *attname,
+ const char *child_adbin)
+{
+ List *parents;
+ ListCell *lc;
+ bool result = false;
+
+ parents = find_inheritance_parents(childOid, NoLock);
+
+ foreach(lc, parents)
+ {
+ Oid parentOid = lfirst_oid(lc);
+ Relation parentRel;
+ AttrNumber parentAttnum;
+ TupleConstr *constr;
+
+ parentRel = try_table_open(parentOid, AccessShareLock);
+ if (parentRel == NULL)
+ continue;
+
+ parentAttnum = get_attnum(parentOid, attname);
+ if (parentAttnum != InvalidAttrNumber)
+ {
+ Form_pg_attribute parentAtt =
+ TupleDescAttr(RelationGetDescr(parentRel), parentAttnum - 1);
+
+ if (parentAtt->atthasdef)
+ {
+ constr = RelationGetDescr(parentRel)->constr;
+ if (constr != NULL)
+ {
+ for (int j = 0; j < constr->num_defval; j++)
+ {
+ if (constr->defval[j].adnum == parentAttnum &&
+ strcmp(constr->defval[j].adbin, child_adbin) == 0)
+ {
+ result = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ table_close(parentRel, AccessShareLock);
+ if (result)
+ break;
+ }
+
+ list_free(parents);
+ return result;
+}
+
+/*
+ * parent_has_default_for_col
+ * Return true if any immediate parent of childOid has a non-generated
+ * DEFAULT expression on the column named attname.
+ *
+ * Used to detect inherited columns where the child dropped the parent's
+ * default: after PARTITION OF / INHERITS the parent default is re-applied,
+ * so we must emit ALTER TABLE ONLY child ALTER COLUMN col DROP DEFAULT.
+ */
+static bool
+parent_has_default_for_col(Oid childOid, const char *attname)
+{
+ List *parents;
+ ListCell *lc;
+ bool result = false;
+
+ parents = find_inheritance_parents(childOid, NoLock);
+ foreach(lc, parents)
+ {
+ Oid parentOid = lfirst_oid(lc);
+ AttrNumber parentAttnum = get_attnum(parentOid, attname);
+
+ if (parentAttnum != InvalidAttrNumber)
+ {
+ HeapTuple attTup = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(parentOid),
+ Int16GetDatum(parentAttnum));
+
+ if (HeapTupleIsValid(attTup))
+ {
+ Form_pg_attribute parentAtt = (Form_pg_attribute) GETSTRUCT(attTup);
+
+ if (parentAtt->atthasdef && parentAtt->attgenerated == '\0')
+ result = true;
+ ReleaseSysCache(attTup);
+ }
+ }
+ if (result)
+ break;
+ }
+ list_free(parents);
+ return result;
+}
+
+/*
+ * emit_child_default_overrides
+ * ALTER TABLE ONLY qualname ALTER COLUMN col SET DEFAULT expr - one per
+ * inherited (attislocal=false) non-generated column that carries a
+ * default which differs from every immediate parent's default. Using
+ * ONLY prevents the statement from cascading into grandchildren and
+ * overwriting their own defaults. Defaults that are simply inherited
+ * unchanged from a parent are skipped to avoid redundant output.
+ * Generated columns are skipped: their expression is inherited
+ * automatically and SET DEFAULT would fail at replay.
+ *
+ * Also emits ALTER TABLE ONLY ... DROP DEFAULT for inherited columns
+ * where the child explicitly dropped the parent's default: after
+ * PARTITION OF / INHERITS the parent default is re-applied, and the
+ * DROP DEFAULT is needed to restore the child's state.
+ */
+static void
+emit_child_default_overrides(TableDdlContext *ctx)
+{
+ TupleDesc tupdesc = RelationGetDescr(ctx->rel);
+ TupleConstr *constr = tupdesc->constr;
+ List *dpcontext = NIL;
+
+ for (int i = 0; i < tupdesc->natts; i++)
+ {
+ Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+ const char *adbin = NULL;
+ char *defstr;
+
+ if (att->attisdropped || att->attislocal)
+ continue;
+ if (att->attgenerated != '\0')
+ continue;
+
+ if (!att->atthasdef)
+ {
+ /*
+ * Column has no default but a parent does: after PARTITION OF /
+ * INHERITS the parent's default is re-applied, so emit DROP
+ * DEFAULT to restore the dropped state.
+ */
+ if (parent_has_default_for_col(ctx->relid, NameStr(att->attname)))
+ {
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE ONLY %s ALTER COLUMN %s DROP DEFAULT;",
+ ctx->qualname,
+ quote_identifier(NameStr(att->attname)));
+ append_stmt(ctx);
+ }
+ continue;
+ }
+
+ /* Locate the raw adbin for parent-comparison and for deparse. */
+ if (constr != NULL)
+ {
+ for (int j = 0; j < constr->num_defval; j++)
+ {
+ if (constr->defval[j].adnum == att->attnum)
+ {
+ adbin = constr->defval[j].adbin;
+ break;
+ }
+ }
+ }
+ if (adbin == NULL)
+ continue;
+
+ /*
+ * Skip columns whose default expression is identical to a parent's;
+ * the child merely inherited it (typical for partition children).
+ * An explicit local override will have a different adbin.
+ */
+ if (child_default_inherited_from_parent(ctx->relid,
+ NameStr(att->attname),
+ adbin))
+ continue;
+
+ defstr = find_attrdef_text(ctx->rel, att->attnum, &dpcontext);
+ if (defstr == NULL)
+ continue;
+
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE ONLY %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_typed_column_storage
+ * For typed tables (CREATE TABLE ... OF type_name), emit per-column
+ * STORAGE and COMPRESSION overrides as ALTER TABLE statements.
+ *
+ * The typed-table grammar allows only DEFAULT / NOT NULL / CHECK in the
+ * CREATE TABLE column-options list (columnOptions -> ColQualList), so
+ * STORAGE and COMPRESSION cannot appear inline there. They must be
+ * emitted post-CREATE, matching what pg_dump produces.
+ */
+static void
+emit_typed_column_storage(TableDdlContext *ctx)
+{
+ TupleDesc tupdesc;
+
+ if (!OidIsValid(ctx->rel->rd_rel->reloftype))
+ return;
+
+ tupdesc = RelationGetDescr(ctx->rel);
+
+ for (int i = 0; i < tupdesc->natts; i++)
+ {
+ Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+
+ if (att->attisdropped)
+ continue;
+
+ if (att->attstorage != get_typstorage(att->atttypid))
+ {
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE ONLY %s ALTER COLUMN %s SET STORAGE %s;",
+ ctx->qualname,
+ quote_identifier(NameStr(att->attname)),
+ storage_name(att->attstorage));
+ append_stmt(ctx);
+ }
+
+ if (CompressionMethodIsValid(att->attcompression))
+ {
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE ONLY %s ALTER COLUMN %s SET COMPRESSION %s;",
+ ctx->qualname,
+ quote_identifier(NameStr(att->attname)),
+ GetCompressionMethodName(att->attcompression));
+ append_stmt(ctx);
+ }
+ }
+}
+
+/*
+ * 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);
+ HeapTuple indTup;
+ Form_pg_index idxform;
+ char *idxdef;
+ int16 indnkeyatts;
+
+ if (OidIsValid(get_index_constraint(idxoid)))
+ continue;
+
+ /*
+ * Skip indexes that were inherited from a parent partitioned index.
+ * They are created automatically when the parent index DDL is
+ * replayed and the partition is attached, so emitting them separately
+ * would produce a "relation already exists" error on replay.
+ */
+ if (get_rel_relispartition(idxoid))
+ continue;
+
+ indTup = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(idxoid));
+ if (!HeapTupleIsValid(indTup))
+ continue;
+ idxform = (Form_pg_index) GETSTRUCT(indTup);
+
+ /* Skip invalid indexes; they may be left over from a failed CREATE INDEX CONCURRENTLY. */
+ if (!idxform->indisvalid)
+ {
+ ReleaseSysCache(indTup);
+ continue;
+ }
+
+ indnkeyatts = idxform->indnkeyatts;
+ ReleaseSysCache(indTup);
+
+ idxdef = pg_get_indexdef_ddl(idxoid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", idxdef);
+ append_stmt(ctx);
+ pfree(idxdef);
+
+ /*
+ * Emit ALTER INDEX ... ALTER COLUMN n SET STATISTICS for any key
+ * column that has a non-default statistics target. Expression
+ * columns (indnkeyatts covers them) are the only kind that can have
+ * a statistics target on an index.
+ */
+ {
+ char *idxqualname = lookup_relname_for_emit(idxoid,
+ ctx->schema_qualified,
+ ctx->base_namespace);
+
+ for (int j = 1; j <= indnkeyatts; j++)
+ {
+ HeapTuple attTup = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(idxoid),
+ Int16GetDatum(j));
+
+ if (HeapTupleIsValid(attTup))
+ {
+ bool isnull;
+ Datum stattargetDatum =
+ SysCacheGetAttr(ATTNUM, attTup,
+ Anum_pg_attribute_attstattarget,
+ &isnull);
+
+ if (!isnull && DatumGetInt16(stattargetDatum) >= 0)
+ {
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER INDEX %s ALTER COLUMN %d SET STATISTICS %d;",
+ idxqualname, j,
+ (int) DatumGetInt16(stattargetDatum));
+ append_stmt(ctx);
+ }
+ ReleaseSysCache(attTup);
+ }
+ }
+ pfree(idxqualname);
+ }
+ }
+ list_free(indexoids);
+}
+
+/*
+ * emit_cluster_on
+ * ALTER TABLE qualname CLUSTER ON index_name - emitted when any index
+ * on the relation has indisclustered=true.
+ */
+static void
+emit_cluster_on(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);
+ HeapTuple indTup;
+ Form_pg_index idxform;
+
+ indTup = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(idxoid));
+ if (!HeapTupleIsValid(indTup))
+ continue;
+ idxform = (Form_pg_index) GETSTRUCT(indTup);
+
+ if (idxform->indisclustered)
+ {
+ char *idxname = get_rel_name(idxoid);
+
+ ReleaseSysCache(indTup);
+ if (idxname != NULL)
+ {
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE %s CLUSTER ON %s;",
+ ctx->qualname,
+ quote_identifier(idxname));
+ append_stmt(ctx);
+ pfree(idxname);
+ }
+ break; /* at most one index per table can be clustered */
+ }
+ ReleaseSysCache(indTup);
+ }
+ 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).
+ *
+ * NOT NULL constraints are tracked via skip_notnull_oids to avoid
+ * double-emission. The set covers: (a) local columns, where the
+ * column-emit helpers materialise the constraint inline; (b) inherited
+ * columns with a user-defined name, where emit_create_table_stmt
+ * emits a table-level CONSTRAINT clause so the name survives the
+ * PARTITION OF / INHERITS replay; and (c) auto-named NOT NULLs on
+ * partition children, which are re-created by PARTITION OF and would
+ * collide with an out-of-line ALTER TABLE ADD CONSTRAINT. Any NOT
+ * NULL not in skip_notnull_oids is emitted here (e.g. inherited
+ * columns with an auto-name on a plain INHERITS child).
+ *
+ * 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;
+ List *fk_stmts = NIL;
+
+ 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.
+ *
+ * skip_notnull_oids covers all cases where NOT NULL was
+ * already materialised inline: local columns (via
+ * append_column_defs), inherited columns with a user-defined
+ * name (via the table-level CONSTRAINT clause emitted by
+ * emit_create_table_stmt), and auto-named NOT NULLs on
+ * partition children (re-created by PARTITION OF).
+ */
+ if (!is_kind_included(ctx, TABLE_DDL_KIND_TABLE))
+ continue;
+ if (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);
+
+ /*
+ * Defer FK statements so they are emitted after all other
+ * constraints. A self-referencing FK (REFERENCES same_table)
+ * requires the PK/UNIQUE it targets to exist first, and because
+ * the catalog scan returns constraints in name order, an FK whose
+ * name sorts before the PK name would otherwise be emitted first
+ * and fail with "there is no unique constraint matching given
+ * keys".
+ */
+ if (con->contype == CONSTRAINT_FOREIGN)
+ fk_stmts = lappend(fk_stmts, pstrdup(ctx->buf.data));
+ else
+ append_stmt(ctx);
+
+ pfree(conbody);
+ }
+ }
+ systable_endscan(conScan);
+ table_close(conRel, AccessShareLock);
+
+ /* Append deferred FK statements after all other constraints. */
+ ctx->statements = list_concat(ctx->statements, fk_stmts);
+}
+
+/*
+ * 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 enabled = ctx->rel->rd_rules->rules[i]->enabled;
+ char *ruledef_str;
+
+ ruledef_str = pg_get_ruledef_ddl(ruleid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfoString(&ctx->buf, ruledef_str);
+ append_stmt(ctx);
+ pfree(ruledef_str);
+
+ /*
+ * If the rule's firing state differs from the default (ORIGIN),
+ * emit the appropriate ALTER TABLE ... ENABLE/DISABLE RULE.
+ * We need the rule's name from pg_rewrite for this statement.
+ */
+ if (enabled != RULE_FIRES_ON_ORIGIN)
+ {
+ Relation rewriteRel;
+ ScanKeyData scankey;
+ SysScanDesc scan;
+ HeapTuple ruleTup;
+ char *rulename;
+
+ rewriteRel = table_open(RewriteRelationId, AccessShareLock);
+ ScanKeyInit(&scankey,
+ Anum_pg_rewrite_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(ruleid));
+ scan = systable_beginscan(rewriteRel, RewriteOidIndexId,
+ true, NULL, 1, &scankey);
+ ruleTup = systable_getnext(scan);
+ if (!HeapTupleIsValid(ruleTup))
+ elog(ERROR, "cache lookup failed for rule %u", ruleid);
+ rulename = pstrdup(NameStr(((Form_pg_rewrite) GETSTRUCT(ruleTup))->rulename));
+ systable_endscan(scan);
+ table_close(rewriteRel, AccessShareLock);
+
+ switch (enabled)
+ {
+ case RULE_DISABLED:
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE %s DISABLE RULE %s;",
+ ctx->qualname,
+ quote_identifier(rulename));
+ append_stmt(ctx);
+ break;
+ case RULE_FIRES_ON_REPLICA:
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE %s ENABLE REPLICA RULE %s;",
+ ctx->qualname,
+ quote_identifier(rulename));
+ append_stmt(ctx);
+ break;
+ case RULE_FIRES_ALWAYS:
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE %s ENABLE ALWAYS RULE %s;",
+ ctx->qualname,
+ quote_identifier(rulename));
+ append_stmt(ctx);
+ break;
+ default:
+ break;
+ }
+ pfree(rulename);
+ }
+ }
+}
+
+/*
+ * 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;
+
+ /*
+ * When schema_qualified is false, the contract is that the output
+ * is replayable with the parent's schema in search_path. That
+ * contract cannot hold for a child that lives in a different schema:
+ * its own name would be ambiguous (landing in whatever schema is
+ * first in search_path) and references back to the parent would have
+ * to be schema-qualified anyway. Force full qualification so the
+ * child's DDL is unambiguous regardless of the caller's search_path.
+ */
+ if (!childctx.schema_qualified &&
+ get_rel_namespace(childoid) != ctx->base_namespace)
+ childctx.schema_qualified = true;
+
+ if (childctx.only_kinds != NULL &&
+ bms_is_member((int) TABLE_DDL_KIND_PARTITION,
+ childctx.only_kinds))
+ {
+ Bitmapset *child_only = bms_copy(childctx.only_kinds);
+
+ child_only = bms_del_member(child_only,
+ (int) TABLE_DDL_KIND_PARTITION);
+ if (bms_is_empty(child_only))
+ {
+ bms_free(child_only);
+ childctx.only_kinds = NULL;
+ }
+ else
+ childctx.only_kinds = child_only;
+ }
+
+ childstmts = pg_get_table_ddl_internal(&childctx);
+ ctx->statements = list_concat(ctx->statements, childstmts);
+ }
+ list_free(children);
+}
+
+/*
+ * pg_get_table_ddl_internal
+ * Generate DDL statements to recreate a regular or partitioned table.
+ *
+ * The caller initializes *ctx with the user-supplied option fields
+ * (relid, pretty, no_owner, no_tablespace, schema_qualified,
+ * only_kinds, except_kinds). This function opens the relation,
+ * validates access, populates the derived fields (rel, qualname,
+ * nn_entries, ...), runs the emission passes, and returns the
+ * accumulated statement list.
+ *
+ * Each emission helper consults is_kind_included() to decide whether
+ * it should run. The table-proper passes (CREATE TABLE / OWNER /
+ * ALTER COLUMN ... SET DEFAULT / ALTER COLUMN ... SET (...)) are
+ * grouped under KIND_TABLE so the FK-only / sub-object-only workflow
+ * is expressible as a single only or except list.
+ *
+ * Trigger and policy emission are scaffolded but currently disabled
+ * (#if 0) - they will become a single helper call once the standalone
+ * pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+ */
+static List *
+pg_get_table_ddl_internal(TableDdlContext *ctx)
+{
+ Relation rel;
+ char relkind;
+ AclResult aclresult;
+
+ rel = table_open(ctx->relid, AccessShareLock);
+
+ relkind = rel->rd_rel->relkind;
+
+ /*
+ * The initial cut only supports ordinary and partitioned tables. Views,
+ * matviews, foreign tables, sequences, indexes, composite types, and
+ * TOAST tables are out of scope for now.
+ */
+ if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ char *relname = pstrdup(RelationGetRelationName(rel));
+
+ table_close(rel, AccessShareLock);
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not an ordinary or partitioned table",
+ relname)));
+ }
+
+ /* Caller needs SELECT on the table to read its definition. */
+ aclresult = pg_class_aclcheck(ctx->relid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLE,
+ RelationGetRelationName(rel));
+
+ /*
+ * Validation: if the table has REPLICA IDENTITY USING INDEX and the
+ * referenced index would not be emitted under the active filter, the
+ * emitted REPLICA IDENTITY clause would reference an index the same DDL
+ * never produced. Determine which kind would emit the index (one of
+ * primary_key / unique / exclusion for constraint-backed indexes,
+ * otherwise the generic "index" kind) and require it to be in scope
+ * whenever "replica_identity" is. The check uses is_kind_included so
+ * it covers both forms naturally: an "except" list that omits the
+ * source kind, and an "only" list that omits it.
+ */
+ if (rel->rd_rel->relreplident == REPLICA_IDENTITY_INDEX &&
+ is_kind_included(ctx, TABLE_DDL_KIND_REPLICA_IDENTITY))
+ {
+ Oid replidx = RelationGetReplicaIndex(rel);
+
+ if (OidIsValid(replidx))
+ {
+ TableDdlKind idx_kind = TABLE_DDL_KIND_INDEX;
+ Oid conoid = get_index_constraint(replidx);
+
+ if (OidIsValid(conoid))
+ {
+ HeapTuple conTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conoid));
+
+ if (HeapTupleIsValid(conTup))
+ {
+ Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
+
+ switch (con->contype)
+ {
+ case CONSTRAINT_PRIMARY:
+ idx_kind = TABLE_DDL_KIND_PRIMARY_KEY;
+ break;
+ case CONSTRAINT_UNIQUE:
+ idx_kind = TABLE_DDL_KIND_UNIQUE;
+ break;
+ case CONSTRAINT_EXCLUSION:
+ idx_kind = TABLE_DDL_KIND_EXCLUSION;
+ break;
+ default:
+ break;
+ }
+ ReleaseSysCache(conTup);
+ }
+ }
+
+ if (!is_kind_included(ctx, idx_kind))
+ {
+ char *relname = pstrdup(RelationGetRelationName(rel));
+ const char *idx_name = table_ddl_kind_names[(int) idx_kind].name;
+
+ table_close(rel, AccessShareLock);
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("REPLICA IDENTITY for table \"%s\" requires kind \"%s\" to be emitted",
+ relname, idx_name),
+ errdetail("The table's REPLICA IDENTITY USING INDEX references an index produced by the \"%s\" kind, which is not in the active filter.",
+ idx_name),
+ errhint("Either add \"%s\" to the filter or remove \"replica_identity\" from it.",
+ idx_name)));
+ }
+ }
+ }
+
+ /*
+ * Populate derived fields now that the relation is open and validated.
+ * The remaining derived fields (nn_entries, skip_notnull_oids, buf,
+ * statements) start zeroed via the caller's `TableDdlContext ctx = {0}`
+ * initializer; the NOT NULL cache is populated by collect_local_not_null,
+ * the buffer is initStringInfo'd, and each emit pass appends to
+ * statements via lappend.
+ */
+ ctx->rel = rel;
+ ctx->base_namespace = RelationGetNamespace(rel);
+ ctx->save_nestlevel = -1;
+ ctx->qualname = lookup_relname_for_emit(ctx->relid, ctx->schema_qualified,
+ ctx->base_namespace);
+
+ /*
+ * Temporarily override search_path so that the ruleutils helpers
+ * (pg_get_indexdef_ddl, pg_get_constraintdef_body,
+ * pg_get_statisticsobjdef_ddl, pg_get_ruledef_ddl, etc.) produce names
+ * that match the schema_qualified flag. Those helpers decide whether to
+ * qualify a name by calling RelationIsVisible(), which checks whether the
+ * object's schema appears in the active search_path.
+ *
+ * schema_qualified = false: narrow to the base schema so that same-schema
+ * references in DEFAULT expressions, FK targets, indexes, rules, and
+ * statistics come out unqualified automatically. Cross-schema references
+ * stay qualified, which is the correctness requirement.
+ *
+ * schema_qualified = true: narrow to pg_catalog only so that objects in
+ * the base schema (or any user schema the caller placed on search_path)
+ * are not reachable without qualification, forcing fully-qualified output
+ * from every helper regardless of the caller's session search_path.
+ *
+ * AtEOXact_GUC cleans up at xact end if anything throws between here and
+ * the explicit restore below; on the normal path we restore right before
+ * returning.
+ */
+ if (!ctx->schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx->base_namespace);
+
+ if (nspname != NULL)
+ {
+ const char *qnsp = quote_identifier(nspname);
+
+ ctx->save_nestlevel = NewGUCNestLevel();
+ (void) set_config_option("search_path", qnsp,
+ PGC_USERSET, PGC_S_SESSION,
+ GUC_ACTION_SAVE, true, 0, false);
+ if (qnsp != nspname)
+ pfree((char *) qnsp);
+ pfree(nspname);
+ }
+ }
+ else
+ {
+ ctx->save_nestlevel = NewGUCNestLevel();
+ (void) set_config_option("search_path", "pg_catalog",
+ PGC_USERSET, PGC_S_SESSION,
+ GUC_ACTION_SAVE, true, 0, false);
+ }
+
+ /*
+ * Cache locally-declared NOT NULL constraint metadata so the column- emit
+ * helpers can produce "CONSTRAINT name NOT NULL" inline for user-named
+ * constraints, and so the constraint loop can avoid double-emitting them.
+ */
+ ctx->nn_entries = collect_local_not_null(rel, &ctx->skip_notnull_oids);
+
+ initStringInfo(&ctx->buf);
+
+ /*
+ * Emission passes. Order is significant: CREATE TABLE first; OWNER and
+ * the per-column ALTER COLUMN passes before sub-object emission;
+ * sub-objects in dependency-friendly order (indexes before constraints,
+ * since constraint-backed indexes are emitted out-of-line by the
+ * constraint loop); partition children last so the parent already exists
+ * at replay time. Each helper gates itself on is_kind_included for the
+ * relevant TABLE_DDL_KIND_*; the four "table itself" passes are grouped
+ * here under KIND_TABLE so all of CREATE TABLE / OWNER / SET DEFAULT /
+ * SET (...) drop out together when the user asks for only sub-objects
+ * (e.g. only => 'foreign_key' for the second pass of a two-pass FK
+ * clone).
+ */
+ if (is_kind_included(ctx, TABLE_DDL_KIND_TABLE))
+ {
+ emit_create_table_stmt(ctx);
+ emit_owner_stmt(ctx);
+ emit_child_default_overrides(ctx);
+ emit_attoptions(ctx);
+ emit_typed_column_storage(ctx);
+ }
+ emit_indexes(ctx);
+ emit_cluster_on(ctx);
+ emit_local_constraints(ctx);
+ emit_rules(ctx);
+ emit_statistics(ctx);
+ emit_replica_identity(ctx);
+ emit_rls_toggles(ctx);
+
+ /*
+ * Triggers and row-level security policies - disabled until the
+ * standalone pg_get_trigger_ddl() and pg_get_policy_ddl() helpers land.
+ * The scan + lock + filter scaffolding below is preserved (inside #if 0)
+ * so wiring up each emission becomes a one-liner in the loop body once
+ * those helpers are available.
+ */
+#if 0
+ if (is_kind_included(ctx, TABLE_DDL_KIND_TRIGGER))
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(ctx->relid));
+ trigScan = systable_beginscan(trigRel, TriggerRelidNameIndexId,
+ true, NULL, 1, &trigKey);
+ while (HeapTupleIsValid(trigTup = systable_getnext(trigScan)))
+ {
+ Form_pg_trigger trg = (Form_pg_trigger) GETSTRUCT(trigTup);
+
+ if (trg->tgisinternal)
+ continue;
+
+ /* TODO: append pg_get_trigger_ddl(trg->oid) output here. */
+ }
+ systable_endscan(trigScan);
+ table_close(trigRel, AccessShareLock);
+ }
+
+ if (is_kind_included(ctx, TABLE_DDL_KIND_POLICY))
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(ctx->relid));
+ polScan = systable_beginscan(polRel, PolicyPolrelidPolnameIndexId,
+ true, NULL, 1, &polKey);
+ while (HeapTupleIsValid(polTup = systable_getnext(polScan)))
+ {
+ /* TODO: append pg_get_policy_ddl(relid, polname) output here. */
+ }
+ systable_endscan(polScan);
+ table_close(polRel, AccessShareLock);
+ }
+#endif
+
+ emit_partition_children(ctx);
+
+ {
+ int natts = RelationGetDescr(rel)->natts;
+
+ for (int i = 1; i <= natts; i++)
+ if (ctx->nn_entries[i].name != NULL)
+ pfree(ctx->nn_entries[i].name);
+ }
+ pfree(ctx->nn_entries);
+
+ table_close(rel, AccessShareLock);
+ pfree(ctx->buf.data);
+ pfree(ctx->qualname);
+ list_free(ctx->skip_notnull_oids);
+
+ /*
+ * Pop the narrowed search_path now that all helpers have run. Errors
+ * thrown earlier are cleaned up by AtEOXact_GUC at xact end.
+ */
+ if (ctx->save_nestlevel >= 0)
+ AtEOXact_GUC(true, ctx->save_nestlevel);
+
+ return ctx->statements;
+}
+
+/*
+ * pg_get_table_ddl
+ * Return DDL to recreate a table as a set of text rows.
+ */
+Datum
+pg_get_table_ddl(PG_FUNCTION_ARGS)
+{
+ FuncCallContext *funcctx;
+ List *statements;
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcontext;
+ TableDdlContext ctx = {0};
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ if (!PG_ARGISNULL(5) && !PG_ARGISNULL(6))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"only_kinds\" and \"except_kinds\" parameters are mutually exclusive")));
+
+ /* Option defaults (match proargdefaults in pg_proc.dat). */
+ ctx.relid = PG_GETARG_OID(0);
+ ctx.pretty = false;
+ ctx.no_owner = false; /* owner DEFAULT true -> no_owner = false */
+ ctx.no_tablespace = false; /* tablespace DEFAULT true -> no_tablespace = false */
+ ctx.schema_qualified = true;
+ ctx.only_kinds = NULL;
+ ctx.except_kinds = NULL;
+
+ /* Override defaults with any explicitly supplied values. */
+ if (!PG_ARGISNULL(1))
+ ctx.pretty = PG_GETARG_BOOL(1);
+ if (!PG_ARGISNULL(2))
+ ctx.no_owner = !PG_GETARG_BOOL(2);
+ if (!PG_ARGISNULL(3))
+ ctx.no_tablespace = !PG_GETARG_BOOL(3);
+ if (!PG_ARGISNULL(4))
+ ctx.schema_qualified = PG_GETARG_BOOL(4);
+ if (!PG_ARGISNULL(5))
+ ctx.only_kinds = parse_kind_array("only_kinds",
+ PG_GETARG_ARRAYTYPE_P(5));
+ if (!PG_ARGISNULL(6))
+ ctx.except_kinds = parse_kind_array("except_kinds",
+ PG_GETARG_ARRAYTYPE_P(6));
+
+ statements = pg_get_table_ddl_internal(&ctx);
+ funcctx->user_fctx = statements;
+ funcctx->max_calls = list_length(statements);
+
+ MemoryContextSwitchTo(oldcontext);
+ }
+
+ funcctx = SRF_PERCALL_SETUP();
+ statements = (List *) funcctx->user_fctx;
+
+ if (funcctx->call_cntr < funcctx->max_calls)
+ {
+ char *stmt;
+
+ stmt = list_nth(statements, funcctx->call_cntr);
+
+ SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+ }
+ else
+ {
+ list_free_deep(statements);
+ SRF_RETURN_DONE(funcctx);
+ }
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 819631781c0..2f3e543f455 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,
@@ -605,6 +606,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)
@@ -1243,6 +1257,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)
@@ -1971,7 +2001,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();
@@ -1986,7 +2016,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);
}
/*
@@ -1999,7 +2042,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();
@@ -2011,7 +2054,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;
@@ -2071,10 +2115,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
@@ -2165,10 +2217,9 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
{
Node *expr = (Node *) lfirst(lc);
char *str;
- int prettyFlags = PRETTYFLAG_PAREN;
str = deparse_expression_pretty(expr, context, false, false,
- prettyFlags, 0);
+ PRETTYFLAG_PAREN, 0);
if (colno > 0)
appendStringInfoString(&buf, ", ");
@@ -2546,6 +2597,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 3cb84359adf..d86af56ba78 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8634,6 +8634,14 @@
proargtypes => 'regdatabase bool bool bool',
proargnames => '{database,pretty,owner,tablespace}',
proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' },
+{ oid => '8215', descr => 'get DDL to recreate a table',
+ proname => 'pg_get_table_ddl', prorows => '50',
+ proisstrict => 'f', proretset => 't', provolatile => 's', proparallel => 'r',
+ pronargdefaults => '6', prorettype => 'text',
+ proargtypes => 'regclass bool bool bool bool _text _text',
+ proargnames => '{relation,pretty,owner,tablespace,schema_qualified,only_kinds,except_kinds}',
+ proargdefaults => '{false,true,true,true,NULL,NULL}',
+ prosrc => 'pg_get_table_ddl' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index c3d8518cb62..d33563fdead 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -92,6 +92,7 @@ extern void check_of_type(HeapTuple typetuple);
extern void register_on_commit_action(Oid relid, OnCommitAction action);
extern void remove_on_commit_action(Oid relid);
+extern OnCommitAction get_on_commit_action(Oid relid);
extern void PreCommit_on_commit_actions(void);
extern void AtEOXact_on_commit_actions(bool isCommit);
@@ -108,4 +109,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
List *partConstraint);
+extern const char *storage_name(char c);
+
#endif /* TABLECMDS_H */
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 25c05e2f649..c3b3dc65c93 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -25,6 +25,7 @@ typedef struct PlannedStmt PlannedStmt;
#define RULE_INDEXDEF_KEYS_ONLY 0x02 /* ignore included attributes */
extern char *pg_get_indexdef_string(Oid indexrelid);
+extern char *pg_get_indexdef_ddl(Oid indexrelid);
extern char *pg_get_indexdef_columns(Oid indexrelid, bool pretty);
extern char *pg_get_indexdef_columns_extended(Oid indexrelid,
uint16 flags);
@@ -34,6 +35,7 @@ extern char *pg_get_partkeydef_columns(Oid relid, bool pretty);
extern char *pg_get_partconstrdef_string(Oid partitionId, char *aliasname);
extern char *pg_get_constraintdef_command(Oid constraintId);
+extern char *pg_get_constraintdef_body(Oid constraintId);
extern char *deparse_expression(Node *expr, List *dpcontext,
bool forceprefix, bool showimplicit);
extern List *deparse_context_for(const char *aliasname, Oid relid);
@@ -54,5 +56,7 @@ extern char *get_range_partbound_string(List *bound_datums);
extern void get_reloptions(StringInfo buf, Datum reloptions);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern char *pg_get_statisticsobjdef_ddl(Oid statextid);
+extern char *pg_get_ruledef_ddl(Oid ruleoid);
#endif /* RULEUTILS_H */
diff --git a/src/test/regress/expected/pg_get_table_ddl.out b/src/test/regress/expected/pg_get_table_ddl.out
new file mode 100644
index 00000000000..8e3b94c11f6
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,1612 @@
+--
+-- pg_get_table_ddl
+--
+-- All tests pass owner=>false so the ALTER TABLE OWNER TO line is not
+-- emitted, keeping output stable across test runners (which may run under
+-- different role names).
+--
+CREATE SCHEMA pgtbl_ddl_test;
+SET search_path = pgtbl_ddl_test;
+-- Basic table with PRIMARY KEY, NOT NULL, DEFAULT, COLLATE.
+CREATE TABLE basic (
+ id int PRIMARY KEY,
+ name text NOT NULL DEFAULT 'anon' COLLATE "C"
+);
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.basic (id integer NOT NULL, name text COLLATE "C" DEFAULT 'anon'::text NOT NULL);
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- Identity columns (with default and custom sequence options).
+CREATE TABLE id_cols (
+ id_always int GENERATED ALWAYS AS IDENTITY,
+ id_default int GENERATED BY DEFAULT AS IDENTITY
+);
+SELECT * FROM pg_get_table_ddl('id_cols'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.id_cols (id_always integer GENERATED ALWAYS AS IDENTITY NOT NULL, id_default integer GENERATED BY DEFAULT AS IDENTITY NOT NULL);
+(1 row)
+
+CREATE TABLE id_custom (
+ v int GENERATED ALWAYS AS IDENTITY (
+ SEQUENCE NAME id_custom_v_seq
+ START WITH 100 INCREMENT BY 5
+ MINVALUE 50 MAXVALUE 1000
+ CACHE 10 CYCLE
+ )
+);
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.id_custom (v integer GENERATED ALWAYS AS IDENTITY (START WITH 100 INCREMENT BY 5 MINVALUE 50 MAXVALUE 1000 CACHE 10 CYCLE) NOT NULL);
+(1 row)
+
+-- Generated stored column.
+CREATE TABLE gen_cols (
+ cents int,
+ dollars numeric GENERATED ALWAYS AS (cents / 100.0) STORED
+);
+SELECT * FROM pg_get_table_ddl('gen_cols'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.gen_cols (cents integer, dollars numeric GENERATED ALWAYS AS (((cents)::numeric / 100.0)) STORED);
+(1 row)
+
+-- Generated virtual column.
+CREATE TABLE gen_virtual (
+ base int,
+ derived int GENERATED ALWAYS AS (base * 2) VIRTUAL
+);
+SELECT * FROM pg_get_table_ddl('gen_virtual'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.gen_virtual (base integer, derived integer GENERATED ALWAYS AS ((base * 2)) VIRTUAL);
+(1 row)
+
+-- STORAGE and COMPRESSION (only emitted when non-default for the type).
+CREATE TABLE storage_cols (
+ a text STORAGE EXTERNAL,
+ b text STORAGE MAIN,
+ c text COMPRESSION pglz
+);
+SELECT * FROM pg_get_table_ddl('storage_cols'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.storage_cols (a text STORAGE EXTERNAL, b text STORAGE MAIN, c text COMPRESSION pglz);
+(1 row)
+
+-- Constraints: CHECK, UNIQUE, FOREIGN KEY (DEFERRABLE).
+CREATE TABLE refd (id int PRIMARY KEY);
+CREATE TABLE cons (
+ a int CHECK (a > 0),
+ b int UNIQUE,
+ c int REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED
+);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(3 rows)
+
+-- UNIQUE NULLS NOT DISTINCT (PG 15): null values are treated as equal for
+-- uniqueness purposes, so the NULLS NOT DISTINCT clause must be emitted.
+CREATE TABLE nulls_nd (
+ a int,
+ b int,
+ UNIQUE NULLS NOT DISTINCT (a, b)
+);
+SELECT * FROM pg_get_table_ddl('nulls_nd'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.nulls_nd (a integer, b integer);
+ ALTER TABLE pgtbl_ddl_test.nulls_nd ADD CONSTRAINT nulls_nd_a_b_key UNIQUE NULLS NOT DISTINCT (a, b);
+(2 rows)
+
+-- INCLUDE columns on a constraint-backed UNIQUE index (PG 11): the
+-- covering columns appear in an INCLUDE (...) clause on the ALTER TABLE
+-- ADD CONSTRAINT statement, not in the key column list.
+CREATE TABLE idx_inc (
+ id int PRIMARY KEY,
+ name text,
+ extra text,
+ UNIQUE (name) INCLUDE (extra)
+);
+SELECT * FROM pg_get_table_ddl('idx_inc'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idx_inc (id integer NOT NULL, name text, extra text);
+ ALTER TABLE pgtbl_ddl_test.idx_inc ADD CONSTRAINT idx_inc_name_extra_key UNIQUE (name) INCLUDE (extra);
+ ALTER TABLE pgtbl_ddl_test.idx_inc ADD CONSTRAINT idx_inc_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- FOREIGN KEY with ON DELETE / ON UPDATE referential actions and MATCH
+-- clause. Each variant must appear verbatim in the reconstructed DDL.
+CREATE TABLE fk_acts_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_acts (
+ a int,
+ b int,
+ c int,
+ d int,
+ CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES fk_acts_tgt(id)
+ ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET NULL,
+ CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET DEFAULT,
+ CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES fk_acts_tgt(id)
+ MATCH FULL
+);
+SELECT * FROM pg_get_table_ddl('fk_acts'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.fk_acts (a integer, b integer, c integer, d integer);
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON UPDATE RESTRICT ON DELETE CASCADE;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) MATCH FULL;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON DELETE SET DEFAULT;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON DELETE SET NULL;
+(5 rows)
+
+-- NOT ENFORCED foreign key (PG 17): the constraint is recorded in the
+-- catalog but not validated by the engine; NOT ENFORCED must appear in
+-- the reconstructed DDL.
+CREATE TABLE notenf_tgt (id int PRIMARY KEY);
+CREATE TABLE notenf (
+ a int,
+ CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES notenf_tgt(id)
+ NOT ENFORCED
+);
+SELECT * FROM pg_get_table_ddl('notenf'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.notenf (a integer);
+ ALTER TABLE pgtbl_ddl_test.notenf ADD CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.notenf_tgt(id) NOT ENFORCED;
+(2 rows)
+
+-- Self-referencing FK: the PK must appear before the FK that references it.
+-- The catalog scan returns constraints in name order; without the fix,
+-- "self_ref_parent_id_fkey" sorts before "self_ref_pkey" and the FK ADD
+-- CONSTRAINT fails with "there is no unique constraint matching given keys".
+CREATE TABLE self_ref (
+ id int PRIMARY KEY,
+ parent_id int REFERENCES self_ref(id)
+);
+SELECT * FROM pg_get_table_ddl('self_ref'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.self_ref (id integer NOT NULL, parent_id integer);
+ ALTER TABLE pgtbl_ddl_test.self_ref ADD CONSTRAINT self_ref_pkey PRIMARY KEY (id);
+ ALTER TABLE pgtbl_ddl_test.self_ref ADD CONSTRAINT self_ref_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES pgtbl_ddl_test.self_ref(id);
+(3 rows)
+
+-- WITHOUT OVERLAPS on a PRIMARY KEY (PG 17): the range-type period
+-- column forms a temporal primary key; WITHOUT OVERLAPS must appear
+-- inside the key column list in the reconstructed ALTER TABLE.
+-- Both columns use range types with native GiST support so no
+-- btree_gist extension is needed.
+CREATE TABLE temporal_pk (
+ grp int4range,
+ during daterange,
+ PRIMARY KEY (grp, during WITHOUT OVERLAPS)
+);
+SELECT * FROM pg_get_table_ddl('temporal_pk'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.temporal_pk (grp int4range NOT NULL, during daterange NOT NULL);
+ ALTER TABLE pgtbl_ddl_test.temporal_pk ADD CONSTRAINT temporal_pk_pkey PRIMARY KEY (grp, during WITHOUT OVERLAPS);
+(2 rows)
+
+-- Indexes: functional and partial. Constraint-backing indexes are
+-- suppressed (they are emitted by the constraint loop).
+CREATE TABLE idxd (id int PRIMARY KEY, name text);
+CREATE INDEX idxd_lower ON idxd (lower(name));
+CREATE INDEX idxd_partial ON idxd (id) WHERE id > 100;
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- Inheritance, including a child DEFAULT override on an inherited column.
+CREATE TABLE par (a int DEFAULT 1, b text);
+CREATE TABLE ch (c int) INHERITS (par);
+ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ch (c integer) INHERITS (pgtbl_ddl_test.par);
+ ALTER TABLE ONLY 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 ONLY 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)
+
+-- Non-constraint-backed index on a partitioned table: the parent's index DDL
+-- must appear once; the auto-created child partition indexes (relispartition=t)
+-- must be suppressed so the DDL round-trips without "relation already exists".
+CREATE TABLE parted_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_idx_a PARTITION OF parted_idx FOR VALUES IN ('a');
+CREATE UNIQUE INDEX parted_idx_uidx ON parted_idx (id, region);
+SELECT * FROM pg_get_table_ddl('parted_idx'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_idx (id integer, region text) PARTITION BY LIST (region);
+ CREATE UNIQUE INDEX parted_idx_uidx ON pgtbl_ddl_test.parted_idx USING btree (id, region);
+ CREATE TABLE pgtbl_ddl_test.parted_idx_a PARTITION OF pgtbl_ddl_test.parted_idx FOR VALUES IN ('a');
+(3 rows)
+
+-- Plain (non-unique) CREATE INDEX on partitioned table: the auto-created child
+-- index (relispartition=true) is suppressed, just like the UNIQUE case.
+CREATE TABLE parted_plain (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE parted_plain_a PARTITION OF parted_plain FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_plain_id_idx ON parted_plain (id);
+SELECT * FROM pg_get_table_ddl('parted_plain'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_plain (id integer, val text) PARTITION BY RANGE (id);
+ CREATE INDEX parted_plain_id_idx ON pgtbl_ddl_test.parted_plain USING btree (id);
+ CREATE TABLE pgtbl_ddl_test.parted_plain_a PARTITION OF pgtbl_ddl_test.parted_plain FOR VALUES FROM (0) TO (100);
+(3 rows)
+
+-- Constraint-backed PRIMARY KEY on partitioned table: the child's inherited
+-- constraint (conislocal=false) and its backing index are both suppressed.
+CREATE TABLE parted_pk (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_pk_a PARTITION OF parted_pk FOR VALUES IN ('a');
+ALTER TABLE parted_pk ADD PRIMARY KEY (id, region);
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_pk (id integer NOT NULL, region text NOT NULL) PARTITION BY LIST (region);
+ ALTER TABLE pgtbl_ddl_test.parted_pk ADD CONSTRAINT parted_pk_pkey PRIMARY KEY (id, region);
+ CREATE TABLE pgtbl_ddl_test.parted_pk_a PARTITION OF pgtbl_ddl_test.parted_pk FOR VALUES IN ('a');
+(3 rows)
+
+-- Multi-level (sub-partitioned) table with a CREATE INDEX on the root.
+-- The mid-level partitioned index (relispartition=true, relkind='I') and the
+-- leaf index (relispartition=true, relkind='i') are both suppressed.
+CREATE TABLE parted_sub (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_sub_a PARTITION OF parted_sub
+ FOR VALUES IN ('a') PARTITION BY RANGE (id);
+CREATE TABLE parted_sub_a1 PARTITION OF parted_sub_a FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_sub_id_idx ON parted_sub (id);
+SELECT * FROM pg_get_table_ddl('parted_sub'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_sub (id integer, region text) PARTITION BY LIST (region);
+ CREATE INDEX parted_sub_id_idx ON pgtbl_ddl_test.parted_sub USING btree (id);
+ CREATE TABLE pgtbl_ddl_test.parted_sub_a PARTITION OF pgtbl_ddl_test.parted_sub FOR VALUES IN ('a') PARTITION BY RANGE (id);
+ CREATE TABLE pgtbl_ddl_test.parted_sub_a1 PARTITION OF pgtbl_ddl_test.parted_sub_a FOR VALUES FROM (0) TO (100);
+(4 rows)
+
+-- Partition child with its own locally-defined index (relispartition=false):
+-- this is NOT inherited from any parent partitioned index and must be emitted.
+CREATE TABLE parted_child_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_child_idx_a PARTITION OF parted_child_idx FOR VALUES IN ('a');
+CREATE INDEX parted_child_idx_a_id ON parted_child_idx_a (id);
+SELECT * FROM pg_get_table_ddl('parted_child_idx'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_child_idx (id integer, region text) PARTITION BY LIST (region);
+ CREATE TABLE pgtbl_ddl_test.parted_child_idx_a PARTITION OF pgtbl_ddl_test.parted_child_idx FOR VALUES IN ('a');
+ CREATE INDEX parted_child_idx_a_id ON pgtbl_ddl_test.parted_child_idx_a USING btree (id);
+(3 rows)
+
+-- Mix: parent has a CREATE INDEX (auto-propagated to child, relispartition=true,
+-- suppressed) AND the child has its own local index (relispartition=false, emitted).
+CREATE TABLE parted_mixed (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_mixed_a PARTITION OF parted_mixed FOR VALUES IN ('a');
+CREATE INDEX parted_mixed_id ON parted_mixed (id);
+CREATE INDEX parted_mixed_a_region ON parted_mixed_a (region);
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_mixed (id integer, region text) PARTITION BY LIST (region);
+ CREATE INDEX parted_mixed_id ON pgtbl_ddl_test.parted_mixed USING btree (id);
+ CREATE TABLE pgtbl_ddl_test.parted_mixed_a PARTITION OF pgtbl_ddl_test.parted_mixed FOR VALUES IN ('a');
+ CREATE INDEX parted_mixed_a_region ON pgtbl_ddl_test.parted_mixed_a USING btree (region);
+(4 rows)
+
+-- FK on a partitioned table: declared on the parent; partition children inherit
+-- the constraint with conislocal=false and must not re-emit it.
+CREATE TABLE parted_fk_tgt (id int PRIMARY KEY);
+CREATE TABLE parted_fk (id int, tgt_id int REFERENCES parted_fk_tgt(id))
+ PARTITION BY RANGE (id);
+CREATE TABLE parted_fk_a PARTITION OF parted_fk FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('parted_fk'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_fk (id integer, tgt_id integer) PARTITION BY RANGE (id);
+ ALTER TABLE pgtbl_ddl_test.parted_fk ADD CONSTRAINT parted_fk_tgt_id_fkey FOREIGN KEY (tgt_id) REFERENCES pgtbl_ddl_test.parted_fk_tgt(id);
+ CREATE TABLE pgtbl_ddl_test.parted_fk_a PARTITION OF pgtbl_ddl_test.parted_fk FOR VALUES FROM (0) TO (100);
+(3 rows)
+
+-- Statistics declared explicitly on a partition child (not auto-propagated):
+-- the child's statistics object appears inside the child's DDL block.
+CREATE TABLE parted_stx (id int, region text, val float) PARTITION BY LIST (region);
+CREATE TABLE parted_stx_a PARTITION OF parted_stx FOR VALUES IN ('a');
+CREATE STATISTICS parted_stx_stat (ndistinct) ON id, region FROM parted_stx;
+CREATE STATISTICS parted_stx_a_stat (ndistinct) ON id, val FROM parted_stx_a;
+SELECT * FROM pg_get_table_ddl('parted_stx'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_stx (id integer, region text, val double precision) PARTITION BY LIST (region);
+ CREATE STATISTICS pgtbl_ddl_test.parted_stx_stat (ndistinct) ON id, region FROM pgtbl_ddl_test.parted_stx;
+ CREATE TABLE pgtbl_ddl_test.parted_stx_a PARTITION OF pgtbl_ddl_test.parted_stx FOR VALUES IN ('a');
+ CREATE STATISTICS pgtbl_ddl_test.parted_stx_a_stat (ndistinct) ON id, val FROM pgtbl_ddl_test.parted_stx_a;
+(4 rows)
+
+DROP TABLE parted_stx;
+-- NO INHERIT CHECK constraint: the NO INHERIT flag must appear verbatim in the
+-- inline constraint definition inside the CREATE TABLE body.
+CREATE TABLE noinhchk (id int, val int,
+ CONSTRAINT val_pos CHECK (val > 0) NO INHERIT);
+SELECT * FROM pg_get_table_ddl('noinhchk'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.noinhchk (id integer, val integer, CONSTRAINT val_pos CHECK ((val > 0)) NO INHERIT);
+(1 row)
+
+DROP TABLE noinhchk;
+-- Multiple inheritance parents: the INHERITS clause must list both parents in
+-- declaration order; only the locally-defined column appears in the column list.
+CREATE TABLE mi_p1 (a int DEFAULT 1);
+CREATE TABLE mi_p2 (b text DEFAULT 'x');
+CREATE TABLE mi_child (c float) INHERITS (mi_p1, mi_p2);
+SELECT * FROM pg_get_table_ddl('mi_child'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.mi_child (c double precision) INHERITS (pgtbl_ddl_test.mi_p1, pgtbl_ddl_test.mi_p2);
+(1 row)
+
+DROP TABLE mi_p1, mi_p2 CASCADE;
+NOTICE: drop cascades to table mi_child
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO pgtbl_ddl_test.rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM pgtbl_ddl_test.stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx (a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(3 rows)
+
+-- UNLOGGED + reloptions.
+CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70);
+SELECT * FROM pg_get_table_ddl('uno'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE UNLOGGED TABLE pgtbl_ddl_test.uno (id integer) WITH (fillfactor='70');
+(1 row)
+
+-- Typed table (CREATE TABLE OF type_name). Columns inherited from the
+-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come
+-- out through a single "(col WITH OPTIONS ...)" list.
+CREATE TYPE typed_t AS (a int, b text);
+CREATE TABLE typed_plain OF typed_t;
+SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_plain OF pgtbl_ddl_test.typed_t;
+(1 row)
+
+CREATE TABLE typed_over OF typed_t (
+ a WITH OPTIONS DEFAULT 7 NOT NULL,
+ b WITH OPTIONS NOT NULL,
+ CONSTRAINT b_nonempty CHECK (length(b) > 0)
+);
+SELECT * FROM pg_get_table_ddl('typed_over'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_over OF pgtbl_ddl_test.typed_t (a WITH OPTIONS DEFAULT 7 NOT NULL, b WITH OPTIONS NOT NULL, CONSTRAINT b_nonempty CHECK ((length(b) > 0)));
+(1 row)
+
+-- Temporary tables + ON COMMIT. Temp tables must never be emitted with
+-- the session-private pg_temp_NN schema prefix -- the TEMPORARY keyword
+-- already places the table in pg_temp and the prefix is non-replayable.
+-- ON COMMIT DROP only fires at commit, so the queries that need to see
+-- the catalog entry must run in the same transaction as the CREATE.
+BEGIN;
+CREATE TEMP TABLE temp_default (id int);
+CREATE TEMP TABLE temp_delete (id int) ON COMMIT DELETE ROWS;
+CREATE TEMP TABLE temp_drop (id int) ON COMMIT DROP;
+SELECT line FROM pg_get_table_ddl('temp_default'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------
+ CREATE TEMPORARY TABLE temp_default (id integer);
+(1 row)
+
+SELECT line FROM pg_get_table_ddl('temp_delete'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE temp_delete (id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT line FROM pg_get_table_ddl('temp_drop'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------
+ CREATE TEMPORARY TABLE temp_drop (id integer) ON COMMIT DROP;
+(1 row)
+
+ROLLBACK;
+-- Pretty mode.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, pretty => true);
+ pg_get_table_ddl
+------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.basic ( +
+ id integer NOT NULL, +
+ name text COLLATE "C" DEFAULT 'anon'::text NOT NULL +
+ );
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- only_kinds / except_kinds gating: each emits either only the listed kinds
+-- (only_kinds) or every kind except the listed ones (except_kinds). The two
+-- are mutually exclusive. Kind vocabulary: table, index,
+-- primary_key, unique, check, foreign_key, exclusion, rule,
+-- statistics, trigger, policy, rls, replica_identity, partition.
+-- NOT NULL is not in the vocabulary - always emitted to prevent
+-- producing schemas that silently accept NULLs the source would
+-- have rejected.
+--
+-- except_kinds=index hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['index']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- except_kinds=primary_key,unique,check,foreign_key,exclusion hides every
+-- table-level constraint. Inline CHECK in the CREATE TABLE body is
+-- suppressed too.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['primary_key','unique','check','foreign_key','exclusion']);
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=foreign_key suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- first pass of the two-pass FK-clone workflow: emit tables without
+-- cross-table FKs, load data, then re-run with only_kinds=foreign_key
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+(2 rows)
+
+-- only_kinds=foreign_key is the second pass of the two-pass FK-clone
+-- workflow: emit only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN
+-- KEY rows; the CREATE TABLE and every non-FK sub-object are
+-- suppressed (the table already exists from the first pass).
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(1 row)
+
+-- A table with no foreign keys produces no rows under
+-- only_kinds=foreign_key.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- only_kinds=table emits only the bare CREATE TABLE (with any inline
+-- column-level NOT NULL and CHECK) - no indexes, no PRIMARY KEY ALTER,
+-- no other sub-objects. Useful for capturing a table's shape without
+-- pulling in everything attached to it.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table']);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+(1 row)
+
+-- only_kinds=check on a non-partition table. The constraint loop must emit
+-- column-level and table-level CHECK constraints via ALTER TABLE here,
+-- because the inline path in CREATE TABLE never runs (KIND_TABLE is
+-- not in the filter).
+CREATE TABLE chk_only (
+ id int PRIMARY KEY,
+ qty int CHECK (qty > 0),
+ CONSTRAINT chk_only_id_pos CHECK (id > 0)
+);
+SELECT * FROM pg_get_table_ddl('chk_only'::regclass, owner => false, only_kinds => ARRAY['check']);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.chk_only ADD CONSTRAINT chk_only_id_pos CHECK ((id > 0));
+ ALTER TABLE pgtbl_ddl_test.chk_only ADD CONSTRAINT chk_only_qty_check CHECK ((qty > 0));
+(2 rows)
+
+DROP TABLE chk_only;
+-- only_kinds=partition on a partitioned-table parent: each child's full DDL
+-- is emitted (CREATE TABLE ... PARTITION OF ...). The "partition"
+-- keyword is a gate at the parent level; it is stripped from the
+-- propagated filter so the child does not exclude its own
+-- CREATE TABLE.
+CREATE TABLE p_only (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE p_only_a PARTITION OF p_only FOR VALUES FROM (0) TO (100);
+CREATE TABLE p_only_b PARTITION OF p_only FOR VALUES FROM (100) TO (200);
+SELECT * FROM pg_get_table_ddl('p_only'::regclass, owner => false, only_kinds => ARRAY['partition']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.p_only_a PARTITION OF pgtbl_ddl_test.p_only FOR VALUES FROM (0) TO (100);
+ CREATE TABLE pgtbl_ddl_test.p_only_b PARTITION OF pgtbl_ddl_test.p_only FOR VALUES FROM (100) TO (200);
+(2 rows)
+
+DROP TABLE p_only;
+-- only_kinds and except_kinds are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false,
+ only_kinds => ARRAY['foreign_key'],
+ except_kinds => ARRAY['foreign_key']);
+ERROR: "only_kinds" and "except_kinds" parameters are mutually exclusive
+-- Pub/sub schema clone: keep the table and its primary key, drop the
+-- rest of the constraints so the subscriber can replicate without
+-- pulling cross-table dependencies along.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique','check','foreign_key','exclusion']);
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=rule hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, except_kinds => ARRAY['rule']);
+ pg_get_table_ddl
+----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+(1 row)
+
+-- except_kinds=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, except_kinds => ARRAY['statistics']);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, except_kinds => ARRAY['rls']);
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+(1 row)
+
+-- except_kinds=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, except_kinds => ARRAY['replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+(1 row)
+
+-- Partition children are emitted by default; except_kinds=partition
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false, except_kinds => ARRAY['partition']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range (id integer, k integer) PARTITION BY RANGE (id);
+(1 row)
+
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY[' INDEX ',' RULE ']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- schema_qualified=false strips the schema from the target table itself
+-- and from same-schema sibling references (inheritance/partition
+-- parents, identity sequences in the same schema), while cross-schema
+-- references are kept qualified for correctness.
+--
+-- Bare CREATE TABLE for an ordinary table.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE basic (id integer NOT NULL, name text COLLATE "C" DEFAULT 'anon'::text NOT NULL);
+ ALTER TABLE basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- INHERITS parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------
+ CREATE TABLE ch (c integer) INHERITS (par);
+ ALTER TABLE ONLY 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 ONLY 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_pkey PRIMARY KEY (id);
+ ALTER TABLE xschema_fk ADD CONSTRAINT xschema_fk_pid_fkey FOREIGN KEY (pid) REFERENCES pgtbl_ddl_other.parent(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
+-- Cross-schema partition child with schema_qualified=false: the child must
+-- carry its own schema prefix so the output is unambiguous regardless of the
+-- caller's search_path. Before the fix, the child was emitted as the bare
+-- name "pt_c" (landing in whichever schema is first in search_path) while
+-- the parent reference inside the CREATE TABLE came out as the fully-qualified
+-- "pgtbl_ddl_part_s.pt", making the DDL inconsistent and unreplayable.
+CREATE SCHEMA pgtbl_ddl_part_s;
+CREATE SCHEMA pgtbl_ddl_part_other;
+CREATE TABLE pgtbl_ddl_part_s.pt (id int, val int) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_part_other.pt_c
+ PARTITION OF pgtbl_ddl_part_s.pt
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_part_s.pt'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------
+ CREATE TABLE pt (id integer, val integer) PARTITION BY RANGE (id);
+ CREATE TABLE pgtbl_ddl_part_other.pt_c PARTITION OF pgtbl_ddl_part_s.pt FOR VALUES FROM (0) TO (100);
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_part_s CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_part_s.pt
+DROP SCHEMA pgtbl_ddl_part_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);
+ 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 own column list (attislocal=false), so the
+-- user-named NOT NULL is emitted as a table-level CONSTRAINT clause in
+-- the CREATE TABLE body. This preserves the name and prevents a collision
+-- when the parent later propagates its own NOT NULL to the child.
+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, CONSTRAINT my_nn NOT NULL a) INHERITS (pgtbl_ddl_test.nn_par);
+(1 row)
+
+DROP TABLE nn_par CASCADE;
+NOTICE: drop cascades to table nn_ch
+-- Partition child where the parent's default was explicitly dropped.
+-- After PARTITION OF the parent default is re-applied; DROP DEFAULT
+-- must be emitted to restore the dropped state.
+CREATE TABLE drop_def_par (a int DEFAULT 99) PARTITION BY LIST (a);
+CREATE TABLE drop_def_child PARTITION OF drop_def_par FOR VALUES IN (1);
+ALTER TABLE ONLY drop_def_child ALTER COLUMN a DROP DEFAULT;
+SELECT * FROM pg_get_table_ddl('drop_def_child'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.drop_def_child PARTITION OF pgtbl_ddl_test.drop_def_par FOR VALUES IN (1);
+ ALTER TABLE ONLY pgtbl_ddl_test.drop_def_child ALTER COLUMN a DROP DEFAULT;
+(2 rows)
+
+DROP TABLE drop_def_par;
+-- Named NOT NULL on a partition child that was attached from a pre-existing
+-- table. The name must be preserved via the PARTITION OF column spec list
+-- so the child keeps its local name rather than inheriting the parent's
+-- auto-generated name.
+CREATE TABLE nn_part_par (a int NOT NULL) PARTITION BY LIST (a);
+CREATE TABLE nn_part_ch (a int CONSTRAINT nn_part_ch_nn NOT NULL);
+ALTER TABLE nn_part_par ATTACH PARTITION nn_part_ch FOR VALUES IN (1);
+SELECT * FROM pg_get_table_ddl('nn_part_ch'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.nn_part_ch PARTITION OF pgtbl_ddl_test.nn_part_par (CONSTRAINT nn_part_ch_nn NOT NULL a) FOR VALUES IN (1);
+(1 row)
+
+DROP TABLE nn_part_par;
+-- Named NOT NULL in an INHERITS child where the parent also gets a NOT NULL
+-- later: the child name must survive via the inline CONSTRAINT clause so it
+-- does not collide with the propagated parent NOT NULL.
+CREATE TABLE nn_inh_par (a int);
+CREATE TABLE nn_inh_ch (b int) INHERITS (nn_inh_par);
+ALTER TABLE nn_inh_ch ADD CONSTRAINT nn_inh_ch_nn NOT NULL a;
+ALTER TABLE nn_inh_par ADD CONSTRAINT nn_inh_par_nn NOT NULL a;
+SELECT * FROM pg_get_table_ddl('nn_inh_ch'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.nn_inh_ch (b integer, CONSTRAINT nn_inh_ch_nn NOT NULL a) INHERITS (pgtbl_ddl_test.nn_inh_par);
+(1 row)
+
+DROP TABLE nn_inh_par CASCADE;
+NOTICE: drop cascades to table nn_inh_ch
+-- Invalid index (left by a failed CREATE INDEX CONCURRENTLY) must be skipped.
+CREATE TABLE inv_idx (x int);
+INSERT INTO inv_idx VALUES (1), (1);
+CREATE UNIQUE INDEX CONCURRENTLY inv_idx_uidx ON inv_idx (x);
+ERROR: could not create unique index "inv_idx_uidx"
+DETAIL: Key (x)=(1) is duplicated.
+SELECT * FROM pg_get_table_ddl('inv_idx'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.inv_idx (x integer);
+(1 row)
+
+DROP TABLE inv_idx;
+-- CLUSTER ON: emitted after the index is created.
+CREATE TABLE cluster_tbl (id int, val text);
+CREATE INDEX cluster_tbl_id ON cluster_tbl (id);
+CLUSTER cluster_tbl USING cluster_tbl_id;
+SELECT * FROM pg_get_table_ddl('cluster_tbl'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cluster_tbl (id integer, val text);
+ CREATE INDEX cluster_tbl_id ON pgtbl_ddl_test.cluster_tbl USING btree (id);
+ ALTER TABLE pgtbl_ddl_test.cluster_tbl CLUSTER ON cluster_tbl_id;
+(3 rows)
+
+DROP TABLE cluster_tbl;
+-- Disabled rule: DISABLE RULE must be emitted after CREATE RULE.
+CREATE TABLE disabled_rule_tbl (id int);
+CREATE RULE dr_rule AS ON INSERT TO disabled_rule_tbl DO NOTHING;
+ALTER TABLE disabled_rule_tbl DISABLE RULE dr_rule;
+SELECT * FROM pg_get_table_ddl('disabled_rule_tbl'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.disabled_rule_tbl (id integer);
+ CREATE RULE dr_rule AS +
+ ON INSERT TO pgtbl_ddl_test.disabled_rule_tbl DO NOTHING;
+ ALTER TABLE pgtbl_ddl_test.disabled_rule_tbl DISABLE RULE dr_rule;
+(3 rows)
+
+DROP TABLE disabled_rule_tbl;
+-- Index statistics target on an expression index column.
+CREATE TABLE idx_stat_tbl (c1 int);
+CREATE INDEX idx_stat_idx ON idx_stat_tbl ((c1 + 1));
+ALTER INDEX idx_stat_idx ALTER COLUMN 1 SET STATISTICS 400;
+SELECT * FROM pg_get_table_ddl('idx_stat_tbl'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idx_stat_tbl (c1 integer);
+ CREATE INDEX idx_stat_idx ON pgtbl_ddl_test.idx_stat_tbl USING btree (((c1 + 1)));
+ ALTER INDEX pgtbl_ddl_test.idx_stat_idx ALTER COLUMN 1 SET STATISTICS 400;
+(3 rows)
+
+DROP TABLE idx_stat_tbl;
+-- schema_qualified=false must drop the schema prefix from DEFAULT
+-- expressions and inline CHECK bodies that reference same-schema
+-- objects (sequence via nextval, function via direct call). This
+-- relies on the deparser respecting the narrowed search_path rather
+-- than on substring stripping.
+CREATE SCHEMA pgtbl_ddl_xref;
+CREATE SEQUENCE pgtbl_ddl_xref.myseq;
+CREATE FUNCTION pgtbl_ddl_xref.f(int) RETURNS int LANGUAGE sql IMMUTABLE
+ AS 'SELECT $1';
+CREATE TABLE pgtbl_ddl_xref.t (
+ id int DEFAULT nextval('pgtbl_ddl_xref.myseq'),
+ val int,
+ CONSTRAINT chk CHECK (pgtbl_ddl_xref.f(val) > 0)
+);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_xref.t'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE t (id integer DEFAULT nextval('myseq'::regclass), val integer, CONSTRAINT chk CHECK ((f(val) > 0)));
+(1 row)
+
+DROP SCHEMA pgtbl_ddl_xref CASCADE;
+NOTICE: drop cascades to 3 other objects
+DETAIL: drop cascades to sequence pgtbl_ddl_xref.myseq
+drop cascades to function pgtbl_ddl_xref.f(integer)
+drop cascades to table pgtbl_ddl_xref.t
+-- schema_qualified=false must NOT corrupt string literals whose
+-- contents happen to contain the schema name followed by a dot. String
+-- literal text is output verbatim by the deparser and is unaffected by
+-- search_path narrowing.
+CREATE SCHEMA pgtbl_ddl_str;
+CREATE TABLE pgtbl_ddl_str.p (id int, note text) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_str.pc PARTITION OF pgtbl_ddl_str.p
+ (CONSTRAINT chk CHECK (note <> 'pgtbl_ddl_str.secret'))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_str.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK ((note <> 'pgtbl_ddl_str.secret'::text));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_str CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_str.p
+-- schema_qualified=false must NOT corrupt double-quoted identifiers
+-- whose contents happen to contain the schema name followed by a dot
+-- (for example a column literally named "<schema>.weird" inside that
+-- schema). The mechanism is purely search_path narrowing: the deparser
+-- outputs attribute names verbatim from pg_attribute, so the quoted
+-- identifier content is never examined for schema prefixes. This holds
+-- even when the schema name is a single letter.
+CREATE SCHEMA pgtbl_ddl_qid;
+CREATE TABLE pgtbl_ddl_qid.p (id int, "pgtbl_ddl_qid.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_qid.pc PARTITION OF pgtbl_ddl_qid.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_qid.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_qid.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK (("pgtbl_ddl_qid.weird" > 0));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_qid CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_qid.p
+-- Same with a short (single-letter) schema name to confirm there is no
+-- minimum-length assumption in the stripping path.
+CREATE SCHEMA pgtbl_ddl_q1;
+CREATE TABLE pgtbl_ddl_q1.p (id int, "pgtbl_ddl_q1.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_q1.pc PARTITION OF pgtbl_ddl_q1.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_q1.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_q1.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK (("pgtbl_ddl_q1.weird" > 0));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_q1 CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_q1.p
+-- schema_qualified=false when the base schema name itself requires
+-- quoting (its prefix starts with "). Both forms of the prefix -
+-- bare-lowercase and quoted - must be stripped from outer
+-- ALTER TABLE / ON / CREATE INDEX prefixes, while inner quoted
+-- identifiers that happen to begin the same way must be preserved.
+CREATE SCHEMA "PgTbl-Ddl-Q";
+CREATE TABLE "PgTbl-Ddl-Q"."T" (id int, val int,
+ CONSTRAINT chk CHECK (val > 0));
+CREATE INDEX "T_val_idx" ON "PgTbl-Ddl-Q"."T" (val);
+SELECT * FROM pg_get_table_ddl('"PgTbl-Ddl-Q"."T"'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE TABLE "T" (id integer, val integer, CONSTRAINT chk CHECK ((val > 0)));
+ CREATE INDEX "T_val_idx" ON "T" USING btree (val);
+(2 rows)
+
+DROP SCHEMA "PgTbl-Ddl-Q" CASCADE;
+NOTICE: drop cascades to table "PgTbl-Ddl-Q"."T"
+-- Error: not an ordinary or partitioned table.
+CREATE VIEW v AS SELECT 1 AS x;
+SELECT * FROM pg_get_table_ddl('v'::regclass);
+ERROR: "v" is not an ordinary or partitioned table
+CREATE SEQUENCE s;
+SELECT * FROM pg_get_table_ddl('s'::regclass);
+ERROR: "s" is not an ordinary or partitioned table
+-- NULL argument returns no rows.
+SELECT * FROM pg_get_table_ddl(NULL);
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- REPLICA IDENTITY USING INDEX validation: if the referenced index
+-- would not be emitted (because its source kind is suppressed) but
+-- replica_identity itself would be, the function raises an error so
+-- the emitted DDL never dangles. Exercise all three constraint-backed
+-- forms (PK, UNIQUE, EXCLUSION) and a bare CREATE UNIQUE INDEX, in
+-- both the except-list and only-list shapes.
+--
+-- PK-backed: except_kinds=primary_key without except_kinds=replica_identity.
+CREATE TABLE ri_pk_excl (a int PRIMARY KEY);
+ALTER TABLE ri_pk_excl REPLICA IDENTITY USING INDEX ri_pk_excl_pkey;
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+ERROR: REPLICA IDENTITY for table "ri_pk_excl" requires kind "primary_key" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "primary_key" kind, which is not in the active filter.
+HINT: Either add "primary_key" to the filter or remove "replica_identity" from it.
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_pk_excl (a integer NOT NULL);
+(1 row)
+
+DROP TABLE ri_pk_excl;
+-- UNIQUE-constraint-backed replica identity index.
+CREATE TABLE ri_uniq_excl (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_uniq_excl REPLICA IDENTITY USING INDEX ri_uniq_excl_a_key;
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique']);
+ERROR: REPLICA IDENTITY for table "ri_uniq_excl" requires kind "unique" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "unique" kind, which is not in the active filter.
+HINT: Either add "unique" to the filter or remove "replica_identity" from it.
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique','replica_identity']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_uniq_excl (a integer NOT NULL, b integer);
+(1 row)
+
+DROP TABLE ri_uniq_excl;
+-- Bare CREATE UNIQUE INDEX (no backing constraint) used as REPLICA
+-- IDENTITY: the index is emitted by the "index" kind, so excluding
+-- "index" without also excluding "replica_identity" must error.
+CREATE TABLE ri_idx_excl (a int NOT NULL, b int);
+CREATE UNIQUE INDEX ri_idx_excl_a_uniq ON ri_idx_excl (a);
+ALTER TABLE ri_idx_excl REPLICA IDENTITY USING INDEX ri_idx_excl_a_uniq;
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index']);
+ERROR: REPLICA IDENTITY for table "ri_idx_excl" requires kind "index" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "index" kind, which is not in the active filter.
+HINT: Either add "index" to the filter or remove "replica_identity" from it.
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx_excl (a integer NOT NULL, b integer);
+(1 row)
+
+DROP TABLE ri_idx_excl;
+-- Include-side analog: only_kinds=replica_identity must include the kind
+-- that emits the index, or the function errors.
+CREATE TABLE ri_only (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ERROR: REPLICA IDENTITY for table "ri_only" requires kind "unique" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "unique" kind, which is not in the active filter.
+HINT: Either add "unique" to the filter or remove "replica_identity" from it.
+-- Including both kinds succeeds.
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['unique','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.ri_only ADD CONSTRAINT ri_only_a_key UNIQUE (a);
+ ALTER TABLE pgtbl_ddl_test.ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+(2 rows)
+
+DROP TABLE ri_only;
+-- Per-kind only_kinds / except_kinds coverage. For each kind in the vocabulary,
+-- verify that only_kinds=K emits just that kind's output and that except_kinds=K
+-- omits just that kind's output. Kinds with no fixture above (trigger,
+-- policy) are still recognized but currently emit nothing; only_kinds=K
+-- returns zero rows for them. Fixtures used:
+-- idxd: PK + two secondary CREATE INDEX
+-- cons: PK + UNIQUE + CHECK + FK (constraint-backed)
+-- rt: CREATE RULE
+-- stx: CREATE STATISTICS
+-- rls: ENABLE/FORCE ROW LEVEL SECURITY
+-- ri_full: REPLICA IDENTITY FULL (no source-kind dependency)
+CREATE TABLE excl_fix (rng int4range,
+ EXCLUDE USING gist (rng WITH &&));
+-- only_kinds=K: emit only kind K's statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['unique']);
+ pg_get_table_ddl
+-----------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.excl_fix ADD CONSTRAINT excl_fix_rng_excl EXCLUDE USING gist (rng WITH &&);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, only_kinds => ARRAY['rule']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO pgtbl_ddl_test.rt_log (id)+
+ VALUES (new.id);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM pgtbl_ddl_test.stx;
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, only_kinds => ARRAY['rls']);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(1 row)
+
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['policy']);
+-- except_kinds=K: emit everything except kind K's statements. PK/UNIQUE/
+-- INDEX are exercised on tables without REPLICA IDENTITY USING INDEX
+-- so the validation does not trip.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['check']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, except_kinds => ARRAY['exclusion']);
+ pg_get_table_ddl
+-------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.excl_fix (rng int4range);
+(1 row)
+
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['policy']);
+-- except_kinds=table suppresses the CREATE TABLE + OWNER + child-default
+-- SET DEFAULT + attoptions passes, leaving only the sub-object passes.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['table']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- Multi-kind compositions.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table','index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['primary_key','foreign_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(1 row)
+
+-- Duplicate entries in the list are silently de-duplicated by the
+-- Bitmapset.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index','index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+DROP TABLE excl_fix;
+-- Kind-filter tests on partitioned tables with indexes.
+--
+-- only_kinds=index on a partitioned table: only the root CREATE INDEX is
+-- emitted; no CREATE TABLE, no partition children (KIND_PARTITION absent).
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ only_kinds => ARRAY['index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE INDEX parted_mixed_id ON pgtbl_ddl_test.parted_mixed USING btree (id);
+(1 row)
+
+-- except_kinds=index on a partitioned table: CREATE TABLE for the parent
+-- and all children are emitted; no index statements.
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ except_kinds => ARRAY['index']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_mixed (id integer, region text) PARTITION BY LIST (region);
+ CREATE TABLE pgtbl_ddl_test.parted_mixed_a PARTITION OF pgtbl_ddl_test.parted_mixed FOR VALUES IN ('a');
+(2 rows)
+
+-- only_kinds=primary_key on a partitioned table: only the ALTER TABLE ADD
+-- CONSTRAINT PK on the parent; the child's inherited PK (conislocal=false)
+-- is skipped; no CREATE TABLE, no partition children.
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false,
+ only_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.parted_pk ADD CONSTRAINT parted_pk_pkey PRIMARY KEY (id, region);
+(1 row)
+
+-- only_kinds=K on a table that does not have any K of that kind is a valid
+-- filter and returns zero rows. Tested previously with foreign_key
+-- on 'refd'; here also rule, statistics, rls, replica_identity, and
+-- exclusion on 'basic' (which has only PK + columns).
+SELECT count(*) AS rule_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rule']);
+ rule_rows
+-----------
+ 0
+(1 row)
+
+SELECT count(*) AS stats_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+ stats_rows
+------------
+ 0
+(1 row)
+
+SELECT count(*) AS rls_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rls']);
+ rls_rows
+----------
+ 0
+(1 row)
+
+SELECT count(*) AS ri_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ ri_rows
+---------
+ 0
+(1 row)
+
+SELECT count(*) AS excl_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+ excl_rows
+-----------
+ 0
+(1 row)
+
+-- Input-validation edge cases for the kind array. All
+-- of these raise an error; the function never reaches relation open.
+-- Empty array:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[]::text[]);
+ERROR: parameter "only_kinds" must specify at least one kind
+-- NULL element:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[NULL::text]);
+ERROR: parameter "only_kinds" must not contain NULL elements
+-- Unrecognized kind:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY['no_such_kind']);
+ERROR: unrecognized kind "no_such_kind" in parameter "only_kinds"
+-- Same kind specified through both parameters is still mutex-rejected.
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ only_kinds => ARRAY['index'],
+ except_kinds => ARRAY['rule']);
+ERROR: "only_kinds" and "except_kinds" parameters are mutually exclusive
+-- Round-trip verification. For every test table, capture the DDL the
+-- function emits, drop the schema, replay each table's DDL in
+-- dependency order, and confirm that pg_get_table_ddl on the recreated
+-- relation matches the original line-for-line. The final SELECT must
+-- return zero rows.
+CREATE TEMP TABLE pgtbl_ddl_rt_orig (name text, ord int, line text);
+INSERT INTO pgtbl_ddl_rt_orig
+SELECT t.name, o.ord, o.line
+FROM (VALUES
+ ('basic'), ('id_cols'), ('id_custom'), ('gen_cols'), ('gen_virtual'), ('storage_cols'),
+ ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'),
+ ('fk_acts_tgt'), ('fk_acts'), ('notenf_tgt'), ('notenf'), ('self_ref'), ('temporal_pk'),
+ ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('parted_list'),
+ ('parted_idx'), ('parted_idx_a'),
+ ('parted_plain'), ('parted_plain_a'),
+ ('parted_pk'), ('parted_pk_a'),
+ ('parted_sub'), ('parted_sub_a'), ('parted_sub_a1'),
+ ('parted_child_idx'), ('parted_child_idx_a'),
+ ('parted_mixed'), ('parted_mixed_a'),
+ ('parted_fk_tgt'), ('parted_fk'), ('parted_fk_a'),
+ ('rt_log'), ('rt'),
+ ('stx'), ('rls'), ('ri_full'), ('ri_idx'), ('uno')
+ ) AS t(name),
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord);
+DO $$
+DECLARE
+ tables CONSTANT text[] := ARRAY[
+ 'basic', 'id_cols', 'id_custom', 'gen_cols', 'gen_virtual', 'storage_cols',
+ 'refd', 'cons', 'nulls_nd', 'idx_inc',
+ 'fk_acts_tgt', 'fk_acts', 'notenf_tgt', 'notenf', 'self_ref', 'temporal_pk',
+ 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ 'parted_list',
+ 'parted_idx', 'parted_idx_a',
+ 'parted_plain', 'parted_plain_a',
+ 'parted_pk', 'parted_pk_a',
+ 'parted_sub', 'parted_sub_a', 'parted_sub_a1',
+ 'parted_child_idx', 'parted_child_idx_a',
+ 'parted_mixed', 'parted_mixed_a',
+ 'parted_fk_tgt', 'parted_fk', 'parted_fk_a',
+ 'rt_log', 'rt',
+ 'stx', 'rls', 'ri_full', 'ri_idx', 'uno'
+ ];
+ t text;
+ stmt text;
+BEGIN
+ DROP SCHEMA pgtbl_ddl_test CASCADE;
+ CREATE SCHEMA pgtbl_ddl_test;
+ FOREACH t IN ARRAY tables LOOP
+ FOR stmt IN
+ SELECT line FROM pgtbl_ddl_rt_orig WHERE name = t ORDER BY ord
+ LOOP
+ BEGIN
+ EXECUTE stmt;
+ EXCEPTION WHEN duplicate_table THEN
+ NULL; -- partition child already created by its parent's DDL
+ END;
+ END LOOP;
+ END LOOP;
+END $$;
+NOTICE: drop cascades to 43 other objects
+DETAIL: drop cascades to table basic
+drop cascades to table id_cols
+drop cascades to table id_custom
+drop cascades to table gen_cols
+drop cascades to table gen_virtual
+drop cascades to table storage_cols
+drop cascades to table refd
+drop cascades to table cons
+drop cascades to table nulls_nd
+drop cascades to table idx_inc
+drop cascades to table fk_acts_tgt
+drop cascades to table fk_acts
+drop cascades to table notenf_tgt
+drop cascades to table notenf
+drop cascades to table self_ref
+drop cascades to table temporal_pk
+drop cascades to table idxd
+drop cascades to table par
+drop cascades to table ch
+drop cascades to table attopt
+drop cascades to table parted_range
+drop cascades to table parted_hash
+drop cascades to table parted_list
+drop cascades to table parted_idx
+drop cascades to table parted_plain
+drop cascades to table parted_pk
+drop cascades to table parted_sub
+drop cascades to table parted_child_idx
+drop cascades to table parted_mixed
+drop cascades to table parted_fk_tgt
+drop cascades to table parted_fk
+drop cascades to table rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 38 other objects
+DETAIL: drop cascades to table basic
+drop cascades to table id_cols
+drop cascades to table id_custom
+drop cascades to table gen_cols
+drop cascades to table gen_virtual
+drop cascades to table storage_cols
+drop cascades to table refd
+drop cascades to table cons
+drop cascades to table nulls_nd
+drop cascades to table idx_inc
+drop cascades to table fk_acts_tgt
+drop cascades to table fk_acts
+drop cascades to table notenf_tgt
+drop cascades to table notenf
+drop cascades to table self_ref
+drop cascades to table temporal_pk
+drop cascades to table idxd
+drop cascades to table par
+drop cascades to table ch
+drop cascades to table attopt
+drop cascades to table parted_range
+drop cascades to table parted_hash
+drop cascades to table parted_list
+drop cascades to table parted_idx
+drop cascades to table parted_plain
+drop cascades to table parted_pk
+drop cascades to table parted_sub
+drop cascades to table parted_child_idx
+drop cascades to table parted_mixed
+drop cascades to table parted_fk_tgt
+drop cascades to table parted_fk
+drop cascades to table rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..e9ea12d7611
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,937 @@
+--
+-- pg_get_table_ddl
+--
+-- All tests pass owner=>false so the ALTER TABLE OWNER TO line is not
+-- emitted, keeping output stable across test runners (which may run under
+-- different role names).
+--
+CREATE SCHEMA pgtbl_ddl_test;
+SET search_path = pgtbl_ddl_test;
+
+-- Basic table with PRIMARY KEY, NOT NULL, DEFAULT, COLLATE.
+CREATE TABLE basic (
+ id int PRIMARY KEY,
+ name text NOT NULL DEFAULT 'anon' COLLATE "C"
+);
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false);
+
+-- Identity columns (with default and custom sequence options).
+CREATE TABLE id_cols (
+ id_always int GENERATED ALWAYS AS IDENTITY,
+ id_default int GENERATED BY DEFAULT AS IDENTITY
+);
+SELECT * FROM pg_get_table_ddl('id_cols'::regclass, owner => false);
+
+CREATE TABLE id_custom (
+ v int GENERATED ALWAYS AS IDENTITY (
+ SEQUENCE NAME id_custom_v_seq
+ START WITH 100 INCREMENT BY 5
+ MINVALUE 50 MAXVALUE 1000
+ CACHE 10 CYCLE
+ )
+);
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false);
+
+-- Generated stored column.
+CREATE TABLE gen_cols (
+ cents int,
+ dollars numeric GENERATED ALWAYS AS (cents / 100.0) STORED
+);
+SELECT * FROM pg_get_table_ddl('gen_cols'::regclass, owner => false);
+
+-- Generated virtual column.
+CREATE TABLE gen_virtual (
+ base int,
+ derived int GENERATED ALWAYS AS (base * 2) VIRTUAL
+);
+SELECT * FROM pg_get_table_ddl('gen_virtual'::regclass, owner => false);
+
+-- STORAGE and COMPRESSION (only emitted when non-default for the type).
+CREATE TABLE storage_cols (
+ a text STORAGE EXTERNAL,
+ b text STORAGE MAIN,
+ c text COMPRESSION pglz
+);
+SELECT * FROM pg_get_table_ddl('storage_cols'::regclass, owner => false);
+
+-- Constraints: CHECK, UNIQUE, FOREIGN KEY (DEFERRABLE).
+CREATE TABLE refd (id int PRIMARY KEY);
+CREATE TABLE cons (
+ a int CHECK (a > 0),
+ b int UNIQUE,
+ c int REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED
+);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false);
+
+-- UNIQUE NULLS NOT DISTINCT (PG 15): null values are treated as equal for
+-- uniqueness purposes, so the NULLS NOT DISTINCT clause must be emitted.
+CREATE TABLE nulls_nd (
+ a int,
+ b int,
+ UNIQUE NULLS NOT DISTINCT (a, b)
+);
+SELECT * FROM pg_get_table_ddl('nulls_nd'::regclass, owner => false);
+
+-- INCLUDE columns on a constraint-backed UNIQUE index (PG 11): the
+-- covering columns appear in an INCLUDE (...) clause on the ALTER TABLE
+-- ADD CONSTRAINT statement, not in the key column list.
+CREATE TABLE idx_inc (
+ id int PRIMARY KEY,
+ name text,
+ extra text,
+ UNIQUE (name) INCLUDE (extra)
+);
+SELECT * FROM pg_get_table_ddl('idx_inc'::regclass, owner => false);
+
+-- FOREIGN KEY with ON DELETE / ON UPDATE referential actions and MATCH
+-- clause. Each variant must appear verbatim in the reconstructed DDL.
+CREATE TABLE fk_acts_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_acts (
+ a int,
+ b int,
+ c int,
+ d int,
+ CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES fk_acts_tgt(id)
+ ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET NULL,
+ CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET DEFAULT,
+ CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES fk_acts_tgt(id)
+ MATCH FULL
+);
+SELECT * FROM pg_get_table_ddl('fk_acts'::regclass, owner => false);
+
+-- NOT ENFORCED foreign key (PG 17): the constraint is recorded in the
+-- catalog but not validated by the engine; NOT ENFORCED must appear in
+-- the reconstructed DDL.
+CREATE TABLE notenf_tgt (id int PRIMARY KEY);
+CREATE TABLE notenf (
+ a int,
+ CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES notenf_tgt(id)
+ NOT ENFORCED
+);
+SELECT * FROM pg_get_table_ddl('notenf'::regclass, owner => false);
+
+-- Self-referencing FK: the PK must appear before the FK that references it.
+-- The catalog scan returns constraints in name order; without the fix,
+-- "self_ref_parent_id_fkey" sorts before "self_ref_pkey" and the FK ADD
+-- CONSTRAINT fails with "there is no unique constraint matching given keys".
+CREATE TABLE self_ref (
+ id int PRIMARY KEY,
+ parent_id int REFERENCES self_ref(id)
+);
+SELECT * FROM pg_get_table_ddl('self_ref'::regclass, owner => false);
+
+-- WITHOUT OVERLAPS on a PRIMARY KEY (PG 17): the range-type period
+-- column forms a temporal primary key; WITHOUT OVERLAPS must appear
+-- inside the key column list in the reconstructed ALTER TABLE.
+-- Both columns use range types with native GiST support so no
+-- btree_gist extension is needed.
+CREATE TABLE temporal_pk (
+ grp int4range,
+ during daterange,
+ PRIMARY KEY (grp, during WITHOUT OVERLAPS)
+);
+SELECT * FROM pg_get_table_ddl('temporal_pk'::regclass, owner => false);
+
+-- Indexes: functional and partial. Constraint-backing indexes are
+-- suppressed (they are emitted by the constraint loop).
+CREATE TABLE idxd (id int PRIMARY KEY, name text);
+CREATE INDEX idxd_lower ON idxd (lower(name));
+CREATE INDEX idxd_partial ON idxd (id) WHERE id > 100;
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false);
+
+-- Inheritance, including a child DEFAULT override on an inherited column.
+CREATE TABLE par (a int DEFAULT 1, b text);
+CREATE TABLE ch (c int) INHERITS (par);
+ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false);
+
+-- Per-column attoptions: emitted as ALTER COLUMN SET (...).
+CREATE TABLE attopt (a int, b text);
+ALTER TABLE attopt ALTER COLUMN a SET (n_distinct = 100);
+SELECT * FROM pg_get_table_ddl('attopt'::regclass, owner => false);
+
+-- Partitioned table parents (RANGE, HASH, and LIST).
+CREATE TABLE parted_range (id int, k int) PARTITION BY RANGE (id);
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false);
+
+CREATE TABLE parted_hash (id int) PARTITION BY HASH (id);
+SELECT * FROM pg_get_table_ddl('parted_hash'::regclass, owner => false);
+
+CREATE TABLE parted_list (id int, region text) PARTITION BY LIST (region);
+SELECT * FROM pg_get_table_ddl('parted_list'::regclass, owner => false);
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false);
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, owner => false);
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, owner => false);
+
+-- Non-constraint-backed index on a partitioned table: the parent's index DDL
+-- must appear once; the auto-created child partition indexes (relispartition=t)
+-- must be suppressed so the DDL round-trips without "relation already exists".
+CREATE TABLE parted_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_idx_a PARTITION OF parted_idx FOR VALUES IN ('a');
+CREATE UNIQUE INDEX parted_idx_uidx ON parted_idx (id, region);
+SELECT * FROM pg_get_table_ddl('parted_idx'::regclass, owner => false);
+
+-- Plain (non-unique) CREATE INDEX on partitioned table: the auto-created child
+-- index (relispartition=true) is suppressed, just like the UNIQUE case.
+CREATE TABLE parted_plain (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE parted_plain_a PARTITION OF parted_plain FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_plain_id_idx ON parted_plain (id);
+SELECT * FROM pg_get_table_ddl('parted_plain'::regclass, owner => false);
+
+-- Constraint-backed PRIMARY KEY on partitioned table: the child's inherited
+-- constraint (conislocal=false) and its backing index are both suppressed.
+CREATE TABLE parted_pk (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_pk_a PARTITION OF parted_pk FOR VALUES IN ('a');
+ALTER TABLE parted_pk ADD PRIMARY KEY (id, region);
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false);
+
+-- Multi-level (sub-partitioned) table with a CREATE INDEX on the root.
+-- The mid-level partitioned index (relispartition=true, relkind='I') and the
+-- leaf index (relispartition=true, relkind='i') are both suppressed.
+CREATE TABLE parted_sub (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_sub_a PARTITION OF parted_sub
+ FOR VALUES IN ('a') PARTITION BY RANGE (id);
+CREATE TABLE parted_sub_a1 PARTITION OF parted_sub_a FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_sub_id_idx ON parted_sub (id);
+SELECT * FROM pg_get_table_ddl('parted_sub'::regclass, owner => false);
+
+-- Partition child with its own locally-defined index (relispartition=false):
+-- this is NOT inherited from any parent partitioned index and must be emitted.
+CREATE TABLE parted_child_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_child_idx_a PARTITION OF parted_child_idx FOR VALUES IN ('a');
+CREATE INDEX parted_child_idx_a_id ON parted_child_idx_a (id);
+SELECT * FROM pg_get_table_ddl('parted_child_idx'::regclass, owner => false);
+
+-- Mix: parent has a CREATE INDEX (auto-propagated to child, relispartition=true,
+-- suppressed) AND the child has its own local index (relispartition=false, emitted).
+CREATE TABLE parted_mixed (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_mixed_a PARTITION OF parted_mixed FOR VALUES IN ('a');
+CREATE INDEX parted_mixed_id ON parted_mixed (id);
+CREATE INDEX parted_mixed_a_region ON parted_mixed_a (region);
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false);
+
+-- FK on a partitioned table: declared on the parent; partition children inherit
+-- the constraint with conislocal=false and must not re-emit it.
+CREATE TABLE parted_fk_tgt (id int PRIMARY KEY);
+CREATE TABLE parted_fk (id int, tgt_id int REFERENCES parted_fk_tgt(id))
+ PARTITION BY RANGE (id);
+CREATE TABLE parted_fk_a PARTITION OF parted_fk FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('parted_fk'::regclass, owner => false);
+
+-- Statistics declared explicitly on a partition child (not auto-propagated):
+-- the child's statistics object appears inside the child's DDL block.
+CREATE TABLE parted_stx (id int, region text, val float) PARTITION BY LIST (region);
+CREATE TABLE parted_stx_a PARTITION OF parted_stx FOR VALUES IN ('a');
+CREATE STATISTICS parted_stx_stat (ndistinct) ON id, region FROM parted_stx;
+CREATE STATISTICS parted_stx_a_stat (ndistinct) ON id, val FROM parted_stx_a;
+SELECT * FROM pg_get_table_ddl('parted_stx'::regclass, owner => false);
+DROP TABLE parted_stx;
+
+-- NO INHERIT CHECK constraint: the NO INHERIT flag must appear verbatim in the
+-- inline constraint definition inside the CREATE TABLE body.
+CREATE TABLE noinhchk (id int, val int,
+ CONSTRAINT val_pos CHECK (val > 0) NO INHERIT);
+SELECT * FROM pg_get_table_ddl('noinhchk'::regclass, owner => false);
+DROP TABLE noinhchk;
+
+-- Multiple inheritance parents: the INHERITS clause must list both parents in
+-- declaration order; only the locally-defined column appears in the column list.
+CREATE TABLE mi_p1 (a int DEFAULT 1);
+CREATE TABLE mi_p2 (b text DEFAULT 'x');
+CREATE TABLE mi_child (c float) INHERITS (mi_p1, mi_p2);
+SELECT * FROM pg_get_table_ddl('mi_child'::regclass, owner => false);
+DROP TABLE mi_p1, mi_p2 CASCADE;
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false);
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false);
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false);
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false);
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, owner => false);
+
+-- UNLOGGED + reloptions.
+CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70);
+SELECT * FROM pg_get_table_ddl('uno'::regclass, owner => false);
+
+-- Typed table (CREATE TABLE OF type_name). Columns inherited from the
+-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come
+-- out through a single "(col WITH OPTIONS ...)" list.
+CREATE TYPE typed_t AS (a int, b text);
+CREATE TABLE typed_plain OF typed_t;
+SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, owner => false);
+
+CREATE TABLE typed_over OF typed_t (
+ a WITH OPTIONS DEFAULT 7 NOT NULL,
+ b WITH OPTIONS NOT NULL,
+ CONSTRAINT b_nonempty CHECK (length(b) > 0)
+);
+SELECT * FROM pg_get_table_ddl('typed_over'::regclass, owner => false);
+
+-- Temporary tables + ON COMMIT. Temp tables must never be emitted with
+-- the session-private pg_temp_NN schema prefix -- the TEMPORARY keyword
+-- already places the table in pg_temp and the prefix is non-replayable.
+-- ON COMMIT DROP only fires at commit, so the queries that need to see
+-- the catalog entry must run in the same transaction as the CREATE.
+BEGIN;
+CREATE TEMP TABLE temp_default (id int);
+CREATE TEMP TABLE temp_delete (id int) ON COMMIT DELETE ROWS;
+CREATE TEMP TABLE temp_drop (id int) ON COMMIT DROP;
+
+SELECT line FROM pg_get_table_ddl('temp_default'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT line FROM pg_get_table_ddl('temp_delete'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT line FROM pg_get_table_ddl('temp_drop'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ROLLBACK;
+
+-- Pretty mode.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, pretty => true);
+
+-- only_kinds / except_kinds gating: each emits either only the listed kinds
+-- (only_kinds) or every kind except the listed ones (except_kinds). The two
+-- are mutually exclusive. Kind vocabulary: table, index,
+-- primary_key, unique, check, foreign_key, exclusion, rule,
+-- statistics, trigger, policy, rls, replica_identity, partition.
+-- NOT NULL is not in the vocabulary - always emitted to prevent
+-- producing schemas that silently accept NULLs the source would
+-- have rejected.
+--
+-- except_kinds=index hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['index']);
+
+-- except_kinds=primary_key,unique,check,foreign_key,exclusion hides every
+-- table-level constraint. Inline CHECK in the CREATE TABLE body is
+-- suppressed too.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['primary_key','unique','check','foreign_key','exclusion']);
+
+-- except_kinds=foreign_key suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- first pass of the two-pass FK-clone workflow: emit tables without
+-- cross-table FKs, load data, then re-run with only_kinds=foreign_key
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['foreign_key']);
+
+-- only_kinds=foreign_key is the second pass of the two-pass FK-clone
+-- workflow: emit only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN
+-- KEY rows; the CREATE TABLE and every non-FK sub-object are
+-- suppressed (the table already exists from the first pass).
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+
+-- A table with no foreign keys produces no rows under
+-- only_kinds=foreign_key.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+
+-- only_kinds=table emits only the bare CREATE TABLE (with any inline
+-- column-level NOT NULL and CHECK) - no indexes, no PRIMARY KEY ALTER,
+-- no other sub-objects. Useful for capturing a table's shape without
+-- pulling in everything attached to it.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table']);
+
+-- only_kinds=check on a non-partition table. The constraint loop must emit
+-- column-level and table-level CHECK constraints via ALTER TABLE here,
+-- because the inline path in CREATE TABLE never runs (KIND_TABLE is
+-- not in the filter).
+CREATE TABLE chk_only (
+ id int PRIMARY KEY,
+ qty int CHECK (qty > 0),
+ CONSTRAINT chk_only_id_pos CHECK (id > 0)
+);
+SELECT * FROM pg_get_table_ddl('chk_only'::regclass, owner => false, only_kinds => ARRAY['check']);
+DROP TABLE chk_only;
+
+-- only_kinds=partition on a partitioned-table parent: each child's full DDL
+-- is emitted (CREATE TABLE ... PARTITION OF ...). The "partition"
+-- keyword is a gate at the parent level; it is stripped from the
+-- propagated filter so the child does not exclude its own
+-- CREATE TABLE.
+CREATE TABLE p_only (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE p_only_a PARTITION OF p_only FOR VALUES FROM (0) TO (100);
+CREATE TABLE p_only_b PARTITION OF p_only FOR VALUES FROM (100) TO (200);
+SELECT * FROM pg_get_table_ddl('p_only'::regclass, owner => false, only_kinds => ARRAY['partition']);
+DROP TABLE p_only;
+
+-- only_kinds and except_kinds are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false,
+ only_kinds => ARRAY['foreign_key'],
+ except_kinds => ARRAY['foreign_key']);
+
+-- Pub/sub schema clone: keep the table and its primary key, drop the
+-- rest of the constraints so the subscriber can replicate without
+-- pulling cross-table dependencies along.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique','check','foreign_key','exclusion']);
+
+-- except_kinds=rule hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, except_kinds => ARRAY['rule']);
+
+-- except_kinds=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, except_kinds => ARRAY['statistics']);
+
+-- except_kinds=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, except_kinds => ARRAY['rls']);
+
+-- except_kinds=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, except_kinds => ARRAY['replica_identity']);
+
+-- Partition children are emitted by default; except_kinds=partition
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false, except_kinds => ARRAY['partition']);
+
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY[' INDEX ',' RULE ']);
+
+-- schema_qualified=false strips the schema from the target table itself
+-- and from same-schema sibling references (inheritance/partition
+-- parents, identity sequences in the same schema), while cross-schema
+-- references are kept qualified for correctness.
+--
+-- Bare CREATE TABLE for an ordinary table.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false,
+ schema_qualified => false);
+
+-- INHERITS parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Partition parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Cross-schema FK target keeps qualification. The 'refd' table is in
+-- pgtbl_ddl_test; place an FK target in a different schema and verify it
+-- stays qualified even with schema_qualified=false.
+CREATE SCHEMA pgtbl_ddl_other;
+CREATE TABLE pgtbl_ddl_other.parent (id int PRIMARY KEY);
+CREATE TABLE xschema_fk (
+ id int PRIMARY KEY,
+ pid int REFERENCES pgtbl_ddl_other.parent
+);
+SELECT * FROM pg_get_table_ddl('xschema_fk'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Cross-schema FK whose target schema name ends with the base schema name
+-- (e.g. "xpgtbl_ddl_test" ends with "pgtbl_ddl_test"). A post-hoc text
+-- strip would mangle "xpgtbl_ddl_test.reftbl" into "xreftbl"; deciding
+-- qualification at generation time avoids the problem entirely.
+CREATE SCHEMA xpgtbl_ddl_test;
+CREATE TABLE xpgtbl_ddl_test.reftbl (id int PRIMARY KEY);
+CREATE TABLE suffix_schema_fk (
+ id int PRIMARY KEY,
+ ref int REFERENCES xpgtbl_ddl_test.reftbl
+);
+SELECT * FROM pg_get_table_ddl('suffix_schema_fk'::regclass, owner => false,
+ schema_qualified => false);
+DROP TABLE suffix_schema_fk;
+DROP SCHEMA xpgtbl_ddl_test CASCADE;
+
+-- Identity column with a custom sequence name in the same schema is
+-- emitted unqualified.
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Round-trip: with schema_qualified=false, after SET search_path the
+-- DDL can be replayed into a different target schema.
+CREATE SCHEMA pgtbl_ddl_replay;
+DO $$
+DECLARE
+ stmt text;
+BEGIN
+ SET LOCAL search_path = pgtbl_ddl_replay;
+ FOR stmt IN
+ SELECT line FROM pg_get_table_ddl('pgtbl_ddl_test.basic'::regclass,
+ owner => false,
+ schema_qualified => false) AS line
+ LOOP
+ EXECUTE stmt;
+ END LOOP;
+END $$;
+SELECT relnamespace::regnamespace::text, relname
+FROM pg_class WHERE oid = 'pgtbl_ddl_replay.basic'::regclass;
+DROP SCHEMA pgtbl_ddl_replay CASCADE;
+DROP TABLE xschema_fk;
+DROP SCHEMA pgtbl_ddl_other CASCADE;
+
+-- Cross-schema partition child with schema_qualified=false: the child must
+-- carry its own schema prefix so the output is unambiguous regardless of the
+-- caller's search_path. Before the fix, the child was emitted as the bare
+-- name "pt_c" (landing in whichever schema is first in search_path) while
+-- the parent reference inside the CREATE TABLE came out as the fully-qualified
+-- "pgtbl_ddl_part_s.pt", making the DDL inconsistent and unreplayable.
+CREATE SCHEMA pgtbl_ddl_part_s;
+CREATE SCHEMA pgtbl_ddl_part_other;
+CREATE TABLE pgtbl_ddl_part_s.pt (id int, val int) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_part_other.pt_c
+ PARTITION OF pgtbl_ddl_part_s.pt
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_part_s.pt'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_part_s CASCADE;
+DROP SCHEMA pgtbl_ddl_part_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 own column list (attislocal=false), so the
+-- user-named NOT NULL is emitted as a table-level CONSTRAINT clause in
+-- the CREATE TABLE body. This preserves the name and prevents a collision
+-- when the parent later propagates its own NOT NULL to the child.
+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;
+
+-- Partition child where the parent's default was explicitly dropped.
+-- After PARTITION OF the parent default is re-applied; DROP DEFAULT
+-- must be emitted to restore the dropped state.
+CREATE TABLE drop_def_par (a int DEFAULT 99) PARTITION BY LIST (a);
+CREATE TABLE drop_def_child PARTITION OF drop_def_par FOR VALUES IN (1);
+ALTER TABLE ONLY drop_def_child ALTER COLUMN a DROP DEFAULT;
+SELECT * FROM pg_get_table_ddl('drop_def_child'::regclass, owner => false);
+DROP TABLE drop_def_par;
+
+-- Named NOT NULL on a partition child that was attached from a pre-existing
+-- table. The name must be preserved via the PARTITION OF column spec list
+-- so the child keeps its local name rather than inheriting the parent's
+-- auto-generated name.
+CREATE TABLE nn_part_par (a int NOT NULL) PARTITION BY LIST (a);
+CREATE TABLE nn_part_ch (a int CONSTRAINT nn_part_ch_nn NOT NULL);
+ALTER TABLE nn_part_par ATTACH PARTITION nn_part_ch FOR VALUES IN (1);
+SELECT * FROM pg_get_table_ddl('nn_part_ch'::regclass, owner => false);
+DROP TABLE nn_part_par;
+
+-- Named NOT NULL in an INHERITS child where the parent also gets a NOT NULL
+-- later: the child name must survive via the inline CONSTRAINT clause so it
+-- does not collide with the propagated parent NOT NULL.
+CREATE TABLE nn_inh_par (a int);
+CREATE TABLE nn_inh_ch (b int) INHERITS (nn_inh_par);
+ALTER TABLE nn_inh_ch ADD CONSTRAINT nn_inh_ch_nn NOT NULL a;
+ALTER TABLE nn_inh_par ADD CONSTRAINT nn_inh_par_nn NOT NULL a;
+SELECT * FROM pg_get_table_ddl('nn_inh_ch'::regclass, owner => false);
+DROP TABLE nn_inh_par CASCADE;
+
+-- Invalid index (left by a failed CREATE INDEX CONCURRENTLY) must be skipped.
+CREATE TABLE inv_idx (x int);
+INSERT INTO inv_idx VALUES (1), (1);
+CREATE UNIQUE INDEX CONCURRENTLY inv_idx_uidx ON inv_idx (x);
+SELECT * FROM pg_get_table_ddl('inv_idx'::regclass, owner => false);
+DROP TABLE inv_idx;
+
+-- CLUSTER ON: emitted after the index is created.
+CREATE TABLE cluster_tbl (id int, val text);
+CREATE INDEX cluster_tbl_id ON cluster_tbl (id);
+CLUSTER cluster_tbl USING cluster_tbl_id;
+SELECT * FROM pg_get_table_ddl('cluster_tbl'::regclass, owner => false);
+DROP TABLE cluster_tbl;
+
+-- Disabled rule: DISABLE RULE must be emitted after CREATE RULE.
+CREATE TABLE disabled_rule_tbl (id int);
+CREATE RULE dr_rule AS ON INSERT TO disabled_rule_tbl DO NOTHING;
+ALTER TABLE disabled_rule_tbl DISABLE RULE dr_rule;
+SELECT * FROM pg_get_table_ddl('disabled_rule_tbl'::regclass, owner => false);
+DROP TABLE disabled_rule_tbl;
+
+-- Index statistics target on an expression index column.
+CREATE TABLE idx_stat_tbl (c1 int);
+CREATE INDEX idx_stat_idx ON idx_stat_tbl ((c1 + 1));
+ALTER INDEX idx_stat_idx ALTER COLUMN 1 SET STATISTICS 400;
+SELECT * FROM pg_get_table_ddl('idx_stat_tbl'::regclass, owner => false);
+DROP TABLE idx_stat_tbl;
+
+-- schema_qualified=false must drop the schema prefix from DEFAULT
+-- expressions and inline CHECK bodies that reference same-schema
+-- objects (sequence via nextval, function via direct call). This
+-- relies on the deparser respecting the narrowed search_path rather
+-- than on substring stripping.
+CREATE SCHEMA pgtbl_ddl_xref;
+CREATE SEQUENCE pgtbl_ddl_xref.myseq;
+CREATE FUNCTION pgtbl_ddl_xref.f(int) RETURNS int LANGUAGE sql IMMUTABLE
+ AS 'SELECT $1';
+CREATE TABLE pgtbl_ddl_xref.t (
+ id int DEFAULT nextval('pgtbl_ddl_xref.myseq'),
+ val int,
+ CONSTRAINT chk CHECK (pgtbl_ddl_xref.f(val) > 0)
+);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_xref.t'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_xref CASCADE;
+
+-- schema_qualified=false must NOT corrupt string literals whose
+-- contents happen to contain the schema name followed by a dot. String
+-- literal text is output verbatim by the deparser and is unaffected by
+-- search_path narrowing.
+CREATE SCHEMA pgtbl_ddl_str;
+CREATE TABLE pgtbl_ddl_str.p (id int, note text) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_str.pc PARTITION OF pgtbl_ddl_str.p
+ (CONSTRAINT chk CHECK (note <> 'pgtbl_ddl_str.secret'))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_str.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_str CASCADE;
+
+-- schema_qualified=false must NOT corrupt double-quoted identifiers
+-- whose contents happen to contain the schema name followed by a dot
+-- (for example a column literally named "<schema>.weird" inside that
+-- schema). The mechanism is purely search_path narrowing: the deparser
+-- outputs attribute names verbatim from pg_attribute, so the quoted
+-- identifier content is never examined for schema prefixes. This holds
+-- even when the schema name is a single letter.
+CREATE SCHEMA pgtbl_ddl_qid;
+CREATE TABLE pgtbl_ddl_qid.p (id int, "pgtbl_ddl_qid.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_qid.pc PARTITION OF pgtbl_ddl_qid.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_qid.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_qid.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_qid CASCADE;
+-- Same with a short (single-letter) schema name to confirm there is no
+-- minimum-length assumption in the stripping path.
+CREATE SCHEMA pgtbl_ddl_q1;
+CREATE TABLE pgtbl_ddl_q1.p (id int, "pgtbl_ddl_q1.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_q1.pc PARTITION OF pgtbl_ddl_q1.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_q1.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_q1.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_q1 CASCADE;
+
+-- schema_qualified=false when the base schema name itself requires
+-- quoting (its prefix starts with "). Both forms of the prefix -
+-- bare-lowercase and quoted - must be stripped from outer
+-- ALTER TABLE / ON / CREATE INDEX prefixes, while inner quoted
+-- identifiers that happen to begin the same way must be preserved.
+CREATE SCHEMA "PgTbl-Ddl-Q";
+CREATE TABLE "PgTbl-Ddl-Q"."T" (id int, val int,
+ CONSTRAINT chk CHECK (val > 0));
+CREATE INDEX "T_val_idx" ON "PgTbl-Ddl-Q"."T" (val);
+SELECT * FROM pg_get_table_ddl('"PgTbl-Ddl-Q"."T"'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA "PgTbl-Ddl-Q" CASCADE;
+
+-- Error: not an ordinary or partitioned table.
+CREATE VIEW v AS SELECT 1 AS x;
+SELECT * FROM pg_get_table_ddl('v'::regclass);
+
+CREATE SEQUENCE s;
+SELECT * FROM pg_get_table_ddl('s'::regclass);
+
+-- NULL argument returns no rows.
+SELECT * FROM pg_get_table_ddl(NULL);
+
+-- REPLICA IDENTITY USING INDEX validation: if the referenced index
+-- would not be emitted (because its source kind is suppressed) but
+-- replica_identity itself would be, the function raises an error so
+-- the emitted DDL never dangles. Exercise all three constraint-backed
+-- forms (PK, UNIQUE, EXCLUSION) and a bare CREATE UNIQUE INDEX, in
+-- both the except-list and only-list shapes.
+--
+-- PK-backed: except_kinds=primary_key without except_kinds=replica_identity.
+CREATE TABLE ri_pk_excl (a int PRIMARY KEY);
+ALTER TABLE ri_pk_excl REPLICA IDENTITY USING INDEX ri_pk_excl_pkey;
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key','replica_identity']);
+DROP TABLE ri_pk_excl;
+
+-- UNIQUE-constraint-backed replica identity index.
+CREATE TABLE ri_uniq_excl (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_uniq_excl REPLICA IDENTITY USING INDEX ri_uniq_excl_a_key;
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique','replica_identity']);
+DROP TABLE ri_uniq_excl;
+
+-- Bare CREATE UNIQUE INDEX (no backing constraint) used as REPLICA
+-- IDENTITY: the index is emitted by the "index" kind, so excluding
+-- "index" without also excluding "replica_identity" must error.
+CREATE TABLE ri_idx_excl (a int NOT NULL, b int);
+CREATE UNIQUE INDEX ri_idx_excl_a_uniq ON ri_idx_excl (a);
+ALTER TABLE ri_idx_excl REPLICA IDENTITY USING INDEX ri_idx_excl_a_uniq;
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index']);
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index','replica_identity']);
+DROP TABLE ri_idx_excl;
+
+-- Include-side analog: only_kinds=replica_identity must include the kind
+-- that emits the index, or the function errors.
+CREATE TABLE ri_only (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+-- Including both kinds succeeds.
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['unique','replica_identity']);
+DROP TABLE ri_only;
+
+-- Per-kind only_kinds / except_kinds coverage. For each kind in the vocabulary,
+-- verify that only_kinds=K emits just that kind's output and that except_kinds=K
+-- omits just that kind's output. Kinds with no fixture above (trigger,
+-- policy) are still recognized but currently emit nothing; only_kinds=K
+-- returns zero rows for them. Fixtures used:
+-- idxd: PK + two secondary CREATE INDEX
+-- cons: PK + UNIQUE + CHECK + FK (constraint-backed)
+-- rt: CREATE RULE
+-- stx: CREATE STATISTICS
+-- rls: ENABLE/FORCE ROW LEVEL SECURITY
+-- ri_full: REPLICA IDENTITY FULL (no source-kind dependency)
+CREATE TABLE excl_fix (rng int4range,
+ EXCLUDE USING gist (rng WITH &&));
+
+-- only_kinds=K: emit only kind K's statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index']);
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['primary_key']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, only_kinds => ARRAY['rule']);
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, only_kinds => ARRAY['rls']);
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['policy']);
+
+-- except_kinds=K: emit everything except kind K's statements. PK/UNIQUE/
+-- INDEX are exercised on tables without REPLICA IDENTITY USING INDEX
+-- so the validation does not trip.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['check']);
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, except_kinds => ARRAY['exclusion']);
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['policy']);
+-- except_kinds=table suppresses the CREATE TABLE + OWNER + child-default
+-- SET DEFAULT + attoptions passes, leaving only the sub-object passes.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['table']);
+
+-- Multi-kind compositions.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table','index']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['primary_key','foreign_key']);
+-- Duplicate entries in the list are silently de-duplicated by the
+-- Bitmapset.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index','index']);
+
+DROP TABLE excl_fix;
+
+-- Kind-filter tests on partitioned tables with indexes.
+--
+-- only_kinds=index on a partitioned table: only the root CREATE INDEX is
+-- emitted; no CREATE TABLE, no partition children (KIND_PARTITION absent).
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ only_kinds => ARRAY['index']);
+
+-- except_kinds=index on a partitioned table: CREATE TABLE for the parent
+-- and all children are emitted; no index statements.
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ except_kinds => ARRAY['index']);
+
+-- only_kinds=primary_key on a partitioned table: only the ALTER TABLE ADD
+-- CONSTRAINT PK on the parent; the child's inherited PK (conislocal=false)
+-- is skipped; no CREATE TABLE, no partition children.
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false,
+ only_kinds => ARRAY['primary_key']);
+
+-- only_kinds=K on a table that does not have any K of that kind is a valid
+-- filter and returns zero rows. Tested previously with foreign_key
+-- on 'refd'; here also rule, statistics, rls, replica_identity, and
+-- exclusion on 'basic' (which has only PK + columns).
+SELECT count(*) AS rule_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rule']);
+SELECT count(*) AS stats_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+SELECT count(*) AS rls_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rls']);
+SELECT count(*) AS ri_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+SELECT count(*) AS excl_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+
+-- Input-validation edge cases for the kind array. All
+-- of these raise an error; the function never reaches relation open.
+-- Empty array:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[]::text[]);
+-- NULL element:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[NULL::text]);
+-- Unrecognized kind:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY['no_such_kind']);
+-- Same kind specified through both parameters is still mutex-rejected.
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ only_kinds => ARRAY['index'],
+ except_kinds => ARRAY['rule']);
+
+-- Round-trip verification. For every test table, capture the DDL the
+-- function emits, drop the schema, replay each table's DDL in
+-- dependency order, and confirm that pg_get_table_ddl on the recreated
+-- relation matches the original line-for-line. The final SELECT must
+-- return zero rows.
+CREATE TEMP TABLE pgtbl_ddl_rt_orig (name text, ord int, line text);
+INSERT INTO pgtbl_ddl_rt_orig
+SELECT t.name, o.ord, o.line
+FROM (VALUES
+ ('basic'), ('id_cols'), ('id_custom'), ('gen_cols'), ('gen_virtual'), ('storage_cols'),
+ ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'),
+ ('fk_acts_tgt'), ('fk_acts'), ('notenf_tgt'), ('notenf'), ('self_ref'), ('temporal_pk'),
+ ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('parted_list'),
+ ('parted_idx'), ('parted_idx_a'),
+ ('parted_plain'), ('parted_plain_a'),
+ ('parted_pk'), ('parted_pk_a'),
+ ('parted_sub'), ('parted_sub_a'), ('parted_sub_a1'),
+ ('parted_child_idx'), ('parted_child_idx_a'),
+ ('parted_mixed'), ('parted_mixed_a'),
+ ('parted_fk_tgt'), ('parted_fk'), ('parted_fk_a'),
+ ('rt_log'), ('rt'),
+ ('stx'), ('rls'), ('ri_full'), ('ri_idx'), ('uno')
+ ) AS t(name),
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord);
+
+DO $$
+DECLARE
+ tables CONSTANT text[] := ARRAY[
+ 'basic', 'id_cols', 'id_custom', 'gen_cols', 'gen_virtual', 'storage_cols',
+ 'refd', 'cons', 'nulls_nd', 'idx_inc',
+ 'fk_acts_tgt', 'fk_acts', 'notenf_tgt', 'notenf', 'self_ref', 'temporal_pk',
+ 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ 'parted_list',
+ 'parted_idx', 'parted_idx_a',
+ 'parted_plain', 'parted_plain_a',
+ 'parted_pk', 'parted_pk_a',
+ 'parted_sub', 'parted_sub_a', 'parted_sub_a1',
+ 'parted_child_idx', 'parted_child_idx_a',
+ 'parted_mixed', 'parted_mixed_a',
+ 'parted_fk_tgt', 'parted_fk', 'parted_fk_a',
+ 'rt_log', 'rt',
+ 'stx', 'rls', 'ri_full', 'ri_idx', 'uno'
+ ];
+ t text;
+ stmt text;
+BEGIN
+ DROP SCHEMA pgtbl_ddl_test CASCADE;
+ CREATE SCHEMA pgtbl_ddl_test;
+ FOREACH t IN ARRAY tables LOOP
+ FOR stmt IN
+ SELECT line FROM pgtbl_ddl_rt_orig WHERE name = t ORDER BY ord
+ LOOP
+ BEGIN
+ EXECUTE stmt;
+ EXCEPTION WHEN duplicate_table THEN
+ NULL; -- partition child already created by its parent's DDL
+ END;
+ END LOOP;
+ END LOOP;
+END $$;
+
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ffb413ab612..be539e9ac72 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1648,6 +1648,7 @@ ListenerEntry
LoInfo
LoadStmt
LocalBufferLookupEnt
+LocalNotNullEntry
LocalPgBackendStatus
LocalTransactionId
Location
@@ -3152,6 +3153,7 @@ TableFuncScan
TableFuncScanState
TableFuncType
TableInfo
+TableDdlContext
TableLikeClause
TableSampleClause
TableScanDesc
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-08 13:58 Marcos Pegoraro <[email protected]>
parent: Akshay Joshi <[email protected]>
0 siblings, 1 reply; 54+ messages in thread
From: Marcos Pegoraro @ 2026-07-08 13:58 UTC (permalink / raw)
To: Akshay Joshi <[email protected]>; +Cc: Rui Zhao <[email protected]>; Zsolt Parragi <[email protected]>; [email protected]
Em qua., 8 de jul. de 2026 às 08:56, Akshay Joshi <
[email protected]> escreveu:
The v18 patch is now ready for your review.
On SGML part, schema_qualified is a param which comes before only_kinds.
But the <para> explaining schema_qualified is the latest to be explained,
why ?
Shouldn't it be placed right after tablespace ?
regards
Marcos
^ permalink raw reply [nested|flat] 54+ messages in thread
* Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
@ 2026-07-09 08:53 Akshay Joshi <[email protected]>
parent: Marcos Pegoraro <[email protected]>
0 siblings, 0 replies; 54+ messages in thread
From: Akshay Joshi @ 2026-07-09 08:53 UTC (permalink / raw)
To: Marcos Pegoraro <[email protected]>; +Cc: Rui Zhao <[email protected]>; Zsolt Parragi <[email protected]>; [email protected]
Fixed the documentation. v19 patch is now ready for review.
On Wed, Jul 8, 2026 at 7:29 PM Marcos Pegoraro <[email protected]> wrote:
> Em qua., 8 de jul. de 2026 às 08:56, Akshay Joshi <
> [email protected]> escreveu:
> The v18 patch is now ready for your review.
>
> On SGML part, schema_qualified is a param which comes before only_kinds.
> But the <para> explaining schema_qualified is the latest to be explained,
> why ?
> Shouldn't it be placed right after tablespace ?
>
> regards
> Marcos
>
Attachments:
[application/octet-stream] v19-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch (249.7K, ../../CANxoLDfhDaXvJs687yS+=eh0pmHXw5WqNuZd=oGCabr3Sw+f_w@mail.gmail.com/3-v19-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch)
download | inline diff:
From 97b9a2e596471208aacd36a03cbfde4cec35388b Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v19] Add pg_get_table_ddl() to reconstruct CREATE TABLE
statements
The function reconstructs the CREATE TABLE statement for an ordinary or
partitioned table, followed by the ALTER TABLE / CREATE INDEX /
CREATE RULE / CREATE STATISTICS statements needed to restore its full
definition. Each statement is returned as a separate row.
Supported per-column features: data type with type modifiers, COLLATE,
STORAGE, COMPRESSION (pglz / lz4), GENERATED ALWAYS AS (expr)
STORED/VIRTUAL, GENERATED ALWAYS|BY DEFAULT AS IDENTITY (with sequence
options), DEFAULT, NOT NULL (including named NOT NULL constraints), and
per-column attoptions emitted as ALTER COLUMN SET (...).
Supported table-level features: UNLOGGED, INHERITS, PARTITION BY (RANGE
/ LIST / HASH parents), PARTITION OF parent FOR VALUES (FROM/TO, WITH
modulus/remainder, DEFAULT), USING table access method, WITH
(reloptions), TABLESPACE, and inline CHECK constraints in the CREATE
TABLE body.
Supported sub-objects (re-using existing deparse helpers from
ruleutils.c): indexes (including partial and functional) via
pg_get_indexdef_ddl; constraints (PRIMARY KEY with WITHOUT OVERLAPS for
temporal keys, UNIQUE with NULLS NOT DISTINCT and INCLUDE columns,
FOREIGN KEY with ON DELETE/UPDATE referential actions and MATCH clause,
NOT ENFORCED foreign keys, EXCLUDE, named NOT NULL) via
pg_get_constraintdef_body; rules via pg_get_ruledef_ddl; extended
statistics via pg_get_statisticsobjdef_ddl; REPLICA IDENTITY
NOTHING/FULL/USING INDEX; ALTER TABLE ENABLE/FORCE ROW LEVEL SECURITY;
and child-local DEFAULT overrides on inheritance/partition children.
DDL for partition children of a partitioned-table parent is appended
after the parent by default.
The function signature follows the named-parameter convention
established by pg_get_role_ddl(), pg_get_tablespace_ddl(), and
pg_get_database_ddl():
pg_get_table_ddl(relation regclass,
pretty boolean DEFAULT false,
owner boolean DEFAULT true,
tablespace boolean DEFAULT true,
schema_qualified boolean DEFAULT true,
only_kinds text[] DEFAULT NULL,
except_kinds text[] DEFAULT NULL)
pretty controls pretty-printed output. owner controls emission of the
ALTER TABLE ... OWNER TO statement. tablespace controls the TABLESPACE
clause on CREATE TABLE. schema_qualified controls whether object names
are emitted with their schema prefix: when true (the default) the
active search_path is temporarily narrowed to pg_catalog so every
deparse helper produces fully-qualified names; when false it is narrowed
to the target table's own schema so same-schema references come out
unqualified while cross-schema references remain qualified for
correctness. Temporary tables are never schema-qualified regardless of
this setting: the TEMPORARY keyword already places them in pg_temp, and
emitting pg_temp_NN.relname would produce non-replayable DDL.
Object-class filtering uses two mutually-exclusive text-array
parameters, only_kinds and except_kinds. When only_kinds is set, only
the listed kinds are emitted; when except_kinds is set, every kind
except the listed ones is emitted; when neither is set every kind is
emitted. Each array element is matched case-insensitively with
leading/trailing whitespace trimmed; an unrecognized name raises an
error. The kind vocabulary is:
table, index, primary_key, unique, check, foreign_key, exclusion,
rule, statistics, rls, replica_identity, partition
trigger and policy are accepted in the vocabulary but currently produce
no output; they are reserved for when standalone pg_get_trigger_ddl()
and pg_get_policy_ddl() helpers become available.
NOT NULL is not part of the vocabulary: it is always emitted to prevent
producing schemas that silently accept NULLs the source would reject.
When replica_identity is in the active filter and the table's REPLICA
IDENTITY USING INDEX references an index that the filter would suppress,
the function raises an error before emitting any output, so the
generated DDL never references an index it did not produce.
Default omission convention: every optional clause is omitted when its
value matches what the server would reapply on round-trip, including
type-default COLLATE, per-type STORAGE, the auto-generated identity
sequence name and parameter defaults, heap access method, default
REPLICA IDENTITY, disabled RLS toggles, empty reloptions, and the
default tablespace.
Author: Akshay Joshi <[email protected]>
Reviewed-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Kyotaro Horiguchi <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Rui Zhao <[email protected]>
---
doc/src/sgml/func/func-info.sgml | 193 ++
src/backend/catalog/pg_inherits.c | 93 +
src/backend/commands/tablecmds.c | 30 +-
src/backend/utils/adt/ddlutils.c | 2660 ++++++++++++++++-
src/backend/utils/adt/ruleutils.c | 87 +-
src/include/catalog/pg_inherits.h | 1 +
src/include/catalog/pg_proc.dat | 8 +
src/include/commands/tablecmds.h | 3 +
src/include/utils/ruleutils.h | 4 +
.../regress/expected/pg_get_table_ddl.out | 1612 ++++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/pg_get_table_ddl.sql | 937 ++++++
src/tools/pgindent/typedefs.list | 2 +
13 files changed, 5615 insertions(+), 17 deletions(-)
create mode 100644 src/test/regress/expected/pg_get_table_ddl.out
create mode 100644 src/test/regress/sql/pg_get_table_ddl.sql
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 69ef3857cfa..11130b51968 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3960,6 +3960,199 @@ 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>relation</parameter> <type>regclass</type>
+ <optional>, <parameter>pretty</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> false</optional>
+ <optional>, <parameter>owner</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>tablespace</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>schema_qualified</parameter> <type>boolean</type>
+ <literal>DEFAULT</literal> true</optional>
+ <optional>, <parameter>only_kinds</parameter> <type>text[]</type>
+ <literal>DEFAULT</literal> NULL</optional>
+ <optional>, <parameter>except_kinds</parameter> <type>text[]</type>
+ <literal>DEFAULT</literal> NULL</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.
+ This includes per-index statistics targets
+ (<command>ALTER INDEX ... SET STATISTICS</command>),
+ the clustering index (<command>ALTER TABLE ... CLUSTER ON</command>),
+ and non-default rule firing states
+ (<command>ALTER TABLE ... DISABLE/ENABLE RULE</command>).
+ 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.
+ Foreign-key constraints are always emitted after all other
+ constraints so that self-referencing foreign keys (where the
+ referenced primary or unique key is on the same table) can be
+ added once that key already exists.
+ When <parameter>pretty</parameter> is true, the output is
+ pretty-printed. When <parameter>owner</parameter> is false, the
+ <command>ALTER TABLE ... OWNER TO</command> statement is omitted.
+ When <parameter>tablespace</parameter> is false, the
+ <literal>TABLESPACE</literal> clause is omitted from the
+ <command>CREATE TABLE</command> statement. The
+ <parameter>only_kinds</parameter> and <parameter>except_kinds</parameter>
+ parameters described below control which object classes are emitted.
+ </para>
+ <para>
+ The <parameter>schema_qualified</parameter> parameter (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. Partition children that reside in a different
+ schema than the target table are always emitted with full schema
+ qualification regardless of this parameter, because the output
+ would otherwise place the child in the wrong schema when
+ replayed.
+ </para>
+ <para>
+ The <parameter>only_kinds</parameter> and <parameter>except_kinds</parameter>
+ parameters each take a text array of object-class kind names
+ and are mutually exclusive (specifying both raises an
+ error). When <parameter>only_kinds</parameter> is set, only the
+ listed kinds are emitted; when <parameter>except_kinds</parameter> is
+ set, every kind <emphasis>except</emphasis> the listed ones is
+ emitted. When neither is set (the default), every kind is
+ emitted. Leading and trailing whitespace in each array element
+ is ignored and matching is case-insensitive; an unrecognized
+ kind name raises an error.
+ The kind vocabulary is
+ <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).
+ Note that <literal>trigger</literal> and <literal>policy</literal>
+ are accepted in the vocabulary but currently produce no output;
+ they are reserved for future use when standalone
+ <function>pg_get_trigger_ddl</function> and
+ <function>pg_get_policy_ddl</function> helpers become available. 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_kinds => ARRAY['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_kinds => ARRAY['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>
+ 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>
+ <para>
+ The following table-related objects are intentionally outside the
+ scope of this function and are not emitted:
+ <itemizedlist>
+ <listitem>
+ <para>
+ <emphasis>Sequences</emphasis> owned by serial columns
+ (<type>serial</type>, <type>bigserial</type>,
+ <type>smallserial</type>) —sequences are independent catalog
+ objects. Use <function>pg_get_sequence_ddl</function> (if
+ available) or <productname>pg_dump</productname> to capture them
+ alongside the table.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <emphasis>Partition children whose column order differs from the
+ parent</emphasis> —the <literal>PARTITION OF</literal> syntax
+ can only reproduce the parent's column order. Such partitions
+ require a standalone <command>CREATE TABLE</command> followed by
+ <command>ALTER TABLE ... ATTACH PARTITION</command>, which
+ <productname>pg_dump</productname> performs automatically.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <emphasis>Triggers and row-level security policies</emphasis> —
+ reserved for future use when standalone
+ <function>pg_get_trigger_ddl</function> and
+ <function>pg_get_policy_ddl</function> helpers are available.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ <emphasis><command>COMMENT ON</command> and
+ <command>GRANT</command>/<command>REVOKE</command></emphasis> —
+ these are separate from the table's structural DDL and are not
+ emitted.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </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 cb93c3e935a..5f4ec0122f6 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -747,7 +747,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);
@@ -2525,7 +2524,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)
@@ -20013,6 +20012,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..e65633f6fde 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -5,7 +5,7 @@
*
* This file contains the pg_get_*_ddl family of functions that generate
* DDL statements to recreate database objects such as roles, tablespaces,
- * and databases, along with common infrastructure for option parsing and
+ * databases, and tables, along with common infrastructure for
* pretty-printing.
*
* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
@@ -21,30 +21,92 @@
#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_index.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_policy.h"
+#include "catalog/pg_rewrite.h"
+#include "catalog/pg_sequence.h"
+#include "catalog/pg_statistic_ext.h"
#include "catalog/pg_tablespace.h"
+#include "catalog/pg_trigger.h"
+#include "rewrite/rewriteDefine.h"
+#include "commands/defrem.h"
+#include "commands/tablecmds.h"
#include "commands/tablespace.h"
-#include "common/relpath.h"
#include "funcapi.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "utils/acl.h"
+#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/datetime.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
-#include "utils/pg_locale.h"
#include "utils/rel.h"
#include "utils/ruleutils.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
#include "utils/varlena.h"
+/*
+ * Object-class kinds that the only / except options on
+ * pg_get_table_ddl can filter on. Members are stored as integers in a
+ * Bitmapset on TableDdlContext. Keep table_ddl_kind_names[] in sync
+ * with the order of additions here.
+ */
+typedef enum TableDdlKind
+{
+ TABLE_DDL_KIND_TABLE,
+ TABLE_DDL_KIND_INDEX,
+ TABLE_DDL_KIND_PRIMARY_KEY,
+ TABLE_DDL_KIND_UNIQUE,
+ TABLE_DDL_KIND_CHECK,
+ TABLE_DDL_KIND_FOREIGN_KEY,
+ TABLE_DDL_KIND_EXCLUSION,
+ TABLE_DDL_KIND_RULE,
+ TABLE_DDL_KIND_STATISTICS,
+ TABLE_DDL_KIND_TRIGGER,
+ TABLE_DDL_KIND_POLICY,
+ TABLE_DDL_KIND_RLS,
+ TABLE_DDL_KIND_REPLICA_IDENTITY,
+ TABLE_DDL_KIND_PARTITION,
+} TableDdlKind;
+
+static const struct
+{
+ const char *name;
+ TableDdlKind kind;
+} table_ddl_kind_names[] =
+{
+ {"table", TABLE_DDL_KIND_TABLE},
+ {"index", TABLE_DDL_KIND_INDEX},
+ {"primary_key", TABLE_DDL_KIND_PRIMARY_KEY},
+ {"unique", TABLE_DDL_KIND_UNIQUE},
+ {"check", TABLE_DDL_KIND_CHECK},
+ {"foreign_key", TABLE_DDL_KIND_FOREIGN_KEY},
+ {"exclusion", TABLE_DDL_KIND_EXCLUSION},
+ {"rule", TABLE_DDL_KIND_RULE},
+ {"statistics", TABLE_DDL_KIND_STATISTICS},
+ {"trigger", TABLE_DDL_KIND_TRIGGER},
+ {"policy", TABLE_DDL_KIND_POLICY},
+ {"rls", TABLE_DDL_KIND_RLS},
+ {"replica_identity", TABLE_DDL_KIND_REPLICA_IDENTITY},
+ {"partition", TABLE_DDL_KIND_PARTITION},
+};
+
+static Bitmapset *parse_kind_array(const char *paramname, ArrayType *arr);
static void append_ddl_option(StringInfo buf, bool pretty, int indent,
const char *fmt, ...)
pg_attribute_printf(4, 5);
@@ -57,6 +119,196 @@ 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).
+ *
+ * inherited_notnull is set when a NOT NULL constraint row exists in
+ * pg_constraint but conislocal=false (purely inherited). In that case
+ * the NOT NULL must not be emitted in the column list: the INHERITS /
+ * PARTITION OF clause already propagates it, and emitting it here would
+ * flip conislocal to true on replay.
+ */
+typedef struct LocalNotNullEntry
+{
+ Oid conoid;
+ char *name;
+ bool is_auto; /* matches "<table>_<col>_not_null" */
+ bool no_inherit;
+ bool inherited_notnull; /* row exists but conislocal=false */
+} 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 bool child_default_inherited_from_parent(Oid childOid,
+ const char *attname,
+ const char *child_adbin);
+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 bool parent_has_default_for_col(Oid childOid, const char *attname);
+
+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_typed_column_storage(TableDdlContext *ctx);
+static void emit_indexes(TableDdlContext *ctx);
+static void emit_cluster_on(TableDdlContext *ctx);
+static void emit_local_constraints(TableDdlContext *ctx);
+static void emit_rules(TableDdlContext *ctx);
+static void emit_statistics(TableDdlContext *ctx);
+static void emit_replica_identity(TableDdlContext *ctx);
+static void emit_rls_toggles(TableDdlContext *ctx);
+static void emit_partition_children(TableDdlContext *ctx);
+
+
+/*
+ * parse_kind_array
+ * Parse a text[] of object-class kind names into a Bitmapset of
+ * TableDdlKind values.
+ *
+ * Each element is matched case-insensitively; surrounding whitespace is
+ * stripped. NULL elements are rejected. Unknown kind names raise an error
+ * citing the supplied parameter name. An empty array is also rejected.
+ * Duplicate entries are silently de-duplicated by the Bitmapset.
+ */
+static Bitmapset *
+parse_kind_array(const char *paramname, ArrayType *arr)
+{
+ Datum *elems;
+ bool *nulls;
+ int nelems;
+ Bitmapset *result = NULL;
+
+ deconstruct_array_builtin(arr, TEXTOID, &elems, &nulls, &nelems);
+
+ if (nelems == 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" must specify at least one kind",
+ paramname)));
+
+ for (int i = 0; i < nelems; i++)
+ {
+ char *raw;
+ char *token;
+ char *end;
+ bool found = false;
+
+ if (nulls[i])
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("parameter \"%s\" must not contain NULL elements",
+ paramname)));
+
+ raw = text_to_cstring(DatumGetTextPP(elems[i]));
+
+ /* Trim leading whitespace. */
+ token = raw;
+ while (*token == ' ' || *token == '\t')
+ token++;
+
+ /* Trim trailing whitespace. */
+ end = token + strlen(token);
+ while (end > token && (end[-1] == ' ' || end[-1] == '\t'))
+ end--;
+ *end = '\0';
+
+ for (size_t j = 0; j < lengthof(table_ddl_kind_names); j++)
+ {
+ if (pg_strcasecmp(token, table_ddl_kind_names[j].name) == 0)
+ {
+ result = bms_add_member(result,
+ (int) table_ddl_kind_names[j].kind);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("unrecognized kind \"%s\" in parameter \"%s\"",
+ token, paramname)));
+
+ pfree(raw);
+ }
+
+ return result;
+}
+
+/*
+ * is_kind_included
+ * Determine whether DDL for the given object-class kind should be
+ * emitted under the current options.
+ */
+static bool
+is_kind_included(const TableDdlContext *ctx, TableDdlKind kind)
+{
+ if (ctx->only_kinds != NULL)
+ return bms_is_member((int) kind, ctx->only_kinds);
+ if (ctx->except_kinds != NULL)
+ return !bms_is_member((int) kind, ctx->except_kinds);
+ return true;
+}
/*
* Helper to append a formatted string with optional pretty-printing.
@@ -975,3 +1227,2405 @@ 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;
+
+ if (con->contype != CONSTRAINT_NOTNULL)
+ 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;
+
+ /*
+ * Inherited-only constraint: record the fact so column-emit helpers
+ * can suppress inline NOT NULL (the parent's INHERITS clause already
+ * propagates it; re-emitting it would flip conislocal to true on
+ * replay).
+ */
+ if (!con->conislocal)
+ {
+ entries[attnum].inherited_notnull = true;
+ continue;
+ }
+
+ att = TupleDescAttr(tupdesc, attnum - 1);
+ autoname = makeObjectName(relname, NameStr(att->attname), "not_null");
+
+ 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;
+ pfree(autoname);
+
+ /*
+ * Track which NOT NULL OIDs must not be re-emitted out-of-line by
+ * emit_local_constraints:
+ *
+ * - For attislocal columns: the inline pass in append_column_defs
+ * materializes the constraint; a second ALTER TABLE would collide.
+ *
+ * - For inherited columns with a user-defined name (!is_auto):
+ * emit_create_table_stmt emits these as table-level CONSTRAINT
+ * clauses in the CREATE TABLE or PARTITION OF body so the name is
+ * preserved when the inherited NOT NULL is established. An
+ * out-of-line ALTER TABLE ADD CONSTRAINT would collide with the
+ * already-propagated inherited NOT NULL.
+ *
+ * - For inherited columns with an auto-name on a partition child:
+ * PARTITION OF re-creates an equivalent constraint (possibly under
+ * the parent-derived name), making the out-of-line ALTER TABLE
+ * redundant and collision-prone. Suppress it; the auto-name
+ * change is acceptable.
+ *
+ * For inherited columns with an auto-name on a plain INHERITS child,
+ * the out-of-line ALTER TABLE is still safe and preserves the name.
+ */
+ if (skip_oids != NULL &&
+ (att->attislocal ||
+ !entries[attnum].is_auto ||
+ rel->rd_rel->relispartition))
+ *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;
+
+ 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);
+
+ autoname = makeObjectName(RelationGetRelationName(rel),
+ NameStr(att->attname), "seq");
+ 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);
+ }
+ pfree(autoname);
+ 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];
+
+ /*
+ * Suppress NOT NULL when the constraint is inherited-only
+ * (conislocal=false). The INHERITS clause already propagates it;
+ * emitting it here would make conislocal true on replay. When
+ * there is no pg_constraint row at all (pre-PG18 catalog with
+ * attnotnull=true), inherited_notnull is false so we fall through
+ * and emit plain NOT NULL as before.
+ */
+ if (!nn->inherited_notnull)
+ {
+ 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);
+ /* inherited-only NOT NULL is not an override; treat as absent */
+ has_notnull = att->attnotnull &&
+ !nn_entries[att->attnum].inherited_notnull;
+
+ 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", parentQual);
+
+ /*
+ * Include NOT NULL constraints in the PARTITION OF column spec
+ * when the child's constraint name differs from what automatic
+ * inheritance would give. When PARTITION OF runs, the parent's
+ * NOT NULL propagates to the child under the parent's constraint
+ * name. If the child carries a different name - whether from a
+ * user-specified CONSTRAINT clause or from a pre-existing table
+ * that was later attached as a partition - we must specify it in
+ * the column spec so the name is preserved on replay.
+ *
+ * We scan pg_constraint directly rather than relying on nn_entries,
+ * because after ATTACH PARTITION the child's constraint can have
+ * conislocal=false (purely inherited) while still carrying the
+ * original local name - a case that collect_local_not_null tracks
+ * only as inherited_notnull without preserving the name.
+ */
+ {
+ Relation conRel;
+ SysScanDesc conScan;
+ ScanKeyData conKey;
+ HeapTuple conTup;
+ StringInfoData colspec;
+ bool hasspecs = false;
+
+ initStringInfo(&colspec);
+
+ 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);
+ Datum conkeyDat;
+ bool conkeyNull;
+ ArrayType *conkeyArr;
+ AttrNumber attnum;
+ char *conname;
+ char *parent_conname;
+ char *attname;
+
+ if (con->contype != CONSTRAINT_NOTNULL)
+ continue;
+
+ conkeyDat = heap_getattr(conTup, Anum_pg_constraint_conkey,
+ RelationGetDescr(conRel), &conkeyNull);
+ if (conkeyNull)
+ continue;
+ conkeyArr = DatumGetArrayTypeP(conkeyDat);
+ if (ARR_NDIM(conkeyArr) != 1 || ARR_DIMS(conkeyArr)[0] < 1 ||
+ ARR_HASNULL(conkeyArr) || ARR_ELEMTYPE(conkeyArr) != INT2OID)
+ continue;
+ attnum = ((int16 *) ARR_DATA_PTR(conkeyArr))[0];
+
+ attname = get_attname(ctx->relid, attnum, true);
+ if (attname == NULL)
+ continue;
+
+ conname = pstrdup(NameStr(con->conname));
+
+ /*
+ * Check if this name is what PARTITION OF would auto-create.
+ * The parent propagates its own NOT NULL name to the child.
+ * Look up the parent's NOT NULL constraint name for this column.
+ */
+ parent_conname = NULL;
+ {
+ AttrNumber parentAttnum = get_attnum(parentOid, attname);
+
+ if (parentAttnum != InvalidAttrNumber)
+ {
+ Relation pconRel =
+ table_open(ConstraintRelationId, AccessShareLock);
+ ScanKeyData pconKey;
+ SysScanDesc pconScan;
+ HeapTuple pconTup;
+
+ ScanKeyInit(&pconKey,
+ Anum_pg_constraint_conrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(parentOid));
+ pconScan = systable_beginscan(pconRel,
+ ConstraintRelidTypidNameIndexId,
+ true, NULL, 1, &pconKey);
+ while (HeapTupleIsValid(pconTup = systable_getnext(pconScan)))
+ {
+ Form_pg_constraint pcon =
+ (Form_pg_constraint) GETSTRUCT(pconTup);
+ Datum pconkeyDat;
+ bool pconkeyNull;
+ ArrayType *pconkeyArr;
+
+ if (pcon->contype != CONSTRAINT_NOTNULL)
+ continue;
+ pconkeyDat = heap_getattr(pconTup,
+ Anum_pg_constraint_conkey,
+ RelationGetDescr(pconRel),
+ &pconkeyNull);
+ if (pconkeyNull)
+ continue;
+ pconkeyArr = DatumGetArrayTypeP(pconkeyDat);
+ if (ARR_NDIM(pconkeyArr) == 1 &&
+ ARR_DIMS(pconkeyArr)[0] >= 1 &&
+ !ARR_HASNULL(pconkeyArr) &&
+ ARR_ELEMTYPE(pconkeyArr) == INT2OID &&
+ ((int16 *) ARR_DATA_PTR(pconkeyArr))[0] ==
+ parentAttnum)
+ {
+ parent_conname = pstrdup(NameStr(pcon->conname));
+ break;
+ }
+ }
+ systable_endscan(pconScan);
+ table_close(pconRel, AccessShareLock);
+ }
+ }
+
+ /*
+ * The child's NOT NULL needs inline specification when its name
+ * differs from the parent's constraint name (which is what
+ * PARTITION OF would automatically propagate). If the parent has
+ * no NOT NULL on this column, the child's constraint was purely
+ * local and also needs to be specified inline.
+ */
+ if (parent_conname == NULL ||
+ strcmp(conname, parent_conname) != 0)
+ {
+ bool no_inherit = con->connoinherit;
+
+ if (hasspecs)
+ appendStringInfoChar(&colspec, ',');
+ if (ctx->pretty)
+ appendStringInfoString(&colspec, "\n ");
+ else if (hasspecs)
+ appendStringInfoChar(&colspec, ' ');
+ hasspecs = true;
+
+ appendStringInfo(&colspec, "CONSTRAINT %s NOT NULL %s",
+ quote_identifier(conname),
+ quote_identifier(attname));
+ if (no_inherit)
+ appendStringInfoString(&colspec, " NO INHERIT");
+
+ /*
+ * Also add to skip_notnull_oids so emit_local_constraints
+ * does not try to emit it again as ALTER TABLE ADD CONSTRAINT.
+ */
+ ctx->skip_notnull_oids =
+ lappend_oid(ctx->skip_notnull_oids, con->oid);
+ }
+
+ pfree(conname);
+ pfree(attname);
+ if (parent_conname)
+ pfree(parent_conname);
+ }
+
+ systable_endscan(conScan);
+ table_close(conRel, AccessShareLock);
+
+ if (hasspecs)
+ {
+ if (ctx->pretty)
+ appendStringInfo(&ctx->buf, " (\n%s\n)", colspec.data);
+ else
+ appendStringInfo(&ctx->buf, " (%s)", colspec.data);
+ }
+ pfree(colspec.data);
+ }
+
+ appendStringInfo(&ctx->buf, " %s", 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;
+ int buf_len_before;
+
+ appendStringInfoString(&ctx->buf, " (");
+
+ buf_len_before = ctx->buf.len;
+ append_column_defs(&ctx->buf, rel, ctx->pretty,
+ is_kind_included(ctx, TABLE_DDL_KIND_CHECK),
+ ctx->schema_qualified, ctx->nn_entries);
+
+ /*
+ * For INHERITS children, inherited columns with a local NOT NULL
+ * carrying a user-defined name need a table-level CONSTRAINT clause
+ * here. Without it, when the parent's NOT NULL is added and
+ * propagates to the child, a subsequent out-of-line ALTER TABLE ADD
+ * CONSTRAINT fails because a NOT NULL already covers the column.
+ * Emitting the constraint inline at CREATE TABLE time causes it to
+ * merge correctly with the parent's propagated constraint.
+ */
+ {
+ TupleDesc tupdesc = RelationGetDescr(rel);
+ bool any_content = (ctx->buf.len > buf_len_before);
+
+ for (int i = 0; i < tupdesc->natts; i++)
+ {
+ Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+ LocalNotNullEntry *nn;
+
+ if (att->attisdropped || att->attislocal)
+ continue;
+
+ nn = &ctx->nn_entries[att->attnum];
+ if (nn->conoid == InvalidOid || nn->is_auto)
+ continue;
+
+ if (any_content)
+ appendStringInfoChar(&ctx->buf, ',');
+ if (ctx->pretty)
+ appendStringInfoString(&ctx->buf, "\n ");
+ else if (any_content)
+ appendStringInfoChar(&ctx->buf, ' ');
+ any_content = true;
+
+ appendStringInfo(&ctx->buf, "CONSTRAINT %s NOT NULL %s",
+ quote_identifier(nn->name),
+ quote_identifier(NameStr(att->attname)));
+ if (nn->no_inherit)
+ appendStringInfoString(&ctx->buf, " NO INHERIT");
+ }
+ }
+
+ 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 (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");
+ }
+
+ 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);
+ }
+ }
+
+ 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);
+}
+
+/*
+ * child_default_inherited_from_parent
+ * Return true if child_adbin is identical to the adbin stored for
+ * attname on any immediate parent of childOid.
+ *
+ * PostgreSQL copies the adbin expression verbatim from parent to child
+ * when a partition is created, so a byte-for-byte match means the child
+ * did not override the default locally. For regular inheritance the
+ * parent typically has no default at all (atthasdef=false), so the
+ * function returns false and the override is correctly emitted.
+ */
+static bool
+child_default_inherited_from_parent(Oid childOid, const char *attname,
+ const char *child_adbin)
+{
+ List *parents;
+ ListCell *lc;
+ bool result = false;
+
+ parents = find_inheritance_parents(childOid, NoLock);
+
+ foreach(lc, parents)
+ {
+ Oid parentOid = lfirst_oid(lc);
+ Relation parentRel;
+ AttrNumber parentAttnum;
+ TupleConstr *constr;
+
+ parentRel = try_table_open(parentOid, AccessShareLock);
+ if (parentRel == NULL)
+ continue;
+
+ parentAttnum = get_attnum(parentOid, attname);
+ if (parentAttnum != InvalidAttrNumber)
+ {
+ Form_pg_attribute parentAtt =
+ TupleDescAttr(RelationGetDescr(parentRel), parentAttnum - 1);
+
+ if (parentAtt->atthasdef)
+ {
+ constr = RelationGetDescr(parentRel)->constr;
+ if (constr != NULL)
+ {
+ for (int j = 0; j < constr->num_defval; j++)
+ {
+ if (constr->defval[j].adnum == parentAttnum &&
+ strcmp(constr->defval[j].adbin, child_adbin) == 0)
+ {
+ result = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ table_close(parentRel, AccessShareLock);
+ if (result)
+ break;
+ }
+
+ list_free(parents);
+ return result;
+}
+
+/*
+ * parent_has_default_for_col
+ * Return true if any immediate parent of childOid has a non-generated
+ * DEFAULT expression on the column named attname.
+ *
+ * Used to detect inherited columns where the child dropped the parent's
+ * default: after PARTITION OF / INHERITS the parent default is re-applied,
+ * so we must emit ALTER TABLE ONLY child ALTER COLUMN col DROP DEFAULT.
+ */
+static bool
+parent_has_default_for_col(Oid childOid, const char *attname)
+{
+ List *parents;
+ ListCell *lc;
+ bool result = false;
+
+ parents = find_inheritance_parents(childOid, NoLock);
+ foreach(lc, parents)
+ {
+ Oid parentOid = lfirst_oid(lc);
+ AttrNumber parentAttnum = get_attnum(parentOid, attname);
+
+ if (parentAttnum != InvalidAttrNumber)
+ {
+ HeapTuple attTup = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(parentOid),
+ Int16GetDatum(parentAttnum));
+
+ if (HeapTupleIsValid(attTup))
+ {
+ Form_pg_attribute parentAtt = (Form_pg_attribute) GETSTRUCT(attTup);
+
+ if (parentAtt->atthasdef && parentAtt->attgenerated == '\0')
+ result = true;
+ ReleaseSysCache(attTup);
+ }
+ }
+ if (result)
+ break;
+ }
+ list_free(parents);
+ return result;
+}
+
+/*
+ * emit_child_default_overrides
+ * ALTER TABLE ONLY qualname ALTER COLUMN col SET DEFAULT expr - one per
+ * inherited (attislocal=false) non-generated column that carries a
+ * default which differs from every immediate parent's default. Using
+ * ONLY prevents the statement from cascading into grandchildren and
+ * overwriting their own defaults. Defaults that are simply inherited
+ * unchanged from a parent are skipped to avoid redundant output.
+ * Generated columns are skipped: their expression is inherited
+ * automatically and SET DEFAULT would fail at replay.
+ *
+ * Also emits ALTER TABLE ONLY ... DROP DEFAULT for inherited columns
+ * where the child explicitly dropped the parent's default: after
+ * PARTITION OF / INHERITS the parent default is re-applied, and the
+ * DROP DEFAULT is needed to restore the child's state.
+ */
+static void
+emit_child_default_overrides(TableDdlContext *ctx)
+{
+ TupleDesc tupdesc = RelationGetDescr(ctx->rel);
+ TupleConstr *constr = tupdesc->constr;
+ List *dpcontext = NIL;
+
+ for (int i = 0; i < tupdesc->natts; i++)
+ {
+ Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+ const char *adbin = NULL;
+ char *defstr;
+
+ if (att->attisdropped || att->attislocal)
+ continue;
+ if (att->attgenerated != '\0')
+ continue;
+
+ if (!att->atthasdef)
+ {
+ /*
+ * Column has no default but a parent does: after PARTITION OF /
+ * INHERITS the parent's default is re-applied, so emit DROP
+ * DEFAULT to restore the dropped state.
+ */
+ if (parent_has_default_for_col(ctx->relid, NameStr(att->attname)))
+ {
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE ONLY %s ALTER COLUMN %s DROP DEFAULT;",
+ ctx->qualname,
+ quote_identifier(NameStr(att->attname)));
+ append_stmt(ctx);
+ }
+ continue;
+ }
+
+ /* Locate the raw adbin for parent-comparison and for deparse. */
+ if (constr != NULL)
+ {
+ for (int j = 0; j < constr->num_defval; j++)
+ {
+ if (constr->defval[j].adnum == att->attnum)
+ {
+ adbin = constr->defval[j].adbin;
+ break;
+ }
+ }
+ }
+ if (adbin == NULL)
+ continue;
+
+ /*
+ * Skip columns whose default expression is identical to a parent's;
+ * the child merely inherited it (typical for partition children).
+ * An explicit local override will have a different adbin.
+ */
+ if (child_default_inherited_from_parent(ctx->relid,
+ NameStr(att->attname),
+ adbin))
+ continue;
+
+ defstr = find_attrdef_text(ctx->rel, att->attnum, &dpcontext);
+ if (defstr == NULL)
+ continue;
+
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE ONLY %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_typed_column_storage
+ * For typed tables (CREATE TABLE ... OF type_name), emit per-column
+ * STORAGE and COMPRESSION overrides as ALTER TABLE statements.
+ *
+ * The typed-table grammar allows only DEFAULT / NOT NULL / CHECK in the
+ * CREATE TABLE column-options list (columnOptions -> ColQualList), so
+ * STORAGE and COMPRESSION cannot appear inline there. They must be
+ * emitted post-CREATE, matching what pg_dump produces.
+ */
+static void
+emit_typed_column_storage(TableDdlContext *ctx)
+{
+ TupleDesc tupdesc;
+
+ if (!OidIsValid(ctx->rel->rd_rel->reloftype))
+ return;
+
+ tupdesc = RelationGetDescr(ctx->rel);
+
+ for (int i = 0; i < tupdesc->natts; i++)
+ {
+ Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+
+ if (att->attisdropped)
+ continue;
+
+ if (att->attstorage != get_typstorage(att->atttypid))
+ {
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE ONLY %s ALTER COLUMN %s SET STORAGE %s;",
+ ctx->qualname,
+ quote_identifier(NameStr(att->attname)),
+ storage_name(att->attstorage));
+ append_stmt(ctx);
+ }
+
+ if (CompressionMethodIsValid(att->attcompression))
+ {
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE ONLY %s ALTER COLUMN %s SET COMPRESSION %s;",
+ ctx->qualname,
+ quote_identifier(NameStr(att->attname)),
+ GetCompressionMethodName(att->attcompression));
+ append_stmt(ctx);
+ }
+ }
+}
+
+/*
+ * 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);
+ HeapTuple indTup;
+ Form_pg_index idxform;
+ char *idxdef;
+ int16 indnkeyatts;
+
+ if (OidIsValid(get_index_constraint(idxoid)))
+ continue;
+
+ /*
+ * Skip indexes that were inherited from a parent partitioned index.
+ * They are created automatically when the parent index DDL is
+ * replayed and the partition is attached, so emitting them separately
+ * would produce a "relation already exists" error on replay.
+ */
+ if (get_rel_relispartition(idxoid))
+ continue;
+
+ indTup = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(idxoid));
+ if (!HeapTupleIsValid(indTup))
+ continue;
+ idxform = (Form_pg_index) GETSTRUCT(indTup);
+
+ /* Skip invalid indexes; they may be left over from a failed CREATE INDEX CONCURRENTLY. */
+ if (!idxform->indisvalid)
+ {
+ ReleaseSysCache(indTup);
+ continue;
+ }
+
+ indnkeyatts = idxform->indnkeyatts;
+ ReleaseSysCache(indTup);
+
+ idxdef = pg_get_indexdef_ddl(idxoid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf, "%s;", idxdef);
+ append_stmt(ctx);
+ pfree(idxdef);
+
+ /*
+ * Emit ALTER INDEX ... ALTER COLUMN n SET STATISTICS for any key
+ * column that has a non-default statistics target. Expression
+ * columns (indnkeyatts covers them) are the only kind that can have
+ * a statistics target on an index.
+ */
+ {
+ char *idxqualname = lookup_relname_for_emit(idxoid,
+ ctx->schema_qualified,
+ ctx->base_namespace);
+
+ for (int j = 1; j <= indnkeyatts; j++)
+ {
+ HeapTuple attTup = SearchSysCache2(ATTNUM,
+ ObjectIdGetDatum(idxoid),
+ Int16GetDatum(j));
+
+ if (HeapTupleIsValid(attTup))
+ {
+ bool isnull;
+ Datum stattargetDatum =
+ SysCacheGetAttr(ATTNUM, attTup,
+ Anum_pg_attribute_attstattarget,
+ &isnull);
+
+ if (!isnull && DatumGetInt16(stattargetDatum) >= 0)
+ {
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER INDEX %s ALTER COLUMN %d SET STATISTICS %d;",
+ idxqualname, j,
+ (int) DatumGetInt16(stattargetDatum));
+ append_stmt(ctx);
+ }
+ ReleaseSysCache(attTup);
+ }
+ }
+ pfree(idxqualname);
+ }
+ }
+ list_free(indexoids);
+}
+
+/*
+ * emit_cluster_on
+ * ALTER TABLE qualname CLUSTER ON index_name - emitted when any index
+ * on the relation has indisclustered=true.
+ */
+static void
+emit_cluster_on(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);
+ HeapTuple indTup;
+ Form_pg_index idxform;
+
+ indTup = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(idxoid));
+ if (!HeapTupleIsValid(indTup))
+ continue;
+ idxform = (Form_pg_index) GETSTRUCT(indTup);
+
+ if (idxform->indisclustered)
+ {
+ char *idxname = get_rel_name(idxoid);
+
+ ReleaseSysCache(indTup);
+ if (idxname != NULL)
+ {
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE %s CLUSTER ON %s;",
+ ctx->qualname,
+ quote_identifier(idxname));
+ append_stmt(ctx);
+ pfree(idxname);
+ }
+ break; /* at most one index per table can be clustered */
+ }
+ ReleaseSysCache(indTup);
+ }
+ 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).
+ *
+ * NOT NULL constraints are tracked via skip_notnull_oids to avoid
+ * double-emission. The set covers: (a) local columns, where the
+ * column-emit helpers materialise the constraint inline; (b) inherited
+ * columns with a user-defined name, where emit_create_table_stmt
+ * emits a table-level CONSTRAINT clause so the name survives the
+ * PARTITION OF / INHERITS replay; and (c) auto-named NOT NULLs on
+ * partition children, which are re-created by PARTITION OF and would
+ * collide with an out-of-line ALTER TABLE ADD CONSTRAINT. Any NOT
+ * NULL not in skip_notnull_oids is emitted here (e.g. inherited
+ * columns with an auto-name on a plain INHERITS child).
+ *
+ * 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;
+ List *fk_stmts = NIL;
+
+ 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.
+ *
+ * skip_notnull_oids covers all cases where NOT NULL was
+ * already materialised inline: local columns (via
+ * append_column_defs), inherited columns with a user-defined
+ * name (via the table-level CONSTRAINT clause emitted by
+ * emit_create_table_stmt), and auto-named NOT NULLs on
+ * partition children (re-created by PARTITION OF).
+ */
+ if (!is_kind_included(ctx, TABLE_DDL_KIND_TABLE))
+ continue;
+ if (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);
+
+ /*
+ * Defer FK statements so they are emitted after all other
+ * constraints. A self-referencing FK (REFERENCES same_table)
+ * requires the PK/UNIQUE it targets to exist first, and because
+ * the catalog scan returns constraints in name order, an FK whose
+ * name sorts before the PK name would otherwise be emitted first
+ * and fail with "there is no unique constraint matching given
+ * keys".
+ */
+ if (con->contype == CONSTRAINT_FOREIGN)
+ fk_stmts = lappend(fk_stmts, pstrdup(ctx->buf.data));
+ else
+ append_stmt(ctx);
+
+ pfree(conbody);
+ }
+ }
+ systable_endscan(conScan);
+ table_close(conRel, AccessShareLock);
+
+ /* Append deferred FK statements after all other constraints. */
+ ctx->statements = list_concat(ctx->statements, fk_stmts);
+}
+
+/*
+ * 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 enabled = ctx->rel->rd_rules->rules[i]->enabled;
+ char *ruledef_str;
+
+ ruledef_str = pg_get_ruledef_ddl(ruleid);
+ resetStringInfo(&ctx->buf);
+ appendStringInfoString(&ctx->buf, ruledef_str);
+ append_stmt(ctx);
+ pfree(ruledef_str);
+
+ /*
+ * If the rule's firing state differs from the default (ORIGIN),
+ * emit the appropriate ALTER TABLE ... ENABLE/DISABLE RULE.
+ * We need the rule's name from pg_rewrite for this statement.
+ */
+ if (enabled != RULE_FIRES_ON_ORIGIN)
+ {
+ Relation rewriteRel;
+ ScanKeyData scankey;
+ SysScanDesc scan;
+ HeapTuple ruleTup;
+ char *rulename;
+
+ rewriteRel = table_open(RewriteRelationId, AccessShareLock);
+ ScanKeyInit(&scankey,
+ Anum_pg_rewrite_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(ruleid));
+ scan = systable_beginscan(rewriteRel, RewriteOidIndexId,
+ true, NULL, 1, &scankey);
+ ruleTup = systable_getnext(scan);
+ if (!HeapTupleIsValid(ruleTup))
+ elog(ERROR, "cache lookup failed for rule %u", ruleid);
+ rulename = pstrdup(NameStr(((Form_pg_rewrite) GETSTRUCT(ruleTup))->rulename));
+ systable_endscan(scan);
+ table_close(rewriteRel, AccessShareLock);
+
+ switch (enabled)
+ {
+ case RULE_DISABLED:
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE %s DISABLE RULE %s;",
+ ctx->qualname,
+ quote_identifier(rulename));
+ append_stmt(ctx);
+ break;
+ case RULE_FIRES_ON_REPLICA:
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE %s ENABLE REPLICA RULE %s;",
+ ctx->qualname,
+ quote_identifier(rulename));
+ append_stmt(ctx);
+ break;
+ case RULE_FIRES_ALWAYS:
+ resetStringInfo(&ctx->buf);
+ appendStringInfo(&ctx->buf,
+ "ALTER TABLE %s ENABLE ALWAYS RULE %s;",
+ ctx->qualname,
+ quote_identifier(rulename));
+ append_stmt(ctx);
+ break;
+ default:
+ break;
+ }
+ pfree(rulename);
+ }
+ }
+}
+
+/*
+ * 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;
+
+ /*
+ * When schema_qualified is false, the contract is that the output
+ * is replayable with the parent's schema in search_path. That
+ * contract cannot hold for a child that lives in a different schema:
+ * its own name would be ambiguous (landing in whatever schema is
+ * first in search_path) and references back to the parent would have
+ * to be schema-qualified anyway. Force full qualification so the
+ * child's DDL is unambiguous regardless of the caller's search_path.
+ */
+ if (!childctx.schema_qualified &&
+ get_rel_namespace(childoid) != ctx->base_namespace)
+ childctx.schema_qualified = true;
+
+ if (childctx.only_kinds != NULL &&
+ bms_is_member((int) TABLE_DDL_KIND_PARTITION,
+ childctx.only_kinds))
+ {
+ Bitmapset *child_only = bms_copy(childctx.only_kinds);
+
+ child_only = bms_del_member(child_only,
+ (int) TABLE_DDL_KIND_PARTITION);
+ if (bms_is_empty(child_only))
+ {
+ bms_free(child_only);
+ childctx.only_kinds = NULL;
+ }
+ else
+ childctx.only_kinds = child_only;
+ }
+
+ childstmts = pg_get_table_ddl_internal(&childctx);
+ ctx->statements = list_concat(ctx->statements, childstmts);
+ }
+ list_free(children);
+}
+
+/*
+ * pg_get_table_ddl_internal
+ * Generate DDL statements to recreate a regular or partitioned table.
+ *
+ * The caller initializes *ctx with the user-supplied option fields
+ * (relid, pretty, no_owner, no_tablespace, schema_qualified,
+ * only_kinds, except_kinds). This function opens the relation,
+ * validates access, populates the derived fields (rel, qualname,
+ * nn_entries, ...), runs the emission passes, and returns the
+ * accumulated statement list.
+ *
+ * Each emission helper consults is_kind_included() to decide whether
+ * it should run. The table-proper passes (CREATE TABLE / OWNER /
+ * ALTER COLUMN ... SET DEFAULT / ALTER COLUMN ... SET (...)) are
+ * grouped under KIND_TABLE so the FK-only / sub-object-only workflow
+ * is expressible as a single only or except list.
+ *
+ * Trigger and policy emission are scaffolded but currently disabled
+ * (#if 0) - they will become a single helper call once the standalone
+ * pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+ */
+static List *
+pg_get_table_ddl_internal(TableDdlContext *ctx)
+{
+ Relation rel;
+ char relkind;
+ AclResult aclresult;
+
+ rel = table_open(ctx->relid, AccessShareLock);
+
+ relkind = rel->rd_rel->relkind;
+
+ /*
+ * The initial cut only supports ordinary and partitioned tables. Views,
+ * matviews, foreign tables, sequences, indexes, composite types, and
+ * TOAST tables are out of scope for now.
+ */
+ if (relkind != RELKIND_RELATION && relkind != RELKIND_PARTITIONED_TABLE)
+ {
+ char *relname = pstrdup(RelationGetRelationName(rel));
+
+ table_close(rel, AccessShareLock);
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not an ordinary or partitioned table",
+ relname)));
+ }
+
+ /* Caller needs SELECT on the table to read its definition. */
+ aclresult = pg_class_aclcheck(ctx->relid, GetUserId(), ACL_SELECT);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLE,
+ RelationGetRelationName(rel));
+
+ /*
+ * Validation: if the table has REPLICA IDENTITY USING INDEX and the
+ * referenced index would not be emitted under the active filter, the
+ * emitted REPLICA IDENTITY clause would reference an index the same DDL
+ * never produced. Determine which kind would emit the index (one of
+ * primary_key / unique / exclusion for constraint-backed indexes,
+ * otherwise the generic "index" kind) and require it to be in scope
+ * whenever "replica_identity" is. The check uses is_kind_included so
+ * it covers both forms naturally: an "except" list that omits the
+ * source kind, and an "only" list that omits it.
+ */
+ if (rel->rd_rel->relreplident == REPLICA_IDENTITY_INDEX &&
+ is_kind_included(ctx, TABLE_DDL_KIND_REPLICA_IDENTITY))
+ {
+ Oid replidx = RelationGetReplicaIndex(rel);
+
+ if (OidIsValid(replidx))
+ {
+ TableDdlKind idx_kind = TABLE_DDL_KIND_INDEX;
+ Oid conoid = get_index_constraint(replidx);
+
+ if (OidIsValid(conoid))
+ {
+ HeapTuple conTup = SearchSysCache1(CONSTROID,
+ ObjectIdGetDatum(conoid));
+
+ if (HeapTupleIsValid(conTup))
+ {
+ Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(conTup);
+
+ switch (con->contype)
+ {
+ case CONSTRAINT_PRIMARY:
+ idx_kind = TABLE_DDL_KIND_PRIMARY_KEY;
+ break;
+ case CONSTRAINT_UNIQUE:
+ idx_kind = TABLE_DDL_KIND_UNIQUE;
+ break;
+ case CONSTRAINT_EXCLUSION:
+ idx_kind = TABLE_DDL_KIND_EXCLUSION;
+ break;
+ default:
+ break;
+ }
+ ReleaseSysCache(conTup);
+ }
+ }
+
+ if (!is_kind_included(ctx, idx_kind))
+ {
+ char *relname = pstrdup(RelationGetRelationName(rel));
+ const char *idx_name = table_ddl_kind_names[(int) idx_kind].name;
+
+ table_close(rel, AccessShareLock);
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("REPLICA IDENTITY for table \"%s\" requires kind \"%s\" to be emitted",
+ relname, idx_name),
+ errdetail("The table's REPLICA IDENTITY USING INDEX references an index produced by the \"%s\" kind, which is not in the active filter.",
+ idx_name),
+ errhint("Either add \"%s\" to the filter or remove \"replica_identity\" from it.",
+ idx_name)));
+ }
+ }
+ }
+
+ /*
+ * Populate derived fields now that the relation is open and validated.
+ * The remaining derived fields (nn_entries, skip_notnull_oids, buf,
+ * statements) start zeroed via the caller's `TableDdlContext ctx = {0}`
+ * initializer; the NOT NULL cache is populated by collect_local_not_null,
+ * the buffer is initStringInfo'd, and each emit pass appends to
+ * statements via lappend.
+ */
+ ctx->rel = rel;
+ ctx->base_namespace = RelationGetNamespace(rel);
+ ctx->save_nestlevel = -1;
+ ctx->qualname = lookup_relname_for_emit(ctx->relid, ctx->schema_qualified,
+ ctx->base_namespace);
+
+ /*
+ * Temporarily override search_path so that the ruleutils helpers
+ * (pg_get_indexdef_ddl, pg_get_constraintdef_body,
+ * pg_get_statisticsobjdef_ddl, pg_get_ruledef_ddl, etc.) produce names
+ * that match the schema_qualified flag. Those helpers decide whether to
+ * qualify a name by calling RelationIsVisible(), which checks whether the
+ * object's schema appears in the active search_path.
+ *
+ * schema_qualified = false: narrow to the base schema so that same-schema
+ * references in DEFAULT expressions, FK targets, indexes, rules, and
+ * statistics come out unqualified automatically. Cross-schema references
+ * stay qualified, which is the correctness requirement.
+ *
+ * schema_qualified = true: narrow to pg_catalog only so that objects in
+ * the base schema (or any user schema the caller placed on search_path)
+ * are not reachable without qualification, forcing fully-qualified output
+ * from every helper regardless of the caller's session search_path.
+ *
+ * AtEOXact_GUC cleans up at xact end if anything throws between here and
+ * the explicit restore below; on the normal path we restore right before
+ * returning.
+ */
+ if (!ctx->schema_qualified)
+ {
+ char *nspname = get_namespace_name(ctx->base_namespace);
+
+ if (nspname != NULL)
+ {
+ const char *qnsp = quote_identifier(nspname);
+
+ ctx->save_nestlevel = NewGUCNestLevel();
+ (void) set_config_option("search_path", qnsp,
+ PGC_USERSET, PGC_S_SESSION,
+ GUC_ACTION_SAVE, true, 0, false);
+ if (qnsp != nspname)
+ pfree((char *) qnsp);
+ pfree(nspname);
+ }
+ }
+ else
+ {
+ ctx->save_nestlevel = NewGUCNestLevel();
+ (void) set_config_option("search_path", "pg_catalog",
+ PGC_USERSET, PGC_S_SESSION,
+ GUC_ACTION_SAVE, true, 0, false);
+ }
+
+ /*
+ * Cache locally-declared NOT NULL constraint metadata so the column- emit
+ * helpers can produce "CONSTRAINT name NOT NULL" inline for user-named
+ * constraints, and so the constraint loop can avoid double-emitting them.
+ */
+ ctx->nn_entries = collect_local_not_null(rel, &ctx->skip_notnull_oids);
+
+ initStringInfo(&ctx->buf);
+
+ /*
+ * Emission passes. Order is significant: CREATE TABLE first; OWNER and
+ * the per-column ALTER COLUMN passes before sub-object emission;
+ * sub-objects in dependency-friendly order (indexes before constraints,
+ * since constraint-backed indexes are emitted out-of-line by the
+ * constraint loop); partition children last so the parent already exists
+ * at replay time. Each helper gates itself on is_kind_included for the
+ * relevant TABLE_DDL_KIND_*; the four "table itself" passes are grouped
+ * here under KIND_TABLE so all of CREATE TABLE / OWNER / SET DEFAULT /
+ * SET (...) drop out together when the user asks for only sub-objects
+ * (e.g. only => 'foreign_key' for the second pass of a two-pass FK
+ * clone).
+ */
+ if (is_kind_included(ctx, TABLE_DDL_KIND_TABLE))
+ {
+ emit_create_table_stmt(ctx);
+ emit_owner_stmt(ctx);
+ emit_child_default_overrides(ctx);
+ emit_attoptions(ctx);
+ emit_typed_column_storage(ctx);
+ }
+ emit_indexes(ctx);
+ emit_cluster_on(ctx);
+ emit_local_constraints(ctx);
+ emit_rules(ctx);
+ emit_statistics(ctx);
+ emit_replica_identity(ctx);
+ emit_rls_toggles(ctx);
+
+ /*
+ * Triggers and row-level security policies - disabled until the
+ * standalone pg_get_trigger_ddl() and pg_get_policy_ddl() helpers land.
+ * The scan + lock + filter scaffolding below is preserved (inside #if 0)
+ * so wiring up each emission becomes a one-liner in the loop body once
+ * those helpers are available.
+ */
+#if 0
+ if (is_kind_included(ctx, TABLE_DDL_KIND_TRIGGER))
+ {
+ Relation trigRel;
+ SysScanDesc trigScan;
+ ScanKeyData trigKey;
+ HeapTuple trigTup;
+
+ trigRel = table_open(TriggerRelationId, AccessShareLock);
+ ScanKeyInit(&trigKey,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(ctx->relid));
+ trigScan = systable_beginscan(trigRel, TriggerRelidNameIndexId,
+ true, NULL, 1, &trigKey);
+ while (HeapTupleIsValid(trigTup = systable_getnext(trigScan)))
+ {
+ Form_pg_trigger trg = (Form_pg_trigger) GETSTRUCT(trigTup);
+
+ if (trg->tgisinternal)
+ continue;
+
+ /* TODO: append pg_get_trigger_ddl(trg->oid) output here. */
+ }
+ systable_endscan(trigScan);
+ table_close(trigRel, AccessShareLock);
+ }
+
+ if (is_kind_included(ctx, TABLE_DDL_KIND_POLICY))
+ {
+ Relation polRel;
+ SysScanDesc polScan;
+ ScanKeyData polKey;
+ HeapTuple polTup;
+
+ polRel = table_open(PolicyRelationId, AccessShareLock);
+ ScanKeyInit(&polKey,
+ Anum_pg_policy_polrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(ctx->relid));
+ polScan = systable_beginscan(polRel, PolicyPolrelidPolnameIndexId,
+ true, NULL, 1, &polKey);
+ while (HeapTupleIsValid(polTup = systable_getnext(polScan)))
+ {
+ /* TODO: append pg_get_policy_ddl(relid, polname) output here. */
+ }
+ systable_endscan(polScan);
+ table_close(polRel, AccessShareLock);
+ }
+#endif
+
+ emit_partition_children(ctx);
+
+ {
+ int natts = RelationGetDescr(rel)->natts;
+
+ for (int i = 1; i <= natts; i++)
+ if (ctx->nn_entries[i].name != NULL)
+ pfree(ctx->nn_entries[i].name);
+ }
+ pfree(ctx->nn_entries);
+
+ table_close(rel, AccessShareLock);
+ pfree(ctx->buf.data);
+ pfree(ctx->qualname);
+ list_free(ctx->skip_notnull_oids);
+
+ /*
+ * Pop the narrowed search_path now that all helpers have run. Errors
+ * thrown earlier are cleaned up by AtEOXact_GUC at xact end.
+ */
+ if (ctx->save_nestlevel >= 0)
+ AtEOXact_GUC(true, ctx->save_nestlevel);
+
+ return ctx->statements;
+}
+
+/*
+ * pg_get_table_ddl
+ * Return DDL to recreate a table as a set of text rows.
+ */
+Datum
+pg_get_table_ddl(PG_FUNCTION_ARGS)
+{
+ FuncCallContext *funcctx;
+ List *statements;
+
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcontext;
+ TableDdlContext ctx = {0};
+
+ funcctx = SRF_FIRSTCALL_INIT();
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ if (PG_ARGISNULL(0))
+ {
+ MemoryContextSwitchTo(oldcontext);
+ SRF_RETURN_DONE(funcctx);
+ }
+
+ if (!PG_ARGISNULL(5) && !PG_ARGISNULL(6))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"only_kinds\" and \"except_kinds\" parameters are mutually exclusive")));
+
+ /* Option defaults (match proargdefaults in pg_proc.dat). */
+ ctx.relid = PG_GETARG_OID(0);
+ ctx.pretty = false;
+ ctx.no_owner = false; /* owner DEFAULT true -> no_owner = false */
+ ctx.no_tablespace = false; /* tablespace DEFAULT true -> no_tablespace = false */
+ ctx.schema_qualified = true;
+ ctx.only_kinds = NULL;
+ ctx.except_kinds = NULL;
+
+ /* Override defaults with any explicitly supplied values. */
+ if (!PG_ARGISNULL(1))
+ ctx.pretty = PG_GETARG_BOOL(1);
+ if (!PG_ARGISNULL(2))
+ ctx.no_owner = !PG_GETARG_BOOL(2);
+ if (!PG_ARGISNULL(3))
+ ctx.no_tablespace = !PG_GETARG_BOOL(3);
+ if (!PG_ARGISNULL(4))
+ ctx.schema_qualified = PG_GETARG_BOOL(4);
+ if (!PG_ARGISNULL(5))
+ ctx.only_kinds = parse_kind_array("only_kinds",
+ PG_GETARG_ARRAYTYPE_P(5));
+ if (!PG_ARGISNULL(6))
+ ctx.except_kinds = parse_kind_array("except_kinds",
+ PG_GETARG_ARRAYTYPE_P(6));
+
+ statements = pg_get_table_ddl_internal(&ctx);
+ funcctx->user_fctx = statements;
+ funcctx->max_calls = list_length(statements);
+
+ MemoryContextSwitchTo(oldcontext);
+ }
+
+ funcctx = SRF_PERCALL_SETUP();
+ statements = (List *) funcctx->user_fctx;
+
+ if (funcctx->call_cntr < funcctx->max_calls)
+ {
+ char *stmt;
+
+ stmt = list_nth(statements, funcctx->call_cntr);
+
+ SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+ }
+ else
+ {
+ list_free_deep(statements);
+ SRF_RETURN_DONE(funcctx);
+ }
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 819631781c0..2f3e543f455 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,
@@ -605,6 +606,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)
@@ -1243,6 +1257,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)
@@ -1971,7 +2001,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();
@@ -1986,7 +2016,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);
}
/*
@@ -1999,7 +2042,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();
@@ -2011,7 +2054,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;
@@ -2071,10 +2115,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
@@ -2165,10 +2217,9 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
{
Node *expr = (Node *) lfirst(lc);
char *str;
- int prettyFlags = PRETTYFLAG_PAREN;
str = deparse_expression_pretty(expr, context, false, false,
- prettyFlags, 0);
+ PRETTYFLAG_PAREN, 0);
if (colno > 0)
appendStringInfoString(&buf, ", ");
@@ -2546,6 +2597,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 3cb84359adf..d86af56ba78 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8634,6 +8634,14 @@
proargtypes => 'regdatabase bool bool bool',
proargnames => '{database,pretty,owner,tablespace}',
proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' },
+{ oid => '8215', descr => 'get DDL to recreate a table',
+ proname => 'pg_get_table_ddl', prorows => '50',
+ proisstrict => 'f', proretset => 't', provolatile => 's', proparallel => 'r',
+ pronargdefaults => '6', prorettype => 'text',
+ proargtypes => 'regclass bool bool bool bool _text _text',
+ proargnames => '{relation,pretty,owner,tablespace,schema_qualified,only_kinds,except_kinds}',
+ proargdefaults => '{false,true,true,true,NULL,NULL}',
+ prosrc => 'pg_get_table_ddl' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index c3d8518cb62..d33563fdead 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -92,6 +92,7 @@ extern void check_of_type(HeapTuple typetuple);
extern void register_on_commit_action(Oid relid, OnCommitAction action);
extern void remove_on_commit_action(Oid relid);
+extern OnCommitAction get_on_commit_action(Oid relid);
extern void PreCommit_on_commit_actions(void);
extern void AtEOXact_on_commit_actions(bool isCommit);
@@ -108,4 +109,6 @@ extern void RangeVarCallbackOwnsRelation(const RangeVar *relation,
extern bool PartConstraintImpliedByRelConstraint(Relation scanrel,
List *partConstraint);
+extern const char *storage_name(char c);
+
#endif /* TABLECMDS_H */
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 25c05e2f649..c3b3dc65c93 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -25,6 +25,7 @@ typedef struct PlannedStmt PlannedStmt;
#define RULE_INDEXDEF_KEYS_ONLY 0x02 /* ignore included attributes */
extern char *pg_get_indexdef_string(Oid indexrelid);
+extern char *pg_get_indexdef_ddl(Oid indexrelid);
extern char *pg_get_indexdef_columns(Oid indexrelid, bool pretty);
extern char *pg_get_indexdef_columns_extended(Oid indexrelid,
uint16 flags);
@@ -34,6 +35,7 @@ extern char *pg_get_partkeydef_columns(Oid relid, bool pretty);
extern char *pg_get_partconstrdef_string(Oid partitionId, char *aliasname);
extern char *pg_get_constraintdef_command(Oid constraintId);
+extern char *pg_get_constraintdef_body(Oid constraintId);
extern char *deparse_expression(Node *expr, List *dpcontext,
bool forceprefix, bool showimplicit);
extern List *deparse_context_for(const char *aliasname, Oid relid);
@@ -54,5 +56,7 @@ extern char *get_range_partbound_string(List *bound_datums);
extern void get_reloptions(StringInfo buf, Datum reloptions);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern char *pg_get_statisticsobjdef_ddl(Oid statextid);
+extern char *pg_get_ruledef_ddl(Oid ruleoid);
#endif /* RULEUTILS_H */
diff --git a/src/test/regress/expected/pg_get_table_ddl.out b/src/test/regress/expected/pg_get_table_ddl.out
new file mode 100644
index 00000000000..8e3b94c11f6
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,1612 @@
+--
+-- pg_get_table_ddl
+--
+-- All tests pass owner=>false so the ALTER TABLE OWNER TO line is not
+-- emitted, keeping output stable across test runners (which may run under
+-- different role names).
+--
+CREATE SCHEMA pgtbl_ddl_test;
+SET search_path = pgtbl_ddl_test;
+-- Basic table with PRIMARY KEY, NOT NULL, DEFAULT, COLLATE.
+CREATE TABLE basic (
+ id int PRIMARY KEY,
+ name text NOT NULL DEFAULT 'anon' COLLATE "C"
+);
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.basic (id integer NOT NULL, name text COLLATE "C" DEFAULT 'anon'::text NOT NULL);
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- Identity columns (with default and custom sequence options).
+CREATE TABLE id_cols (
+ id_always int GENERATED ALWAYS AS IDENTITY,
+ id_default int GENERATED BY DEFAULT AS IDENTITY
+);
+SELECT * FROM pg_get_table_ddl('id_cols'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.id_cols (id_always integer GENERATED ALWAYS AS IDENTITY NOT NULL, id_default integer GENERATED BY DEFAULT AS IDENTITY NOT NULL);
+(1 row)
+
+CREATE TABLE id_custom (
+ v int GENERATED ALWAYS AS IDENTITY (
+ SEQUENCE NAME id_custom_v_seq
+ START WITH 100 INCREMENT BY 5
+ MINVALUE 50 MAXVALUE 1000
+ CACHE 10 CYCLE
+ )
+);
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.id_custom (v integer GENERATED ALWAYS AS IDENTITY (START WITH 100 INCREMENT BY 5 MINVALUE 50 MAXVALUE 1000 CACHE 10 CYCLE) NOT NULL);
+(1 row)
+
+-- Generated stored column.
+CREATE TABLE gen_cols (
+ cents int,
+ dollars numeric GENERATED ALWAYS AS (cents / 100.0) STORED
+);
+SELECT * FROM pg_get_table_ddl('gen_cols'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.gen_cols (cents integer, dollars numeric GENERATED ALWAYS AS (((cents)::numeric / 100.0)) STORED);
+(1 row)
+
+-- Generated virtual column.
+CREATE TABLE gen_virtual (
+ base int,
+ derived int GENERATED ALWAYS AS (base * 2) VIRTUAL
+);
+SELECT * FROM pg_get_table_ddl('gen_virtual'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.gen_virtual (base integer, derived integer GENERATED ALWAYS AS ((base * 2)) VIRTUAL);
+(1 row)
+
+-- STORAGE and COMPRESSION (only emitted when non-default for the type).
+CREATE TABLE storage_cols (
+ a text STORAGE EXTERNAL,
+ b text STORAGE MAIN,
+ c text COMPRESSION pglz
+);
+SELECT * FROM pg_get_table_ddl('storage_cols'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.storage_cols (a text STORAGE EXTERNAL, b text STORAGE MAIN, c text COMPRESSION pglz);
+(1 row)
+
+-- Constraints: CHECK, UNIQUE, FOREIGN KEY (DEFERRABLE).
+CREATE TABLE refd (id int PRIMARY KEY);
+CREATE TABLE cons (
+ a int CHECK (a > 0),
+ b int UNIQUE,
+ c int REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED
+);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(3 rows)
+
+-- UNIQUE NULLS NOT DISTINCT (PG 15): null values are treated as equal for
+-- uniqueness purposes, so the NULLS NOT DISTINCT clause must be emitted.
+CREATE TABLE nulls_nd (
+ a int,
+ b int,
+ UNIQUE NULLS NOT DISTINCT (a, b)
+);
+SELECT * FROM pg_get_table_ddl('nulls_nd'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.nulls_nd (a integer, b integer);
+ ALTER TABLE pgtbl_ddl_test.nulls_nd ADD CONSTRAINT nulls_nd_a_b_key UNIQUE NULLS NOT DISTINCT (a, b);
+(2 rows)
+
+-- INCLUDE columns on a constraint-backed UNIQUE index (PG 11): the
+-- covering columns appear in an INCLUDE (...) clause on the ALTER TABLE
+-- ADD CONSTRAINT statement, not in the key column list.
+CREATE TABLE idx_inc (
+ id int PRIMARY KEY,
+ name text,
+ extra text,
+ UNIQUE (name) INCLUDE (extra)
+);
+SELECT * FROM pg_get_table_ddl('idx_inc'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idx_inc (id integer NOT NULL, name text, extra text);
+ ALTER TABLE pgtbl_ddl_test.idx_inc ADD CONSTRAINT idx_inc_name_extra_key UNIQUE (name) INCLUDE (extra);
+ ALTER TABLE pgtbl_ddl_test.idx_inc ADD CONSTRAINT idx_inc_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- FOREIGN KEY with ON DELETE / ON UPDATE referential actions and MATCH
+-- clause. Each variant must appear verbatim in the reconstructed DDL.
+CREATE TABLE fk_acts_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_acts (
+ a int,
+ b int,
+ c int,
+ d int,
+ CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES fk_acts_tgt(id)
+ ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET NULL,
+ CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET DEFAULT,
+ CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES fk_acts_tgt(id)
+ MATCH FULL
+);
+SELECT * FROM pg_get_table_ddl('fk_acts'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.fk_acts (a integer, b integer, c integer, d integer);
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON UPDATE RESTRICT ON DELETE CASCADE;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) MATCH FULL;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON DELETE SET DEFAULT;
+ ALTER TABLE pgtbl_ddl_test.fk_acts ADD CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES pgtbl_ddl_test.fk_acts_tgt(id) ON DELETE SET NULL;
+(5 rows)
+
+-- NOT ENFORCED foreign key (PG 17): the constraint is recorded in the
+-- catalog but not validated by the engine; NOT ENFORCED must appear in
+-- the reconstructed DDL.
+CREATE TABLE notenf_tgt (id int PRIMARY KEY);
+CREATE TABLE notenf (
+ a int,
+ CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES notenf_tgt(id)
+ NOT ENFORCED
+);
+SELECT * FROM pg_get_table_ddl('notenf'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.notenf (a integer);
+ ALTER TABLE pgtbl_ddl_test.notenf ADD CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.notenf_tgt(id) NOT ENFORCED;
+(2 rows)
+
+-- Self-referencing FK: the PK must appear before the FK that references it.
+-- The catalog scan returns constraints in name order; without the fix,
+-- "self_ref_parent_id_fkey" sorts before "self_ref_pkey" and the FK ADD
+-- CONSTRAINT fails with "there is no unique constraint matching given keys".
+CREATE TABLE self_ref (
+ id int PRIMARY KEY,
+ parent_id int REFERENCES self_ref(id)
+);
+SELECT * FROM pg_get_table_ddl('self_ref'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.self_ref (id integer NOT NULL, parent_id integer);
+ ALTER TABLE pgtbl_ddl_test.self_ref ADD CONSTRAINT self_ref_pkey PRIMARY KEY (id);
+ ALTER TABLE pgtbl_ddl_test.self_ref ADD CONSTRAINT self_ref_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES pgtbl_ddl_test.self_ref(id);
+(3 rows)
+
+-- WITHOUT OVERLAPS on a PRIMARY KEY (PG 17): the range-type period
+-- column forms a temporal primary key; WITHOUT OVERLAPS must appear
+-- inside the key column list in the reconstructed ALTER TABLE.
+-- Both columns use range types with native GiST support so no
+-- btree_gist extension is needed.
+CREATE TABLE temporal_pk (
+ grp int4range,
+ during daterange,
+ PRIMARY KEY (grp, during WITHOUT OVERLAPS)
+);
+SELECT * FROM pg_get_table_ddl('temporal_pk'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.temporal_pk (grp int4range NOT NULL, during daterange NOT NULL);
+ ALTER TABLE pgtbl_ddl_test.temporal_pk ADD CONSTRAINT temporal_pk_pkey PRIMARY KEY (grp, during WITHOUT OVERLAPS);
+(2 rows)
+
+-- Indexes: functional and partial. Constraint-backing indexes are
+-- suppressed (they are emitted by the constraint loop).
+CREATE TABLE idxd (id int PRIMARY KEY, name text);
+CREATE INDEX idxd_lower ON idxd (lower(name));
+CREATE INDEX idxd_partial ON idxd (id) WHERE id > 100;
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(4 rows)
+
+-- Inheritance, including a child DEFAULT override on an inherited column.
+CREATE TABLE par (a int DEFAULT 1, b text);
+CREATE TABLE ch (c int) INHERITS (par);
+ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ch (c integer) INHERITS (pgtbl_ddl_test.par);
+ ALTER TABLE ONLY 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 ONLY 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)
+
+-- Non-constraint-backed index on a partitioned table: the parent's index DDL
+-- must appear once; the auto-created child partition indexes (relispartition=t)
+-- must be suppressed so the DDL round-trips without "relation already exists".
+CREATE TABLE parted_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_idx_a PARTITION OF parted_idx FOR VALUES IN ('a');
+CREATE UNIQUE INDEX parted_idx_uidx ON parted_idx (id, region);
+SELECT * FROM pg_get_table_ddl('parted_idx'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_idx (id integer, region text) PARTITION BY LIST (region);
+ CREATE UNIQUE INDEX parted_idx_uidx ON pgtbl_ddl_test.parted_idx USING btree (id, region);
+ CREATE TABLE pgtbl_ddl_test.parted_idx_a PARTITION OF pgtbl_ddl_test.parted_idx FOR VALUES IN ('a');
+(3 rows)
+
+-- Plain (non-unique) CREATE INDEX on partitioned table: the auto-created child
+-- index (relispartition=true) is suppressed, just like the UNIQUE case.
+CREATE TABLE parted_plain (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE parted_plain_a PARTITION OF parted_plain FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_plain_id_idx ON parted_plain (id);
+SELECT * FROM pg_get_table_ddl('parted_plain'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_plain (id integer, val text) PARTITION BY RANGE (id);
+ CREATE INDEX parted_plain_id_idx ON pgtbl_ddl_test.parted_plain USING btree (id);
+ CREATE TABLE pgtbl_ddl_test.parted_plain_a PARTITION OF pgtbl_ddl_test.parted_plain FOR VALUES FROM (0) TO (100);
+(3 rows)
+
+-- Constraint-backed PRIMARY KEY on partitioned table: the child's inherited
+-- constraint (conislocal=false) and its backing index are both suppressed.
+CREATE TABLE parted_pk (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_pk_a PARTITION OF parted_pk FOR VALUES IN ('a');
+ALTER TABLE parted_pk ADD PRIMARY KEY (id, region);
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_pk (id integer NOT NULL, region text NOT NULL) PARTITION BY LIST (region);
+ ALTER TABLE pgtbl_ddl_test.parted_pk ADD CONSTRAINT parted_pk_pkey PRIMARY KEY (id, region);
+ CREATE TABLE pgtbl_ddl_test.parted_pk_a PARTITION OF pgtbl_ddl_test.parted_pk FOR VALUES IN ('a');
+(3 rows)
+
+-- Multi-level (sub-partitioned) table with a CREATE INDEX on the root.
+-- The mid-level partitioned index (relispartition=true, relkind='I') and the
+-- leaf index (relispartition=true, relkind='i') are both suppressed.
+CREATE TABLE parted_sub (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_sub_a PARTITION OF parted_sub
+ FOR VALUES IN ('a') PARTITION BY RANGE (id);
+CREATE TABLE parted_sub_a1 PARTITION OF parted_sub_a FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_sub_id_idx ON parted_sub (id);
+SELECT * FROM pg_get_table_ddl('parted_sub'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_sub (id integer, region text) PARTITION BY LIST (region);
+ CREATE INDEX parted_sub_id_idx ON pgtbl_ddl_test.parted_sub USING btree (id);
+ CREATE TABLE pgtbl_ddl_test.parted_sub_a PARTITION OF pgtbl_ddl_test.parted_sub FOR VALUES IN ('a') PARTITION BY RANGE (id);
+ CREATE TABLE pgtbl_ddl_test.parted_sub_a1 PARTITION OF pgtbl_ddl_test.parted_sub_a FOR VALUES FROM (0) TO (100);
+(4 rows)
+
+-- Partition child with its own locally-defined index (relispartition=false):
+-- this is NOT inherited from any parent partitioned index and must be emitted.
+CREATE TABLE parted_child_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_child_idx_a PARTITION OF parted_child_idx FOR VALUES IN ('a');
+CREATE INDEX parted_child_idx_a_id ON parted_child_idx_a (id);
+SELECT * FROM pg_get_table_ddl('parted_child_idx'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_child_idx (id integer, region text) PARTITION BY LIST (region);
+ CREATE TABLE pgtbl_ddl_test.parted_child_idx_a PARTITION OF pgtbl_ddl_test.parted_child_idx FOR VALUES IN ('a');
+ CREATE INDEX parted_child_idx_a_id ON pgtbl_ddl_test.parted_child_idx_a USING btree (id);
+(3 rows)
+
+-- Mix: parent has a CREATE INDEX (auto-propagated to child, relispartition=true,
+-- suppressed) AND the child has its own local index (relispartition=false, emitted).
+CREATE TABLE parted_mixed (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_mixed_a PARTITION OF parted_mixed FOR VALUES IN ('a');
+CREATE INDEX parted_mixed_id ON parted_mixed (id);
+CREATE INDEX parted_mixed_a_region ON parted_mixed_a (region);
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_mixed (id integer, region text) PARTITION BY LIST (region);
+ CREATE INDEX parted_mixed_id ON pgtbl_ddl_test.parted_mixed USING btree (id);
+ CREATE TABLE pgtbl_ddl_test.parted_mixed_a PARTITION OF pgtbl_ddl_test.parted_mixed FOR VALUES IN ('a');
+ CREATE INDEX parted_mixed_a_region ON pgtbl_ddl_test.parted_mixed_a USING btree (region);
+(4 rows)
+
+-- FK on a partitioned table: declared on the parent; partition children inherit
+-- the constraint with conislocal=false and must not re-emit it.
+CREATE TABLE parted_fk_tgt (id int PRIMARY KEY);
+CREATE TABLE parted_fk (id int, tgt_id int REFERENCES parted_fk_tgt(id))
+ PARTITION BY RANGE (id);
+CREATE TABLE parted_fk_a PARTITION OF parted_fk FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('parted_fk'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_fk (id integer, tgt_id integer) PARTITION BY RANGE (id);
+ ALTER TABLE pgtbl_ddl_test.parted_fk ADD CONSTRAINT parted_fk_tgt_id_fkey FOREIGN KEY (tgt_id) REFERENCES pgtbl_ddl_test.parted_fk_tgt(id);
+ CREATE TABLE pgtbl_ddl_test.parted_fk_a PARTITION OF pgtbl_ddl_test.parted_fk FOR VALUES FROM (0) TO (100);
+(3 rows)
+
+-- Statistics declared explicitly on a partition child (not auto-propagated):
+-- the child's statistics object appears inside the child's DDL block.
+CREATE TABLE parted_stx (id int, region text, val float) PARTITION BY LIST (region);
+CREATE TABLE parted_stx_a PARTITION OF parted_stx FOR VALUES IN ('a');
+CREATE STATISTICS parted_stx_stat (ndistinct) ON id, region FROM parted_stx;
+CREATE STATISTICS parted_stx_a_stat (ndistinct) ON id, val FROM parted_stx_a;
+SELECT * FROM pg_get_table_ddl('parted_stx'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_stx (id integer, region text, val double precision) PARTITION BY LIST (region);
+ CREATE STATISTICS pgtbl_ddl_test.parted_stx_stat (ndistinct) ON id, region FROM pgtbl_ddl_test.parted_stx;
+ CREATE TABLE pgtbl_ddl_test.parted_stx_a PARTITION OF pgtbl_ddl_test.parted_stx FOR VALUES IN ('a');
+ CREATE STATISTICS pgtbl_ddl_test.parted_stx_a_stat (ndistinct) ON id, val FROM pgtbl_ddl_test.parted_stx_a;
+(4 rows)
+
+DROP TABLE parted_stx;
+-- NO INHERIT CHECK constraint: the NO INHERIT flag must appear verbatim in the
+-- inline constraint definition inside the CREATE TABLE body.
+CREATE TABLE noinhchk (id int, val int,
+ CONSTRAINT val_pos CHECK (val > 0) NO INHERIT);
+SELECT * FROM pg_get_table_ddl('noinhchk'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.noinhchk (id integer, val integer, CONSTRAINT val_pos CHECK ((val > 0)) NO INHERIT);
+(1 row)
+
+DROP TABLE noinhchk;
+-- Multiple inheritance parents: the INHERITS clause must list both parents in
+-- declaration order; only the locally-defined column appears in the column list.
+CREATE TABLE mi_p1 (a int DEFAULT 1);
+CREATE TABLE mi_p2 (b text DEFAULT 'x');
+CREATE TABLE mi_child (c float) INHERITS (mi_p1, mi_p2);
+SELECT * FROM pg_get_table_ddl('mi_child'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.mi_child (c double precision) INHERITS (pgtbl_ddl_test.mi_p1, pgtbl_ddl_test.mi_p2);
+(1 row)
+
+DROP TABLE mi_p1, mi_p2 CASCADE;
+NOTICE: drop cascades to table mi_child
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO pgtbl_ddl_test.rt_log (id)+
+ VALUES (new.id);
+(2 rows)
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM pgtbl_ddl_test.stx;
+(2 rows)
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(3 rows)
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(2 rows)
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx (a integer NOT NULL);
+ CREATE UNIQUE INDEX ri_idx_a ON pgtbl_ddl_test.ri_idx USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+(3 rows)
+
+-- UNLOGGED + reloptions.
+CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70);
+SELECT * FROM pg_get_table_ddl('uno'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE UNLOGGED TABLE pgtbl_ddl_test.uno (id integer) WITH (fillfactor='70');
+(1 row)
+
+-- Typed table (CREATE TABLE OF type_name). Columns inherited from the
+-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come
+-- out through a single "(col WITH OPTIONS ...)" list.
+CREATE TYPE typed_t AS (a int, b text);
+CREATE TABLE typed_plain OF typed_t;
+SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_plain OF pgtbl_ddl_test.typed_t;
+(1 row)
+
+CREATE TABLE typed_over OF typed_t (
+ a WITH OPTIONS DEFAULT 7 NOT NULL,
+ b WITH OPTIONS NOT NULL,
+ CONSTRAINT b_nonempty CHECK (length(b) > 0)
+);
+SELECT * FROM pg_get_table_ddl('typed_over'::regclass, owner => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.typed_over OF pgtbl_ddl_test.typed_t (a WITH OPTIONS DEFAULT 7 NOT NULL, b WITH OPTIONS NOT NULL, CONSTRAINT b_nonempty CHECK ((length(b) > 0)));
+(1 row)
+
+-- Temporary tables + ON COMMIT. Temp tables must never be emitted with
+-- the session-private pg_temp_NN schema prefix -- the TEMPORARY keyword
+-- already places the table in pg_temp and the prefix is non-replayable.
+-- ON COMMIT DROP only fires at commit, so the queries that need to see
+-- the catalog entry must run in the same transaction as the CREATE.
+BEGIN;
+CREATE TEMP TABLE temp_default (id int);
+CREATE TEMP TABLE temp_delete (id int) ON COMMIT DELETE ROWS;
+CREATE TEMP TABLE temp_drop (id int) ON COMMIT DROP;
+SELECT line FROM pg_get_table_ddl('temp_default'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------
+ CREATE TEMPORARY TABLE temp_default (id integer);
+(1 row)
+
+SELECT line FROM pg_get_table_ddl('temp_delete'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+------------------------------------------------------------------------
+ CREATE TEMPORARY TABLE temp_delete (id integer) ON COMMIT DELETE ROWS;
+(1 row)
+
+SELECT line FROM pg_get_table_ddl('temp_drop'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ line
+---------------------------------------------------------------
+ CREATE TEMPORARY TABLE temp_drop (id integer) ON COMMIT DROP;
+(1 row)
+
+ROLLBACK;
+-- Pretty mode.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, pretty => true);
+ pg_get_table_ddl
+------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.basic ( +
+ id integer NOT NULL, +
+ name text COLLATE "C" DEFAULT 'anon'::text NOT NULL +
+ );
+ ALTER TABLE pgtbl_ddl_test.basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- only_kinds / except_kinds gating: each emits either only the listed kinds
+-- (only_kinds) or every kind except the listed ones (except_kinds). The two
+-- are mutually exclusive. Kind vocabulary: table, index,
+-- primary_key, unique, check, foreign_key, exclusion, rule,
+-- statistics, trigger, policy, rls, replica_identity, partition.
+-- NOT NULL is not in the vocabulary - always emitted to prevent
+-- producing schemas that silently accept NULLs the source would
+-- have rejected.
+--
+-- except_kinds=index hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['index']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- except_kinds=primary_key,unique,check,foreign_key,exclusion hides every
+-- table-level constraint. Inline CHECK in the CREATE TABLE body is
+-- suppressed too.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['primary_key','unique','check','foreign_key','exclusion']);
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=foreign_key suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- first pass of the two-pass FK-clone workflow: emit tables without
+-- cross-table FKs, load data, then re-run with only_kinds=foreign_key
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+(2 rows)
+
+-- only_kinds=foreign_key is the second pass of the two-pass FK-clone
+-- workflow: emit only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN
+-- KEY rows; the CREATE TABLE and every non-FK sub-object are
+-- suppressed (the table already exists from the first pass).
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(1 row)
+
+-- A table with no foreign keys produces no rows under
+-- only_kinds=foreign_key.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- only_kinds=table emits only the bare CREATE TABLE (with any inline
+-- column-level NOT NULL and CHECK) - no indexes, no PRIMARY KEY ALTER,
+-- no other sub-objects. Useful for capturing a table's shape without
+-- pulling in everything attached to it.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table']);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+(1 row)
+
+-- only_kinds=check on a non-partition table. The constraint loop must emit
+-- column-level and table-level CHECK constraints via ALTER TABLE here,
+-- because the inline path in CREATE TABLE never runs (KIND_TABLE is
+-- not in the filter).
+CREATE TABLE chk_only (
+ id int PRIMARY KEY,
+ qty int CHECK (qty > 0),
+ CONSTRAINT chk_only_id_pos CHECK (id > 0)
+);
+SELECT * FROM pg_get_table_ddl('chk_only'::regclass, owner => false, only_kinds => ARRAY['check']);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.chk_only ADD CONSTRAINT chk_only_id_pos CHECK ((id > 0));
+ ALTER TABLE pgtbl_ddl_test.chk_only ADD CONSTRAINT chk_only_qty_check CHECK ((qty > 0));
+(2 rows)
+
+DROP TABLE chk_only;
+-- only_kinds=partition on a partitioned-table parent: each child's full DDL
+-- is emitted (CREATE TABLE ... PARTITION OF ...). The "partition"
+-- keyword is a gate at the parent level; it is stripped from the
+-- propagated filter so the child does not exclude its own
+-- CREATE TABLE.
+CREATE TABLE p_only (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE p_only_a PARTITION OF p_only FOR VALUES FROM (0) TO (100);
+CREATE TABLE p_only_b PARTITION OF p_only FOR VALUES FROM (100) TO (200);
+SELECT * FROM pg_get_table_ddl('p_only'::regclass, owner => false, only_kinds => ARRAY['partition']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.p_only_a PARTITION OF pgtbl_ddl_test.p_only FOR VALUES FROM (0) TO (100);
+ CREATE TABLE pgtbl_ddl_test.p_only_b PARTITION OF pgtbl_ddl_test.p_only FOR VALUES FROM (100) TO (200);
+(2 rows)
+
+DROP TABLE p_only;
+-- only_kinds and except_kinds are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false,
+ only_kinds => ARRAY['foreign_key'],
+ except_kinds => ARRAY['foreign_key']);
+ERROR: "only_kinds" and "except_kinds" parameters are mutually exclusive
+-- Pub/sub schema clone: keep the table and its primary key, drop the
+-- rest of the constraints so the subscriber can replicate without
+-- pulling cross-table dependencies along.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique','check','foreign_key','exclusion']);
+ pg_get_table_ddl
+---------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=rule hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, except_kinds => ARRAY['rule']);
+ pg_get_table_ddl
+----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rt (id integer);
+(1 row)
+
+-- except_kinds=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, except_kinds => ARRAY['statistics']);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.stx (a integer, b integer, c integer);
+(1 row)
+
+-- except_kinds=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, except_kinds => ARRAY['rls']);
+ pg_get_table_ddl
+-----------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls (id integer);
+(1 row)
+
+-- except_kinds=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, except_kinds => ARRAY['replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_full (a integer);
+(1 row)
+
+-- Partition children are emitted by default; except_kinds=partition
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false, except_kinds => ARRAY['partition']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_range (id integer, k integer) PARTITION BY RANGE (id);
+(1 row)
+
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY[' INDEX ',' RULE ']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- schema_qualified=false strips the schema from the target table itself
+-- and from same-schema sibling references (inheritance/partition
+-- parents, identity sequences in the same schema), while cross-schema
+-- references are kept qualified for correctness.
+--
+-- Bare CREATE TABLE for an ordinary table.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------------------
+ CREATE TABLE basic (id integer NOT NULL, name text COLLATE "C" DEFAULT 'anon'::text NOT NULL);
+ ALTER TABLE basic ADD CONSTRAINT basic_pkey PRIMARY KEY (id);
+(2 rows)
+
+-- INHERITS parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------
+ CREATE TABLE ch (c integer) INHERITS (par);
+ ALTER TABLE ONLY 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 ONLY 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_pkey PRIMARY KEY (id);
+ ALTER TABLE xschema_fk ADD CONSTRAINT xschema_fk_pid_fkey FOREIGN KEY (pid) REFERENCES pgtbl_ddl_other.parent(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
+-- Cross-schema partition child with schema_qualified=false: the child must
+-- carry its own schema prefix so the output is unambiguous regardless of the
+-- caller's search_path. Before the fix, the child was emitted as the bare
+-- name "pt_c" (landing in whichever schema is first in search_path) while
+-- the parent reference inside the CREATE TABLE came out as the fully-qualified
+-- "pgtbl_ddl_part_s.pt", making the DDL inconsistent and unreplayable.
+CREATE SCHEMA pgtbl_ddl_part_s;
+CREATE SCHEMA pgtbl_ddl_part_other;
+CREATE TABLE pgtbl_ddl_part_s.pt (id int, val int) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_part_other.pt_c
+ PARTITION OF pgtbl_ddl_part_s.pt
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_part_s.pt'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------
+ CREATE TABLE pt (id integer, val integer) PARTITION BY RANGE (id);
+ CREATE TABLE pgtbl_ddl_part_other.pt_c PARTITION OF pgtbl_ddl_part_s.pt FOR VALUES FROM (0) TO (100);
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_part_s CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_part_s.pt
+DROP SCHEMA pgtbl_ddl_part_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);
+ 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 own column list (attislocal=false), so the
+-- user-named NOT NULL is emitted as a table-level CONSTRAINT clause in
+-- the CREATE TABLE body. This preserves the name and prevents a collision
+-- when the parent later propagates its own NOT NULL to the child.
+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, CONSTRAINT my_nn NOT NULL a) INHERITS (pgtbl_ddl_test.nn_par);
+(1 row)
+
+DROP TABLE nn_par CASCADE;
+NOTICE: drop cascades to table nn_ch
+-- Partition child where the parent's default was explicitly dropped.
+-- After PARTITION OF the parent default is re-applied; DROP DEFAULT
+-- must be emitted to restore the dropped state.
+CREATE TABLE drop_def_par (a int DEFAULT 99) PARTITION BY LIST (a);
+CREATE TABLE drop_def_child PARTITION OF drop_def_par FOR VALUES IN (1);
+ALTER TABLE ONLY drop_def_child ALTER COLUMN a DROP DEFAULT;
+SELECT * FROM pg_get_table_ddl('drop_def_child'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.drop_def_child PARTITION OF pgtbl_ddl_test.drop_def_par FOR VALUES IN (1);
+ ALTER TABLE ONLY pgtbl_ddl_test.drop_def_child ALTER COLUMN a DROP DEFAULT;
+(2 rows)
+
+DROP TABLE drop_def_par;
+-- Named NOT NULL on a partition child that was attached from a pre-existing
+-- table. The name must be preserved via the PARTITION OF column spec list
+-- so the child keeps its local name rather than inheriting the parent's
+-- auto-generated name.
+CREATE TABLE nn_part_par (a int NOT NULL) PARTITION BY LIST (a);
+CREATE TABLE nn_part_ch (a int CONSTRAINT nn_part_ch_nn NOT NULL);
+ALTER TABLE nn_part_par ATTACH PARTITION nn_part_ch FOR VALUES IN (1);
+SELECT * FROM pg_get_table_ddl('nn_part_ch'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.nn_part_ch PARTITION OF pgtbl_ddl_test.nn_part_par (CONSTRAINT nn_part_ch_nn NOT NULL a) FOR VALUES IN (1);
+(1 row)
+
+DROP TABLE nn_part_par;
+-- Named NOT NULL in an INHERITS child where the parent also gets a NOT NULL
+-- later: the child name must survive via the inline CONSTRAINT clause so it
+-- does not collide with the propagated parent NOT NULL.
+CREATE TABLE nn_inh_par (a int);
+CREATE TABLE nn_inh_ch (b int) INHERITS (nn_inh_par);
+ALTER TABLE nn_inh_ch ADD CONSTRAINT nn_inh_ch_nn NOT NULL a;
+ALTER TABLE nn_inh_par ADD CONSTRAINT nn_inh_par_nn NOT NULL a;
+SELECT * FROM pg_get_table_ddl('nn_inh_ch'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.nn_inh_ch (b integer, CONSTRAINT nn_inh_ch_nn NOT NULL a) INHERITS (pgtbl_ddl_test.nn_inh_par);
+(1 row)
+
+DROP TABLE nn_inh_par CASCADE;
+NOTICE: drop cascades to table nn_inh_ch
+-- Invalid index (left by a failed CREATE INDEX CONCURRENTLY) must be skipped.
+CREATE TABLE inv_idx (x int);
+INSERT INTO inv_idx VALUES (1), (1);
+CREATE UNIQUE INDEX CONCURRENTLY inv_idx_uidx ON inv_idx (x);
+ERROR: could not create unique index "inv_idx_uidx"
+DETAIL: Key (x)=(1) is duplicated.
+SELECT * FROM pg_get_table_ddl('inv_idx'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.inv_idx (x integer);
+(1 row)
+
+DROP TABLE inv_idx;
+-- CLUSTER ON: emitted after the index is created.
+CREATE TABLE cluster_tbl (id int, val text);
+CREATE INDEX cluster_tbl_id ON cluster_tbl (id);
+CLUSTER cluster_tbl USING cluster_tbl_id;
+SELECT * FROM pg_get_table_ddl('cluster_tbl'::regclass, owner => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cluster_tbl (id integer, val text);
+ CREATE INDEX cluster_tbl_id ON pgtbl_ddl_test.cluster_tbl USING btree (id);
+ ALTER TABLE pgtbl_ddl_test.cluster_tbl CLUSTER ON cluster_tbl_id;
+(3 rows)
+
+DROP TABLE cluster_tbl;
+-- Disabled rule: DISABLE RULE must be emitted after CREATE RULE.
+CREATE TABLE disabled_rule_tbl (id int);
+CREATE RULE dr_rule AS ON INSERT TO disabled_rule_tbl DO NOTHING;
+ALTER TABLE disabled_rule_tbl DISABLE RULE dr_rule;
+SELECT * FROM pg_get_table_ddl('disabled_rule_tbl'::regclass, owner => false);
+ pg_get_table_ddl
+--------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.disabled_rule_tbl (id integer);
+ CREATE RULE dr_rule AS +
+ ON INSERT TO pgtbl_ddl_test.disabled_rule_tbl DO NOTHING;
+ ALTER TABLE pgtbl_ddl_test.disabled_rule_tbl DISABLE RULE dr_rule;
+(3 rows)
+
+DROP TABLE disabled_rule_tbl;
+-- Index statistics target on an expression index column.
+CREATE TABLE idx_stat_tbl (c1 int);
+CREATE INDEX idx_stat_idx ON idx_stat_tbl ((c1 + 1));
+ALTER INDEX idx_stat_idx ALTER COLUMN 1 SET STATISTICS 400;
+SELECT * FROM pg_get_table_ddl('idx_stat_tbl'::regclass, owner => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idx_stat_tbl (c1 integer);
+ CREATE INDEX idx_stat_idx ON pgtbl_ddl_test.idx_stat_tbl USING btree (((c1 + 1)));
+ ALTER INDEX pgtbl_ddl_test.idx_stat_idx ALTER COLUMN 1 SET STATISTICS 400;
+(3 rows)
+
+DROP TABLE idx_stat_tbl;
+-- schema_qualified=false must drop the schema prefix from DEFAULT
+-- expressions and inline CHECK bodies that reference same-schema
+-- objects (sequence via nextval, function via direct call). This
+-- relies on the deparser respecting the narrowed search_path rather
+-- than on substring stripping.
+CREATE SCHEMA pgtbl_ddl_xref;
+CREATE SEQUENCE pgtbl_ddl_xref.myseq;
+CREATE FUNCTION pgtbl_ddl_xref.f(int) RETURNS int LANGUAGE sql IMMUTABLE
+ AS 'SELECT $1';
+CREATE TABLE pgtbl_ddl_xref.t (
+ id int DEFAULT nextval('pgtbl_ddl_xref.myseq'),
+ val int,
+ CONSTRAINT chk CHECK (pgtbl_ddl_xref.f(val) > 0)
+);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_xref.t'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE t (id integer DEFAULT nextval('myseq'::regclass), val integer, CONSTRAINT chk CHECK ((f(val) > 0)));
+(1 row)
+
+DROP SCHEMA pgtbl_ddl_xref CASCADE;
+NOTICE: drop cascades to 3 other objects
+DETAIL: drop cascades to sequence pgtbl_ddl_xref.myseq
+drop cascades to function pgtbl_ddl_xref.f(integer)
+drop cascades to table pgtbl_ddl_xref.t
+-- schema_qualified=false must NOT corrupt string literals whose
+-- contents happen to contain the schema name followed by a dot. String
+-- literal text is output verbatim by the deparser and is unaffected by
+-- search_path narrowing.
+CREATE SCHEMA pgtbl_ddl_str;
+CREATE TABLE pgtbl_ddl_str.p (id int, note text) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_str.pc PARTITION OF pgtbl_ddl_str.p
+ (CONSTRAINT chk CHECK (note <> 'pgtbl_ddl_str.secret'))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_str.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK ((note <> 'pgtbl_ddl_str.secret'::text));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_str CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_str.p
+-- schema_qualified=false must NOT corrupt double-quoted identifiers
+-- whose contents happen to contain the schema name followed by a dot
+-- (for example a column literally named "<schema>.weird" inside that
+-- schema). The mechanism is purely search_path narrowing: the deparser
+-- outputs attribute names verbatim from pg_attribute, so the quoted
+-- identifier content is never examined for schema prefixes. This holds
+-- even when the schema name is a single letter.
+CREATE SCHEMA pgtbl_ddl_qid;
+CREATE TABLE pgtbl_ddl_qid.p (id int, "pgtbl_ddl_qid.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_qid.pc PARTITION OF pgtbl_ddl_qid.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_qid.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_qid.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+------------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK (("pgtbl_ddl_qid.weird" > 0));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_qid CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_qid.p
+-- Same with a short (single-letter) schema name to confirm there is no
+-- minimum-length assumption in the stripping path.
+CREATE SCHEMA pgtbl_ddl_q1;
+CREATE TABLE pgtbl_ddl_q1.p (id int, "pgtbl_ddl_q1.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_q1.pc PARTITION OF pgtbl_ddl_q1.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_q1.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_q1.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-----------------------------------------------------------------------
+ CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
+ ALTER TABLE pc ADD CONSTRAINT chk CHECK (("pgtbl_ddl_q1.weird" > 0));
+(2 rows)
+
+DROP SCHEMA pgtbl_ddl_q1 CASCADE;
+NOTICE: drop cascades to table pgtbl_ddl_q1.p
+-- schema_qualified=false when the base schema name itself requires
+-- quoting (its prefix starts with "). Both forms of the prefix -
+-- bare-lowercase and quoted - must be stripped from outer
+-- ALTER TABLE / ON / CREATE INDEX prefixes, while inner quoted
+-- identifiers that happen to begin the same way must be preserved.
+CREATE SCHEMA "PgTbl-Ddl-Q";
+CREATE TABLE "PgTbl-Ddl-Q"."T" (id int, val int,
+ CONSTRAINT chk CHECK (val > 0));
+CREATE INDEX "T_val_idx" ON "PgTbl-Ddl-Q"."T" (val);
+SELECT * FROM pg_get_table_ddl('"PgTbl-Ddl-Q"."T"'::regclass,
+ owner => false,
+ schema_qualified => false);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE TABLE "T" (id integer, val integer, CONSTRAINT chk CHECK ((val > 0)));
+ CREATE INDEX "T_val_idx" ON "T" USING btree (val);
+(2 rows)
+
+DROP SCHEMA "PgTbl-Ddl-Q" CASCADE;
+NOTICE: drop cascades to table "PgTbl-Ddl-Q"."T"
+-- Error: not an ordinary or partitioned table.
+CREATE VIEW v AS SELECT 1 AS x;
+SELECT * FROM pg_get_table_ddl('v'::regclass);
+ERROR: "v" is not an ordinary or partitioned table
+CREATE SEQUENCE s;
+SELECT * FROM pg_get_table_ddl('s'::regclass);
+ERROR: "s" is not an ordinary or partitioned table
+-- NULL argument returns no rows.
+SELECT * FROM pg_get_table_ddl(NULL);
+ pg_get_table_ddl
+------------------
+(0 rows)
+
+-- REPLICA IDENTITY USING INDEX validation: if the referenced index
+-- would not be emitted (because its source kind is suppressed) but
+-- replica_identity itself would be, the function raises an error so
+-- the emitted DDL never dangles. Exercise all three constraint-backed
+-- forms (PK, UNIQUE, EXCLUSION) and a bare CREATE UNIQUE INDEX, in
+-- both the except-list and only-list shapes.
+--
+-- PK-backed: except_kinds=primary_key without except_kinds=replica_identity.
+CREATE TABLE ri_pk_excl (a int PRIMARY KEY);
+ALTER TABLE ri_pk_excl REPLICA IDENTITY USING INDEX ri_pk_excl_pkey;
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+ERROR: REPLICA IDENTITY for table "ri_pk_excl" requires kind "primary_key" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "primary_key" kind, which is not in the active filter.
+HINT: Either add "primary_key" to the filter or remove "replica_identity" from it.
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_pk_excl (a integer NOT NULL);
+(1 row)
+
+DROP TABLE ri_pk_excl;
+-- UNIQUE-constraint-backed replica identity index.
+CREATE TABLE ri_uniq_excl (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_uniq_excl REPLICA IDENTITY USING INDEX ri_uniq_excl_a_key;
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique']);
+ERROR: REPLICA IDENTITY for table "ri_uniq_excl" requires kind "unique" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "unique" kind, which is not in the active filter.
+HINT: Either add "unique" to the filter or remove "replica_identity" from it.
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique','replica_identity']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_uniq_excl (a integer NOT NULL, b integer);
+(1 row)
+
+DROP TABLE ri_uniq_excl;
+-- Bare CREATE UNIQUE INDEX (no backing constraint) used as REPLICA
+-- IDENTITY: the index is emitted by the "index" kind, so excluding
+-- "index" without also excluding "replica_identity" must error.
+CREATE TABLE ri_idx_excl (a int NOT NULL, b int);
+CREATE UNIQUE INDEX ri_idx_excl_a_uniq ON ri_idx_excl (a);
+ALTER TABLE ri_idx_excl REPLICA IDENTITY USING INDEX ri_idx_excl_a_uniq;
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index']);
+ERROR: REPLICA IDENTITY for table "ri_idx_excl" requires kind "index" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "index" kind, which is not in the active filter.
+HINT: Either add "index" to the filter or remove "replica_identity" from it.
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ri_idx_excl (a integer NOT NULL, b integer);
+(1 row)
+
+DROP TABLE ri_idx_excl;
+-- Include-side analog: only_kinds=replica_identity must include the kind
+-- that emits the index, or the function errors.
+CREATE TABLE ri_only (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ERROR: REPLICA IDENTITY for table "ri_only" requires kind "unique" to be emitted
+DETAIL: The table's REPLICA IDENTITY USING INDEX references an index produced by the "unique" kind, which is not in the active filter.
+HINT: Either add "unique" to the filter or remove "replica_identity" from it.
+-- Including both kinds succeeds.
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['unique','replica_identity']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.ri_only ADD CONSTRAINT ri_only_a_key UNIQUE (a);
+ ALTER TABLE pgtbl_ddl_test.ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+(2 rows)
+
+DROP TABLE ri_only;
+-- Per-kind only_kinds / except_kinds coverage. For each kind in the vocabulary,
+-- verify that only_kinds=K emits just that kind's output and that except_kinds=K
+-- omits just that kind's output. Kinds with no fixture above (trigger,
+-- policy) are still recognized but currently emit nothing; only_kinds=K
+-- returns zero rows for them. Fixtures used:
+-- idxd: PK + two secondary CREATE INDEX
+-- cons: PK + UNIQUE + CHECK + FK (constraint-backed)
+-- rt: CREATE RULE
+-- stx: CREATE STATISTICS
+-- rls: ENABLE/FORCE ROW LEVEL SECURITY
+-- ri_full: REPLICA IDENTITY FULL (no source-kind dependency)
+CREATE TABLE excl_fix (rng int4range,
+ EXCLUDE USING gist (rng WITH &&));
+-- only_kinds=K: emit only kind K's statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['unique']);
+ pg_get_table_ddl
+-----------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+ pg_get_table_ddl
+--------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.excl_fix ADD CONSTRAINT excl_fix_rng_excl EXCLUDE USING gist (rng WITH &&);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, only_kinds => ARRAY['rule']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE RULE rt_log_insert AS +
+ ON INSERT TO pgtbl_ddl_test.rt DO INSERT INTO pgtbl_ddl_test.rt_log (id)+
+ VALUES (new.id);
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+ pg_get_table_ddl
+---------------------------------------------------------------------------------------
+ CREATE STATISTICS pgtbl_ddl_test.stx_ndv (ndistinct) ON a, b FROM pgtbl_ddl_test.stx;
+(1 row)
+
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, only_kinds => ARRAY['rls']);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.rls ENABLE ROW LEVEL SECURITY;
+ ALTER TABLE pgtbl_ddl_test.rls FORCE ROW LEVEL SECURITY;
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ pg_get_table_ddl
+-----------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.ri_full REPLICA IDENTITY FULL;
+(1 row)
+
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['policy']);
+-- except_kinds=K: emit everything except kind K's statements. PK/UNIQUE/
+-- INDEX are exercised on tables without REPLICA IDENTITY USING INDEX
+-- so the validation does not trip.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer, CONSTRAINT cons_a_check CHECK ((a > 0)));
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(2 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['check']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cons (a integer, b integer, c integer);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_b_key UNIQUE (b);
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, except_kinds => ARRAY['exclusion']);
+ pg_get_table_ddl
+-------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.excl_fix (rng int4range);
+(1 row)
+
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['policy']);
+-- except_kinds=table suppresses the CREATE TABLE + OWNER + child-default
+-- SET DEFAULT + attoptions passes, leaving only the sub-object passes.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['table']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+ ALTER TABLE pgtbl_ddl_test.idxd ADD CONSTRAINT idxd_pkey PRIMARY KEY (id);
+(3 rows)
+
+-- Multi-kind compositions.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table','index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idxd (id integer NOT NULL, name text);
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(3 rows)
+
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['primary_key','foreign_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.cons ADD CONSTRAINT cons_c_fkey FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.refd(id) DEFERRABLE INITIALLY DEFERRED;
+(1 row)
+
+-- Duplicate entries in the list are silently de-duplicated by the
+-- Bitmapset.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index','index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------------
+ CREATE INDEX idxd_lower ON pgtbl_ddl_test.idxd USING btree (lower(name));
+ CREATE INDEX idxd_partial ON pgtbl_ddl_test.idxd USING btree (id) WHERE (id > 100);
+(2 rows)
+
+DROP TABLE excl_fix;
+-- Kind-filter tests on partitioned tables with indexes.
+--
+-- only_kinds=index on a partitioned table: only the root CREATE INDEX is
+-- emitted; no CREATE TABLE, no partition children (KIND_PARTITION absent).
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ only_kinds => ARRAY['index']);
+ pg_get_table_ddl
+-------------------------------------------------------------------------------
+ CREATE INDEX parted_mixed_id ON pgtbl_ddl_test.parted_mixed USING btree (id);
+(1 row)
+
+-- except_kinds=index on a partitioned table: CREATE TABLE for the parent
+-- and all children are emitted; no index statements.
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ except_kinds => ARRAY['index']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_mixed (id integer, region text) PARTITION BY LIST (region);
+ CREATE TABLE pgtbl_ddl_test.parted_mixed_a PARTITION OF pgtbl_ddl_test.parted_mixed FOR VALUES IN ('a');
+(2 rows)
+
+-- only_kinds=primary_key on a partitioned table: only the ALTER TABLE ADD
+-- CONSTRAINT PK on the parent; the child's inherited PK (conislocal=false)
+-- is skipped; no CREATE TABLE, no partition children.
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false,
+ only_kinds => ARRAY['primary_key']);
+ pg_get_table_ddl
+----------------------------------------------------------------------------------------------
+ ALTER TABLE pgtbl_ddl_test.parted_pk ADD CONSTRAINT parted_pk_pkey PRIMARY KEY (id, region);
+(1 row)
+
+-- only_kinds=K on a table that does not have any K of that kind is a valid
+-- filter and returns zero rows. Tested previously with foreign_key
+-- on 'refd'; here also rule, statistics, rls, replica_identity, and
+-- exclusion on 'basic' (which has only PK + columns).
+SELECT count(*) AS rule_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rule']);
+ rule_rows
+-----------
+ 0
+(1 row)
+
+SELECT count(*) AS stats_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+ stats_rows
+------------
+ 0
+(1 row)
+
+SELECT count(*) AS rls_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rls']);
+ rls_rows
+----------
+ 0
+(1 row)
+
+SELECT count(*) AS ri_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+ ri_rows
+---------
+ 0
+(1 row)
+
+SELECT count(*) AS excl_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+ excl_rows
+-----------
+ 0
+(1 row)
+
+-- Input-validation edge cases for the kind array. All
+-- of these raise an error; the function never reaches relation open.
+-- Empty array:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[]::text[]);
+ERROR: parameter "only_kinds" must specify at least one kind
+-- NULL element:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[NULL::text]);
+ERROR: parameter "only_kinds" must not contain NULL elements
+-- Unrecognized kind:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY['no_such_kind']);
+ERROR: unrecognized kind "no_such_kind" in parameter "only_kinds"
+-- Same kind specified through both parameters is still mutex-rejected.
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ only_kinds => ARRAY['index'],
+ except_kinds => ARRAY['rule']);
+ERROR: "only_kinds" and "except_kinds" parameters are mutually exclusive
+-- Round-trip verification. For every test table, capture the DDL the
+-- function emits, drop the schema, replay each table's DDL in
+-- dependency order, and confirm that pg_get_table_ddl on the recreated
+-- relation matches the original line-for-line. The final SELECT must
+-- return zero rows.
+CREATE TEMP TABLE pgtbl_ddl_rt_orig (name text, ord int, line text);
+INSERT INTO pgtbl_ddl_rt_orig
+SELECT t.name, o.ord, o.line
+FROM (VALUES
+ ('basic'), ('id_cols'), ('id_custom'), ('gen_cols'), ('gen_virtual'), ('storage_cols'),
+ ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'),
+ ('fk_acts_tgt'), ('fk_acts'), ('notenf_tgt'), ('notenf'), ('self_ref'), ('temporal_pk'),
+ ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('parted_list'),
+ ('parted_idx'), ('parted_idx_a'),
+ ('parted_plain'), ('parted_plain_a'),
+ ('parted_pk'), ('parted_pk_a'),
+ ('parted_sub'), ('parted_sub_a'), ('parted_sub_a1'),
+ ('parted_child_idx'), ('parted_child_idx_a'),
+ ('parted_mixed'), ('parted_mixed_a'),
+ ('parted_fk_tgt'), ('parted_fk'), ('parted_fk_a'),
+ ('rt_log'), ('rt'),
+ ('stx'), ('rls'), ('ri_full'), ('ri_idx'), ('uno')
+ ) AS t(name),
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord);
+DO $$
+DECLARE
+ tables CONSTANT text[] := ARRAY[
+ 'basic', 'id_cols', 'id_custom', 'gen_cols', 'gen_virtual', 'storage_cols',
+ 'refd', 'cons', 'nulls_nd', 'idx_inc',
+ 'fk_acts_tgt', 'fk_acts', 'notenf_tgt', 'notenf', 'self_ref', 'temporal_pk',
+ 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ 'parted_list',
+ 'parted_idx', 'parted_idx_a',
+ 'parted_plain', 'parted_plain_a',
+ 'parted_pk', 'parted_pk_a',
+ 'parted_sub', 'parted_sub_a', 'parted_sub_a1',
+ 'parted_child_idx', 'parted_child_idx_a',
+ 'parted_mixed', 'parted_mixed_a',
+ 'parted_fk_tgt', 'parted_fk', 'parted_fk_a',
+ 'rt_log', 'rt',
+ 'stx', 'rls', 'ri_full', 'ri_idx', 'uno'
+ ];
+ t text;
+ stmt text;
+BEGIN
+ DROP SCHEMA pgtbl_ddl_test CASCADE;
+ CREATE SCHEMA pgtbl_ddl_test;
+ FOREACH t IN ARRAY tables LOOP
+ FOR stmt IN
+ SELECT line FROM pgtbl_ddl_rt_orig WHERE name = t ORDER BY ord
+ LOOP
+ BEGIN
+ EXECUTE stmt;
+ EXCEPTION WHEN duplicate_table THEN
+ NULL; -- partition child already created by its parent's DDL
+ END;
+ END LOOP;
+ END LOOP;
+END $$;
+NOTICE: drop cascades to 43 other objects
+DETAIL: drop cascades to table basic
+drop cascades to table id_cols
+drop cascades to table id_custom
+drop cascades to table gen_cols
+drop cascades to table gen_virtual
+drop cascades to table storage_cols
+drop cascades to table refd
+drop cascades to table cons
+drop cascades to table nulls_nd
+drop cascades to table idx_inc
+drop cascades to table fk_acts_tgt
+drop cascades to table fk_acts
+drop cascades to table notenf_tgt
+drop cascades to table notenf
+drop cascades to table self_ref
+drop cascades to table temporal_pk
+drop cascades to table idxd
+drop cascades to table par
+drop cascades to table ch
+drop cascades to table attopt
+drop cascades to table parted_range
+drop cascades to table parted_hash
+drop cascades to table parted_list
+drop cascades to table parted_idx
+drop cascades to table parted_plain
+drop cascades to table parted_pk
+drop cascades to table parted_sub
+drop cascades to table parted_child_idx
+drop cascades to table parted_mixed
+drop cascades to table parted_fk_tgt
+drop cascades to table parted_fk
+drop cascades to table rt
+drop cascades to table rt_log
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
+drop cascades to type typed_t
+drop cascades to table typed_plain
+drop cascades to table typed_over
+drop cascades to view v
+drop cascades to sequence s
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+ kind | name | ord | line
+------+------+-----+------
+(0 rows)
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE: drop cascades to 38 other objects
+DETAIL: drop cascades to table basic
+drop cascades to table id_cols
+drop cascades to table id_custom
+drop cascades to table gen_cols
+drop cascades to table gen_virtual
+drop cascades to table storage_cols
+drop cascades to table refd
+drop cascades to table cons
+drop cascades to table nulls_nd
+drop cascades to table idx_inc
+drop cascades to table fk_acts_tgt
+drop cascades to table fk_acts
+drop cascades to table notenf_tgt
+drop cascades to table notenf
+drop cascades to table self_ref
+drop cascades to table temporal_pk
+drop cascades to table idxd
+drop cascades to table par
+drop cascades to table ch
+drop cascades to table attopt
+drop cascades to table parted_range
+drop cascades to table parted_hash
+drop cascades to table parted_list
+drop cascades to table parted_idx
+drop cascades to table parted_plain
+drop cascades to table parted_pk
+drop cascades to table parted_sub
+drop cascades to table parted_child_idx
+drop cascades to table parted_mixed
+drop cascades to table parted_fk_tgt
+drop cascades to table parted_fk
+drop cascades to table rt_log
+drop cascades to table rt
+drop cascades to table stx
+drop cascades to table rls
+drop cascades to table ri_full
+drop cascades to table ri_idx
+drop cascades to table uno
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..9eca64cca3b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -81,7 +81,7 @@ test: create_table_like alter_generic alter_operator misc async dbsize merge mis
# collate.linux.utf8 and collate.icu.utf8 tests cannot be run in parallel with each other
# psql depends on create_am
# amutils depends on geometry, create_index_spgist, hash_index, brin
-test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252
+test: rules psql psql_crosstab psql_pipeline amutils stats_ext collate.linux.utf8 collate.windows.win1252 pg_get_table_ddl
# ----------
# Run these alone so they don't run out of parallel workers
diff --git a/src/test/regress/sql/pg_get_table_ddl.sql b/src/test/regress/sql/pg_get_table_ddl.sql
new file mode 100644
index 00000000000..e9ea12d7611
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,937 @@
+--
+-- pg_get_table_ddl
+--
+-- All tests pass owner=>false so the ALTER TABLE OWNER TO line is not
+-- emitted, keeping output stable across test runners (which may run under
+-- different role names).
+--
+CREATE SCHEMA pgtbl_ddl_test;
+SET search_path = pgtbl_ddl_test;
+
+-- Basic table with PRIMARY KEY, NOT NULL, DEFAULT, COLLATE.
+CREATE TABLE basic (
+ id int PRIMARY KEY,
+ name text NOT NULL DEFAULT 'anon' COLLATE "C"
+);
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false);
+
+-- Identity columns (with default and custom sequence options).
+CREATE TABLE id_cols (
+ id_always int GENERATED ALWAYS AS IDENTITY,
+ id_default int GENERATED BY DEFAULT AS IDENTITY
+);
+SELECT * FROM pg_get_table_ddl('id_cols'::regclass, owner => false);
+
+CREATE TABLE id_custom (
+ v int GENERATED ALWAYS AS IDENTITY (
+ SEQUENCE NAME id_custom_v_seq
+ START WITH 100 INCREMENT BY 5
+ MINVALUE 50 MAXVALUE 1000
+ CACHE 10 CYCLE
+ )
+);
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false);
+
+-- Generated stored column.
+CREATE TABLE gen_cols (
+ cents int,
+ dollars numeric GENERATED ALWAYS AS (cents / 100.0) STORED
+);
+SELECT * FROM pg_get_table_ddl('gen_cols'::regclass, owner => false);
+
+-- Generated virtual column.
+CREATE TABLE gen_virtual (
+ base int,
+ derived int GENERATED ALWAYS AS (base * 2) VIRTUAL
+);
+SELECT * FROM pg_get_table_ddl('gen_virtual'::regclass, owner => false);
+
+-- STORAGE and COMPRESSION (only emitted when non-default for the type).
+CREATE TABLE storage_cols (
+ a text STORAGE EXTERNAL,
+ b text STORAGE MAIN,
+ c text COMPRESSION pglz
+);
+SELECT * FROM pg_get_table_ddl('storage_cols'::regclass, owner => false);
+
+-- Constraints: CHECK, UNIQUE, FOREIGN KEY (DEFERRABLE).
+CREATE TABLE refd (id int PRIMARY KEY);
+CREATE TABLE cons (
+ a int CHECK (a > 0),
+ b int UNIQUE,
+ c int REFERENCES refd(id) DEFERRABLE INITIALLY DEFERRED
+);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false);
+
+-- UNIQUE NULLS NOT DISTINCT (PG 15): null values are treated as equal for
+-- uniqueness purposes, so the NULLS NOT DISTINCT clause must be emitted.
+CREATE TABLE nulls_nd (
+ a int,
+ b int,
+ UNIQUE NULLS NOT DISTINCT (a, b)
+);
+SELECT * FROM pg_get_table_ddl('nulls_nd'::regclass, owner => false);
+
+-- INCLUDE columns on a constraint-backed UNIQUE index (PG 11): the
+-- covering columns appear in an INCLUDE (...) clause on the ALTER TABLE
+-- ADD CONSTRAINT statement, not in the key column list.
+CREATE TABLE idx_inc (
+ id int PRIMARY KEY,
+ name text,
+ extra text,
+ UNIQUE (name) INCLUDE (extra)
+);
+SELECT * FROM pg_get_table_ddl('idx_inc'::regclass, owner => false);
+
+-- FOREIGN KEY with ON DELETE / ON UPDATE referential actions and MATCH
+-- clause. Each variant must appear verbatim in the reconstructed DDL.
+CREATE TABLE fk_acts_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_acts (
+ a int,
+ b int,
+ c int,
+ d int,
+ CONSTRAINT fk_cascade FOREIGN KEY (a) REFERENCES fk_acts_tgt(id)
+ ON DELETE CASCADE ON UPDATE RESTRICT,
+ CONSTRAINT fk_setnull FOREIGN KEY (b) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET NULL,
+ CONSTRAINT fk_setdef FOREIGN KEY (c) REFERENCES fk_acts_tgt(id)
+ ON DELETE SET DEFAULT,
+ CONSTRAINT fk_match FOREIGN KEY (d) REFERENCES fk_acts_tgt(id)
+ MATCH FULL
+);
+SELECT * FROM pg_get_table_ddl('fk_acts'::regclass, owner => false);
+
+-- NOT ENFORCED foreign key (PG 17): the constraint is recorded in the
+-- catalog but not validated by the engine; NOT ENFORCED must appear in
+-- the reconstructed DDL.
+CREATE TABLE notenf_tgt (id int PRIMARY KEY);
+CREATE TABLE notenf (
+ a int,
+ CONSTRAINT notenf_fk FOREIGN KEY (a) REFERENCES notenf_tgt(id)
+ NOT ENFORCED
+);
+SELECT * FROM pg_get_table_ddl('notenf'::regclass, owner => false);
+
+-- Self-referencing FK: the PK must appear before the FK that references it.
+-- The catalog scan returns constraints in name order; without the fix,
+-- "self_ref_parent_id_fkey" sorts before "self_ref_pkey" and the FK ADD
+-- CONSTRAINT fails with "there is no unique constraint matching given keys".
+CREATE TABLE self_ref (
+ id int PRIMARY KEY,
+ parent_id int REFERENCES self_ref(id)
+);
+SELECT * FROM pg_get_table_ddl('self_ref'::regclass, owner => false);
+
+-- WITHOUT OVERLAPS on a PRIMARY KEY (PG 17): the range-type period
+-- column forms a temporal primary key; WITHOUT OVERLAPS must appear
+-- inside the key column list in the reconstructed ALTER TABLE.
+-- Both columns use range types with native GiST support so no
+-- btree_gist extension is needed.
+CREATE TABLE temporal_pk (
+ grp int4range,
+ during daterange,
+ PRIMARY KEY (grp, during WITHOUT OVERLAPS)
+);
+SELECT * FROM pg_get_table_ddl('temporal_pk'::regclass, owner => false);
+
+-- Indexes: functional and partial. Constraint-backing indexes are
+-- suppressed (they are emitted by the constraint loop).
+CREATE TABLE idxd (id int PRIMARY KEY, name text);
+CREATE INDEX idxd_lower ON idxd (lower(name));
+CREATE INDEX idxd_partial ON idxd (id) WHERE id > 100;
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false);
+
+-- Inheritance, including a child DEFAULT override on an inherited column.
+CREATE TABLE par (a int DEFAULT 1, b text);
+CREATE TABLE ch (c int) INHERITS (par);
+ALTER TABLE ch ALTER COLUMN a SET DEFAULT 999;
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false);
+
+-- Per-column attoptions: emitted as ALTER COLUMN SET (...).
+CREATE TABLE attopt (a int, b text);
+ALTER TABLE attopt ALTER COLUMN a SET (n_distinct = 100);
+SELECT * FROM pg_get_table_ddl('attopt'::regclass, owner => false);
+
+-- Partitioned table parents (RANGE, HASH, and LIST).
+CREATE TABLE parted_range (id int, k int) PARTITION BY RANGE (id);
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false);
+
+CREATE TABLE parted_hash (id int) PARTITION BY HASH (id);
+SELECT * FROM pg_get_table_ddl('parted_hash'::regclass, owner => false);
+
+CREATE TABLE parted_list (id int, region text) PARTITION BY LIST (region);
+SELECT * FROM pg_get_table_ddl('parted_list'::regclass, owner => false);
+
+-- Partition children: FROM/TO, WITH (modulus, remainder), DEFAULT.
+CREATE TABLE parted_range_1 PARTITION OF parted_range
+ FOR VALUES FROM (0) TO (100);
+ALTER TABLE parted_range_1 ALTER COLUMN k SET DEFAULT 7;
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false);
+
+CREATE TABLE parted_hash_0 PARTITION OF parted_hash
+ FOR VALUES WITH (modulus 2, remainder 0);
+SELECT * FROM pg_get_table_ddl('parted_hash_0'::regclass, owner => false);
+
+CREATE TABLE parted_range_def PARTITION OF parted_range DEFAULT;
+SELECT * FROM pg_get_table_ddl('parted_range_def'::regclass, owner => false);
+
+-- Non-constraint-backed index on a partitioned table: the parent's index DDL
+-- must appear once; the auto-created child partition indexes (relispartition=t)
+-- must be suppressed so the DDL round-trips without "relation already exists".
+CREATE TABLE parted_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_idx_a PARTITION OF parted_idx FOR VALUES IN ('a');
+CREATE UNIQUE INDEX parted_idx_uidx ON parted_idx (id, region);
+SELECT * FROM pg_get_table_ddl('parted_idx'::regclass, owner => false);
+
+-- Plain (non-unique) CREATE INDEX on partitioned table: the auto-created child
+-- index (relispartition=true) is suppressed, just like the UNIQUE case.
+CREATE TABLE parted_plain (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE parted_plain_a PARTITION OF parted_plain FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_plain_id_idx ON parted_plain (id);
+SELECT * FROM pg_get_table_ddl('parted_plain'::regclass, owner => false);
+
+-- Constraint-backed PRIMARY KEY on partitioned table: the child's inherited
+-- constraint (conislocal=false) and its backing index are both suppressed.
+CREATE TABLE parted_pk (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_pk_a PARTITION OF parted_pk FOR VALUES IN ('a');
+ALTER TABLE parted_pk ADD PRIMARY KEY (id, region);
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false);
+
+-- Multi-level (sub-partitioned) table with a CREATE INDEX on the root.
+-- The mid-level partitioned index (relispartition=true, relkind='I') and the
+-- leaf index (relispartition=true, relkind='i') are both suppressed.
+CREATE TABLE parted_sub (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_sub_a PARTITION OF parted_sub
+ FOR VALUES IN ('a') PARTITION BY RANGE (id);
+CREATE TABLE parted_sub_a1 PARTITION OF parted_sub_a FOR VALUES FROM (0) TO (100);
+CREATE INDEX parted_sub_id_idx ON parted_sub (id);
+SELECT * FROM pg_get_table_ddl('parted_sub'::regclass, owner => false);
+
+-- Partition child with its own locally-defined index (relispartition=false):
+-- this is NOT inherited from any parent partitioned index and must be emitted.
+CREATE TABLE parted_child_idx (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_child_idx_a PARTITION OF parted_child_idx FOR VALUES IN ('a');
+CREATE INDEX parted_child_idx_a_id ON parted_child_idx_a (id);
+SELECT * FROM pg_get_table_ddl('parted_child_idx'::regclass, owner => false);
+
+-- Mix: parent has a CREATE INDEX (auto-propagated to child, relispartition=true,
+-- suppressed) AND the child has its own local index (relispartition=false, emitted).
+CREATE TABLE parted_mixed (id int, region text) PARTITION BY LIST (region);
+CREATE TABLE parted_mixed_a PARTITION OF parted_mixed FOR VALUES IN ('a');
+CREATE INDEX parted_mixed_id ON parted_mixed (id);
+CREATE INDEX parted_mixed_a_region ON parted_mixed_a (region);
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false);
+
+-- FK on a partitioned table: declared on the parent; partition children inherit
+-- the constraint with conislocal=false and must not re-emit it.
+CREATE TABLE parted_fk_tgt (id int PRIMARY KEY);
+CREATE TABLE parted_fk (id int, tgt_id int REFERENCES parted_fk_tgt(id))
+ PARTITION BY RANGE (id);
+CREATE TABLE parted_fk_a PARTITION OF parted_fk FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('parted_fk'::regclass, owner => false);
+
+-- Statistics declared explicitly on a partition child (not auto-propagated):
+-- the child's statistics object appears inside the child's DDL block.
+CREATE TABLE parted_stx (id int, region text, val float) PARTITION BY LIST (region);
+CREATE TABLE parted_stx_a PARTITION OF parted_stx FOR VALUES IN ('a');
+CREATE STATISTICS parted_stx_stat (ndistinct) ON id, region FROM parted_stx;
+CREATE STATISTICS parted_stx_a_stat (ndistinct) ON id, val FROM parted_stx_a;
+SELECT * FROM pg_get_table_ddl('parted_stx'::regclass, owner => false);
+DROP TABLE parted_stx;
+
+-- NO INHERIT CHECK constraint: the NO INHERIT flag must appear verbatim in the
+-- inline constraint definition inside the CREATE TABLE body.
+CREATE TABLE noinhchk (id int, val int,
+ CONSTRAINT val_pos CHECK (val > 0) NO INHERIT);
+SELECT * FROM pg_get_table_ddl('noinhchk'::regclass, owner => false);
+DROP TABLE noinhchk;
+
+-- Multiple inheritance parents: the INHERITS clause must list both parents in
+-- declaration order; only the locally-defined column appears in the column list.
+CREATE TABLE mi_p1 (a int DEFAULT 1);
+CREATE TABLE mi_p2 (b text DEFAULT 'x');
+CREATE TABLE mi_child (c float) INHERITS (mi_p1, mi_p2);
+SELECT * FROM pg_get_table_ddl('mi_child'::regclass, owner => false);
+DROP TABLE mi_p1, mi_p2 CASCADE;
+
+-- Rules.
+CREATE TABLE rt (id int);
+CREATE TABLE rt_log (id int);
+CREATE RULE rt_log_insert AS ON INSERT TO rt
+ DO ALSO INSERT INTO rt_log VALUES (NEW.id);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false);
+
+-- Extended statistics.
+CREATE TABLE stx (a int, b int, c int);
+CREATE STATISTICS stx_ndv (ndistinct) ON a, b FROM stx;
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false);
+
+-- Row-level security toggles.
+CREATE TABLE rls (id int);
+ALTER TABLE rls ENABLE ROW LEVEL SECURITY;
+ALTER TABLE rls FORCE ROW LEVEL SECURITY;
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false);
+
+-- REPLICA IDENTITY: emitted only when not the default.
+CREATE TABLE ri_full (a int);
+ALTER TABLE ri_full REPLICA IDENTITY FULL;
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false);
+
+CREATE TABLE ri_idx (a int NOT NULL);
+CREATE UNIQUE INDEX ri_idx_a ON ri_idx (a);
+ALTER TABLE ri_idx REPLICA IDENTITY USING INDEX ri_idx_a;
+SELECT * FROM pg_get_table_ddl('ri_idx'::regclass, owner => false);
+
+-- UNLOGGED + reloptions.
+CREATE UNLOGGED TABLE uno (id int) WITH (fillfactor = 70);
+SELECT * FROM pg_get_table_ddl('uno'::regclass, owner => false);
+
+-- Typed table (CREATE TABLE OF type_name). Columns inherited from the
+-- type emit nothing; locally-applied DEFAULT, NOT NULL and CHECK come
+-- out through a single "(col WITH OPTIONS ...)" list.
+CREATE TYPE typed_t AS (a int, b text);
+CREATE TABLE typed_plain OF typed_t;
+SELECT * FROM pg_get_table_ddl('typed_plain'::regclass, owner => false);
+
+CREATE TABLE typed_over OF typed_t (
+ a WITH OPTIONS DEFAULT 7 NOT NULL,
+ b WITH OPTIONS NOT NULL,
+ CONSTRAINT b_nonempty CHECK (length(b) > 0)
+);
+SELECT * FROM pg_get_table_ddl('typed_over'::regclass, owner => false);
+
+-- Temporary tables + ON COMMIT. Temp tables must never be emitted with
+-- the session-private pg_temp_NN schema prefix -- the TEMPORARY keyword
+-- already places the table in pg_temp and the prefix is non-replayable.
+-- ON COMMIT DROP only fires at commit, so the queries that need to see
+-- the catalog entry must run in the same transaction as the CREATE.
+BEGIN;
+CREATE TEMP TABLE temp_default (id int);
+CREATE TEMP TABLE temp_delete (id int) ON COMMIT DELETE ROWS;
+CREATE TEMP TABLE temp_drop (id int) ON COMMIT DROP;
+
+SELECT line FROM pg_get_table_ddl('temp_default'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT line FROM pg_get_table_ddl('temp_delete'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+
+SELECT line FROM pg_get_table_ddl('temp_drop'::regclass, owner => false) AS line
+WHERE line LIKE 'CREATE %';
+ROLLBACK;
+
+-- Pretty mode.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, pretty => true);
+
+-- only_kinds / except_kinds gating: each emits either only the listed kinds
+-- (only_kinds) or every kind except the listed ones (except_kinds). The two
+-- are mutually exclusive. Kind vocabulary: table, index,
+-- primary_key, unique, check, foreign_key, exclusion, rule,
+-- statistics, trigger, policy, rls, replica_identity, partition.
+-- NOT NULL is not in the vocabulary - always emitted to prevent
+-- producing schemas that silently accept NULLs the source would
+-- have rejected.
+--
+-- except_kinds=index hides the CREATE INDEX statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['index']);
+
+-- except_kinds=primary_key,unique,check,foreign_key,exclusion hides every
+-- table-level constraint. Inline CHECK in the CREATE TABLE body is
+-- suppressed too.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['primary_key','unique','check','foreign_key','exclusion']);
+
+-- except_kinds=foreign_key suppresses only FOREIGN KEY constraints;
+-- PRIMARY KEY, UNIQUE, CHECK, and named NOT NULL stay. This is the
+-- first pass of the two-pass FK-clone workflow: emit tables without
+-- cross-table FKs, load data, then re-run with only_kinds=foreign_key
+-- to add them once all targets exist.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['foreign_key']);
+
+-- only_kinds=foreign_key is the second pass of the two-pass FK-clone
+-- workflow: emit only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN
+-- KEY rows; the CREATE TABLE and every non-FK sub-object are
+-- suppressed (the table already exists from the first pass).
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+
+-- A table with no foreign keys produces no rows under
+-- only_kinds=foreign_key.
+SELECT * FROM pg_get_table_ddl('refd'::regclass, owner => false, only_kinds => ARRAY['foreign_key']);
+
+-- only_kinds=table emits only the bare CREATE TABLE (with any inline
+-- column-level NOT NULL and CHECK) - no indexes, no PRIMARY KEY ALTER,
+-- no other sub-objects. Useful for capturing a table's shape without
+-- pulling in everything attached to it.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table']);
+
+-- only_kinds=check on a non-partition table. The constraint loop must emit
+-- column-level and table-level CHECK constraints via ALTER TABLE here,
+-- because the inline path in CREATE TABLE never runs (KIND_TABLE is
+-- not in the filter).
+CREATE TABLE chk_only (
+ id int PRIMARY KEY,
+ qty int CHECK (qty > 0),
+ CONSTRAINT chk_only_id_pos CHECK (id > 0)
+);
+SELECT * FROM pg_get_table_ddl('chk_only'::regclass, owner => false, only_kinds => ARRAY['check']);
+DROP TABLE chk_only;
+
+-- only_kinds=partition on a partitioned-table parent: each child's full DDL
+-- is emitted (CREATE TABLE ... PARTITION OF ...). The "partition"
+-- keyword is a gate at the parent level; it is stripped from the
+-- propagated filter so the child does not exclude its own
+-- CREATE TABLE.
+CREATE TABLE p_only (id int, val text) PARTITION BY RANGE (id);
+CREATE TABLE p_only_a PARTITION OF p_only FOR VALUES FROM (0) TO (100);
+CREATE TABLE p_only_b PARTITION OF p_only FOR VALUES FROM (100) TO (200);
+SELECT * FROM pg_get_table_ddl('p_only'::regclass, owner => false, only_kinds => ARRAY['partition']);
+DROP TABLE p_only;
+
+-- only_kinds and except_kinds are mutually exclusive.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false,
+ only_kinds => ARRAY['foreign_key'],
+ except_kinds => ARRAY['foreign_key']);
+
+-- Pub/sub schema clone: keep the table and its primary key, drop the
+-- rest of the constraints so the subscriber can replicate without
+-- pulling cross-table dependencies along.
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique','check','foreign_key','exclusion']);
+
+-- except_kinds=rule hides the CREATE RULE.
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, except_kinds => ARRAY['rule']);
+
+-- except_kinds=statistics hides the CREATE STATISTICS.
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, except_kinds => ARRAY['statistics']);
+
+-- except_kinds=rls hides the ENABLE/FORCE ROW LEVEL SECURITY toggles.
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, except_kinds => ARRAY['rls']);
+
+-- except_kinds=replica_identity hides the REPLICA IDENTITY clause.
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, except_kinds => ARRAY['replica_identity']);
+
+-- Partition children are emitted by default; except_kinds=partition
+-- suppresses them so only the partitioned-table parent comes out.
+SELECT * FROM pg_get_table_ddl('parted_range'::regclass, owner => false, except_kinds => ARRAY['partition']);
+
+-- Whitespace around kind names is allowed; case is folded.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY[' INDEX ',' RULE ']);
+
+-- schema_qualified=false strips the schema from the target table itself
+-- and from same-schema sibling references (inheritance/partition
+-- parents, identity sequences in the same schema), while cross-schema
+-- references are kept qualified for correctness.
+--
+-- Bare CREATE TABLE for an ordinary table.
+SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false,
+ schema_qualified => false);
+
+-- INHERITS parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('ch'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Partition parent in the same schema is emitted unqualified.
+SELECT * FROM pg_get_table_ddl('parted_range_1'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Cross-schema FK target keeps qualification. The 'refd' table is in
+-- pgtbl_ddl_test; place an FK target in a different schema and verify it
+-- stays qualified even with schema_qualified=false.
+CREATE SCHEMA pgtbl_ddl_other;
+CREATE TABLE pgtbl_ddl_other.parent (id int PRIMARY KEY);
+CREATE TABLE xschema_fk (
+ id int PRIMARY KEY,
+ pid int REFERENCES pgtbl_ddl_other.parent
+);
+SELECT * FROM pg_get_table_ddl('xschema_fk'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Cross-schema FK whose target schema name ends with the base schema name
+-- (e.g. "xpgtbl_ddl_test" ends with "pgtbl_ddl_test"). A post-hoc text
+-- strip would mangle "xpgtbl_ddl_test.reftbl" into "xreftbl"; deciding
+-- qualification at generation time avoids the problem entirely.
+CREATE SCHEMA xpgtbl_ddl_test;
+CREATE TABLE xpgtbl_ddl_test.reftbl (id int PRIMARY KEY);
+CREATE TABLE suffix_schema_fk (
+ id int PRIMARY KEY,
+ ref int REFERENCES xpgtbl_ddl_test.reftbl
+);
+SELECT * FROM pg_get_table_ddl('suffix_schema_fk'::regclass, owner => false,
+ schema_qualified => false);
+DROP TABLE suffix_schema_fk;
+DROP SCHEMA xpgtbl_ddl_test CASCADE;
+
+-- Identity column with a custom sequence name in the same schema is
+-- emitted unqualified.
+SELECT * FROM pg_get_table_ddl('id_custom'::regclass, owner => false,
+ schema_qualified => false);
+
+-- Round-trip: with schema_qualified=false, after SET search_path the
+-- DDL can be replayed into a different target schema.
+CREATE SCHEMA pgtbl_ddl_replay;
+DO $$
+DECLARE
+ stmt text;
+BEGIN
+ SET LOCAL search_path = pgtbl_ddl_replay;
+ FOR stmt IN
+ SELECT line FROM pg_get_table_ddl('pgtbl_ddl_test.basic'::regclass,
+ owner => false,
+ schema_qualified => false) AS line
+ LOOP
+ EXECUTE stmt;
+ END LOOP;
+END $$;
+SELECT relnamespace::regnamespace::text, relname
+FROM pg_class WHERE oid = 'pgtbl_ddl_replay.basic'::regclass;
+DROP SCHEMA pgtbl_ddl_replay CASCADE;
+DROP TABLE xschema_fk;
+DROP SCHEMA pgtbl_ddl_other CASCADE;
+
+-- Cross-schema partition child with schema_qualified=false: the child must
+-- carry its own schema prefix so the output is unambiguous regardless of the
+-- caller's search_path. Before the fix, the child was emitted as the bare
+-- name "pt_c" (landing in whichever schema is first in search_path) while
+-- the parent reference inside the CREATE TABLE came out as the fully-qualified
+-- "pgtbl_ddl_part_s.pt", making the DDL inconsistent and unreplayable.
+CREATE SCHEMA pgtbl_ddl_part_s;
+CREATE SCHEMA pgtbl_ddl_part_other;
+CREATE TABLE pgtbl_ddl_part_s.pt (id int, val int) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_part_other.pt_c
+ PARTITION OF pgtbl_ddl_part_s.pt
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_part_s.pt'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_part_s CASCADE;
+DROP SCHEMA pgtbl_ddl_part_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 own column list (attislocal=false), so the
+-- user-named NOT NULL is emitted as a table-level CONSTRAINT clause in
+-- the CREATE TABLE body. This preserves the name and prevents a collision
+-- when the parent later propagates its own NOT NULL to the child.
+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;
+
+-- Partition child where the parent's default was explicitly dropped.
+-- After PARTITION OF the parent default is re-applied; DROP DEFAULT
+-- must be emitted to restore the dropped state.
+CREATE TABLE drop_def_par (a int DEFAULT 99) PARTITION BY LIST (a);
+CREATE TABLE drop_def_child PARTITION OF drop_def_par FOR VALUES IN (1);
+ALTER TABLE ONLY drop_def_child ALTER COLUMN a DROP DEFAULT;
+SELECT * FROM pg_get_table_ddl('drop_def_child'::regclass, owner => false);
+DROP TABLE drop_def_par;
+
+-- Named NOT NULL on a partition child that was attached from a pre-existing
+-- table. The name must be preserved via the PARTITION OF column spec list
+-- so the child keeps its local name rather than inheriting the parent's
+-- auto-generated name.
+CREATE TABLE nn_part_par (a int NOT NULL) PARTITION BY LIST (a);
+CREATE TABLE nn_part_ch (a int CONSTRAINT nn_part_ch_nn NOT NULL);
+ALTER TABLE nn_part_par ATTACH PARTITION nn_part_ch FOR VALUES IN (1);
+SELECT * FROM pg_get_table_ddl('nn_part_ch'::regclass, owner => false);
+DROP TABLE nn_part_par;
+
+-- Named NOT NULL in an INHERITS child where the parent also gets a NOT NULL
+-- later: the child name must survive via the inline CONSTRAINT clause so it
+-- does not collide with the propagated parent NOT NULL.
+CREATE TABLE nn_inh_par (a int);
+CREATE TABLE nn_inh_ch (b int) INHERITS (nn_inh_par);
+ALTER TABLE nn_inh_ch ADD CONSTRAINT nn_inh_ch_nn NOT NULL a;
+ALTER TABLE nn_inh_par ADD CONSTRAINT nn_inh_par_nn NOT NULL a;
+SELECT * FROM pg_get_table_ddl('nn_inh_ch'::regclass, owner => false);
+DROP TABLE nn_inh_par CASCADE;
+
+-- Invalid index (left by a failed CREATE INDEX CONCURRENTLY) must be skipped.
+CREATE TABLE inv_idx (x int);
+INSERT INTO inv_idx VALUES (1), (1);
+CREATE UNIQUE INDEX CONCURRENTLY inv_idx_uidx ON inv_idx (x);
+SELECT * FROM pg_get_table_ddl('inv_idx'::regclass, owner => false);
+DROP TABLE inv_idx;
+
+-- CLUSTER ON: emitted after the index is created.
+CREATE TABLE cluster_tbl (id int, val text);
+CREATE INDEX cluster_tbl_id ON cluster_tbl (id);
+CLUSTER cluster_tbl USING cluster_tbl_id;
+SELECT * FROM pg_get_table_ddl('cluster_tbl'::regclass, owner => false);
+DROP TABLE cluster_tbl;
+
+-- Disabled rule: DISABLE RULE must be emitted after CREATE RULE.
+CREATE TABLE disabled_rule_tbl (id int);
+CREATE RULE dr_rule AS ON INSERT TO disabled_rule_tbl DO NOTHING;
+ALTER TABLE disabled_rule_tbl DISABLE RULE dr_rule;
+SELECT * FROM pg_get_table_ddl('disabled_rule_tbl'::regclass, owner => false);
+DROP TABLE disabled_rule_tbl;
+
+-- Index statistics target on an expression index column.
+CREATE TABLE idx_stat_tbl (c1 int);
+CREATE INDEX idx_stat_idx ON idx_stat_tbl ((c1 + 1));
+ALTER INDEX idx_stat_idx ALTER COLUMN 1 SET STATISTICS 400;
+SELECT * FROM pg_get_table_ddl('idx_stat_tbl'::regclass, owner => false);
+DROP TABLE idx_stat_tbl;
+
+-- schema_qualified=false must drop the schema prefix from DEFAULT
+-- expressions and inline CHECK bodies that reference same-schema
+-- objects (sequence via nextval, function via direct call). This
+-- relies on the deparser respecting the narrowed search_path rather
+-- than on substring stripping.
+CREATE SCHEMA pgtbl_ddl_xref;
+CREATE SEQUENCE pgtbl_ddl_xref.myseq;
+CREATE FUNCTION pgtbl_ddl_xref.f(int) RETURNS int LANGUAGE sql IMMUTABLE
+ AS 'SELECT $1';
+CREATE TABLE pgtbl_ddl_xref.t (
+ id int DEFAULT nextval('pgtbl_ddl_xref.myseq'),
+ val int,
+ CONSTRAINT chk CHECK (pgtbl_ddl_xref.f(val) > 0)
+);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_xref.t'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_xref CASCADE;
+
+-- schema_qualified=false must NOT corrupt string literals whose
+-- contents happen to contain the schema name followed by a dot. String
+-- literal text is output verbatim by the deparser and is unaffected by
+-- search_path narrowing.
+CREATE SCHEMA pgtbl_ddl_str;
+CREATE TABLE pgtbl_ddl_str.p (id int, note text) PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_str.pc PARTITION OF pgtbl_ddl_str.p
+ (CONSTRAINT chk CHECK (note <> 'pgtbl_ddl_str.secret'))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_str.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_str CASCADE;
+
+-- schema_qualified=false must NOT corrupt double-quoted identifiers
+-- whose contents happen to contain the schema name followed by a dot
+-- (for example a column literally named "<schema>.weird" inside that
+-- schema). The mechanism is purely search_path narrowing: the deparser
+-- outputs attribute names verbatim from pg_attribute, so the quoted
+-- identifier content is never examined for schema prefixes. This holds
+-- even when the schema name is a single letter.
+CREATE SCHEMA pgtbl_ddl_qid;
+CREATE TABLE pgtbl_ddl_qid.p (id int, "pgtbl_ddl_qid.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_qid.pc PARTITION OF pgtbl_ddl_qid.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_qid.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_qid.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_qid CASCADE;
+-- Same with a short (single-letter) schema name to confirm there is no
+-- minimum-length assumption in the stripping path.
+CREATE SCHEMA pgtbl_ddl_q1;
+CREATE TABLE pgtbl_ddl_q1.p (id int, "pgtbl_ddl_q1.weird" int)
+ PARTITION BY RANGE (id);
+CREATE TABLE pgtbl_ddl_q1.pc PARTITION OF pgtbl_ddl_q1.p
+ (CONSTRAINT chk CHECK ("pgtbl_ddl_q1.weird" > 0))
+ FOR VALUES FROM (0) TO (100);
+SELECT * FROM pg_get_table_ddl('pgtbl_ddl_q1.pc'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA pgtbl_ddl_q1 CASCADE;
+
+-- schema_qualified=false when the base schema name itself requires
+-- quoting (its prefix starts with "). Both forms of the prefix -
+-- bare-lowercase and quoted - must be stripped from outer
+-- ALTER TABLE / ON / CREATE INDEX prefixes, while inner quoted
+-- identifiers that happen to begin the same way must be preserved.
+CREATE SCHEMA "PgTbl-Ddl-Q";
+CREATE TABLE "PgTbl-Ddl-Q"."T" (id int, val int,
+ CONSTRAINT chk CHECK (val > 0));
+CREATE INDEX "T_val_idx" ON "PgTbl-Ddl-Q"."T" (val);
+SELECT * FROM pg_get_table_ddl('"PgTbl-Ddl-Q"."T"'::regclass,
+ owner => false,
+ schema_qualified => false);
+DROP SCHEMA "PgTbl-Ddl-Q" CASCADE;
+
+-- Error: not an ordinary or partitioned table.
+CREATE VIEW v AS SELECT 1 AS x;
+SELECT * FROM pg_get_table_ddl('v'::regclass);
+
+CREATE SEQUENCE s;
+SELECT * FROM pg_get_table_ddl('s'::regclass);
+
+-- NULL argument returns no rows.
+SELECT * FROM pg_get_table_ddl(NULL);
+
+-- REPLICA IDENTITY USING INDEX validation: if the referenced index
+-- would not be emitted (because its source kind is suppressed) but
+-- replica_identity itself would be, the function raises an error so
+-- the emitted DDL never dangles. Exercise all three constraint-backed
+-- forms (PK, UNIQUE, EXCLUSION) and a bare CREATE UNIQUE INDEX, in
+-- both the except-list and only-list shapes.
+--
+-- PK-backed: except_kinds=primary_key without except_kinds=replica_identity.
+CREATE TABLE ri_pk_excl (a int PRIMARY KEY);
+ALTER TABLE ri_pk_excl REPLICA IDENTITY USING INDEX ri_pk_excl_pkey;
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+-- Same call but also excluding replica_identity succeeds.
+SELECT * FROM pg_get_table_ddl('ri_pk_excl'::regclass, owner => false, except_kinds => ARRAY['primary_key','replica_identity']);
+DROP TABLE ri_pk_excl;
+
+-- UNIQUE-constraint-backed replica identity index.
+CREATE TABLE ri_uniq_excl (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_uniq_excl REPLICA IDENTITY USING INDEX ri_uniq_excl_a_key;
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('ri_uniq_excl'::regclass, owner => false, except_kinds => ARRAY['unique','replica_identity']);
+DROP TABLE ri_uniq_excl;
+
+-- Bare CREATE UNIQUE INDEX (no backing constraint) used as REPLICA
+-- IDENTITY: the index is emitted by the "index" kind, so excluding
+-- "index" without also excluding "replica_identity" must error.
+CREATE TABLE ri_idx_excl (a int NOT NULL, b int);
+CREATE UNIQUE INDEX ri_idx_excl_a_uniq ON ri_idx_excl (a);
+ALTER TABLE ri_idx_excl REPLICA IDENTITY USING INDEX ri_idx_excl_a_uniq;
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index']);
+SELECT * FROM pg_get_table_ddl('ri_idx_excl'::regclass, owner => false, except_kinds => ARRAY['index','replica_identity']);
+DROP TABLE ri_idx_excl;
+
+-- Include-side analog: only_kinds=replica_identity must include the kind
+-- that emits the index, or the function errors.
+CREATE TABLE ri_only (a int NOT NULL UNIQUE, b int);
+ALTER TABLE ri_only REPLICA IDENTITY USING INDEX ri_only_a_key;
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+-- Including both kinds succeeds.
+SELECT * FROM pg_get_table_ddl('ri_only'::regclass, owner => false, only_kinds => ARRAY['unique','replica_identity']);
+DROP TABLE ri_only;
+
+-- Per-kind only_kinds / except_kinds coverage. For each kind in the vocabulary,
+-- verify that only_kinds=K emits just that kind's output and that except_kinds=K
+-- omits just that kind's output. Kinds with no fixture above (trigger,
+-- policy) are still recognized but currently emit nothing; only_kinds=K
+-- returns zero rows for them. Fixtures used:
+-- idxd: PK + two secondary CREATE INDEX
+-- cons: PK + UNIQUE + CHECK + FK (constraint-backed)
+-- rt: CREATE RULE
+-- stx: CREATE STATISTICS
+-- rls: ENABLE/FORCE ROW LEVEL SECURITY
+-- ri_full: REPLICA IDENTITY FULL (no source-kind dependency)
+CREATE TABLE excl_fix (rng int4range,
+ EXCLUDE USING gist (rng WITH &&));
+
+-- only_kinds=K: emit only kind K's statements.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index']);
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['primary_key']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+SELECT * FROM pg_get_table_ddl('rt'::regclass, owner => false, only_kinds => ARRAY['rule']);
+SELECT * FROM pg_get_table_ddl('stx'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+SELECT * FROM pg_get_table_ddl('rls'::regclass, owner => false, only_kinds => ARRAY['rls']);
+SELECT * FROM pg_get_table_ddl('ri_full'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['policy']);
+
+-- except_kinds=K: emit everything except kind K's statements. PK/UNIQUE/
+-- INDEX are exercised on tables without REPLICA IDENTITY USING INDEX
+-- so the validation does not trip.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['primary_key']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['unique']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, except_kinds => ARRAY['check']);
+SELECT * FROM pg_get_table_ddl('excl_fix'::regclass, owner => false, except_kinds => ARRAY['exclusion']);
+-- trigger / policy are not yet implemented; uncomment when
+-- pg_get_trigger_ddl / pg_get_policy_ddl helpers land.
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['trigger']);
+-- SELECT * FROM pg_get_table_ddl('basic'::regclass, owner => false, except_kinds => ARRAY['policy']);
+-- except_kinds=table suppresses the CREATE TABLE + OWNER + child-default
+-- SET DEFAULT + attoptions passes, leaving only the sub-object passes.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, except_kinds => ARRAY['table']);
+
+-- Multi-kind compositions.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['table','index']);
+SELECT * FROM pg_get_table_ddl('cons'::regclass, owner => false, only_kinds => ARRAY['primary_key','foreign_key']);
+-- Duplicate entries in the list are silently de-duplicated by the
+-- Bitmapset.
+SELECT * FROM pg_get_table_ddl('idxd'::regclass, owner => false, only_kinds => ARRAY['index','index']);
+
+DROP TABLE excl_fix;
+
+-- Kind-filter tests on partitioned tables with indexes.
+--
+-- only_kinds=index on a partitioned table: only the root CREATE INDEX is
+-- emitted; no CREATE TABLE, no partition children (KIND_PARTITION absent).
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ only_kinds => ARRAY['index']);
+
+-- except_kinds=index on a partitioned table: CREATE TABLE for the parent
+-- and all children are emitted; no index statements.
+SELECT * FROM pg_get_table_ddl('parted_mixed'::regclass, owner => false,
+ except_kinds => ARRAY['index']);
+
+-- only_kinds=primary_key on a partitioned table: only the ALTER TABLE ADD
+-- CONSTRAINT PK on the parent; the child's inherited PK (conislocal=false)
+-- is skipped; no CREATE TABLE, no partition children.
+SELECT * FROM pg_get_table_ddl('parted_pk'::regclass, owner => false,
+ only_kinds => ARRAY['primary_key']);
+
+-- only_kinds=K on a table that does not have any K of that kind is a valid
+-- filter and returns zero rows. Tested previously with foreign_key
+-- on 'refd'; here also rule, statistics, rls, replica_identity, and
+-- exclusion on 'basic' (which has only PK + columns).
+SELECT count(*) AS rule_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rule']);
+SELECT count(*) AS stats_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['statistics']);
+SELECT count(*) AS rls_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['rls']);
+SELECT count(*) AS ri_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['replica_identity']);
+SELECT count(*) AS excl_rows
+ FROM pg_get_table_ddl('basic'::regclass, owner => false, only_kinds => ARRAY['exclusion']);
+
+-- Input-validation edge cases for the kind array. All
+-- of these raise an error; the function never reaches relation open.
+-- Empty array:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[]::text[]);
+-- NULL element:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY[NULL::text]);
+-- Unrecognized kind:
+SELECT * FROM pg_get_table_ddl('basic'::regclass, only_kinds => ARRAY['no_such_kind']);
+-- Same kind specified through both parameters is still mutex-rejected.
+SELECT * FROM pg_get_table_ddl('basic'::regclass,
+ only_kinds => ARRAY['index'],
+ except_kinds => ARRAY['rule']);
+
+-- Round-trip verification. For every test table, capture the DDL the
+-- function emits, drop the schema, replay each table's DDL in
+-- dependency order, and confirm that pg_get_table_ddl on the recreated
+-- relation matches the original line-for-line. The final SELECT must
+-- return zero rows.
+CREATE TEMP TABLE pgtbl_ddl_rt_orig (name text, ord int, line text);
+INSERT INTO pgtbl_ddl_rt_orig
+SELECT t.name, o.ord, o.line
+FROM (VALUES
+ ('basic'), ('id_cols'), ('id_custom'), ('gen_cols'), ('gen_virtual'), ('storage_cols'),
+ ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'),
+ ('fk_acts_tgt'), ('fk_acts'), ('notenf_tgt'), ('notenf'), ('self_ref'), ('temporal_pk'),
+ ('idxd'),
+ ('par'), ('ch'), ('attopt'),
+ ('parted_range'), ('parted_range_1'), ('parted_range_def'),
+ ('parted_hash'), ('parted_hash_0'),
+ ('parted_list'),
+ ('parted_idx'), ('parted_idx_a'),
+ ('parted_plain'), ('parted_plain_a'),
+ ('parted_pk'), ('parted_pk_a'),
+ ('parted_sub'), ('parted_sub_a'), ('parted_sub_a1'),
+ ('parted_child_idx'), ('parted_child_idx_a'),
+ ('parted_mixed'), ('parted_mixed_a'),
+ ('parted_fk_tgt'), ('parted_fk'), ('parted_fk_a'),
+ ('rt_log'), ('rt'),
+ ('stx'), ('rls'), ('ri_full'), ('ri_idx'), ('uno')
+ ) AS t(name),
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord);
+
+DO $$
+DECLARE
+ tables CONSTANT text[] := ARRAY[
+ 'basic', 'id_cols', 'id_custom', 'gen_cols', 'gen_virtual', 'storage_cols',
+ 'refd', 'cons', 'nulls_nd', 'idx_inc',
+ 'fk_acts_tgt', 'fk_acts', 'notenf_tgt', 'notenf', 'self_ref', 'temporal_pk',
+ 'idxd',
+ 'par', 'ch', 'attopt',
+ 'parted_range', 'parted_range_1', 'parted_range_def',
+ 'parted_hash', 'parted_hash_0',
+ 'parted_list',
+ 'parted_idx', 'parted_idx_a',
+ 'parted_plain', 'parted_plain_a',
+ 'parted_pk', 'parted_pk_a',
+ 'parted_sub', 'parted_sub_a', 'parted_sub_a1',
+ 'parted_child_idx', 'parted_child_idx_a',
+ 'parted_mixed', 'parted_mixed_a',
+ 'parted_fk_tgt', 'parted_fk', 'parted_fk_a',
+ 'rt_log', 'rt',
+ 'stx', 'rls', 'ri_full', 'ri_idx', 'uno'
+ ];
+ t text;
+ stmt text;
+BEGIN
+ DROP SCHEMA pgtbl_ddl_test CASCADE;
+ CREATE SCHEMA pgtbl_ddl_test;
+ FOREACH t IN ARRAY tables LOOP
+ FOR stmt IN
+ SELECT line FROM pgtbl_ddl_rt_orig WHERE name = t ORDER BY ord
+ LOOP
+ BEGIN
+ EXECUTE stmt;
+ EXCEPTION WHEN duplicate_table THEN
+ NULL; -- partition child already created by its parent's DDL
+ END;
+ END LOOP;
+ END LOOP;
+END $$;
+
+WITH after_ddl AS (
+ SELECT t.name, o.ord, o.line
+ FROM (SELECT DISTINCT name FROM pgtbl_ddl_rt_orig) AS t,
+ LATERAL pg_get_table_ddl(('pgtbl_ddl_test.' || t.name)::regclass,
+ owner => false) WITH ORDINALITY o(line, ord)
+)
+(SELECT 'missing-in-copy' AS kind, name, ord, line FROM pgtbl_ddl_rt_orig
+ EXCEPT
+ SELECT 'missing-in-copy', name, ord, line FROM after_ddl)
+UNION ALL
+(SELECT 'extra-in-copy' AS kind, name, ord, line FROM after_ddl
+ EXCEPT
+ SELECT 'extra-in-copy', name, ord, line FROM pgtbl_ddl_rt_orig)
+ORDER BY kind, name, ord;
+
+-- Cleanup.
+DROP SCHEMA pgtbl_ddl_test CASCADE;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ffb413ab612..be539e9ac72 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1648,6 +1648,7 @@ ListenerEntry
LoInfo
LoadStmt
LocalBufferLookupEnt
+LocalNotNullEntry
LocalPgBackendStatus
LocalTransactionId
Location
@@ -3152,6 +3153,7 @@ TableFuncScan
TableFuncScanState
TableFuncType
TableInfo
+TableDdlContext
TableLikeClause
TableSampleClause
TableScanDesc
--
2.51.0
^ permalink raw reply [nested|flat] 54+ messages in thread
end of thread, other threads:[~2026-07-09 08:53 UTC | newest]
Thread overview: 54+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-06-03 12:58 [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements Akshay Joshi <[email protected]>
2026-06-08 11:30 ` Akshay Joshi <[email protected]>
2026-06-08 19:11 ` Marcos Pegoraro <[email protected]>
2026-06-08 21:12 ` Zsolt Parragi <[email protected]>
2026-06-09 08:58 ` Akshay Joshi <[email protected]>
2026-06-09 19:17 ` Zsolt Parragi <[email protected]>
2026-06-10 11:24 ` Akshay Joshi <[email protected]>
2026-06-10 20:43 ` Zsolt Parragi <[email protected]>
2026-06-11 07:48 ` Akshay Joshi <[email protected]>
2026-06-11 13:07 ` Marcos Pegoraro <[email protected]>
2026-06-15 07:52 ` Akshay Joshi <[email protected]>
2026-06-15 21:04 ` Marcos Pegoraro <[email protected]>
2026-06-19 12:19 ` Akshay Joshi <[email protected]>
2026-06-19 19:45 ` Zsolt Parragi <[email protected]>
2026-06-22 06:26 ` Akshay Joshi <[email protected]>
2026-06-22 06:54 ` Chao Li <[email protected]>
2026-06-22 12:40 ` Akshay Joshi <[email protected]>
2026-06-22 14:00 ` Chao Li <[email protected]>
2026-06-22 14:11 ` Akshay Joshi <[email protected]>
2026-06-23 02:18 ` Chao Li <[email protected]>
2026-06-23 07:21 ` Kyotaro Horiguchi <[email protected]>
2026-06-23 09:22 ` Akshay Joshi <[email protected]>
2026-06-23 13:34 ` Akshay Joshi <[email protected]>
2026-06-23 14:03 ` Marcos Pegoraro <[email protected]>
2026-06-24 08:23 ` Akshay Joshi <[email protected]>
2026-06-23 20:05 ` Zsolt Parragi <[email protected]>
2026-06-24 09:16 ` Akshay Joshi <[email protected]>
2026-06-28 16:44 ` Rui Zhao <[email protected]>
2026-06-29 10:40 ` Akshay Joshi <[email protected]>
2026-06-29 13:38 ` Akshay Joshi <[email protected]>
2026-06-29 21:31 ` Zsolt Parragi <[email protected]>
2026-07-01 14:12 ` Akshay Joshi <[email protected]>
2026-07-01 15:10 ` Marcos Pegoraro <[email protected]>
2026-07-02 07:48 ` Akshay Joshi <[email protected]>
2026-07-02 16:35 ` Marcos Pegoraro <[email protected]>
2026-07-01 22:19 ` Zsolt Parragi <[email protected]>
2026-07-02 10:39 ` Akshay Joshi <[email protected]>
2026-07-02 22:34 ` Zsolt Parragi <[email protected]>
2026-07-03 08:47 ` Akshay Joshi <[email protected]>
2026-07-04 16:35 ` Rui Zhao <[email protected]>
2026-07-06 10:57 ` Akshay Joshi <[email protected]>
2026-07-07 16:53 ` Rui Zhao <[email protected]>
2026-07-08 11:56 ` Akshay Joshi <[email protected]>
2026-07-08 13:58 ` Marcos Pegoraro <[email protected]>
2026-07-09 08:53 ` Akshay Joshi <[email protected]>
2026-07-02 02:17 ` Kyotaro Horiguchi <[email protected]>
2026-07-02 08:48 ` Akshay Joshi <[email protected]>
2026-06-22 14:54 ` Marcos Pegoraro <[email protected]>
2026-06-23 08:57 ` Akshay Joshi <[email protected]>
2026-06-22 18:56 ` Zsolt Parragi <[email protected]>
2026-06-11 21:00 ` Zsolt Parragi <[email protected]>
2026-06-12 01:10 ` Kyotaro Horiguchi <[email protected]>
2026-06-15 08:55 ` Akshay Joshi <[email protected]>
2026-06-23 06:15 ` Kyotaro Horiguchi <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox