From 295c7f228827ed070a9cacb3e4b0ca3e02ef138e Mon Sep 17 00:00:00 2001
From: Akshay Joshi <akshay.joshi@enterprisedb.com>
Date: Tue, 2 Jun 2026 14:18:40 +0530
Subject: [PATCH v22] 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 <akshay.joshi@enterprisedb.com>
Reviewed-by: Marcos Pegoraro <marcos@f10.com.br>
Reviewed-by: Zsolt Parragi <zsolt.parragi@percona.com>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: Rui Zhao <zhaorui126@gmail.com>
---
 doc/src/sgml/func/func-info.sgml              |  193 +
 src/backend/catalog/pg_inherits.c             |   93 +
 src/backend/commands/tablecmds.c              |   30 +-
 src/backend/utils/adt/ddlutils.c              | 3175 ++++++++++++++++-
 src/backend/utils/adt/ruleutils.c             |  139 +-
 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     | 1966 ++++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/pg_get_table_ddl.sql     | 1120 ++++++
 src/tools/pgindent/typedefs.list              |    2 +
 13 files changed, 6701 insertions(+), 35 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>) &mdash;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> &mdash;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> &mdash;
+           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> &mdash;
+           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..d72a74df6a5 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,215 @@ 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 */
+	bool		not_valid;		/* constraint has convalidated=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 *format_name_for_emit(const char *name, Oid nsp,
+								  bool schema_qualified, Oid base_namespace);
+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_identity_sequence_alterations(TableDdlContext *ctx);
+static void emit_index_column_statistics(TableDdlContext *ctx, Oid idxoid,
+										 int indnkeyatts);
+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)
+		{
+			/*
+			 * token points into raw, so copy it before freeing raw to avoid
+			 * a use-after-free in the error message.
+			 */
+			char	   *bad_kind = pstrdup(token);
+
+			pfree(raw);
+			pfree(elems);
+			pfree(nulls);
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unrecognized kind \"%s\" in parameter \"%s\"",
+							bad_kind, paramname)));
+		}
+
+		pfree(raw);
+	}
+
+	pfree(elems);
+	pfree(nulls);
+	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 +1246,2901 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
 		SRF_RETURN_DONE(funcctx);
 	}
 }
+
+/*
+ * format_name_for_emit
+ *		Return a (possibly schema-qualified) SQL identifier for a non-relation
+ *		object (statistics, sequences, etc.) given its name and namespace OID.
+ *		Follows the same schema_qualified / base_namespace semantics as
+ *		lookup_relname_for_emit: bare name only when schema_qualified is false
+ *		and the object lives in base_namespace; qualified otherwise.
+ *
+ * Caller is responsible for pfree()ing the result.
+ */
+static char *
+format_name_for_emit(const char *name, Oid nsp,
+					 bool schema_qualified, Oid base_namespace)
+{
+	if (!schema_qualified && nsp == base_namespace)
+		return pstrdup(quote_identifier(name));
+
+	{
+		char	   *nspname = get_namespace_name(nsp);
+		char	   *result;
+
+		if (nspname == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_OBJECT),
+					 errmsg("schema with OID %u does not exist", nsp),
+					 errdetail("It may have been concurrently dropped.")));
+		result = quote_qualified_identifier(nspname, name);
+		pfree(nspname);
+		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.
+ *
+ * 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;
+		entries[attnum].not_valid = !con->convalidated;
+		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 &&
+			!entries[attnum].not_valid &&	/* don't skip NOT VALID - must go through emit_local_constraints */
+			(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;
+
+		/*
+		 * NOT VALID constraints must not be emitted inline: CREATE TABLE
+		 * always validates the constraint against the (empty) table, so the
+		 * reconstructed constraint would come back as validated even though
+		 * the source had convalidated = false.  Leave them for
+		 * emit_local_constraints, which emits ALTER TABLE ... ADD CONSTRAINT
+		 * ... NOT VALID and preserves the flag.
+		 */
+		if (!con->convalidated)
+			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.  Must use seq->seqtypid (the actual
+						 * sequence type), not att->atttypid (the column type):
+						 * when AS <type> is emitted because the two differ,
+						 * PostgreSQL derives the default MINVALUE/MAXVALUE
+						 * from the sequence type, so comparing against the
+						 * column type's bounds would produce incorrect
+						 * "changed from default" judgements and emit redundant
+						 * options that conflict with the AS clause.
+						 */
+						switch (seq->seqtypid)
+						{
+							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);
+
+						/*
+						 * AS type: only emit when the sequence type was
+						 * explicitly changed from its default.  For identity
+						 * columns the implicit default is the column's own
+						 * type, so omit when they match.
+						 */
+						if (seq->seqtypid != att->atttypid)
+						{
+							char	   *typname = format_type_be(seq->seqtypid);
+
+							appendStringInfo(&opts, "%sAS %s",
+											 first_opt ? "" : " ", typname);
+							first_opt = false;
+							pfree(typname);
+						}
+
+						/*
+						 * 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.
+			 *
+			 * Also suppress NOT VALID constraints here: they must be emitted
+			 * via ALTER TABLE ... ADD CONSTRAINT ... NOT VALID to preserve
+			 * the convalidated=false flag.
+			 */
+			if (!nn->inherited_notnull && !nn->not_valid)
+			{
+				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 and NOT VALID NOT NULLs are not inline overrides */
+		has_notnull = att->attnotnull &&
+			!nn_entries[att->attnum].inherited_notnull &&
+			!nn_entries[att->attnum].not_valid;
+
+		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;
+	char	   *toastOptsStr = NULL;	/* toast.key=value pairs, or NULL */
+
+	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);
+
+	/*
+	 * Read TOAST table reloptions; they live in the TOAST relation's own
+	 * pg_class row and must be emitted as "toast.key=value" entries in the
+	 * WITH clause.  Read them now while we have the main classtup.
+	 */
+	{
+		bool		toastOidIsnull;
+		Datum		toastOidDatum = SysCacheGetAttr(RELOID, classtup,
+													Anum_pg_class_reltoastrelid,
+													&toastOidIsnull);
+		Oid			toastRelOid = toastOidIsnull ? InvalidOid
+			: DatumGetObjectId(toastOidDatum);
+
+		if (OidIsValid(toastRelOid))
+		{
+			HeapTuple	toastTup = SearchSysCache1(RELOID,
+												   ObjectIdGetDatum(toastRelOid));
+
+			if (HeapTupleIsValid(toastTup))
+			{
+				bool		toastOptIsnull;
+				Datum		toastOptDatum = SysCacheGetAttr(RELOID, toastTup,
+															Anum_pg_class_reloptions,
+															&toastOptIsnull);
+
+				if (!toastOptIsnull)
+				{
+					Datum	   *toastopts;
+					int			ntoastopts;
+					StringInfoData tbuf;
+
+					initStringInfo(&tbuf);
+					deconstruct_array_builtin(DatumGetArrayTypeP(toastOptDatum),
+											  TEXTOID, &toastopts, NULL,
+											  &ntoastopts);
+					for (int ti = 0; ti < ntoastopts; ti++)
+					{
+						char	   *opt = TextDatumGetCString(toastopts[ti]);
+						char	   *sep = strchr(opt, '=');
+						char	   *key,
+								   *val;
+
+						if (sep)
+						{
+							*sep = '\0';
+							key = opt;
+							val = sep + 1;
+						}
+						else
+						{
+							key = opt;
+							val = "";
+						}
+						if (ti > 0)
+							appendStringInfoString(&tbuf, ", ");
+						appendStringInfo(&tbuf, "toast.%s=",
+										 quote_identifier(key));
+						if (quote_identifier(val) == val)
+							appendStringInfoString(&tbuf, val);
+						else
+						{
+							char	   *qval = quote_literal_cstr(val);
+
+							appendStringInfoString(&tbuf, qval);
+							pfree(qval);
+						}
+						pfree(opt);
+					}
+					toastOptsStr = tbuf.data; /* palloc'd; free after WITH */
+				}
+				ReleaseSysCache(toastTup);
+			}
+		}
+	}
+
+	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 that carry a local NOT
+		 * NULL constraint need a table-level NOT NULL clause here so that
+		 * the constraint is established before INHERITS propagates the
+		 * parent's version.  Without it, an out-of-line ALTER TABLE ADD
+		 * CONSTRAINT would find a same-column NOT NULL already propagated
+		 * by INHERITS and fail.
+		 *
+		 * User-defined names:  emit CONSTRAINT name NOT NULL col.  The name
+		 * survives and the constraint merges correctly with the inherited one.
+		 *
+		 * Auto-names: emit NOT NULL col (no CONSTRAINT clause).  PostgreSQL
+		 * will assign the child-table auto-name (e.g. ntc_a_not_null),
+		 * matching the original.  An out-of-line ALTER TABLE would collide
+		 * with the constraint that INHERITS has already propagated under the
+		 * parent's name.
+		 */
+		{
+			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)
+					continue;
+
+				if (any_content)
+					appendStringInfoChar(&ctx->buf, ',');
+				if (ctx->pretty)
+					appendStringInfoString(&ctx->buf, "\n    ");
+				else if (any_content)
+					appendStringInfoChar(&ctx->buf, ' ');
+				any_content = true;
+
+				if (nn->is_auto)
+				{
+					appendStringInfo(&ctx->buf, "NOT NULL %s",
+									 quote_identifier(NameStr(att->attname)));
+					/* Mark as emitted so emit_local_constraints skips it. */
+					ctx->skip_notnull_oids =
+						lappend_oid(ctx->skip_notnull_oids, nn->conoid);
+				}
+				else
+				{
+					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) &&
+		(relkind == RELKIND_PARTITIONED_TABLE ||
+		 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 || toastOptsStr != NULL)
+	{
+		appendStringInfoString(&ctx->buf, " WITH (");
+		if (!reloptIsnull)
+			get_reloptions(&ctx->buf, reloptDatum);
+		if (toastOptsStr != NULL)
+		{
+			if (!reloptIsnull)
+				appendStringInfoString(&ctx->buf, ", ");
+			appendStringInfoString(&ctx->buf, toastOptsStr);
+			pfree(toastOptsStr);
+		}
+		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);
+		}
+
+		{
+			bool		statisnull;
+			Datum		statDatum = SysCacheGetAttr(ATTNUM, attTup,
+													Anum_pg_attribute_attstattarget,
+													&statisnull);
+
+			if (!statisnull && DatumGetInt16(statDatum) >= 0)
+			{
+				resetStringInfo(&ctx->buf);
+				appendStringInfo(&ctx->buf,
+								 "ALTER TABLE %s ALTER COLUMN %s SET STATISTICS %d;",
+								 ctx->qualname,
+								 quote_identifier(NameStr(att->attname)),
+								 (int) DatumGetInt16(statDatum));
+				append_stmt(ctx);
+			}
+		}
+
+		ReleaseSysCache(attTup);
+	}
+}
+
+/*
+ * emit_typed_column_storage
+ *		For typed tables (CREATE TABLE ... OF type_name) and partition
+ *		children, emit per-column STORAGE and COMPRESSION overrides as
+ *		ALTER TABLE statements.
+ *
+ *		For typed tables, the grammar only allows DEFAULT / NOT NULL / CHECK
+ *		in the column-options list, so STORAGE and COMPRESSION cannot appear
+ *		inline and must be emitted post-CREATE.
+ *
+ *		For partition children, we compare each column's storage/compression
+ *		against the parent partition's values and emit only when the child
+ *		explicitly overrides.
+ */
+static void
+emit_typed_column_storage(TableDdlContext *ctx)
+{
+	TupleDesc	tupdesc;
+	bool		is_typed = OidIsValid(ctx->rel->rd_rel->reloftype);
+	bool		is_partition = ctx->rel->rd_rel->relispartition;
+	Oid			partParentOid = InvalidOid;
+
+	if (!is_typed && !is_partition)
+		return;
+
+	tupdesc = RelationGetDescr(ctx->rel);
+
+	/*
+	 * For partition children, look up the parent OID once outside the loop;
+	 * each inherited column will need it to find the parent's baseline.
+	 */
+	if (is_partition)
+		partParentOid = get_partition_parent(ctx->relid, false);
+
+	for (int i = 0; i < tupdesc->natts; i++)
+	{
+		Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+		char		baseline_storage;
+		char		baseline_compression;
+
+		if (att->attisdropped)
+			continue;
+
+		if (is_partition)
+		{
+			/*
+			 * For partition children, compare against the parent partition's
+			 * column values; only emit when the child explicitly overrides.
+			 */
+			if (att->attislocal)
+			{
+				/* locally added column: baseline is the type default */
+				baseline_storage = get_typstorage(att->atttypid);
+				baseline_compression = InvalidCompressionMethod;
+			}
+			else
+			{
+				/* inherited column: get parent's values */
+				HeapTuple	parentAttTup =
+					SearchSysCache2(ATTNAME,
+									ObjectIdGetDatum(partParentOid),
+									CStringGetDatum(NameStr(att->attname)));
+
+				if (!HeapTupleIsValid(parentAttTup))
+				{
+					baseline_storage = get_typstorage(att->atttypid);
+					baseline_compression = InvalidCompressionMethod;
+				}
+				else
+				{
+					Form_pg_attribute parentAtt =
+						(Form_pg_attribute) GETSTRUCT(parentAttTup);
+
+					baseline_storage = parentAtt->attstorage;
+					baseline_compression = parentAtt->attcompression;
+					ReleaseSysCache(parentAttTup);
+				}
+			}
+		}
+		else
+		{
+			/* typed table: baseline is always the type default */
+			baseline_storage = get_typstorage(att->atttypid);
+			baseline_compression = InvalidCompressionMethod;
+		}
+
+		if (att->attstorage != baseline_storage)
+		{
+			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 (att->attcompression != baseline_compression &&
+			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_identity_sequence_alterations
+ *		For identity sequence columns that have non-default persistence
+ *		(e.g. UNLOGGED), emit the corresponding ALTER SEQUENCE statement.
+ *
+ *		The CREATE TABLE ... GENERATED AS IDENTITY inline syntax has no way
+ *		to specify sequence persistence, so it must be emitted post-CREATE.
+ */
+static void
+emit_identity_sequence_alterations(TableDdlContext *ctx)
+{
+	TupleDesc	tupdesc = RelationGetDescr(ctx->rel);
+
+	for (int i = 0; i < tupdesc->natts; i++)
+	{
+		Form_pg_attribute att = TupleDescAttr(tupdesc, i);
+		Oid			seqid;
+		HeapTuple	seqClassTup;
+		Form_pg_class seqClass;
+
+		if (att->attisdropped)
+			continue;
+		if (att->attidentity == '\0')
+			continue;
+
+		seqid = getIdentitySequence(ctx->rel, att->attnum, true);
+		if (!OidIsValid(seqid))
+			continue;
+
+		seqClassTup = SearchSysCache1(RELOID, ObjectIdGetDatum(seqid));
+		if (!HeapTupleIsValid(seqClassTup))
+			continue;
+		seqClass = (Form_pg_class) GETSTRUCT(seqClassTup);
+
+		if (seqClass->relpersistence == RELPERSISTENCE_UNLOGGED)
+		{
+			char	   *seqname = lookup_relname_for_emit(seqid,
+														  ctx->schema_qualified,
+														  ctx->base_namespace);
+
+			resetStringInfo(&ctx->buf);
+			appendStringInfo(&ctx->buf, "ALTER SEQUENCE %s SET UNLOGGED;",
+							 seqname);
+			append_stmt(ctx);
+			pfree(seqname);
+		}
+
+		ReleaseSysCache(seqClassTup);
+	}
+}
+
+/*
+ * emit_index_column_statistics
+ *		Emit ALTER INDEX name ALTER COLUMN n SET STATISTICS for each key
+ *		column of idxoid that has a non-default (non-null, >= 0) statistics
+ *		target.  Only expression columns of an index support SET STATISTICS,
+ *		but the catalog check is the same regardless.
+ *
+ *		Called from emit_indexes (non-constraint indexes) and from
+ *		emit_local_constraints (PK/UNIQUE/EXCLUSION backing indexes).
+ */
+static void
+emit_index_column_statistics(TableDdlContext *ctx, Oid idxoid, int indnkeyatts)
+{
+	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);
+}
+
+/*
+ * 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;
+
+		indTup = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(idxoid));
+		if (!HeapTupleIsValid(indTup))
+			continue;
+		idxform = (Form_pg_index) GETSTRUCT(indTup);
+
+		/*
+		 * Skip invalid indexes unless this is a partitioned index
+		 * (relkind='I').  A partitioned index created with ON ONLY, or
+		 * one whose parent was marked invalid by the invalidate_parent
+		 * logic during ATTACH PARTITION, legitimately has indisvalid=false
+		 * and must still be emitted.  A plain index (relkind='i') with
+		 * indisvalid=false was left by a failed CREATE INDEX CONCURRENTLY
+		 * and is intentionally omitted.
+		 */
+		if (!idxform->indisvalid &&
+			get_rel_relkind(idxoid) != RELKIND_PARTITIONED_INDEX)
+		{
+			ReleaseSysCache(indTup);
+			continue;
+		}
+
+		indnkeyatts = idxform->indnkeyatts;
+		ReleaseSysCache(indTup);
+
+		idxdef = pg_get_indexdef_ddl(idxoid, ctx->no_tablespace);
+		resetStringInfo(&ctx->buf);
+		appendStringInfo(&ctx->buf, "%s;", idxdef);
+		append_stmt(ctx);
+		pfree(idxdef);
+
+		emit_index_column_statistics(ctx, idxoid, indnkeyatts);
+
+		/*
+		 * For partitioned indexes (relkind='I'), emit ALTER INDEX ... ATTACH
+		 * PARTITION for each direct partition-child index.  This is done here
+		 * (from the parent index's perspective) rather than from the child
+		 * partition table's perspective because emit_partition_children is now
+		 * called before emit_indexes: by the time we reach this point all
+		 * child partition tables and their CREATE INDEX statements have already
+		 * been emitted, so both sides of the ATTACH exist.
+		 *
+		 * Gate on TABLE_DDL_KIND_PARTITION: if partition children are not
+		 * being emitted (e.g. only_kinds=>'index') the child tables do not
+		 * exist in the replay target and ATTACH PARTITION would fail.
+		 */
+		if (get_rel_relkind(idxoid) == RELKIND_PARTITIONED_INDEX &&
+			is_kind_included(ctx, TABLE_DDL_KIND_PARTITION))
+		{
+			List	   *childIdxOids = find_inheritance_children(idxoid,
+																 AccessShareLock);
+			ListCell   *clc;
+
+			if (childIdxOids != NIL)
+			{
+				char	   *parentIdxName = lookup_relname_for_emit(idxoid,
+																	ctx->schema_qualified,
+																	ctx->base_namespace);
+
+				foreach(clc, childIdxOids)
+				{
+					Oid			childIdxOid = lfirst_oid(clc);
+					char	   *childIdxName = get_rel_name(childIdxOid);
+
+					if (childIdxName)
+					{
+						resetStringInfo(&ctx->buf);
+						appendStringInfo(&ctx->buf,
+										 "ALTER INDEX %s ATTACH PARTITION %s;",
+										 parentIdxName,
+										 quote_identifier(childIdxName));
+						append_stmt(ctx);
+						pfree(childIdxName);
+					}
+				}
+				pfree(parentIdxName);
+			}
+			list_free(childIdxOids);
+		}
+	}
+	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 there are two exceptions:
+				 *
+				 * - When KIND_TABLE is not in the active filter (e.g.
+				 *   only=>'check'), the inline pass never runs; fall through
+				 *   and emit each CHECK via ALTER TABLE.
+				 *
+				 * - NOT VALID constraints are intentionally excluded from
+				 *   the inline pass: CREATE TABLE validates against the empty
+				 *   table and would mark them valid.  They must come through
+				 *   here as ALTER TABLE ... ADD CONSTRAINT ... NOT VALID so
+				 *   the convalidated flag is preserved on round-trip.
+				 */
+				if (!is_partition &&
+					is_kind_included(ctx, TABLE_DDL_KIND_TABLE) &&
+					con->convalidated)
+					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, ctx->no_tablespace);
+
+			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);
+
+			/*
+			 * Emit ALTER INDEX ... ALTER COLUMN n SET STATISTICS for
+			 * constraint-backed indexes (PK, UNIQUE, EXCLUSION).  FK
+			 * constraints have no backing index, so skip them.
+			 */
+			if (con->contype != CONSTRAINT_FOREIGN &&
+				con->contype != CONSTRAINT_CHECK &&
+				con->contype != CONSTRAINT_NOTNULL)
+			{
+				Oid			conidxOid = con->conindid;
+
+				if (OidIsValid(conidxOid))
+				{
+					HeapTuple	indTup2 = SearchSysCache1(INDEXRELID,
+														  ObjectIdGetDatum(conidxOid));
+
+					if (HeapTupleIsValid(indTup2))
+					{
+						int			indnkeyatts2 =
+							((Form_pg_index) GETSTRUCT(indTup2))->indnkeyatts;
+
+						ReleaseSysCache(indTup2);
+						emit_index_column_statistics(ctx, conidxOid, indnkeyatts2);
+					}
+				}
+			}
+		}
+	}
+	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);
+
+		/* Emit ALTER STATISTICS ... SET STATISTICS if target is non-default */
+		{
+			bool		statnull;
+			Datum		stattargetDatum = SysCacheGetAttr(STATEXTOID, statTup,
+														  Anum_pg_statistic_ext_stxstattarget,
+														  &statnull);
+
+			if (!statnull)
+			{
+				int16		stattarget = DatumGetInt16(stattargetDatum);
+
+				if (stattarget >= 0)
+				{
+					char	   *statname =
+						format_name_for_emit(NameStr(stat->stxname),
+											 stat->stxnamespace,
+											 ctx->schema_qualified,
+											 ctx->base_namespace);
+
+					resetStringInfo(&ctx->buf);
+					appendStringInfo(&ctx->buf,
+									 "ALTER STATISTICS %s SET STATISTICS %d;",
+									 statname, (int) stattarget);
+					append_stmt(ctx);
+					pfree(statname);
+				}
+			}
+		}
+	}
+	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.
+ *
+ *		Emits the toggle statements, then warns if the table carries any
+ *		policies that cannot yet be reconstructed (pg_get_policy_ddl is not
+ *		yet wired up).  Without the warning, replaying the output would
+ *		produce a table with RLS enabled but zero policies, silently denying
+ *		all rows to non-superusers.
+ */
+static void
+emit_rls_toggles(TableDdlContext *ctx)
+{
+	if (!is_kind_included(ctx, TABLE_DDL_KIND_RLS))
+		return;
+
+	if (ctx->rel->rd_rel->relrowsecurity)
+	{
+		Relation	polRel;
+		ScanKeyData polKey;
+		SysScanDesc polScan;
+		bool		has_policies;
+
+		resetStringInfo(&ctx->buf);
+		appendStringInfo(&ctx->buf,
+						 "ALTER TABLE %s ENABLE ROW LEVEL SECURITY;",
+						 ctx->qualname);
+		append_stmt(ctx);
+
+		/*
+		 * Warn if the table has policies that cannot yet be reconstructed.
+		 * Without the warning, replaying the output would produce a table
+		 * with RLS enabled but zero policies, silently denying all rows to
+		 * non-superusers.  CREATE POLICY reconstruction is pending
+		 * pg_get_policy_ddl().
+		 */
+		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);
+		has_policies = HeapTupleIsValid(systable_getnext(polScan));
+		systable_endscan(polScan);
+		table_close(polRel, AccessShareLock);
+
+		if (has_policies)
+			ereport(WARNING,
+					(errcode(ERRCODE_WARNING),
+					 errmsg("row-level security policies on table \"%s\" are not reconstructed",
+							RelationGetRelationName(ctx->rel)),
+					 errdetail("pg_get_table_ddl() does not yet emit CREATE POLICY "
+							   "statements; policies must be added manually.")));
+	}
+	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;
+
+		CHECK_FOR_INTERRUPTS();
+
+		/*
+		 * 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;
+		}
+
+		{
+			char		childkind = get_rel_relkind(childoid);
+
+			if (childkind == RELKIND_FOREIGN_TABLE)
+			{
+				char	   *childname = get_rel_name(childoid);
+
+				ereport(WARNING,
+						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						 errmsg("skipping foreign-table partition \"%s\"",
+								childname ? childname : "?"),
+						 errdetail("pg_get_table_ddl() cannot reconstruct foreign table partitions.")));
+				if (childname)
+					pfree(childname);
+				continue;
+			}
+			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);
+
+	/* Caller needs SELECT on the table to read its definition. */
+	aclresult = pg_class_aclcheck(ctx->relid, GetUserId(), ACL_SELECT);
+	if (aclresult != ACLCHECK_OK)
+	{
+		char	   *relname = pstrdup(RelationGetRelationName(rel));
+
+		table_close(rel, AccessShareLock);
+		aclcheck_error(aclresult, OBJECT_TABLE, relname);
+	}
+
+	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)));
+	}
+
+	/*
+	 * 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;
+	 * partition children before parent indexes (see comment on
+	 * emit_partition_children below); non-constraint indexes before
+	 * constraints (constraint-backed indexes are emitted out-of-line by the
+	 * constraint loop).  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_identity_sequence_alterations(ctx);
+	}
+
+	/*
+	 * Emit partition children before the parent's indexes so that
+	 * CREATE TABLE ... PARTITION OF is executed while no parent indexes
+	 * exist yet.  If parent indexes were emitted first, each PARTITION OF
+	 * would auto-create and auto-attach a child index, making the explicit
+	 * CREATE INDEX + ALTER INDEX ... ATTACH PARTITION that follows
+	 * redundant (and the CREATE INDEX would fail with "already exists").
+	 * With children emitted first, the parent's emit_indexes then emits
+	 * CREATE INDEX ON ONLY for each partitioned index and immediately
+	 * follows it with ALTER INDEX ... ATTACH PARTITION for every direct
+	 * child partition index.
+	 */
+	emit_partition_children(ctx);
+	emit_indexes(ctx);
+	emit_local_constraints(ctx);
+	emit_cluster_on(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
+
+	{
+		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 0ea38c18ca4..2f4d607cba9 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,11 +371,12 @@ 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,
-										 int prettyFlags, bool missing_ok);
+										 int prettyFlags, bool missing_ok,
+										 bool no_tablespace);
 static text *pg_get_expr_worker(text *expr, Oid relid, int prettyFlags);
 static int	print_function_arguments(StringInfo buf, HeapTuple proctup,
 									 bool print_table_args, bool print_defaults);
@@ -605,6 +607,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 +1258,26 @@ 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.
+ *
+ * When no_tablespace is true, the TABLESPACE clause is suppressed.
+ * inherits=false is always passed so partitioned indexes emit ON ONLY,
+ * preventing auto-creation of child indexes on replay.
+ */
+char *
+pg_get_indexdef_ddl(Oid indexrelid, bool no_tablespace)
+{
+	return pg_get_indexdef_worker(indexrelid, 0, NULL,
+								  false, false,
+								  !no_tablespace, false,
+								  PRETTYFLAG_SCHEMA, false);
+}
+
 /* Internal version that just reports the key-column definitions */
 char *
 pg_get_indexdef_columns(Oid indexrelid, bool pretty)
@@ -1971,7 +2006,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 +2021,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 +2047,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 +2059,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 +2120,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 +2222,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, ", ");
@@ -2511,7 +2567,7 @@ pg_get_constraintdef(PG_FUNCTION_ARGS)
 
 	prettyFlags = PRETTYFLAG_INDENT;
 
-	res = pg_get_constraintdef_worker(constraintId, false, prettyFlags, true);
+	res = pg_get_constraintdef_worker(constraintId, false, prettyFlags, true, false);
 
 	if (res == NULL)
 		PG_RETURN_NULL();
@@ -2529,7 +2585,7 @@ pg_get_constraintdef_ext(PG_FUNCTION_ARGS)
 
 	prettyFlags = GET_PRETTY_FLAGS(pretty);
 
-	res = pg_get_constraintdef_worker(constraintId, false, prettyFlags, true);
+	res = pg_get_constraintdef_worker(constraintId, false, prettyFlags, true, false);
 
 	if (res == NULL)
 		PG_RETURN_NULL();
@@ -2543,7 +2599,25 @@ pg_get_constraintdef_ext(PG_FUNCTION_ARGS)
 char *
 pg_get_constraintdef_command(Oid constraintId)
 {
-	return pg_get_constraintdef_worker(constraintId, true, 0, false);
+	return pg_get_constraintdef_worker(constraintId, true, 0, false, 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.
+ *
+ * When no_tablespace is true, any USING INDEX TABLESPACE clause that would
+ * otherwise be appended for PK/UNIQUE/EXCLUSION constraints is suppressed.
+ */
+char *
+pg_get_constraintdef_body(Oid constraintId, bool no_tablespace)
+{
+	return pg_get_constraintdef_worker(constraintId, false, 0, false,
+									   no_tablespace);
 }
 
 /*
@@ -2551,7 +2625,8 @@ pg_get_constraintdef_command(Oid constraintId)
  */
 static char *
 pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
-							int prettyFlags, bool missing_ok)
+							int prettyFlags, bool missing_ok,
+							bool no_tablespace)
 {
 	HeapTuple	tup;
 	Form_pg_constraint conForm;
@@ -2808,11 +2883,16 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 				}
 				ReleaseSysCache(indtup);
 
-				/* XXX why do we only print these bits if fullCommand? */
-				if (fullCommand && OidIsValid(indexId))
+				/*
+				 * Emit WITH and USING INDEX TABLESPACE when the backing index
+				 * has non-default options.  These are part of the constraint
+				 * body regardless of whether fullCommand is set; omitting them
+				 * would produce DDL that cannot round-trip when WITH or
+				 * TABLESPACE differs from the defaults.
+				 */
+				if (OidIsValid(indexId))
 				{
 					char	   *options = flatten_reloptions(indexId);
-					Oid			tblspc;
 
 					if (options)
 					{
@@ -2821,15 +2901,17 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 					}
 
 					/*
-					 * Print the tablespace, unless it's the database default.
-					 * This is to help ALTER TABLE usage of this facility,
-					 * which needs this behavior to recreate exact catalog
-					 * state.
+					 * Print the tablespace, unless it's the database default
+					 * or the caller asked to suppress it.
 					 */
-					tblspc = get_rel_tablespace(indexId);
-					if (OidIsValid(tblspc))
-						appendStringInfo(&buf, " USING INDEX TABLESPACE %s",
-										 quote_identifier(get_tablespace_name(tblspc)));
+					if (!no_tablespace)
+					{
+						Oid			tblspc = get_rel_tablespace(indexId);
+
+						if (OidIsValid(tblspc))
+							appendStringInfo(&buf, " USING INDEX TABLESPACE %s",
+											 quote_identifier(get_tablespace_name(tblspc)));
+					}
 				}
 
 				break;
@@ -2935,14 +3017,13 @@ pg_get_constraintdef_worker(Oid constraintId, bool fullCommand,
 					operators[i] = DatumGetObjectId(elems[i]);
 
 				/* pg_get_indexdef_worker does the rest */
-				/* suppress tablespace because pg_dump wants it that way */
 				appendStringInfoString(&buf,
 									   pg_get_indexdef_worker(indexOid,
 															  0,
 															  operators,
 															  false,
 															  false,
-															  false,
+															  !no_tablespace,
 															  false,
 															  prettyFlags,
 															  false));
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..7d960799ff3 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, bool no_tablespace);
 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, bool no_tablespace);
 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..ae42ae2f227
--- /dev/null
+++ b/src/test/regress/expected/pg_get_table_ddl.out
@@ -0,0 +1,1966 @@
+--
+-- 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)
+
+-- STORAGE PLAIN: text defaults to EXTENDED; PLAIN must be emitted when set
+-- explicitly because it changes toast behaviour (forces inline storage).
+CREATE TABLE storage_plain (a text STORAGE PLAIN, b bytea STORAGE PLAIN);
+SELECT * FROM pg_get_table_ddl('storage_plain'::regclass, owner => false);
+                                     pg_get_table_ddl                                     
+------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.storage_plain (a text STORAGE PLAIN, b bytea STORAGE PLAIN);
+(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)
+
+-- ON DELETE RESTRICT and ON UPDATE CASCADE / SET NULL / SET DEFAULT: the
+-- remaining referential-action combinations not covered by fk_acts above.
+CREATE TABLE fk_upd_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_upd_acts (
+    a int CONSTRAINT fk_delete_restrict   REFERENCES fk_upd_tgt(id)
+                          ON DELETE RESTRICT,
+    b int CONSTRAINT fk_update_cascade    REFERENCES fk_upd_tgt(id)
+                          ON UPDATE CASCADE,
+    c int CONSTRAINT fk_update_setdefault REFERENCES fk_upd_tgt(id)
+                          ON UPDATE SET DEFAULT,
+    d int CONSTRAINT fk_update_setnull    REFERENCES fk_upd_tgt(id)
+                          ON UPDATE SET NULL
+);
+SELECT * FROM pg_get_table_ddl('fk_upd_acts'::regclass, owner => false);
+                                                                      pg_get_table_ddl                                                                      
+------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.fk_upd_acts (a integer, b integer, c integer, d integer);
+ ALTER TABLE pgtbl_ddl_test.fk_upd_acts ADD CONSTRAINT fk_delete_restrict FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.fk_upd_tgt(id) ON DELETE RESTRICT;
+ ALTER TABLE pgtbl_ddl_test.fk_upd_acts ADD CONSTRAINT fk_update_cascade FOREIGN KEY (b) REFERENCES pgtbl_ddl_test.fk_upd_tgt(id) ON UPDATE CASCADE;
+ ALTER TABLE pgtbl_ddl_test.fk_upd_acts ADD CONSTRAINT fk_update_setdefault FOREIGN KEY (c) REFERENCES pgtbl_ddl_test.fk_upd_tgt(id) ON UPDATE SET DEFAULT;
+ ALTER TABLE pgtbl_ddl_test.fk_upd_acts ADD CONSTRAINT fk_update_setnull FOREIGN KEY (d) REFERENCES pgtbl_ddl_test.fk_upd_tgt(id) ON UPDATE SET NULL;
+(5 rows)
+
+-- DEFERRABLE INITIALLY IMMEDIATE: the deferrable-but-not-deferred variant.
+-- INITIALLY IMMEDIATE is the default for DEFERRABLE constraints, so the
+-- deparser omits it; only DEFERRABLE itself must appear in the output.
+CREATE TABLE fk_def_imm_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_def_imm (
+    a int REFERENCES fk_def_imm_tgt(id) DEFERRABLE INITIALLY IMMEDIATE
+);
+SELECT * FROM pg_get_table_ddl('fk_def_imm'::regclass, owner => false);
+                                                                pg_get_table_ddl                                                                 
+-------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.fk_def_imm (a integer);
+ ALTER TABLE pgtbl_ddl_test.fk_def_imm ADD CONSTRAINT fk_def_imm_a_fkey FOREIGN KEY (a) REFERENCES pgtbl_ddl_test.fk_def_imm_tgt(id) DEFERRABLE;
+(2 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: partition children are
+-- emitted before the parent's indexes so PARTITION OF runs without any parent
+-- index present (preventing auto-creation of child indexes).  Each child index
+-- is emitted explicitly; the parent index follows with ON ONLY and then
+-- ALTER INDEX ... ATTACH PARTITION for each child.
+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 TABLE pgtbl_ddl_test.parted_idx_a PARTITION OF pgtbl_ddl_test.parted_idx FOR VALUES IN ('a');
+ CREATE UNIQUE INDEX parted_idx_a_id_region_idx ON pgtbl_ddl_test.parted_idx_a USING btree (id, region);
+ CREATE UNIQUE INDEX parted_idx_uidx ON ONLY pgtbl_ddl_test.parted_idx USING btree (id, region);
+ ALTER INDEX pgtbl_ddl_test.parted_idx_uidx ATTACH PARTITION parted_idx_a_id_region_idx;
+(5 rows)
+
+-- Plain (non-unique) CREATE INDEX on partitioned table: same pattern.
+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 TABLE pgtbl_ddl_test.parted_plain_a PARTITION OF pgtbl_ddl_test.parted_plain FOR VALUES FROM (0) TO (100);
+ CREATE INDEX parted_plain_a_id_idx ON pgtbl_ddl_test.parted_plain_a USING btree (id);
+ CREATE INDEX parted_plain_id_idx ON ONLY pgtbl_ddl_test.parted_plain USING btree (id);
+ ALTER INDEX pgtbl_ddl_test.parted_plain_id_idx ATTACH PARTITION parted_plain_a_id_idx;
+(5 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);
+ CREATE TABLE pgtbl_ddl_test.parted_pk_a PARTITION OF pgtbl_ddl_test.parted_pk FOR VALUES IN ('a');
+ ALTER TABLE pgtbl_ddl_test.parted_pk ADD CONSTRAINT parted_pk_pkey PRIMARY KEY (id, region);
+(3 rows)
+
+-- Multi-level (sub-partitioned) table with a CREATE INDEX on the root.
+-- All partition tables are emitted depth-first before any indexes; then each
+-- level emits CREATE INDEX ON ONLY followed by ATTACH PARTITION for its children.
+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 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);
+ CREATE INDEX parted_sub_a1_id_idx ON pgtbl_ddl_test.parted_sub_a1 USING btree (id);
+ CREATE INDEX parted_sub_a_id_idx ON ONLY pgtbl_ddl_test.parted_sub_a USING btree (id);
+ ALTER INDEX pgtbl_ddl_test.parted_sub_a_id_idx ATTACH PARTITION parted_sub_a1_id_idx;
+ CREATE INDEX parted_sub_id_idx ON ONLY pgtbl_ddl_test.parted_sub USING btree (id);
+ ALTER INDEX pgtbl_ddl_test.parted_sub_id_idx ATTACH PARTITION parted_sub_a_id_idx;
+(8 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 (emitted ON ONLY, child gets ATTACH) AND the
+-- child also has its own local index (relispartition=false, emitted separately).
+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 TABLE pgtbl_ddl_test.parted_mixed_a PARTITION OF pgtbl_ddl_test.parted_mixed FOR VALUES IN ('a');
+ CREATE INDEX parted_mixed_a_id_idx ON pgtbl_ddl_test.parted_mixed_a USING btree (id);
+ CREATE INDEX parted_mixed_a_region ON pgtbl_ddl_test.parted_mixed_a USING btree (region);
+ CREATE INDEX parted_mixed_id ON ONLY pgtbl_ddl_test.parted_mixed USING btree (id);
+ ALTER INDEX pgtbl_ddl_test.parted_mixed_id ATTACH PARTITION parted_mixed_a_id_idx;
+(6 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);
+ CREATE TABLE pgtbl_ddl_test.parted_fk_a PARTITION OF pgtbl_ddl_test.parted_fk FOR VALUES FROM (0) TO (100);
+ 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);
+(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 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;
+ CREATE STATISTICS pgtbl_ddl_test.parted_stx_stat (ndistinct) ON id, region FROM pgtbl_ddl_test.parted_stx;
+(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)
+
+-- RLS with policies: a WARNING must be raised because pg_get_table_ddl cannot
+-- yet emit CREATE POLICY statements.  Replaying without the warning would
+-- produce a table with RLS enabled but no policies, silently denying all rows.
+CREATE TABLE rls_pol (id int);
+ALTER TABLE rls_pol ENABLE ROW LEVEL SECURITY;
+CREATE POLICY rls_pol_p ON rls_pol USING (id > 0);
+SELECT * FROM pg_get_table_ddl('rls_pol'::regclass, owner => false);
+WARNING:  row-level security policies on table "rls_pol" are not reconstructed
+DETAIL:  pg_get_table_ddl() does not yet emit CREATE POLICY statements; policies must be added manually.
+                       pg_get_table_ddl                        
+---------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.rls_pol (id integer);
+ ALTER TABLE pgtbl_ddl_test.rls_pol ENABLE ROW LEVEL SECURITY;
+(2 rows)
+
+DROP TABLE rls_pol;
+-- 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)
+
+-- owner => true (the default) emits ALTER TABLE ... OWNER TO.  Verify the
+-- line is present without depending on the role name used by the test runner.
+SELECT count(*) AS owner_lines
+FROM pg_get_table_ddl('basic'::regclass, owner => true)
+WHERE pg_get_table_ddl LIKE 'ALTER TABLE % OWNER TO %';
+ owner_lines 
+-------------
+           1
+(1 row)
+
+-- 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;
+-- Partitioned index created with ON ONLY has indisvalid=false until child
+-- indexes are attached, but must not be treated as a failed-CIC index.
+CREATE TABLE parted_onlyi (a int) PARTITION BY RANGE (a);
+CREATE TABLE parted_onlyi_1 PARTITION OF parted_onlyi FOR VALUES FROM (0) TO (100);
+CREATE INDEX ON ONLY parted_onlyi (a);
+SELECT * FROM pg_get_table_ddl('parted_onlyi'::regclass, owner => false);
+                                                 pg_get_table_ddl                                                  
+-------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.parted_onlyi (a integer) PARTITION BY RANGE (a);
+ CREATE TABLE pgtbl_ddl_test.parted_onlyi_1 PARTITION OF pgtbl_ddl_test.parted_onlyi FOR VALUES FROM (0) TO (100);
+ CREATE INDEX parted_onlyi_a_idx ON ONLY pgtbl_ddl_test.parted_onlyi USING btree (a);
+(3 rows)
+
+DROP TABLE parted_onlyi;
+-- NOT VALID CHECK must be emitted as ALTER TABLE ... ADD CONSTRAINT ... NOT VALID,
+-- not inline in CREATE TABLE.  An inline constraint is always validated against
+-- the empty table, which would silently flip convalidated to true on replay.
+CREATE TABLE not_valid_chk (a int);
+ALTER TABLE not_valid_chk ADD CONSTRAINT chk CHECK (a < 50) NOT VALID;
+SELECT * FROM pg_get_table_ddl('not_valid_chk'::regclass, owner => false);
+                                    pg_get_table_ddl                                     
+-----------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.not_valid_chk (a integer);
+ ALTER TABLE pgtbl_ddl_test.not_valid_chk ADD CONSTRAINT chk CHECK ((a < 50)) NOT VALID;
+(2 rows)
+
+DROP TABLE not_valid_chk;
+-- Auto-named NOT NULL on an INHERITS child's inherited column must be emitted
+-- inline as "NOT NULL col" (no CONSTRAINT clause).  An out-of-line ALTER TABLE
+-- would collide with the NOT NULL that INHERITS propagates from the parent.
+CREATE TABLE nn_inh_auto_par (a int PRIMARY KEY);
+CREATE TABLE nn_inh_auto_ch (PRIMARY KEY (a) DEFERRABLE) INHERITS (nn_inh_auto_par);
+SELECT * FROM pg_get_table_ddl('nn_inh_auto_ch'::regclass, owner => false);
+                                             pg_get_table_ddl                                             
+----------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.nn_inh_auto_ch (NOT NULL a) INHERITS (pgtbl_ddl_test.nn_inh_auto_par);
+ ALTER TABLE pgtbl_ddl_test.nn_inh_auto_ch ADD CONSTRAINT nn_inh_auto_ch_pkey PRIMARY KEY (a) DEFERRABLE;
+(2 rows)
+
+DROP TABLE nn_inh_auto_par CASCADE;
+NOTICE:  drop cascades to table nn_inh_auto_ch
+-- 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 parent's CREATE INDEX (ON ONLY)
+-- 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 ONLY 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'),
+        ('storage_plain'),
+        ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'),
+        ('fk_acts_tgt'), ('fk_acts'),
+        ('fk_upd_tgt'), ('fk_upd_acts'), ('fk_def_imm_tgt'), ('fk_def_imm'),
+        ('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',
+        'storage_plain',
+        'refd', 'cons', 'nulls_nd', 'idx_inc',
+        'fk_acts_tgt', 'fk_acts',
+        'fk_upd_tgt', 'fk_upd_acts', 'fk_def_imm_tgt', 'fk_def_imm',
+        '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 48 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 storage_plain
+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 fk_upd_tgt
+drop cascades to table fk_upd_acts
+drop cascades to table fk_def_imm_tgt
+drop cascades to table fk_def_imm
+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)
+
+-- Issue 1: CLUSTER ON must come after the constraint that creates the index.
+CREATE TABLE clu (a int, b int);
+ALTER TABLE clu ADD CONSTRAINT clu_pk PRIMARY KEY (a);
+CLUSTER clu USING clu_pk;
+SELECT stmt FROM pg_get_table_ddl('clu', owner => false) stmt;
+                                 stmt                                  
+-----------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.clu (a integer NOT NULL, b integer);
+ ALTER TABLE pgtbl_ddl_test.clu ADD CONSTRAINT clu_pk PRIMARY KEY (a);
+ ALTER TABLE pgtbl_ddl_test.clu CLUSTER ON clu_pk;
+(3 rows)
+
+-- Issue 3a: column SET STATISTICS.
+CREATE TABLE cst (a int, b text);
+ALTER TABLE cst ALTER COLUMN a SET STATISTICS 500;
+ALTER TABLE cst ALTER COLUMN b SET STATISTICS 0;
+SELECT stmt FROM pg_get_table_ddl('cst', owner => false) stmt;
+                               stmt                                
+-------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cst (a integer, b text);
+ ALTER TABLE pgtbl_ddl_test.cst ALTER COLUMN a SET STATISTICS 500;
+ ALTER TABLE pgtbl_ddl_test.cst ALTER COLUMN b SET STATISTICS 0;
+(3 rows)
+
+-- Issue 3b: extended statistics SET STATISTICS.
+CREATE TABLE ext_stat_tbl (a int, b int);
+CREATE STATISTICS ext_stat_s (dependencies) ON a, b FROM ext_stat_tbl;
+ALTER STATISTICS ext_stat_s SET STATISTICS 750;
+SELECT stmt FROM pg_get_table_ddl('ext_stat_tbl', owner => false) stmt;
+                                                 stmt                                                 
+------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ext_stat_tbl (a integer, b integer);
+ CREATE STATISTICS pgtbl_ddl_test.ext_stat_s (dependencies) ON a, b FROM pgtbl_ddl_test.ext_stat_tbl;
+ ALTER STATISTICS pgtbl_ddl_test.ext_stat_s SET STATISTICS 750;
+(3 rows)
+
+-- Issue 7: NOT NULL NOT VALID must not be emitted inline.
+CREATE TABLE nnv (a int, b int);
+ALTER TABLE nnv ADD CONSTRAINT nnv_a_nn NOT NULL a NOT VALID;
+SELECT stmt FROM pg_get_table_ddl('nnv', owner => false) stmt;
+                                     stmt                                     
+------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.nnv (a integer, b integer);
+ ALTER TABLE pgtbl_ddl_test.nnv ADD CONSTRAINT nnv_a_nn NOT NULL a NOT VALID;
+(2 rows)
+
+-- Issue 9: ACCESS METHOD on partitioned parent.
+CREATE TABLE pam (a int) PARTITION BY RANGE (a);
+ALTER TABLE pam SET ACCESS METHOD heap;
+SELECT stmt FROM pg_get_table_ddl('pam', owner => false) stmt;
+                                      stmt                                      
+--------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.pam (a integer) PARTITION BY RANGE (a) USING heap;
+(1 row)
+
+-- Issue 11a: AS type in identity sequence options.
+CREATE TABLE idt (a int GENERATED BY DEFAULT AS IDENTITY);
+ALTER SEQUENCE idt_a_seq AS smallint;
+SELECT stmt FROM pg_get_table_ddl('idt', owner => false) stmt;
+                                                 stmt                                                 
+------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idt (a integer GENERATED BY DEFAULT AS IDENTITY (AS smallint) NOT NULL);
+(1 row)
+
+-- Issue 2: CONSTRAINT WITH (fillfactor) and USING INDEX TABLESPACE must be
+-- preserved in the ADD CONSTRAINT body.
+CREATE TABLE pko (a int, b int);
+ALTER TABLE pko ADD CONSTRAINT pko_pk PRIMARY KEY (a) WITH (fillfactor=50);
+ALTER TABLE pko ADD CONSTRAINT pko_u UNIQUE (b) WITH (fillfactor=60);
+SELECT stmt FROM pg_get_table_ddl('pko', owner => false) stmt;
+                                             stmt                                             
+----------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.pko (a integer NOT NULL, b integer);
+ ALTER TABLE pgtbl_ddl_test.pko ADD CONSTRAINT pko_pk PRIMARY KEY (a) WITH (fillfactor='50');
+ ALTER TABLE pgtbl_ddl_test.pko ADD CONSTRAINT pko_u UNIQUE (b) WITH (fillfactor='60');
+(3 rows)
+
+-- Issue 2b: USING INDEX TABLESPACE in ADD CONSTRAINT.
+CREATE TABLE pko_ts (a int, b int);
+ALTER TABLE pko_ts ADD CONSTRAINT pko_ts_pk PRIMARY KEY (a)
+    USING INDEX TABLESPACE regress_tblspace;
+SELECT stmt FROM pg_get_table_ddl('pko_ts', owner => false) stmt;
+                                                        stmt                                                         
+---------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.pko_ts (a integer NOT NULL, b integer);
+ ALTER TABLE pgtbl_ddl_test.pko_ts ADD CONSTRAINT pko_ts_pk PRIMARY KEY (a) USING INDEX TABLESPACE regress_tblspace;
+(2 rows)
+
+-- Issue 4: toast.* storage parameters must appear in the WITH clause.
+CREATE TABLE tst_toast (a int, b text)
+    WITH (toast.autovacuum_enabled = false, fillfactor = 70);
+SELECT stmt FROM pg_get_table_ddl('tst_toast', owner => false) stmt;
+                                                        stmt                                                         
+---------------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.tst_toast (a integer, b text) WITH (fillfactor='70', toast.autovacuum_enabled='false');
+(1 row)
+
+-- Issue 5: STORAGE and COMPRESSION overrides on inherited partition columns
+-- must be emitted as post-CREATE ALTER TABLE statements.
+CREATE TABLE ps_parent (a int, b text) PARTITION BY RANGE (a);
+CREATE TABLE ps_child PARTITION OF ps_parent FOR VALUES FROM (0) TO (10);
+ALTER TABLE ps_child ALTER COLUMN b SET STORAGE EXTERNAL;
+SELECT stmt FROM pg_get_table_ddl('ps_parent', owner => false) stmt;
+                                                  stmt                                                   
+---------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.ps_parent (a integer, b text) PARTITION BY RANGE (a);
+ CREATE TABLE pgtbl_ddl_test.ps_child PARTITION OF pgtbl_ddl_test.ps_parent FOR VALUES FROM (0) TO (10);
+ ALTER TABLE ONLY pgtbl_ddl_test.ps_child ALTER COLUMN b SET STORAGE EXTERNAL;
+(3 rows)
+
+-- Issue 6: a foreign-table partition must produce a WARNING and be skipped
+-- rather than causing an error.
+CREATE FOREIGN DATA WRAPPER dummy_fdw;
+CREATE SERVER dummy_srv FOREIGN DATA WRAPPER dummy_fdw;
+CREATE TABLE fpar (a int) PARTITION BY RANGE (a);
+CREATE TABLE fpar_1 PARTITION OF fpar FOR VALUES FROM (0) TO (10);
+CREATE FOREIGN TABLE fpar_2 PARTITION OF fpar
+    FOR VALUES FROM (10) TO (20) SERVER dummy_srv;
+SELECT stmt FROM pg_get_table_ddl('fpar', owner => false) stmt;
+WARNING:  skipping foreign-table partition "fpar_2"
+DETAIL:  pg_get_table_ddl() cannot reconstruct foreign table partitions.
+                                               stmt                                               
+--------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.fpar (a integer) PARTITION BY RANGE (a);
+ CREATE TABLE pgtbl_ddl_test.fpar_1 PARTITION OF pgtbl_ddl_test.fpar FOR VALUES FROM (0) TO (10);
+(2 rows)
+
+-- Issue 8: a partition child index with a custom name must be emitted as
+-- CREATE INDEX + ATTACH PARTITION; CLUSTER ON that custom index must also work.
+CREATE TABLE cc (a int) PARTITION BY RANGE (a);
+CREATE TABLE cc_1 PARTITION OF cc FOR VALUES FROM (0) TO (10);
+CREATE INDEX ON ONLY cc (a);
+CREATE INDEX cc_custom ON cc_1 (a);
+ALTER INDEX cc_a_idx ATTACH PARTITION cc_custom;
+ALTER TABLE cc_1 CLUSTER ON cc_custom;
+SELECT stmt FROM pg_get_table_ddl('cc', owner => false) stmt;
+                                             stmt                                             
+----------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.cc (a integer) PARTITION BY RANGE (a);
+ CREATE TABLE pgtbl_ddl_test.cc_1 PARTITION OF pgtbl_ddl_test.cc FOR VALUES FROM (0) TO (10);
+ CREATE INDEX cc_custom ON pgtbl_ddl_test.cc_1 USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.cc_1 CLUSTER ON cc_custom;
+ CREATE INDEX cc_a_idx ON ONLY pgtbl_ddl_test.cc USING btree (a);
+ ALTER INDEX pgtbl_ddl_test.cc_a_idx ATTACH PARTITION cc_custom;
+(6 rows)
+
+-- Issue 10: tablespace => false must suppress TABLESPACE from both CREATE INDEX
+-- and ADD CONSTRAINT ... USING INDEX TABLESPACE.
+CREATE TABLE tsp (a int, b int);
+CREATE INDEX tsp_a_idx ON tsp (a) TABLESPACE regress_tblspace;
+ALTER TABLE tsp ADD CONSTRAINT tsp_pk PRIMARY KEY (b)
+    USING INDEX TABLESPACE regress_tblspace;
+-- With tablespace included (default):
+SELECT stmt FROM pg_get_table_ddl('tsp', owner => false) stmt;
+                                                     stmt                                                      
+---------------------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.tsp (a integer, b integer NOT NULL);
+ CREATE INDEX tsp_a_idx ON pgtbl_ddl_test.tsp USING btree (a) TABLESPACE regress_tblspace;
+ ALTER TABLE pgtbl_ddl_test.tsp ADD CONSTRAINT tsp_pk PRIMARY KEY (b) USING INDEX TABLESPACE regress_tblspace;
+(3 rows)
+
+-- With tablespace suppressed:
+SELECT stmt FROM pg_get_table_ddl('tsp', owner => false, tablespace => false) stmt;
+                                 stmt                                  
+-----------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.tsp (a integer, b integer NOT NULL);
+ CREATE INDEX tsp_a_idx ON pgtbl_ddl_test.tsp USING btree (a);
+ ALTER TABLE pgtbl_ddl_test.tsp ADD CONSTRAINT tsp_pk PRIMARY KEY (b);
+(3 rows)
+
+-- Issue 11b: ALTER SEQUENCE SET UNLOGGED on an identity sequence must be
+-- emitted as a post-CREATE ALTER SEQUENCE statement.
+CREATE TABLE idl (a int GENERATED ALWAYS AS IDENTITY);
+ALTER SEQUENCE idl_a_seq SET UNLOGGED;
+SELECT stmt FROM pg_get_table_ddl('idl', owner => false) stmt;
+                                        stmt                                        
+------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.idl (a integer GENERATED ALWAYS AS IDENTITY NOT NULL);
+ ALTER SEQUENCE pgtbl_ddl_test.idl_a_seq SET UNLOGGED;
+(2 rows)
+
+-- Issue 12: ALTER INDEX ... ALTER COLUMN SET STATISTICS on a constraint-backed
+-- index (PK, UNIQUE, EXCLUSION) must be emitted after the ADD CONSTRAINT.
+-- EXCLUSION with an expression column is used because only expression columns
+-- of an index support SET STATISTICS.
+CREATE TABLE exs (a int, b int);
+ALTER TABLE exs ADD CONSTRAINT exs_excl EXCLUDE USING btree ((a + 0) WITH =);
+ALTER INDEX exs_excl ALTER COLUMN 1 SET STATISTICS 900;
+SELECT stmt FROM pg_get_table_ddl('exs', owner => false) stmt;
+                                              stmt                                              
+------------------------------------------------------------------------------------------------
+ CREATE TABLE pgtbl_ddl_test.exs (a integer, b integer);
+ ALTER TABLE pgtbl_ddl_test.exs ADD CONSTRAINT exs_excl EXCLUDE USING btree (((a + 0)) WITH =);
+ ALTER INDEX pgtbl_ddl_test.exs_excl ALTER COLUMN 1 SET STATISTICS 900;
+(3 rows)
+
+-- Cleanup.
+DROP SERVER dummy_srv CASCADE;
+NOTICE:  drop cascades to foreign table fpar_2
+DROP FOREIGN DATA WRAPPER dummy_fdw CASCADE;
+DROP SCHEMA pgtbl_ddl_test CASCADE;
+NOTICE:  drop cascades to 58 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 storage_plain
+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 fk_upd_tgt
+drop cascades to table fk_upd_acts
+drop cascades to table fk_def_imm_tgt
+drop cascades to table fk_def_imm
+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
+drop cascades to table clu
+drop cascades to table cst
+drop cascades to table ext_stat_tbl
+drop cascades to table nnv
+drop cascades to table pam
+drop cascades to table idt
+drop cascades to table pko
+drop cascades to table pko_ts
+drop cascades to table tst_toast
+drop cascades to table ps_parent
+drop cascades to table fpar
+drop cascades to table cc
+drop cascades to table tsp
+drop cascades to table idl
+drop cascades to table exs
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..ed6099656e4
--- /dev/null
+++ b/src/test/regress/sql/pg_get_table_ddl.sql
@@ -0,0 +1,1120 @@
+--
+-- 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);
+
+-- STORAGE PLAIN: text defaults to EXTENDED; PLAIN must be emitted when set
+-- explicitly because it changes toast behaviour (forces inline storage).
+CREATE TABLE storage_plain (a text STORAGE PLAIN, b bytea STORAGE PLAIN);
+SELECT * FROM pg_get_table_ddl('storage_plain'::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);
+
+-- ON DELETE RESTRICT and ON UPDATE CASCADE / SET NULL / SET DEFAULT: the
+-- remaining referential-action combinations not covered by fk_acts above.
+CREATE TABLE fk_upd_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_upd_acts (
+    a int CONSTRAINT fk_delete_restrict   REFERENCES fk_upd_tgt(id)
+                          ON DELETE RESTRICT,
+    b int CONSTRAINT fk_update_cascade    REFERENCES fk_upd_tgt(id)
+                          ON UPDATE CASCADE,
+    c int CONSTRAINT fk_update_setdefault REFERENCES fk_upd_tgt(id)
+                          ON UPDATE SET DEFAULT,
+    d int CONSTRAINT fk_update_setnull    REFERENCES fk_upd_tgt(id)
+                          ON UPDATE SET NULL
+);
+SELECT * FROM pg_get_table_ddl('fk_upd_acts'::regclass, owner => false);
+
+-- DEFERRABLE INITIALLY IMMEDIATE: the deferrable-but-not-deferred variant.
+-- INITIALLY IMMEDIATE is the default for DEFERRABLE constraints, so the
+-- deparser omits it; only DEFERRABLE itself must appear in the output.
+CREATE TABLE fk_def_imm_tgt (id int PRIMARY KEY);
+CREATE TABLE fk_def_imm (
+    a int REFERENCES fk_def_imm_tgt(id) DEFERRABLE INITIALLY IMMEDIATE
+);
+SELECT * FROM pg_get_table_ddl('fk_def_imm'::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: partition children are
+-- emitted before the parent's indexes so PARTITION OF runs without any parent
+-- index present (preventing auto-creation of child indexes).  Each child index
+-- is emitted explicitly; the parent index follows with ON ONLY and then
+-- ALTER INDEX ... ATTACH PARTITION for each child.
+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: same pattern.
+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.
+-- All partition tables are emitted depth-first before any indexes; then each
+-- level emits CREATE INDEX ON ONLY followed by ATTACH PARTITION for its children.
+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 (emitted ON ONLY, child gets ATTACH) AND the
+-- child also has its own local index (relispartition=false, emitted separately).
+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);
+
+-- RLS with policies: a WARNING must be raised because pg_get_table_ddl cannot
+-- yet emit CREATE POLICY statements.  Replaying without the warning would
+-- produce a table with RLS enabled but no policies, silently denying all rows.
+CREATE TABLE rls_pol (id int);
+ALTER TABLE rls_pol ENABLE ROW LEVEL SECURITY;
+CREATE POLICY rls_pol_p ON rls_pol USING (id > 0);
+SELECT * FROM pg_get_table_ddl('rls_pol'::regclass, owner => false);
+DROP TABLE rls_pol;
+
+-- 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);
+
+-- owner => true (the default) emits ALTER TABLE ... OWNER TO.  Verify the
+-- line is present without depending on the role name used by the test runner.
+SELECT count(*) AS owner_lines
+FROM pg_get_table_ddl('basic'::regclass, owner => true)
+WHERE pg_get_table_ddl LIKE 'ALTER TABLE % OWNER TO %';
+
+-- 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;
+
+-- Partitioned index created with ON ONLY has indisvalid=false until child
+-- indexes are attached, but must not be treated as a failed-CIC index.
+CREATE TABLE parted_onlyi (a int) PARTITION BY RANGE (a);
+CREATE TABLE parted_onlyi_1 PARTITION OF parted_onlyi FOR VALUES FROM (0) TO (100);
+CREATE INDEX ON ONLY parted_onlyi (a);
+SELECT * FROM pg_get_table_ddl('parted_onlyi'::regclass, owner => false);
+DROP TABLE parted_onlyi;
+
+-- NOT VALID CHECK must be emitted as ALTER TABLE ... ADD CONSTRAINT ... NOT VALID,
+-- not inline in CREATE TABLE.  An inline constraint is always validated against
+-- the empty table, which would silently flip convalidated to true on replay.
+CREATE TABLE not_valid_chk (a int);
+ALTER TABLE not_valid_chk ADD CONSTRAINT chk CHECK (a < 50) NOT VALID;
+SELECT * FROM pg_get_table_ddl('not_valid_chk'::regclass, owner => false);
+DROP TABLE not_valid_chk;
+
+-- Auto-named NOT NULL on an INHERITS child's inherited column must be emitted
+-- inline as "NOT NULL col" (no CONSTRAINT clause).  An out-of-line ALTER TABLE
+-- would collide with the NOT NULL that INHERITS propagates from the parent.
+CREATE TABLE nn_inh_auto_par (a int PRIMARY KEY);
+CREATE TABLE nn_inh_auto_ch (PRIMARY KEY (a) DEFERRABLE) INHERITS (nn_inh_auto_par);
+SELECT * FROM pg_get_table_ddl('nn_inh_auto_ch'::regclass, owner => false);
+DROP TABLE nn_inh_auto_par CASCADE;
+
+-- 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 parent's CREATE INDEX (ON ONLY)
+-- 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'),
+        ('storage_plain'),
+        ('refd'), ('cons'), ('nulls_nd'), ('idx_inc'),
+        ('fk_acts_tgt'), ('fk_acts'),
+        ('fk_upd_tgt'), ('fk_upd_acts'), ('fk_def_imm_tgt'), ('fk_def_imm'),
+        ('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',
+        'storage_plain',
+        'refd', 'cons', 'nulls_nd', 'idx_inc',
+        'fk_acts_tgt', 'fk_acts',
+        'fk_upd_tgt', 'fk_upd_acts', 'fk_def_imm_tgt', 'fk_def_imm',
+        '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;
+
+-- Issue 1: CLUSTER ON must come after the constraint that creates the index.
+CREATE TABLE clu (a int, b int);
+ALTER TABLE clu ADD CONSTRAINT clu_pk PRIMARY KEY (a);
+CLUSTER clu USING clu_pk;
+SELECT stmt FROM pg_get_table_ddl('clu', owner => false) stmt;
+
+-- Issue 3a: column SET STATISTICS.
+CREATE TABLE cst (a int, b text);
+ALTER TABLE cst ALTER COLUMN a SET STATISTICS 500;
+ALTER TABLE cst ALTER COLUMN b SET STATISTICS 0;
+SELECT stmt FROM pg_get_table_ddl('cst', owner => false) stmt;
+
+-- Issue 3b: extended statistics SET STATISTICS.
+CREATE TABLE ext_stat_tbl (a int, b int);
+CREATE STATISTICS ext_stat_s (dependencies) ON a, b FROM ext_stat_tbl;
+ALTER STATISTICS ext_stat_s SET STATISTICS 750;
+SELECT stmt FROM pg_get_table_ddl('ext_stat_tbl', owner => false) stmt;
+
+-- Issue 7: NOT NULL NOT VALID must not be emitted inline.
+CREATE TABLE nnv (a int, b int);
+ALTER TABLE nnv ADD CONSTRAINT nnv_a_nn NOT NULL a NOT VALID;
+SELECT stmt FROM pg_get_table_ddl('nnv', owner => false) stmt;
+
+-- Issue 9: ACCESS METHOD on partitioned parent.
+CREATE TABLE pam (a int) PARTITION BY RANGE (a);
+ALTER TABLE pam SET ACCESS METHOD heap;
+SELECT stmt FROM pg_get_table_ddl('pam', owner => false) stmt;
+
+-- Issue 11a: AS type in identity sequence options.
+CREATE TABLE idt (a int GENERATED BY DEFAULT AS IDENTITY);
+ALTER SEQUENCE idt_a_seq AS smallint;
+SELECT stmt FROM pg_get_table_ddl('idt', owner => false) stmt;
+
+-- Issue 2: CONSTRAINT WITH (fillfactor) and USING INDEX TABLESPACE must be
+-- preserved in the ADD CONSTRAINT body.
+CREATE TABLE pko (a int, b int);
+ALTER TABLE pko ADD CONSTRAINT pko_pk PRIMARY KEY (a) WITH (fillfactor=50);
+ALTER TABLE pko ADD CONSTRAINT pko_u UNIQUE (b) WITH (fillfactor=60);
+SELECT stmt FROM pg_get_table_ddl('pko', owner => false) stmt;
+
+-- Issue 2b: USING INDEX TABLESPACE in ADD CONSTRAINT.
+CREATE TABLE pko_ts (a int, b int);
+ALTER TABLE pko_ts ADD CONSTRAINT pko_ts_pk PRIMARY KEY (a)
+    USING INDEX TABLESPACE regress_tblspace;
+SELECT stmt FROM pg_get_table_ddl('pko_ts', owner => false) stmt;
+
+-- Issue 4: toast.* storage parameters must appear in the WITH clause.
+CREATE TABLE tst_toast (a int, b text)
+    WITH (toast.autovacuum_enabled = false, fillfactor = 70);
+SELECT stmt FROM pg_get_table_ddl('tst_toast', owner => false) stmt;
+
+-- Issue 5: STORAGE and COMPRESSION overrides on inherited partition columns
+-- must be emitted as post-CREATE ALTER TABLE statements.
+CREATE TABLE ps_parent (a int, b text) PARTITION BY RANGE (a);
+CREATE TABLE ps_child PARTITION OF ps_parent FOR VALUES FROM (0) TO (10);
+ALTER TABLE ps_child ALTER COLUMN b SET STORAGE EXTERNAL;
+SELECT stmt FROM pg_get_table_ddl('ps_parent', owner => false) stmt;
+
+-- Issue 6: a foreign-table partition must produce a WARNING and be skipped
+-- rather than causing an error.
+CREATE FOREIGN DATA WRAPPER dummy_fdw;
+CREATE SERVER dummy_srv FOREIGN DATA WRAPPER dummy_fdw;
+CREATE TABLE fpar (a int) PARTITION BY RANGE (a);
+CREATE TABLE fpar_1 PARTITION OF fpar FOR VALUES FROM (0) TO (10);
+CREATE FOREIGN TABLE fpar_2 PARTITION OF fpar
+    FOR VALUES FROM (10) TO (20) SERVER dummy_srv;
+SELECT stmt FROM pg_get_table_ddl('fpar', owner => false) stmt;
+
+-- Issue 8: a partition child index with a custom name must be emitted as
+-- CREATE INDEX + ATTACH PARTITION; CLUSTER ON that custom index must also work.
+CREATE TABLE cc (a int) PARTITION BY RANGE (a);
+CREATE TABLE cc_1 PARTITION OF cc FOR VALUES FROM (0) TO (10);
+CREATE INDEX ON ONLY cc (a);
+CREATE INDEX cc_custom ON cc_1 (a);
+ALTER INDEX cc_a_idx ATTACH PARTITION cc_custom;
+ALTER TABLE cc_1 CLUSTER ON cc_custom;
+SELECT stmt FROM pg_get_table_ddl('cc', owner => false) stmt;
+
+-- Issue 10: tablespace => false must suppress TABLESPACE from both CREATE INDEX
+-- and ADD CONSTRAINT ... USING INDEX TABLESPACE.
+CREATE TABLE tsp (a int, b int);
+CREATE INDEX tsp_a_idx ON tsp (a) TABLESPACE regress_tblspace;
+ALTER TABLE tsp ADD CONSTRAINT tsp_pk PRIMARY KEY (b)
+    USING INDEX TABLESPACE regress_tblspace;
+
+-- With tablespace included (default):
+SELECT stmt FROM pg_get_table_ddl('tsp', owner => false) stmt;
+
+-- With tablespace suppressed:
+SELECT stmt FROM pg_get_table_ddl('tsp', owner => false, tablespace => false) stmt;
+
+-- Issue 11b: ALTER SEQUENCE SET UNLOGGED on an identity sequence must be
+-- emitted as a post-CREATE ALTER SEQUENCE statement.
+CREATE TABLE idl (a int GENERATED ALWAYS AS IDENTITY);
+ALTER SEQUENCE idl_a_seq SET UNLOGGED;
+SELECT stmt FROM pg_get_table_ddl('idl', owner => false) stmt;
+
+-- Issue 12: ALTER INDEX ... ALTER COLUMN SET STATISTICS on a constraint-backed
+-- index (PK, UNIQUE, EXCLUSION) must be emitted after the ADD CONSTRAINT.
+-- EXCLUSION with an expression column is used because only expression columns
+-- of an index support SET STATISTICS.
+CREATE TABLE exs (a int, b int);
+ALTER TABLE exs ADD CONSTRAINT exs_excl EXCLUDE USING btree ((a + 0) WITH =);
+ALTER INDEX exs_excl ALTER COLUMN 1 SET STATISTICS 900;
+SELECT stmt FROM pg_get_table_ddl('exs', owner => false) stmt;
+
+-- Cleanup.
+DROP SERVER dummy_srv CASCADE;
+DROP FOREIGN DATA WRAPPER dummy_fdw CASCADE;
+DROP SCHEMA pgtbl_ddl_test CASCADE;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 56c1f997f88..6e19e4630d4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1651,6 +1651,7 @@ ListenerEntry
 LoInfo
 LoadStmt
 LocalBufferLookupEnt
+LocalNotNullEntry
 LocalPgBackendStatus
 LocalTransactionId
 Location
@@ -3155,6 +3156,7 @@ TableFuncScan
 TableFuncScanState
 TableFuncType
 TableInfo
+TableDdlContext
 TableLikeClause
 TableSampleClause
 TableScanDesc
-- 
2.51.0

