From d7013060a04c3bc91f8c07b3729a969e63ecc775 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <sawada.mshk@gmail.com>
Date: Fri, 10 Jul 2026 01:34:47 -0700
Subject: [PATCH v1 2/4] Add regression tests for DDL deparse.

The deparser is tested by replacing each supported command with its
deparsed reconstruction and letting the regression tests judge the
result. test_ddl_deparse intercepts a top-level command in a
ProcessUtility hook, runs the original in an internal subtransaction
that is rolled back, deparses the commands collected for it from a
ddl_command_end event trigger, and then executes the reconstruction
for real. A divergence between the two forms therefore shows up as an
ordinary regression diff, at the statement that caused it.

Author:
Reviewed-by:
Discussion: https://postgr.es/m/
---
 src/backend/commands/event_trigger.c          |  18 +
 src/include/commands/event_trigger.h          |   1 +
 src/test/modules/test_ddl_deparse/Makefile    |  14 +-
 .../expected/deparse_create_table.out         | 718 ++++++++++++++++++
 src/test/modules/test_ddl_deparse/meson.build |   7 +
 .../sql/deparse_create_table.sql              | 412 ++++++++++
 .../test_ddl_deparse/t/001_deparse_regress.pl | 130 ++++
 .../test_ddl_deparse--1.0.sql                 |  21 +-
 .../test_ddl_deparse/test_ddl_deparse.c       | 504 ++++++++++++
 9 files changed, 1820 insertions(+), 5 deletions(-)
 create mode 100644 src/test/modules/test_ddl_deparse/expected/deparse_create_table.out
 create mode 100644 src/test/modules/test_ddl_deparse/sql/deparse_create_table.sql
 create mode 100644 src/test/modules/test_ddl_deparse/t/001_deparse_regress.pl

diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index adc6eabc0f4..e08d49590ed 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -1708,6 +1708,24 @@ EventTriggerUndoInhibitCommandCollection(void)
 	currentEventTriggerState->commandCollectionInhibited = false;
 }
 
+/*
+ * Return the commands collected for the complete query now running, a list of
+ * CollectedCommand.
+ *
+ * This is the C-level counterpart of pg_event_trigger_ddl_commands(), for
+ * consumers that need the commands as they are rather than as a set of rows;
+ * deparsing them is the motivating case.  The list belongs to the event
+ * trigger state and is only valid until the current complete query ends.
+ */
+List *
+EventTriggerGetCollectedCommands(void)
+{
+	if (!currentEventTriggerState)
+		return NIL;
+
+	return currentEventTriggerState->commandList;
+}
+
 /*
  * EventTriggerCollectSimpleCommand
  *		Save data about a simple DDL command that was just executed
diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h
index 27340655061..9126aafd113 100644
--- a/src/include/commands/event_trigger.h
+++ b/src/include/commands/event_trigger.h
@@ -72,6 +72,7 @@ extern void EventTriggerSQLDropAddObject(const ObjectAddress *object,
 
 extern void EventTriggerInhibitCommandCollection(void);
 extern void EventTriggerUndoInhibitCommandCollection(void);
+extern List *EventTriggerGetCollectedCommands(void);
 
 extern void EventTriggerCollectSimpleCommand(ObjectAddress address,
 											 ObjectAddress secondaryObject,
diff --git a/src/test/modules/test_ddl_deparse/Makefile b/src/test/modules/test_ddl_deparse/Makefile
index f91a78d8d92..9ee45557338 100644
--- a/src/test/modules/test_ddl_deparse/Makefile
+++ b/src/test/modules/test_ddl_deparse/Makefile
@@ -1,6 +1,9 @@
 # src/test/modules/test_ddl_deparse/Makefile
 
-MODULES = test_ddl_deparse
+MODULE_big = test_ddl_deparse
+OBJS = \
+	$(WIN32RES) \
+	test_ddl_deparse.o
 PGFILEDESC = "test_ddl_deparse - regression testing for DDL deparsing"
 
 EXTENSION = test_ddl_deparse
@@ -28,10 +31,17 @@ REGRESS = test_ddl_deparse \
 	opfamily \
 	defprivs \
 	textsearch \
-	matviews
+	matviews \
+	deparse_create_table
+
+TAP_TESTS = 1
 
 EXTRA_INSTALL = contrib/pg_stat_statements
 
+# needed by the TAP test to locate regress.so for the core regression run
+REGRESS_SHLIB=$(abs_top_builddir)/src/test/regress/regress$(DLSUFFIX)
+export REGRESS_SHLIB
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/src/test/modules/test_ddl_deparse/expected/deparse_create_table.out b/src/test/modules/test_ddl_deparse/expected/deparse_create_table.out
new file mode 100644
index 00000000000..8b6d8cdfd66
--- /dev/null
+++ b/src/test/modules/test_ddl_deparse/expected/deparse_create_table.out
@@ -0,0 +1,718 @@
+--
+-- Tests for DDL deparsing of CREATE TABLE.
+--
+-- Note: the event triggers created by the test_ddl_deparse script (which
+-- must run first in this schedule) remain installed, so their "DDL test:"
+-- NOTICEs appear interleaved below; deparse capture itself does not use
+-- them.
+--
+-- With test_ddl_deparse.execute_deparsed_ddl enabled, the module
+-- intercepts each supported CREATE TABLE: the original command is executed
+-- in a subtransaction that is rolled back, the collected command is
+-- deparsed to JSON, and the reconstructed command is executed for real.
+-- With print_deparsed_ddl enabled the reconstructed command is shown as a
+-- NOTICE, so the expected output asserts the deparsed text itself; exactly
+-- one NOTICE per statement shows that one DDL deparses to one DDL.
+--
+LOAD 'test_ddl_deparse';
+SET test_ddl_deparse.execute_deparsed_ddl = on;
+SET test_ddl_deparse.print_deparsed_ddl = text;
+CREATE SCHEMA deparse_tests;
+NOTICE:  DDL test: type simple, tag CREATE SCHEMA
+-- simple case
+CREATE TABLE deparse_tests.t1 (a int, b text);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t1 (a pg_catalog.int4, b pg_catalog.text)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- serial must stay serial: no sequence creation and no nextval() default
+-- may appear in the deparsed command, but the replayed table must still
+-- have them.
+CREATE TABLE deparse_tests.t_serial (id serial, big bigserial NOT NULL);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_serial (id serial, big bigserial NOT NULL)
+NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
+NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
+NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
+SELECT attname, pg_get_serial_sequence('deparse_tests.t_serial', attname) AS seq
+  FROM pg_attribute
+  WHERE attrelid = 'deparse_tests.t_serial'::regclass AND attnum > 0
+  ORDER BY attnum;
+ attname |              seq               
+---------+--------------------------------
+ id      | deparse_tests.t_serial_id_seq
+ big     | deparse_tests.t_serial_big_seq
+(2 rows)
+
+-- COMPRESSION and STORAGE appear only when the user wrote them; columns
+-- without the clause must adopt the target's defaults on replay
+-- (attcompression stays unset).
+CREATE TABLE deparse_tests.t_options (
+a text COMPRESSION pglz,
+b text STORAGE external,
+c text
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_options (a pg_catalog.text COMPRESSION pglz, b pg_catalog.text STORAGE external, c pg_catalog.text)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+SELECT attname, attcompression, attstorage
+  FROM pg_attribute
+  WHERE attrelid = 'deparse_tests.t_options'::regclass AND attnum > 0
+  ORDER BY attnum;
+ attname | attcompression | attstorage 
+---------+----------------+------------
+ a       | p              | x
+ b       |                | e
+ c       |                | x
+(3 rows)
+
+-- inline constraints are normalized to table constraints, but stay within
+-- the single CREATE TABLE command; unnamed constraints get the same
+-- auto-derived name on replay.
+CREATE TABLE deparse_tests.t_constr (
+a int PRIMARY KEY,
+b int REFERENCES deparse_tests.t_constr (a) NOT NULL,
+c text DEFAULT 'x' CHECK (c <> ''),
+CONSTRAINT named_check CHECK (a > 0)
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_constr (a pg_catalog.int4, b pg_catalog.int4 NOT NULL, c pg_catalog.text DEFAULT 'x'::text, CHECK ((c <> ''::text)), CONSTRAINT named_check CHECK ((a > 0)), PRIMARY KEY (a), FOREIGN KEY (b) REFERENCES deparse_tests.t_constr(a))
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+NOTICE:  DDL test: type alter table, tag ALTER TABLE
+NOTICE:    subcommand: type ADD CONSTRAINT (and recurse) desc constraint t_constr_b_fkey on table deparse_tests.t_constr
+SELECT conname, contype, pg_get_constraintdef(oid)
+  FROM pg_constraint
+  WHERE conrelid = 'deparse_tests.t_constr'::regclass
+  ORDER BY conname COLLATE "C";
+       conname       | contype |                 pg_get_constraintdef                 
+---------------------+---------+------------------------------------------------------
+ named_check         | c       | CHECK ((a > 0))
+ t_constr_a_not_null | n       | NOT NULL a
+ t_constr_b_fkey     | f       | FOREIGN KEY (b) REFERENCES deparse_tests.t_constr(a)
+ t_constr_b_not_null | n       | NOT NULL b
+ t_constr_c_check    | c       | CHECK ((c <> ''::text))
+ t_constr_pkey       | p       | PRIMARY KEY (a)
+(6 rows)
+
+-- table-level NOT NULL constraint syntax
+CREATE TABLE deparse_tests.t_tblnotnull (a int, CONSTRAINT nn_a NOT NULL a);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_tblnotnull (a pg_catalog.int4, CONSTRAINT nn_a NOT NULL a)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- type names are normalized but keep their typmod; interval exercises
+-- SQL-standard types with special typmod rules
+CREATE TABLE deparse_tests.t_types (
+a varchar(10),
+b numeric(10,2),
+c timestamp(3),
+d interval hour to minute,
+e int[]
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_types (a pg_catalog."varchar"(10), b pg_catalog."numeric"(10,2), c TIMESTAMP(3) without time zone, d INTERVAL hour to minute, e pg_catalog.int4[])
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- identity columns: sequence options are emitted only as far as the user
+-- gave them
+CREATE TABLE deparse_tests.t_identity (
+a int GENERATED ALWAYS AS IDENTITY,
+b int GENERATED BY DEFAULT AS IDENTITY (START WITH 10 INCREMENT BY 5)
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_identity (a pg_catalog.int4 GENERATED ALWAYS AS IDENTITY, b pg_catalog.int4 GENERATED BY DEFAULT AS IDENTITY (START WITH 10 INCREMENT BY 5))
+NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
+NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
+NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
+SELECT seqstart, seqincrement
+  FROM pg_sequence
+  WHERE seqrelid = pg_get_serial_sequence('deparse_tests.t_identity', 'b')::regclass;
+ seqstart | seqincrement 
+----------+--------------
+       10 |            5
+(1 row)
+
+-- generated columns
+CREATE TABLE deparse_tests.t_generated (
+a int,
+b int GENERATED ALWAYS AS (a * 2) STORED,
+c int GENERATED ALWAYS AS (a + 1) VIRTUAL
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_generated (a pg_catalog.int4, b pg_catalog.int4 GENERATED ALWAYS AS ((a * 2)) STORED, c pg_catalog.int4 GENERATED ALWAYS AS ((a + 1)) VIRTUAL)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- defaults referencing other objects come out schema-qualified
+CREATE SEQUENCE deparse_tests.myseq;
+NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
+CREATE TABLE deparse_tests.t_defaults (
+a timestamptz DEFAULT now(),
+b int DEFAULT nextval('deparse_tests.myseq')
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_defaults (a TIMESTAMP WITH TIME ZONE DEFAULT now(), b pg_catalog.int4 DEFAULT nextval('deparse_tests.myseq'::regclass))
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- collation
+CREATE TABLE deparse_tests.t_collate (a text COLLATE "C");
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_collate (a pg_catalog.text COLLATE pg_catalog."C")
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- persistence
+CREATE UNLOGGED TABLE deparse_tests.t_unlogged (a int);
+NOTICE:  deparsed DDL: CREATE UNLOGGED TABLE deparse_tests.t_unlogged (a pg_catalog.int4)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- reloptions, only as typed
+CREATE TABLE deparse_tests.t_withopts (a int)
+	WITH (fillfactor = 70, autovacuum_enabled = false);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_withopts (a pg_catalog.int4) WITH (fillfactor = '70', autovacuum_enabled = 'false')
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- inheritance
+CREATE TABLE deparse_tests.t_parent (a int NOT NULL, b text);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_parent (a pg_catalog.int4 NOT NULL, b pg_catalog.text)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+CREATE TABLE deparse_tests.t_child () INHERITS (deparse_tests.t_parent);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_child () INHERITS (deparse_tests.t_parent)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- partitioning
+CREATE TABLE deparse_tests.t_part (a int, b text) PARTITION BY RANGE (a);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_part (a pg_catalog.int4, b pg_catalog.text) PARTITION BY RANGE (a)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+CREATE TABLE deparse_tests.t_part1 PARTITION OF deparse_tests.t_part
+	FOR VALUES FROM (1) TO (10);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_part1 PARTITION OF deparse_tests.t_part FOR VALUES FROM (1) TO (10)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+CREATE TABLE deparse_tests.t_part2 PARTITION OF deparse_tests.t_part
+	(b WITH OPTIONS NOT NULL)
+	FOR VALUES FROM (10) TO (20);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_part2 PARTITION OF deparse_tests.t_part (b WITH OPTIONS NOT NULL) FOR VALUES FROM (10) TO (20)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+CREATE TABLE deparse_tests.t_partd PARTITION OF deparse_tests.t_part DEFAULT;
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_partd PARTITION OF deparse_tests.t_part DEFAULT
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- typed table
+CREATE TYPE deparse_tests.comp AS (x int, y text);
+NOTICE:  DDL test: type simple, tag CREATE TYPE
+CREATE TABLE deparse_tests.t_of OF deparse_tests.comp (x WITH OPTIONS NOT NULL);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_of OF deparse_tests.comp (x WITH OPTIONS NOT NULL)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- exclusion constraint
+CREATE TABLE deparse_tests.t_excl (
+c circle,
+EXCLUDE USING gist (c WITH &&)
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_excl (c pg_catalog.circle, EXCLUDE USING gist (c WITH &&))
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+-- quoted identifiers
+CREATE TABLE deparse_tests."Mixed Case" ("Col A" int, "select" text);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests."Mixed Case" ("Col A" pg_catalog.int4, "select" pg_catalog.text)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- IF NOT EXISTS on an existing relation deparses to nothing and must not
+-- be replayed
+CREATE TABLE IF NOT EXISTS deparse_tests.t1 (a int);
+NOTICE:  relation "t1" already exists, skipping
+-- LIKE contributes columns that are not in the raw statement, so they are
+-- recovered from the catalog.  Bare LIKE copies the type, its collation and
+-- NOT NULL, and nothing else.
+CREATE TABLE deparse_tests.t_like_src (
+a int NOT NULL,
+b text COLLATE "C",
+c numeric(10,2) STORAGE plain,
+d int DEFAULT 7,
+e int GENERATED ALWAYS AS (d * 2) STORED
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_like_src (a pg_catalog.int4 NOT NULL, b pg_catalog.text COLLATE pg_catalog."C", c pg_catalog."numeric"(10,2) STORAGE plain, d pg_catalog.int4 DEFAULT 7, e pg_catalog.int4 GENERATED ALWAYS AS ((d * 2)) STORED)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+CREATE TABLE deparse_tests.t_like (LIKE deparse_tests.t_like_src);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_like (a pg_catalog.int4 CONSTRAINT t_like_src_a_not_null NOT NULL, b pg_catalog.text COLLATE pg_catalog."C", c pg_catalog."numeric"(10,2), d pg_catalog.int4, e pg_catalog.int4)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- columns the user did write are still deparsed from what they wrote, and
+-- the catalog order keeps the two kinds interleaved as the command produced
+-- them (serial stays serial, the copied columns come from pg_attribute)
+CREATE TABLE deparse_tests.t_like_mixed (
+x serial,
+LIKE deparse_tests.t_like_src,
+y int DEFAULT 1
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_like_mixed (x serial, a pg_catalog.int4 CONSTRAINT t_like_src_a_not_null NOT NULL, b pg_catalog.text COLLATE pg_catalog."C", c pg_catalog."numeric"(10,2), d pg_catalog.int4, e pg_catalog.int4, y pg_catalog.int4 DEFAULT 1)
+NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
+-- the options that land in the table definition are all recovered
+CREATE TABLE deparse_tests.t_like_incl (
+LIKE deparse_tests.t_like_src
+	INCLUDING DEFAULTS INCLUDING GENERATED INCLUDING CONSTRAINTS
+	INCLUDING STORAGE INCLUDING COMPRESSION
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_like_incl (a pg_catalog.int4 CONSTRAINT t_like_src_a_not_null NOT NULL, b pg_catalog.text COLLATE pg_catalog."C", c pg_catalog."numeric"(10,2) STORAGE PLAIN, d pg_catalog.int4 DEFAULT 7, e pg_catalog.int4 GENERATED ALWAYS AS ((d * 2)) STORED)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type alter table, tag ALTER TABLE
+NOTICE:    subcommand: type ALTER COLUMN SET DEFAULT (precooked) desc column d of table deparse_tests.t_like_incl
+NOTICE:    subcommand: type ALTER COLUMN SET DEFAULT (precooked) desc column e of table deparse_tests.t_like_incl
+-- identity comes back with the sequence parameters that were copied
+CREATE TABLE deparse_tests.t_like_idsrc (
+a int GENERATED BY DEFAULT AS IDENTITY (START WITH 10 INCREMENT BY 5)
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_like_idsrc (a pg_catalog.int4 GENERATED BY DEFAULT AS IDENTITY (START WITH 10 INCREMENT BY 5))
+NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
+CREATE TABLE deparse_tests.t_like_ident
+  (LIKE deparse_tests.t_like_idsrc INCLUDING IDENTITY);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_like_ident (a pg_catalog.int4 CONSTRAINT t_like_idsrc_a_not_null NOT NULL GENERATED BY DEFAULT AS IDENTITY (CACHE 1 NO CYCLE INCREMENT BY 5 MAXVALUE 2147483647 MINVALUE 1 START WITH 10))
+NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
+SELECT seqstart, seqincrement
+  FROM pg_sequence
+  WHERE seqrelid = pg_get_serial_sequence('deparse_tests.t_like_ident', 'a')::regclass;
+ seqstart | seqincrement 
+----------+--------------
+       10 |            5
+(1 row)
+
+-- options that copy an object needing a statement of its own are declined:
+-- the statement is not intercepted and executes normally
+CREATE INDEX ON deparse_tests.t_like_src (a);
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+COMMENT ON COLUMN deparse_tests.t_like_src.a IS 'a comment';
+NOTICE:  DDL test: type simple, tag COMMENT
+CREATE TABLE deparse_tests.t_like_idx
+  (LIKE deparse_tests.t_like_src INCLUDING INDEXES);
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+CREATE TABLE deparse_tests.t_like_cmt
+  (LIKE deparse_tests.t_like_src INCLUDING COMMENTS);
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag COMMENT
+CREATE TABLE deparse_tests.t_like_all
+  (LIKE deparse_tests.t_like_src INCLUDING ALL);
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type alter table, tag ALTER TABLE
+NOTICE:    subcommand: type ALTER COLUMN SET DEFAULT (precooked) desc column d of table deparse_tests.t_like_all
+NOTICE:    subcommand: type ALTER COLUMN SET DEFAULT (precooked) desc column e of table deparse_tests.t_like_all
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+NOTICE:  DDL test: type simple, tag COMMENT
+SELECT relname FROM pg_class WHERE oid = 'deparse_tests.t_like'::regclass;
+ relname 
+---------
+ t_like
+(1 row)
+
+-- DEFAULT NULL stores no pg_attrdef entry but must still deparse
+CREATE TABLE deparse_tests.t_defnull (a int DEFAULT NULL, b text DEFAULT NULL::text);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_defnull (a pg_catalog.int4 DEFAULT NULL, b pg_catalog.text DEFAULT NULL)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- identity sequence options that parse analysis consumes are still in the
+-- raw parse tree: SEQUENCE NAME must deparse; OWNED BY is dropped, since
+-- execution overrides it (the sequence is always owned by the identity
+-- column) and the name it carries need not be replayable
+CREATE TABLE deparse_tests.t_seqname (
+id int GENERATED ALWAYS AS IDENTITY
+	(SEQUENCE NAME deparse_tests.t_seqname_seq START WITH 5)
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_seqname (id pg_catalog.int4 GENERATED ALWAYS AS IDENTITY (SEQUENCE NAME deparse_tests.t_seqname_seq START WITH 5))
+NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
+SELECT relname FROM pg_class WHERE oid = 'deparse_tests.t_seqname_seq'::regclass;
+    relname    
+---------------
+ t_seqname_seq
+(1 row)
+
+CREATE TABLE deparse_tests.t_ownedby (
+id int GENERATED BY DEFAULT AS IDENTITY
+	(OWNED BY NONE)
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_ownedby (id pg_catalog.int4 GENERATED BY DEFAULT AS IDENTITY)
+NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
+CREATE TABLE deparse_tests.t_owner (x int);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_owner (x pg_catalog.int4)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+CREATE TABLE deparse_tests.t_ownedby2 (
+id int GENERATED ALWAYS AS IDENTITY
+	(OWNED BY deparse_tests.t_owner.x START WITH 7)
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_ownedby2 (id pg_catalog.int4 GENERATED ALWAYS AS IDENTITY (START WITH 7))
+NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
+SELECT pg_get_serial_sequence('deparse_tests.t_ownedby2', 'id');
+     pg_get_serial_sequence      
+---------------------------------
+ deparse_tests.t_ownedby2_id_seq
+(1 row)
+
+-- USING INDEX TABLESPACE must stay attached to the constraint that carried
+-- it, also when several unnamed constraints of the same type exist
+CREATE TABLE deparse_tests.t_idxtblspc (
+a int,
+b int,
+c int,
+UNIQUE (a),
+UNIQUE (b) USING INDEX TABLESPACE pg_default,
+PRIMARY KEY (c)
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_idxtblspc (a pg_catalog.int4, b pg_catalog.int4, c pg_catalog.int4, PRIMARY KEY (c), UNIQUE (a), UNIQUE (b) USING INDEX TABLESPACE pg_default)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+-- UNIQUE constraint variants, all via pg_get_constraintdef
+CREATE TABLE deparse_tests.t_unique (
+a int UNIQUE,
+b int,
+c int,
+d int,
+UNIQUE NULLS NOT DISTINCT (b),
+UNIQUE (c) INCLUDE (d),
+CONSTRAINT defer_u UNIQUE (d) DEFERRABLE INITIALLY DEFERRED
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_unique (a pg_catalog.int4, b pg_catalog.int4, c pg_catalog.int4, d pg_catalog.int4, UNIQUE (a), UNIQUE NULLS NOT DISTINCT (b), UNIQUE (c) INCLUDE (d), CONSTRAINT defer_u UNIQUE (d) DEFERRABLE INITIALLY DEFERRED)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+SELECT conname, pg_get_constraintdef(oid)
+  FROM pg_constraint
+  WHERE conrelid = 'deparse_tests.t_unique'::regclass
+  ORDER BY conname COLLATE "C";
+     conname      |           pg_get_constraintdef           
+------------------+------------------------------------------
+ defer_u          | UNIQUE (d) DEFERRABLE INITIALLY DEFERRED
+ t_unique_a_key   | UNIQUE (a)
+ t_unique_b_key   | UNIQUE NULLS NOT DISTINCT (b)
+ t_unique_c_d_key | UNIQUE (c) INCLUDE (d)
+(4 rows)
+
+-- foreign key matching and referential actions
+CREATE TABLE deparse_tests.t_fkref (a int, b int, PRIMARY KEY (a, b));
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_fkref (a pg_catalog.int4, b pg_catalog.int4, PRIMARY KEY (a, b))
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+CREATE TABLE deparse_tests.t_fk (
+x int,
+y int,
+FOREIGN KEY (x, y) REFERENCES deparse_tests.t_fkref (a, b)
+	MATCH FULL ON DELETE CASCADE ON UPDATE SET NULL
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_fk (x pg_catalog.int4, y pg_catalog.int4, FOREIGN KEY (x, y) REFERENCES deparse_tests.t_fkref(a, b) MATCH FULL ON UPDATE SET NULL ON DELETE CASCADE)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type alter table, tag ALTER TABLE
+NOTICE:    subcommand: type ADD CONSTRAINT (and recurse) desc constraint t_fk_x_y_fkey on table deparse_tests.t_fk
+-- temp tables and ON COMMIT; the temp schema is emitted as pg_temp
+CREATE TEMP TABLE t_temp (a int);
+NOTICE:  deparsed DDL: CREATE TEMPORARY TABLE pg_temp.t_temp (a pg_catalog.int4)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+CREATE TEMP TABLE t_temp_delrows (a int) ON COMMIT DELETE ROWS;
+NOTICE:  deparsed DDL: CREATE TEMPORARY TABLE pg_temp.t_temp_delrows (a pg_catalog.int4) ON COMMIT DELETE ROWS
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+BEGIN;
+CREATE TEMP TABLE t_temp_drop (a int) ON COMMIT DROP;
+NOTICE:  deparsed DDL: CREATE TEMPORARY TABLE pg_temp.t_temp_drop (a pg_catalog.int4) ON COMMIT DROP
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+COMMIT;
+DROP TABLE t_temp, t_temp_delrows;
+-- access method and tablespace, only as typed
+CREATE TABLE deparse_tests.t_tblspc (a int) USING heap TABLESPACE pg_default;
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_tblspc (a pg_catalog.int4) USING heap TABLESPACE pg_default
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- partitioning: list and hash, key expressions, opclass and collation
+CREATE TABLE deparse_tests.t_list (a text COLLATE "C", b int)
+	PARTITION BY LIST (a COLLATE "POSIX" text_pattern_ops);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_list (a pg_catalog.text COLLATE pg_catalog."C", b pg_catalog.int4) PARTITION BY LIST (a COLLATE "POSIX" text_pattern_ops)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+CREATE TABLE deparse_tests.t_list1 PARTITION OF deparse_tests.t_list
+	FOR VALUES IN ('x', 'y');
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_list1 PARTITION OF deparse_tests.t_list FOR VALUES IN ('x', 'y')
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+CREATE TABLE deparse_tests.t_list2 PARTITION OF deparse_tests.t_list
+	(b WITH OPTIONS DEFAULT 42)
+	FOR VALUES IN ('z');
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_list2 PARTITION OF deparse_tests.t_list (b WITH OPTIONS DEFAULT 42) FOR VALUES IN ('z')
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+CREATE TABLE deparse_tests.t_hash (a int, b text)
+	PARTITION BY HASH ((a + length(b)));
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_hash (a pg_catalog.int4, b pg_catalog.text) PARTITION BY HASH (((a + length(b))))
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+CREATE TABLE deparse_tests.t_hash1 PARTITION OF deparse_tests.t_hash
+	FOR VALUES WITH (MODULUS 4, REMAINDER 0);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_hash1 PARTITION OF deparse_tests.t_hash FOR VALUES WITH (modulus 4, remainder 0)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- identity sequence option spread
+CREATE TABLE deparse_tests.t_identity2 (
+a int GENERATED ALWAYS AS IDENTITY
+	(MINVALUE 0 MAXVALUE 100 CACHE 10 CYCLE)
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_identity2 (a pg_catalog.int4 GENERATED ALWAYS AS IDENTITY (MINVALUE 0 MAXVALUE 100 CACHE 10 CYCLE))
+NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
+-- NO INHERIT forms
+CREATE TABLE deparse_tests.t_noinh (
+a int NOT NULL NO INHERIT,
+b int CONSTRAINT chk_b CHECK (b > 0) NO INHERIT,
+c int,
+CONSTRAINT nn_c NOT NULL c NO INHERIT
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_noinh (a pg_catalog.int4 NOT NULL NO INHERIT, b pg_catalog.int4, c pg_catalog.int4, CONSTRAINT nn_c NOT NULL c NO INHERIT, CONSTRAINT chk_b CHECK ((b > 0)) NO INHERIT)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- multiple inheritance parents, in order
+CREATE TABLE deparse_tests.t_parent2 (c int);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_parent2 (c pg_catalog.int4)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+CREATE TABLE deparse_tests.t_multi ()
+	INHERITS (deparse_tests.t_parent, deparse_tests.t_parent2);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_multi () INHERITS (deparse_tests.t_parent, deparse_tests.t_parent2)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- more type and storage spellings: smallserial, array of typmod type,
+-- STORAGE MAIN, and the COMPRESSION DEFAULT keyword
+CREATE TABLE deparse_tests.t_types2 (
+a smallserial,
+b varchar(3)[],
+c text STORAGE main,
+d text COMPRESSION default
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_types2 (a smallserial, b pg_catalog."varchar"(3)[], c pg_catalog.text STORAGE main, d pg_catalog.text COMPRESSION "default")
+NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
+-- hostile names: format-string metacharacters and embedded quotes must
+-- pass through the fmt expansion inertly
+CREATE TABLE deparse_tests."evil %{name}I table" (
+"a""b" int,
+"%{col}s" text DEFAULT 'O''Reilly',
+"100%" numeric
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests."evil %{name}I table" ("a""b" pg_catalog.int4, "%{col}s" pg_catalog.text DEFAULT 'O''Reilly'::text, "100%" pg_catalog."numeric")
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+SELECT relname FROM pg_class WHERE relname = 'evil %{name}I table';
+       relname       
+---------------------
+ evil %{name}I table
+(1 row)
+
+-- parse analysis merges redundant index constraints (the duplicate
+-- UNIQUE (a), and UNIQUE (a) into the primary key); USING INDEX
+-- TABLESPACE attribution must follow the surviving index of each
+-- constraint, not raw-parse-tree positions
+CREATE TABLE deparse_tests.t_dedup (
+a int UNIQUE,
+b int,
+UNIQUE (a),
+UNIQUE (b) USING INDEX TABLESPACE pg_default
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_dedup (a pg_catalog.int4, b pg_catalog.int4, UNIQUE (a), UNIQUE (b) USING INDEX TABLESPACE pg_default)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+CREATE TABLE deparse_tests.t_dedup2 (
+a int,
+b int,
+UNIQUE (a),
+UNIQUE (a),
+UNIQUE (b) USING INDEX TABLESPACE pg_default,
+PRIMARY KEY (a)
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_dedup2 (a pg_catalog.int4, b pg_catalog.int4, PRIMARY KEY (a), UNIQUE (b) USING INDEX TABLESPACE pg_default)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+-- constraint attributes and the exclusion predicate come after USING
+-- INDEX TABLESPACE in the grammar; the deparsed text must order them
+-- accordingly
+CREATE TABLE deparse_tests.t_deferidx (
+a int,
+b int,
+UNIQUE (a) USING INDEX TABLESPACE pg_default DEFERRABLE INITIALLY DEFERRED,
+PRIMARY KEY (b) USING INDEX TABLESPACE pg_default DEFERRABLE
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_deferidx (a pg_catalog.int4, b pg_catalog.int4, PRIMARY KEY (b) USING INDEX TABLESPACE pg_default DEFERRABLE, UNIQUE (a) USING INDEX TABLESPACE pg_default DEFERRABLE INITIALLY DEFERRED)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+CREATE TABLE deparse_tests.t_exclwhere (
+c circle,
+active bool,
+EXCLUDE USING gist (c WITH &&)
+	USING INDEX TABLESPACE pg_default WHERE (active) DEFERRABLE
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_exclwhere (c pg_catalog.circle, active pg_catalog.bool, EXCLUDE USING gist (c WITH &&) USING INDEX TABLESPACE pg_default WHERE (active) DEFERRABLE)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+-- index storage parameters on index-backed constraints, only as typed
+CREATE TABLE deparse_tests.t_idxopts (
+a int,
+b int,
+UNIQUE (a) WITH (fillfactor = 90),
+PRIMARY KEY (b) WITH (fillfactor = 80, deduplicate_items = off)
+	USING INDEX TABLESPACE pg_default DEFERRABLE
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_idxopts (a pg_catalog.int4, b pg_catalog.int4, PRIMARY KEY (b) WITH (fillfactor = '80', deduplicate_items = 'off') USING INDEX TABLESPACE pg_default DEFERRABLE, UNIQUE (a) WITH (fillfactor = '90'))
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+SELECT c.relname, c.reloptions
+  FROM pg_class c
+  WHERE c.relname IN ('t_idxopts_a_key', 't_idxopts_pkey')
+  ORDER BY 1;
+     relname     |              reloptions               
+-----------------+---------------------------------------
+ t_idxopts_a_key | {fillfactor=90}
+ t_idxopts_pkey  | {fillfactor=80,deduplicate_items=off}
+(2 rows)
+
+-- storage parameters of an exclusion constraint are already part of the
+-- pg_get_constraintdef output and must not be emitted twice
+CREATE TABLE deparse_tests.t_exclopts (
+c circle,
+EXCLUDE USING gist (c WITH &&) WITH (fillfactor = 70)
+	USING INDEX TABLESPACE pg_default DEFERRABLE
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_exclopts (c pg_catalog.circle, EXCLUDE USING gist (c WITH &&) WITH (fillfactor='70') USING INDEX TABLESPACE pg_default DEFERRABLE)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+SELECT c.relname, c.reloptions
+  FROM pg_class c
+  JOIN pg_constraint con ON con.conindid = c.oid
+  WHERE con.conrelid = 'deparse_tests.t_exclopts'::regclass;
+      relname      |   reloptions    
+-------------------+-----------------
+ t_exclopts_c_excl | {fillfactor=70}
+(1 row)
+
+-- an exclusion predicate that the (indent-flavored) ruleutils output
+-- renders across multiple lines must still split and replay correctly
+CREATE TABLE deparse_tests.t_exclcase (
+c circle,
+n int,
+EXCLUDE USING gist (c WITH &&)
+	USING INDEX TABLESPACE pg_default
+	WHERE (CASE WHEN n > 0 THEN true ELSE false END)
+	DEFERRABLE
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_exclcase (c pg_catalog.circle, n pg_catalog.int4, EXCLUDE USING gist (c WITH &&) USING INDEX TABLESPACE pg_default WHERE (
+CASE
+    WHEN (n > 0) THEN true
+    ELSE false
+END) DEFERRABLE)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+SELECT pg_get_constraintdef(oid)
+  FROM pg_constraint
+  WHERE conrelid = 'deparse_tests.t_exclcase'::regclass;
+          pg_get_constraintdef          
+----------------------------------------
+ EXCLUDE USING gist (c WITH &&) WHERE (+
+ CASE                                  +
+     WHEN (n > 0) THEN true            +
+     ELSE false                        +
+ END) DEFERRABLE
+(1 row)
+
+-- all optional exclusion-constraint clauses at once: the predicate strip
+-- must operate on a body whose remaining suffix is the embedded WITH
+CREATE TABLE deparse_tests.t_exclall (
+c circle,
+n int,
+x int,
+EXCLUDE USING gist (c WITH &&) INCLUDE (x)
+	WITH (fillfactor = 60)
+	USING INDEX TABLESPACE pg_default
+	WHERE (CASE WHEN n > 0 THEN true ELSE false END)
+	DEFERRABLE INITIALLY DEFERRED
+);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_exclall (c pg_catalog.circle, n pg_catalog.int4, x pg_catalog.int4, EXCLUDE USING gist (c WITH &&) INCLUDE (x) WITH (fillfactor='60') USING INDEX TABLESPACE pg_default WHERE (
+CASE
+    WHEN (n > 0) THEN true
+    ELSE false
+END) DEFERRABLE INITIALLY DEFERRED)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag CREATE INDEX
+SELECT pg_get_constraintdef(oid)
+  FROM pg_constraint
+  WHERE conrelid = 'deparse_tests.t_exclall'::regclass;
+                           pg_get_constraintdef                            
+---------------------------------------------------------------------------
+ EXCLUDE USING gist (c WITH &&) INCLUDE (x) WITH (fillfactor='60') WHERE (+
+ CASE                                                                     +
+     WHEN (n > 0) THEN true                                               +
+     ELSE false                                                           +
+ END) DEFERRABLE INITIALLY DEFERRED
+(1 row)
+
+-- printing works whether or not the deparsed version is executed
+SET test_ddl_deparse.execute_deparsed_ddl = off;
+CREATE TABLE deparse_tests.t_printonly (a int);
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_printonly (a pg_catalog.int4)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+-- the JSON blob itself can be printed, alone or along with the command
+SET test_ddl_deparse.print_deparsed_ddl = json;
+CREATE TABLE deparse_tests.t_json (id serial);
+NOTICE:  deparsed JSON: {"tag": "CREATE TABLE", "command": {"fmt": "CREATE%{persistence}s TABLE%{if_not_exists}s %{identity}D%{of_type}s%{partition_of}s%{table_elements}s%{inherits}s%{partition_bound}s%{partition_by}s%{access_method}s%{with_clause}s%{on_commit}s%{tablespace}s", "of_type": null, "identity": {"objname": "t_json", "schemaname": "deparse_tests"}, "inherits": null, "on_commit": null, "tablespace": null, "persistence": null, "with_clause": null, "partition_by": null, "partition_of": null, "access_method": null, "if_not_exists": null, "table_elements": {"fmt": " (%{elements:, }s)", "elements": [{"fmt": "%{name}I %{coltype}T%{storage}s%{compression}s%{collation}s%{not_null}s%{default}s%{identity_column}s%{generated_column}s", "name": "id", "type": "column", "coltype": {"typmod": "", "typarray": false, "typename": "serial", "schemaname": ""}, "default": null, "storage": null, "not_null": null, "collation": null, "compression": null, "identity_column": null, "generated_column": null}]}, "partition_bound": null}}
+NOTICE:  DDL test: type simple, tag CREATE SEQUENCE
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+NOTICE:  DDL test: type simple, tag ALTER SEQUENCE
+SET test_ddl_deparse.print_deparsed_ddl = 'both';
+SET test_ddl_deparse.execute_deparsed_ddl = on;
+CREATE TABLE deparse_tests.t_both (a int DEFAULT 42);
+NOTICE:  deparsed JSON: {"tag": "CREATE TABLE", "command": {"fmt": "CREATE%{persistence}s TABLE%{if_not_exists}s %{identity}D%{of_type}s%{partition_of}s%{table_elements}s%{inherits}s%{partition_bound}s%{partition_by}s%{access_method}s%{with_clause}s%{on_commit}s%{tablespace}s", "of_type": null, "identity": {"objname": "t_both", "schemaname": "deparse_tests"}, "inherits": null, "on_commit": null, "tablespace": null, "persistence": null, "with_clause": null, "partition_by": null, "partition_of": null, "access_method": null, "if_not_exists": null, "table_elements": {"fmt": " (%{elements:, }s)", "elements": [{"fmt": "%{name}I %{coltype}T%{storage}s%{compression}s%{collation}s%{not_null}s%{default}s%{identity_column}s%{generated_column}s", "name": "a", "type": "column", "coltype": {"typmod": "", "typarray": false, "typename": "int4", "schemaname": "pg_catalog"}, "default": {"fmt": " DEFAULT %{default_expr}s", "default_expr": "42"}, "storage": null, "not_null": null, "collation": null, "compression": null, "identity_column": null, "generated_column": null}]}, "partition_bound": null}}
+NOTICE:  deparsed DDL: CREATE TABLE deparse_tests.t_both (a pg_catalog.int4 DEFAULT 42)
+NOTICE:  DDL test: type simple, tag CREATE TABLE
+RESET test_ddl_deparse.print_deparsed_ddl;
+RESET test_ddl_deparse.execute_deparsed_ddl;
+DROP SCHEMA deparse_tests CASCADE;
+NOTICE:  drop cascades to 58 other objects
+DETAIL:  drop cascades to table deparse_tests.t1
+drop cascades to table deparse_tests.t_serial
+drop cascades to table deparse_tests.t_options
+drop cascades to table deparse_tests.t_constr
+drop cascades to table deparse_tests.t_tblnotnull
+drop cascades to table deparse_tests.t_types
+drop cascades to table deparse_tests.t_identity
+drop cascades to table deparse_tests.t_generated
+drop cascades to sequence deparse_tests.myseq
+drop cascades to table deparse_tests.t_defaults
+drop cascades to table deparse_tests.t_collate
+drop cascades to table deparse_tests.t_unlogged
+drop cascades to table deparse_tests.t_withopts
+drop cascades to table deparse_tests.t_parent
+drop cascades to table deparse_tests.t_child
+drop cascades to table deparse_tests.t_part
+drop cascades to type deparse_tests.comp
+drop cascades to table deparse_tests.t_of
+drop cascades to table deparse_tests.t_excl
+drop cascades to table deparse_tests."Mixed Case"
+drop cascades to table deparse_tests.t_like_src
+drop cascades to table deparse_tests.t_like
+drop cascades to table deparse_tests.t_like_mixed
+drop cascades to table deparse_tests.t_like_incl
+drop cascades to table deparse_tests.t_like_idsrc
+drop cascades to table deparse_tests.t_like_ident
+drop cascades to table deparse_tests.t_like_idx
+drop cascades to table deparse_tests.t_like_cmt
+drop cascades to table deparse_tests.t_like_all
+drop cascades to table deparse_tests.t_defnull
+drop cascades to table deparse_tests.t_seqname
+drop cascades to table deparse_tests.t_ownedby
+drop cascades to table deparse_tests.t_owner
+drop cascades to table deparse_tests.t_ownedby2
+drop cascades to table deparse_tests.t_idxtblspc
+drop cascades to table deparse_tests.t_unique
+drop cascades to table deparse_tests.t_fkref
+drop cascades to table deparse_tests.t_fk
+drop cascades to table deparse_tests.t_tblspc
+drop cascades to table deparse_tests.t_list
+drop cascades to table deparse_tests.t_hash
+drop cascades to table deparse_tests.t_identity2
+drop cascades to table deparse_tests.t_noinh
+drop cascades to table deparse_tests.t_parent2
+drop cascades to table deparse_tests.t_multi
+drop cascades to table deparse_tests.t_types2
+drop cascades to table deparse_tests."evil %{name}I table"
+drop cascades to table deparse_tests.t_dedup
+drop cascades to table deparse_tests.t_dedup2
+drop cascades to table deparse_tests.t_deferidx
+drop cascades to table deparse_tests.t_exclwhere
+drop cascades to table deparse_tests.t_idxopts
+drop cascades to table deparse_tests.t_exclopts
+drop cascades to table deparse_tests.t_exclcase
+drop cascades to table deparse_tests.t_exclall
+drop cascades to table deparse_tests.t_printonly
+drop cascades to table deparse_tests.t_json
+drop cascades to table deparse_tests.t_both
diff --git a/src/test/modules/test_ddl_deparse/meson.build b/src/test/modules/test_ddl_deparse/meson.build
index 85decdf1f2b..a7469c6c86d 100644
--- a/src/test/modules/test_ddl_deparse/meson.build
+++ b/src/test/modules/test_ddl_deparse/meson.build
@@ -49,6 +49,13 @@ tests += {
       'defprivs',
       'textsearch',
       'matviews',
+      'deparse_create_table',
     ],
   },
+  'tap': {
+    'tests': [
+      't/001_deparse_regress.pl',
+    ],
+    'test_kwargs': {'priority': 50}
+  },
 }
diff --git a/src/test/modules/test_ddl_deparse/sql/deparse_create_table.sql b/src/test/modules/test_ddl_deparse/sql/deparse_create_table.sql
new file mode 100644
index 00000000000..4dc0c600297
--- /dev/null
+++ b/src/test/modules/test_ddl_deparse/sql/deparse_create_table.sql
@@ -0,0 +1,412 @@
+--
+-- Tests for DDL deparsing of CREATE TABLE.
+--
+-- Note: the event triggers created by the test_ddl_deparse script (which
+-- must run first in this schedule) remain installed, so their "DDL test:"
+-- NOTICEs appear interleaved below; deparse capture itself does not use
+-- them.
+--
+-- With test_ddl_deparse.execute_deparsed_ddl enabled, the module
+-- intercepts each supported CREATE TABLE: the original command is executed
+-- in a subtransaction that is rolled back, the collected command is
+-- deparsed to JSON, and the reconstructed command is executed for real.
+-- With print_deparsed_ddl enabled the reconstructed command is shown as a
+-- NOTICE, so the expected output asserts the deparsed text itself; exactly
+-- one NOTICE per statement shows that one DDL deparses to one DDL.
+--
+LOAD 'test_ddl_deparse';
+SET test_ddl_deparse.execute_deparsed_ddl = on;
+SET test_ddl_deparse.print_deparsed_ddl = text;
+
+CREATE SCHEMA deparse_tests;
+
+-- simple case
+CREATE TABLE deparse_tests.t1 (a int, b text);
+
+-- serial must stay serial: no sequence creation and no nextval() default
+-- may appear in the deparsed command, but the replayed table must still
+-- have them.
+CREATE TABLE deparse_tests.t_serial (id serial, big bigserial NOT NULL);
+SELECT attname, pg_get_serial_sequence('deparse_tests.t_serial', attname) AS seq
+  FROM pg_attribute
+  WHERE attrelid = 'deparse_tests.t_serial'::regclass AND attnum > 0
+  ORDER BY attnum;
+
+-- COMPRESSION and STORAGE appear only when the user wrote them; columns
+-- without the clause must adopt the target's defaults on replay
+-- (attcompression stays unset).
+CREATE TABLE deparse_tests.t_options (
+a text COMPRESSION pglz,
+b text STORAGE external,
+c text
+);
+SELECT attname, attcompression, attstorage
+  FROM pg_attribute
+  WHERE attrelid = 'deparse_tests.t_options'::regclass AND attnum > 0
+  ORDER BY attnum;
+
+-- inline constraints are normalized to table constraints, but stay within
+-- the single CREATE TABLE command; unnamed constraints get the same
+-- auto-derived name on replay.
+CREATE TABLE deparse_tests.t_constr (
+a int PRIMARY KEY,
+b int REFERENCES deparse_tests.t_constr (a) NOT NULL,
+c text DEFAULT 'x' CHECK (c <> ''),
+CONSTRAINT named_check CHECK (a > 0)
+);
+SELECT conname, contype, pg_get_constraintdef(oid)
+  FROM pg_constraint
+  WHERE conrelid = 'deparse_tests.t_constr'::regclass
+  ORDER BY conname COLLATE "C";
+
+-- table-level NOT NULL constraint syntax
+CREATE TABLE deparse_tests.t_tblnotnull (a int, CONSTRAINT nn_a NOT NULL a);
+
+-- type names are normalized but keep their typmod; interval exercises
+-- SQL-standard types with special typmod rules
+CREATE TABLE deparse_tests.t_types (
+a varchar(10),
+b numeric(10,2),
+c timestamp(3),
+d interval hour to minute,
+e int[]
+);
+
+-- identity columns: sequence options are emitted only as far as the user
+-- gave them
+CREATE TABLE deparse_tests.t_identity (
+a int GENERATED ALWAYS AS IDENTITY,
+b int GENERATED BY DEFAULT AS IDENTITY (START WITH 10 INCREMENT BY 5)
+);
+SELECT seqstart, seqincrement
+  FROM pg_sequence
+  WHERE seqrelid = pg_get_serial_sequence('deparse_tests.t_identity', 'b')::regclass;
+
+-- generated columns
+CREATE TABLE deparse_tests.t_generated (
+a int,
+b int GENERATED ALWAYS AS (a * 2) STORED,
+c int GENERATED ALWAYS AS (a + 1) VIRTUAL
+);
+
+-- defaults referencing other objects come out schema-qualified
+CREATE SEQUENCE deparse_tests.myseq;
+CREATE TABLE deparse_tests.t_defaults (
+a timestamptz DEFAULT now(),
+b int DEFAULT nextval('deparse_tests.myseq')
+);
+
+-- collation
+CREATE TABLE deparse_tests.t_collate (a text COLLATE "C");
+
+-- persistence
+CREATE UNLOGGED TABLE deparse_tests.t_unlogged (a int);
+
+-- reloptions, only as typed
+CREATE TABLE deparse_tests.t_withopts (a int)
+	WITH (fillfactor = 70, autovacuum_enabled = false);
+
+-- inheritance
+CREATE TABLE deparse_tests.t_parent (a int NOT NULL, b text);
+CREATE TABLE deparse_tests.t_child () INHERITS (deparse_tests.t_parent);
+
+-- partitioning
+CREATE TABLE deparse_tests.t_part (a int, b text) PARTITION BY RANGE (a);
+CREATE TABLE deparse_tests.t_part1 PARTITION OF deparse_tests.t_part
+	FOR VALUES FROM (1) TO (10);
+CREATE TABLE deparse_tests.t_part2 PARTITION OF deparse_tests.t_part
+	(b WITH OPTIONS NOT NULL)
+	FOR VALUES FROM (10) TO (20);
+CREATE TABLE deparse_tests.t_partd PARTITION OF deparse_tests.t_part DEFAULT;
+
+-- typed table
+CREATE TYPE deparse_tests.comp AS (x int, y text);
+CREATE TABLE deparse_tests.t_of OF deparse_tests.comp (x WITH OPTIONS NOT NULL);
+
+-- exclusion constraint
+CREATE TABLE deparse_tests.t_excl (
+c circle,
+EXCLUDE USING gist (c WITH &&)
+);
+
+-- quoted identifiers
+CREATE TABLE deparse_tests."Mixed Case" ("Col A" int, "select" text);
+
+-- IF NOT EXISTS on an existing relation deparses to nothing and must not
+-- be replayed
+CREATE TABLE IF NOT EXISTS deparse_tests.t1 (a int);
+
+-- LIKE contributes columns that are not in the raw statement, so they are
+-- recovered from the catalog.  Bare LIKE copies the type, its collation and
+-- NOT NULL, and nothing else.
+CREATE TABLE deparse_tests.t_like_src (
+a int NOT NULL,
+b text COLLATE "C",
+c numeric(10,2) STORAGE plain,
+d int DEFAULT 7,
+e int GENERATED ALWAYS AS (d * 2) STORED
+);
+CREATE TABLE deparse_tests.t_like (LIKE deparse_tests.t_like_src);
+
+-- columns the user did write are still deparsed from what they wrote, and
+-- the catalog order keeps the two kinds interleaved as the command produced
+-- them (serial stays serial, the copied columns come from pg_attribute)
+CREATE TABLE deparse_tests.t_like_mixed (
+x serial,
+LIKE deparse_tests.t_like_src,
+y int DEFAULT 1
+);
+
+-- the options that land in the table definition are all recovered
+CREATE TABLE deparse_tests.t_like_incl (
+LIKE deparse_tests.t_like_src
+	INCLUDING DEFAULTS INCLUDING GENERATED INCLUDING CONSTRAINTS
+	INCLUDING STORAGE INCLUDING COMPRESSION
+);
+
+-- identity comes back with the sequence parameters that were copied
+CREATE TABLE deparse_tests.t_like_idsrc (
+a int GENERATED BY DEFAULT AS IDENTITY (START WITH 10 INCREMENT BY 5)
+);
+CREATE TABLE deparse_tests.t_like_ident
+  (LIKE deparse_tests.t_like_idsrc INCLUDING IDENTITY);
+SELECT seqstart, seqincrement
+  FROM pg_sequence
+  WHERE seqrelid = pg_get_serial_sequence('deparse_tests.t_like_ident', 'a')::regclass;
+
+-- options that copy an object needing a statement of its own are declined:
+-- the statement is not intercepted and executes normally
+CREATE INDEX ON deparse_tests.t_like_src (a);
+COMMENT ON COLUMN deparse_tests.t_like_src.a IS 'a comment';
+CREATE TABLE deparse_tests.t_like_idx
+  (LIKE deparse_tests.t_like_src INCLUDING INDEXES);
+CREATE TABLE deparse_tests.t_like_cmt
+  (LIKE deparse_tests.t_like_src INCLUDING COMMENTS);
+CREATE TABLE deparse_tests.t_like_all
+  (LIKE deparse_tests.t_like_src INCLUDING ALL);
+SELECT relname FROM pg_class WHERE oid = 'deparse_tests.t_like'::regclass;
+
+-- DEFAULT NULL stores no pg_attrdef entry but must still deparse
+CREATE TABLE deparse_tests.t_defnull (a int DEFAULT NULL, b text DEFAULT NULL::text);
+
+-- identity sequence options that parse analysis consumes are still in the
+-- raw parse tree: SEQUENCE NAME must deparse; OWNED BY is dropped, since
+-- execution overrides it (the sequence is always owned by the identity
+-- column) and the name it carries need not be replayable
+CREATE TABLE deparse_tests.t_seqname (
+id int GENERATED ALWAYS AS IDENTITY
+	(SEQUENCE NAME deparse_tests.t_seqname_seq START WITH 5)
+);
+SELECT relname FROM pg_class WHERE oid = 'deparse_tests.t_seqname_seq'::regclass;
+CREATE TABLE deparse_tests.t_ownedby (
+id int GENERATED BY DEFAULT AS IDENTITY
+	(OWNED BY NONE)
+);
+CREATE TABLE deparse_tests.t_owner (x int);
+CREATE TABLE deparse_tests.t_ownedby2 (
+id int GENERATED ALWAYS AS IDENTITY
+	(OWNED BY deparse_tests.t_owner.x START WITH 7)
+);
+SELECT pg_get_serial_sequence('deparse_tests.t_ownedby2', 'id');
+
+-- USING INDEX TABLESPACE must stay attached to the constraint that carried
+-- it, also when several unnamed constraints of the same type exist
+CREATE TABLE deparse_tests.t_idxtblspc (
+a int,
+b int,
+c int,
+UNIQUE (a),
+UNIQUE (b) USING INDEX TABLESPACE pg_default,
+PRIMARY KEY (c)
+);
+
+-- UNIQUE constraint variants, all via pg_get_constraintdef
+CREATE TABLE deparse_tests.t_unique (
+a int UNIQUE,
+b int,
+c int,
+d int,
+UNIQUE NULLS NOT DISTINCT (b),
+UNIQUE (c) INCLUDE (d),
+CONSTRAINT defer_u UNIQUE (d) DEFERRABLE INITIALLY DEFERRED
+);
+SELECT conname, pg_get_constraintdef(oid)
+  FROM pg_constraint
+  WHERE conrelid = 'deparse_tests.t_unique'::regclass
+  ORDER BY conname COLLATE "C";
+
+-- foreign key matching and referential actions
+CREATE TABLE deparse_tests.t_fkref (a int, b int, PRIMARY KEY (a, b));
+CREATE TABLE deparse_tests.t_fk (
+x int,
+y int,
+FOREIGN KEY (x, y) REFERENCES deparse_tests.t_fkref (a, b)
+	MATCH FULL ON DELETE CASCADE ON UPDATE SET NULL
+);
+
+-- temp tables and ON COMMIT; the temp schema is emitted as pg_temp
+CREATE TEMP TABLE t_temp (a int);
+CREATE TEMP TABLE t_temp_delrows (a int) ON COMMIT DELETE ROWS;
+BEGIN;
+CREATE TEMP TABLE t_temp_drop (a int) ON COMMIT DROP;
+COMMIT;
+DROP TABLE t_temp, t_temp_delrows;
+
+-- access method and tablespace, only as typed
+CREATE TABLE deparse_tests.t_tblspc (a int) USING heap TABLESPACE pg_default;
+
+-- partitioning: list and hash, key expressions, opclass and collation
+CREATE TABLE deparse_tests.t_list (a text COLLATE "C", b int)
+	PARTITION BY LIST (a COLLATE "POSIX" text_pattern_ops);
+CREATE TABLE deparse_tests.t_list1 PARTITION OF deparse_tests.t_list
+	FOR VALUES IN ('x', 'y');
+CREATE TABLE deparse_tests.t_list2 PARTITION OF deparse_tests.t_list
+	(b WITH OPTIONS DEFAULT 42)
+	FOR VALUES IN ('z');
+CREATE TABLE deparse_tests.t_hash (a int, b text)
+	PARTITION BY HASH ((a + length(b)));
+CREATE TABLE deparse_tests.t_hash1 PARTITION OF deparse_tests.t_hash
+	FOR VALUES WITH (MODULUS 4, REMAINDER 0);
+
+-- identity sequence option spread
+CREATE TABLE deparse_tests.t_identity2 (
+a int GENERATED ALWAYS AS IDENTITY
+	(MINVALUE 0 MAXVALUE 100 CACHE 10 CYCLE)
+);
+
+-- NO INHERIT forms
+CREATE TABLE deparse_tests.t_noinh (
+a int NOT NULL NO INHERIT,
+b int CONSTRAINT chk_b CHECK (b > 0) NO INHERIT,
+c int,
+CONSTRAINT nn_c NOT NULL c NO INHERIT
+);
+
+-- multiple inheritance parents, in order
+CREATE TABLE deparse_tests.t_parent2 (c int);
+CREATE TABLE deparse_tests.t_multi ()
+	INHERITS (deparse_tests.t_parent, deparse_tests.t_parent2);
+
+-- more type and storage spellings: smallserial, array of typmod type,
+-- STORAGE MAIN, and the COMPRESSION DEFAULT keyword
+CREATE TABLE deparse_tests.t_types2 (
+a smallserial,
+b varchar(3)[],
+c text STORAGE main,
+d text COMPRESSION default
+);
+
+-- hostile names: format-string metacharacters and embedded quotes must
+-- pass through the fmt expansion inertly
+CREATE TABLE deparse_tests."evil %{name}I table" (
+"a""b" int,
+"%{col}s" text DEFAULT 'O''Reilly',
+"100%" numeric
+);
+SELECT relname FROM pg_class WHERE relname = 'evil %{name}I table';
+
+-- parse analysis merges redundant index constraints (the duplicate
+-- UNIQUE (a), and UNIQUE (a) into the primary key); USING INDEX
+-- TABLESPACE attribution must follow the surviving index of each
+-- constraint, not raw-parse-tree positions
+CREATE TABLE deparse_tests.t_dedup (
+a int UNIQUE,
+b int,
+UNIQUE (a),
+UNIQUE (b) USING INDEX TABLESPACE pg_default
+);
+CREATE TABLE deparse_tests.t_dedup2 (
+a int,
+b int,
+UNIQUE (a),
+UNIQUE (a),
+UNIQUE (b) USING INDEX TABLESPACE pg_default,
+PRIMARY KEY (a)
+);
+
+-- constraint attributes and the exclusion predicate come after USING
+-- INDEX TABLESPACE in the grammar; the deparsed text must order them
+-- accordingly
+CREATE TABLE deparse_tests.t_deferidx (
+a int,
+b int,
+UNIQUE (a) USING INDEX TABLESPACE pg_default DEFERRABLE INITIALLY DEFERRED,
+PRIMARY KEY (b) USING INDEX TABLESPACE pg_default DEFERRABLE
+);
+CREATE TABLE deparse_tests.t_exclwhere (
+c circle,
+active bool,
+EXCLUDE USING gist (c WITH &&)
+	USING INDEX TABLESPACE pg_default WHERE (active) DEFERRABLE
+);
+
+-- index storage parameters on index-backed constraints, only as typed
+CREATE TABLE deparse_tests.t_idxopts (
+a int,
+b int,
+UNIQUE (a) WITH (fillfactor = 90),
+PRIMARY KEY (b) WITH (fillfactor = 80, deduplicate_items = off)
+	USING INDEX TABLESPACE pg_default DEFERRABLE
+);
+SELECT c.relname, c.reloptions
+  FROM pg_class c
+  WHERE c.relname IN ('t_idxopts_a_key', 't_idxopts_pkey')
+  ORDER BY 1;
+
+-- storage parameters of an exclusion constraint are already part of the
+-- pg_get_constraintdef output and must not be emitted twice
+CREATE TABLE deparse_tests.t_exclopts (
+c circle,
+EXCLUDE USING gist (c WITH &&) WITH (fillfactor = 70)
+	USING INDEX TABLESPACE pg_default DEFERRABLE
+);
+SELECT c.relname, c.reloptions
+  FROM pg_class c
+  JOIN pg_constraint con ON con.conindid = c.oid
+  WHERE con.conrelid = 'deparse_tests.t_exclopts'::regclass;
+
+-- an exclusion predicate that the (indent-flavored) ruleutils output
+-- renders across multiple lines must still split and replay correctly
+CREATE TABLE deparse_tests.t_exclcase (
+c circle,
+n int,
+EXCLUDE USING gist (c WITH &&)
+	USING INDEX TABLESPACE pg_default
+	WHERE (CASE WHEN n > 0 THEN true ELSE false END)
+	DEFERRABLE
+);
+SELECT pg_get_constraintdef(oid)
+  FROM pg_constraint
+  WHERE conrelid = 'deparse_tests.t_exclcase'::regclass;
+
+-- all optional exclusion-constraint clauses at once: the predicate strip
+-- must operate on a body whose remaining suffix is the embedded WITH
+CREATE TABLE deparse_tests.t_exclall (
+c circle,
+n int,
+x int,
+EXCLUDE USING gist (c WITH &&) INCLUDE (x)
+	WITH (fillfactor = 60)
+	USING INDEX TABLESPACE pg_default
+	WHERE (CASE WHEN n > 0 THEN true ELSE false END)
+	DEFERRABLE INITIALLY DEFERRED
+);
+SELECT pg_get_constraintdef(oid)
+  FROM pg_constraint
+  WHERE conrelid = 'deparse_tests.t_exclall'::regclass;
+
+-- printing works whether or not the deparsed version is executed
+SET test_ddl_deparse.execute_deparsed_ddl = off;
+CREATE TABLE deparse_tests.t_printonly (a int);
+
+-- the JSON blob itself can be printed, alone or along with the command
+SET test_ddl_deparse.print_deparsed_ddl = json;
+CREATE TABLE deparse_tests.t_json (id serial);
+SET test_ddl_deparse.print_deparsed_ddl = 'both';
+SET test_ddl_deparse.execute_deparsed_ddl = on;
+CREATE TABLE deparse_tests.t_both (a int DEFAULT 42);
+
+RESET test_ddl_deparse.print_deparsed_ddl;
+RESET test_ddl_deparse.execute_deparsed_ddl;
+
+DROP SCHEMA deparse_tests CASCADE;
diff --git a/src/test/modules/test_ddl_deparse/t/001_deparse_regress.pl b/src/test/modules/test_ddl_deparse/t/001_deparse_regress.pl
new file mode 100644
index 00000000000..964bf6e1177
--- /dev/null
+++ b/src/test/modules/test_ddl_deparse/t/001_deparse_regress.pl
@@ -0,0 +1,130 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Run the core regression tests while replacing supported DDL commands with
+# their deparsed reconstruction (see test_ddl_deparse.execute_deparsed_ddl).
+# This is meant to reliably detect deparsing support gaps for new syntax:
+# any divergence between a command and its deparsed form shows up as a
+# regression diff.
+use strict;
+use warnings FATAL => 'all';
+
+use Cwd            qw(abs_path);
+use File::Basename qw(dirname);
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Tests that are incompatible with DDL replacement can be excluded here,
+# with a comment explaining why.
+my @excluded_tests = (
+	# This test lists every event trigger in the database, and the capture
+	# trigger the test_ddl_deparse extension installs shows up there.  It
+	# also installs sql_drop/ddl_command_end event triggers whose functions
+	# reference objects by unqualified name, which fail when the replayed
+	# DDL -- executed with search_path cleared, as an apply worker would --
+	# fires them.
+	'event_trigger',
+);
+
+# Initialize the primary node
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init();
+
+$node->append_conf('postgresql.conf', <<EOM);
+shared_preload_libraries = 'test_ddl_deparse'
+test_ddl_deparse.execute_deparsed_ddl = on
+test_ddl_deparse.log_declined_commands = on
+wal_level = replica
+EOM
+$node->start;
+
+my $srcdir = abs_path("../../../..");
+
+# --dlpath is needed to be able to find the location of regress.so
+# and any libraries the regression tests require.
+my $dlpath = dirname($ENV{REGRESS_SHLIB});
+
+# --outputdir points to the path where to place the output files.
+my $outputdir = $PostgreSQL::Test::Utils::tmp_check;
+
+# --inputdir points to the path of the input files.
+my $inputdir = "$srcdir/src/test/regress";
+
+# Build the schedule, dropping any excluded tests.
+my $schedule = "$outputdir/deparse_schedule";
+open my $in, '<', "$inputdir/parallel_schedule"
+  or die "could not open parallel_schedule: $!";
+open my $out, '>', $schedule
+  or die "could not create $schedule: $!";
+while (my $line = <$in>)
+{
+	if ($line =~ /^test:\s+(.*)$/)
+	{
+		my @tests = grep {
+			my $t = $_;
+			!grep { $t eq $_ } @excluded_tests
+		} split(/\s+/, $1);
+		next unless @tests;
+		$line = "test: " . join(' ', @tests) . "\n";
+	}
+	print $out $line;
+}
+close $in;
+close $out;
+
+# Run the tests.
+my $rc =
+  system($ENV{PG_REGRESS} . " "
+	  . "--bindir= "
+	  . "--dlpath=\"$dlpath\" "
+	  . "--load-extension=test_ddl_deparse "
+	  . "--host="
+	  . $node->host . " "
+	  . "--port="
+	  . $node->port . " "
+	  . "--schedule=$schedule "
+	  . "--max-concurrent-tests=20 "
+	  . "--inputdir=\"$inputdir\" "
+	  . "--expecteddir=\"$inputdir\" "
+	  . "--outputdir=\"$outputdir\"");
+
+# Dump out the regression diffs file, if there is one
+if ($rc != 0)
+{
+	my $diffs = "$outputdir/regression.diffs";
+	if (-e $diffs)
+	{
+		print "=== dumping $diffs ===\n";
+		print slurp_file($diffs);
+		print "=== EOF ===\n";
+	}
+}
+
+# Report the commands the deparser declined, grouped by command tag.  These
+# executed unchanged and were therefore not exercised by this run; the list is
+# how a deparser gap becomes visible instead of just being absent from the
+# coverage.  It is printed rather than asserted on, since the counts move with
+# every change to the core tests, and since a command being declined is not by
+# itself a defect.
+my %declined;
+for my $line (split /\n/, slurp_file($node->logfile))
+{
+	next unless $line =~ /test_ddl_deparse: declined ([A-Z][A-Z ]*[A-Z]):/;
+	$declined{$1}++;
+}
+if (%declined)
+{
+	note("commands declined by the deparser during this run:");
+	note(sprintf("  %-40s %d", $_, $declined{$_}))
+	  for sort { $declined{$b} <=> $declined{$a} || $a cmp $b } keys %declined;
+}
+else
+{
+	note("the deparser declined no command during this run");
+}
+
+# Report results
+is($rc, 0, 'regression tests pass');
+
+done_testing();
diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse--1.0.sql b/src/test/modules/test_ddl_deparse/test_ddl_deparse--1.0.sql
index 861d843690b..ca649d48b74 100644
--- a/src/test/modules/test_ddl_deparse/test_ddl_deparse--1.0.sql
+++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse--1.0.sql
@@ -4,15 +4,30 @@
 \echo Use "CREATE EXTENSION test_ddl_deparse" to load this file. \quit
 
 CREATE FUNCTION get_command_type(pg_ddl_command)
-  RETURNS text IMMUTABLE STRICT
+  RETURNS text IMMUTABLE STRICT PARALLEL SAFE
   AS 'MODULE_PATHNAME' LANGUAGE C;
 
 CREATE FUNCTION get_command_tag(pg_ddl_command)
-  RETURNS text IMMUTABLE STRICT
+  RETURNS text IMMUTABLE STRICT PARALLEL SAFE
   AS 'MODULE_PATHNAME' LANGUAGE C;
 
 CREATE FUNCTION get_altertable_subcmdinfo(IN cmd pg_ddl_command,
     OUT cmdtype text,
     OUT objdesc text)
-  RETURNS SETOF record IMMUTABLE STRICT
+  RETURNS SETOF record IMMUTABLE STRICT PARALLEL SAFE
   AS 'MODULE_PATHNAME' LANGUAGE C;
+
+-- Deparse each supported DDL command as it finishes executing.  The event
+-- trigger is created here, rather than by the regression scripts, so that
+-- pg_regress --load-extension is enough to install it for a whole run.
+CREATE FUNCTION capture_deparsed_ddl()
+  RETURNS event_trigger
+  AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE EVENT TRIGGER capture_deparsed_ddl
+  ON ddl_command_end EXECUTE FUNCTION capture_deparsed_ddl();
+
+-- An origin event trigger does not fire while session_replication_role is
+-- 'replica', which would silently stop the capture just where a replication
+-- apply worker runs; the module must keep deparsing there too.
+ALTER EVENT TRIGGER capture_deparsed_ddl ENABLE ALWAYS;
diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
index 64a1dfa9f79..1a0484f8231 100644
--- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
+++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
@@ -10,11 +10,19 @@
  */
 #include "postgres.h"
 
+#include "access/xact.h"
+#include "catalog/namespace.h"
+#include "commands/ddl_deparse.h"
+#include "commands/event_trigger.h"
 #include "funcapi.h"
 #include "nodes/execnodes.h"
 #include "tcop/deparse_utility.h"
+#include "tcop/tcopprot.h"
 #include "tcop/utility.h"
 #include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/memutils.h"
+#include "utils/resowner.h"
 #include "utils/tuplestore.h"
 
 PG_MODULE_MAGIC;
@@ -22,6 +30,502 @@ PG_MODULE_MAGIC;
 PG_FUNCTION_INFO_V1(get_command_type);
 PG_FUNCTION_INFO_V1(get_command_tag);
 PG_FUNCTION_INFO_V1(get_altertable_subcmdinfo);
+PG_FUNCTION_INFO_V1(capture_deparsed_ddl);
+
+typedef struct TestDDLDeparseErrorInfo
+{
+	const char *orig_ddl;
+	const char *deparsed_ddl;
+} TestDDLDeparseErrorInfo;
+
+typedef enum
+{
+	PRINT_DEPARSED_OFF,
+	PRINT_DEPARSED_JSON,
+	PRINT_DEPARSED_TEXT,
+	PRINT_DEPARSED_BOTH,
+} PrintDeparsedDDL;
+
+static const struct config_enum_entry print_deparsed_ddl_options[] = {
+	{"off", PRINT_DEPARSED_OFF, false},
+	{"json", PRINT_DEPARSED_JSON, false},
+	{"text", PRINT_DEPARSED_TEXT, false},
+	{"both", PRINT_DEPARSED_BOTH, false},
+	{NULL, 0, false}
+};
+
+/* GUCs */
+static bool execute_deparsed_ddl = false;
+static int	print_deparsed_ddl = PRINT_DEPARSED_OFF;
+static bool log_declined_commands = false;
+
+static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
+
+/*
+ * The statement currently being captured, as the user wrote it, or NULL when
+ * no capture is in progress.  The deparser works from the raw statement, but
+ * execution rewrites it in place (transformCreateStmt updates relpersistence,
+ * expands serial, and so on), so the module runs every captured statement
+ * with readOnlyTree forced on: what ProcessUtilitySlow scribbles is then a
+ * copy of its own, and the tree this points at stays pristine.
+ *
+ * Being non-NULL is also what tells the event trigger below that it is firing
+ * for a statement this module intercepted, rather than for some unrelated
+ * DDL, and that the raw statement therefore matches the commands it is about
+ * to deparse.
+ */
+static const Node *capture_parsetree = NULL;
+
+/*
+ * JSON blob deparsed for that statement; allocated in TopTransactionContext
+ * so it survives the rollback of the internal subtransaction the trial run
+ * executes in.  Only valid while capture_parsetree is set.
+ *
+ * A single pointer, not a list: one intercepted statement is one complete
+ * query, which fires the capture event trigger exactly once (sub-commands
+ * such as the internal ALTER TABLE ADD CONSTRAINT generated by a CREATE
+ * TABLE with a foreign key are collected into that one firing, not fired
+ * separately), and one firing deparses to exactly one command.  The trigger
+ * errors out if that invariant is ever broken.
+ */
+static char *captured_json = NULL;
+
+/* Are we re-executing a deparsed command ourselves? */
+static bool in_roundtrip = false;
+
+static void test_ddl_deparse_utility_command(PlannedStmt *pstmt,
+											 const char *queryString,
+											 bool readOnlyTree,
+											 ProcessUtilityContext context,
+											 ParamListInfo params,
+											 QueryEnvironment *queryEnv,
+											 DestReceiver *dest,
+											 QueryCompletion *qc);
+static void test_ddl_deparse_error_callback(void *arg);
+static void report_declined_command(Node *stmt, const char *queryString);
+
+void
+_PG_init(void)
+{
+	DefineCustomBoolVariable("test_ddl_deparse.execute_deparsed_ddl",
+							 "Replace supported DDL commands with their deparsed version.",
+							 NULL,
+							 &execute_deparsed_ddl,
+							 false,
+							 PGC_USERSET,
+							 0,
+							 NULL, NULL, NULL);
+
+	DefineCustomEnumVariable("test_ddl_deparse.print_deparsed_ddl",
+							 "Report the deparsed DDL command as a NOTICE, in the given form.",
+							 NULL,
+							 &print_deparsed_ddl,
+							 PRINT_DEPARSED_OFF,
+							 print_deparsed_ddl_options,
+							 PGC_USERSET,
+							 0,
+							 NULL, NULL, NULL);
+
+	DefineCustomBoolVariable("test_ddl_deparse.log_declined_commands",
+							 "Log DDL commands the deparser declined to reproduce.",
+							 NULL,
+							 &log_declined_commands,
+							 false,
+							 PGC_USERSET,
+							 0,
+							 NULL, NULL, NULL);
+
+	MarkGUCPrefixReserved("test_ddl_deparse");
+
+	next_ProcessUtility_hook = ProcessUtility_hook;
+	ProcessUtility_hook = test_ddl_deparse_utility_command;
+}
+
+/*
+ * ddl_command_end event trigger: deparse the commands collected for the
+ * statement being captured, and remember the result for replay.
+ *
+ * This is where the deparser has to run: the collected commands live in the
+ * event trigger state, which ProcessUtilitySlow discards as soon as the
+ * complete query is done, so there is no later moment at which to ask for
+ * them.  The event trigger machinery has already done a
+ * CommandCounterIncrement, so the catalog entries the deparser reads are
+ * visible.
+ *
+ * For a statement whose deparsed form is to be executed, this runs inside the
+ * internal subtransaction started by test_ddl_deparse_utility_command, so the
+ * JSON string is copied into TopTransactionContext, which survives that
+ * subtransaction's rollback.
+ *
+ * With print_deparsed_ddl enabled, the deparsed command is reported as a
+ * NOTICE whether or not it is going to be executed in place of the original.
+ */
+Datum
+capture_deparsed_ddl(PG_FUNCTION_ARGS)
+{
+	char	   *json;
+
+	if (!CALLED_AS_EVENT_TRIGGER(fcinfo))
+		elog(ERROR, "not fired by event trigger manager");
+
+	/*
+	 * Do nothing unless a statement of ours is being captured.  This is the
+	 * common case: the trigger is installed for the whole session, but only
+	 * top-level statements the module intercepts have a raw parse tree saved
+	 * for the deparser to work from.
+	 */
+	if (capture_parsetree == NULL)
+		PG_RETURN_NULL();
+
+	/*
+	 * A second firing while one statement is being captured means a nested
+	 * complete query (say, DDL run by another event trigger function) also
+	 * produced a supported command.  Its raw statement is not the one saved
+	 * above, and replaying both would double-execute the nested one when the
+	 * replayed outer statement triggers it again, so refuse loudly.
+	 */
+	if (captured_json != NULL)
+		elog(ERROR, "multiple DDL deparse captures for a single statement");
+
+	json = deparse_ddl_command(capture_parsetree,
+							   EventTriggerGetCollectedCommands());
+	if (json == NULL)
+		PG_RETURN_NULL();
+
+	if (print_deparsed_ddl == PRINT_DEPARSED_JSON ||
+		print_deparsed_ddl == PRINT_DEPARSED_BOTH)
+		ereport(NOTICE,
+				(errmsg("deparsed JSON: %s", json),
+				 errhidestmt(true),
+				 errhidecontext(true)));
+
+	if (print_deparsed_ddl == PRINT_DEPARSED_TEXT ||
+		print_deparsed_ddl == PRINT_DEPARSED_BOTH)
+		ereport(NOTICE,
+				(errmsg("deparsed DDL: %s", deparse_ddl_json_to_string(json)),
+				 errhidestmt(true),
+				 errhidecontext(true)));
+
+	captured_json = MemoryContextStrdup(TopTransactionContext, json);
+
+	PG_RETURN_NULL();
+}
+
+/*
+ * Continue with the rest of the ProcessUtility hook chain (or standard
+ * processing if we are the last hook).  Used for every execution this module
+ * initiates, so that other loaded modules' hooks are not bypassed; re-entry
+ * into our own interception is prevented by the in_roundtrip flag and by the
+ * fact that this call sits below our hook in the chain.
+ */
+static void
+call_next_ProcessUtility(PlannedStmt *pstmt,
+						 const char *queryString,
+						 bool readOnlyTree,
+						 ProcessUtilityContext context,
+						 ParamListInfo params,
+						 QueryEnvironment *queryEnv,
+						 DestReceiver *dest, QueryCompletion *qc)
+{
+	if (next_ProcessUtility_hook)
+		next_ProcessUtility_hook(pstmt, queryString, readOnlyTree,
+								 context, params, queryEnv, dest, qc);
+	else
+		standard_ProcessUtility(pstmt, queryString, readOnlyTree,
+								context, params, queryEnv, dest, qc);
+}
+
+/*
+ * Parse and execute one deparsed DDL command.
+ *
+ * The whole point of this module is to surface deparser bugs, so any shape
+ * surprise in the deparsed string is reported as an error (with the string
+ * in the error context), not assumed away: one DDL command must deparse to
+ * exactly one DDL command.
+ *
+ */
+static void
+execute_deparsed_command(const char *sql,
+						 ProcessUtilityContext context,
+						 ParamListInfo params,
+						 QueryEnvironment *queryEnv,
+						 DestReceiver *dest, QueryCompletion *qc)
+{
+	List	   *parsetree_list;
+	RawStmt    *rs;
+	List	   *querytree_list;
+	PlannedStmt *pstmt;
+	Query	   *query;
+	int			save_nestlevel;
+
+	save_nestlevel = NewGUCNestLevel();
+
+	/*
+	 * Messages that the replayed command would emit have already been emitted
+	 * by the original execution, so suppress them.
+	 */
+	(void) set_config_option("client_min_messages", "error",
+							 PGC_USERSET, PGC_S_SESSION,
+							 GUC_ACTION_SAVE, true, 0, false);
+
+	/*
+	 * Execute the deparsed command under a restricted search_path, the same
+	 * environment the deparser itself produces its output under.  Deparsed
+	 * DDL is supposed to be safe to execute regardless of the search_path in
+	 * effect (everything schema-qualified except pg_catalog references);
+	 * replaying it this way makes any missing qualification fail visibly
+	 * instead of silently resolving through the session's own search_path.
+	 * The surrounding GUC nest level undoes this below.
+	 */
+	RestrictSearchPath();
+
+	parsetree_list = pg_parse_query(sql);
+	if (list_length(parsetree_list) != 1)
+		elog(ERROR, "deparsed DDL is not a single command: %s", sql);
+
+	rs = linitial_node(RawStmt, parsetree_list);
+	querytree_list = pg_analyze_and_rewrite_fixedparams(rs, sql, NULL, 0,
+														NULL);
+	if (list_length(querytree_list) != 1)
+		elog(ERROR, "deparsed DDL did not analyze to a single query: %s", sql);
+
+	query = linitial_node(Query, querytree_list);
+	if (query->commandType != CMD_UTILITY)
+		elog(ERROR, "deparsed DDL is not a utility command: %s", sql);
+
+	pstmt = makeNode(PlannedStmt);
+	pstmt->commandType = CMD_UTILITY;
+	pstmt->canSetTag = true;
+	pstmt->utilityStmt = query->utilityStmt;
+	pstmt->stmt_location = rs->stmt_location;
+	pstmt->stmt_len = rs->stmt_len;
+
+	call_next_ProcessUtility(pstmt, sql, false,
+							 context, params, queryEnv,
+							 dest, qc);
+
+	AtEOXact_GUC(true, save_nestlevel);
+}
+
+/*
+ * ProcessUtility hook: capture supported DDL commands and, when
+ * execute_deparsed_ddl is set, replace them with their deparsed
+ * reconstruction.
+ *
+ * Capturing a statement means saving it for the event trigger above and
+ * running it with readOnlyTree forced on, so that the saved tree stays as the
+ * user wrote it while the copy execution works on is rewritten in place.
+ *
+ * For the replacement, the original statement is executed in an internal
+ * subtransaction that is rolled back afterwards; its execution fires the
+ * capture event trigger, which deparses the collected commands.  The
+ * reconstructed command is then executed for real.  Divergence between the
+ * two forms thus shows up in the regular regression output.
+ */
+static void
+test_ddl_deparse_utility_command(PlannedStmt *pstmt,
+								 const char *queryString,
+								 bool readOnlyTree,
+								 ProcessUtilityContext context,
+								 ParamListInfo params,
+								 QueryEnvironment *queryEnv,
+								 DestReceiver *dest, QueryCompletion *qc)
+{
+	/*
+	 * Only top-level statements are captured, and only when something is
+	 * going to be done with the result.  Our own replay of a deparsed command
+	 * must not be captured in turn.
+	 */
+	if (context != PROCESS_UTILITY_TOPLEVEL || in_roundtrip ||
+		(!execute_deparsed_ddl && print_deparsed_ddl == PRINT_DEPARSED_OFF))
+	{
+		call_next_ProcessUtility(pstmt, queryString, readOnlyTree,
+								 context, params, queryEnv, dest, qc);
+		return;
+	}
+
+	/*
+	 * A command the deparser declines executes unchanged.  That is the right
+	 * thing for a test run -- one gap must not stop the rest of the suite --
+	 * but it means the command is simply not tested, and nothing says so.
+	 * Report it, so that a deparser gap is a thing one can go and look at
+	 * rather than an absence nobody notices.
+	 */
+	if (!ddl_deparse_command_supported(pstmt->utilityStmt))
+	{
+		report_declined_command(pstmt->utilityStmt, queryString);
+		call_next_ProcessUtility(pstmt, queryString, readOnlyTree,
+								 context, params, queryEnv, dest, qc);
+		return;
+	}
+
+	captured_json = NULL;
+	capture_parsetree = pstmt->utilityStmt;
+
+	PG_TRY();
+	{
+		if (!execute_deparsed_ddl)
+		{
+			/*
+			 * Nothing to replace: run the command for real, captured only so
+			 * that the event trigger reports its deparsed form.
+			 */
+			call_next_ProcessUtility(pstmt, queryString, true,
+									 context, params, queryEnv, dest, qc);
+		}
+		else
+		{
+			MemoryContext oldcontext = CurrentMemoryContext;
+			ResourceOwner oldowner = CurrentResourceOwner;
+
+			/*
+			 * Run the trial in an internal subtransaction, following the
+			 * standard idiom (cf. plpgsql): save and restore the caller's
+			 * memory context and resource owner around it, so the replay
+			 * below runs in the same environment the statement arrived in.
+			 */
+			BeginInternalSubTransaction("test_ddl_deparse");
+			MemoryContextSwitchTo(oldcontext);
+			PG_TRY(2);
+			{
+				call_next_ProcessUtility(pstmt, queryString, true,
+										 context, params, queryEnv, dest, qc);
+
+				RollbackAndReleaseCurrentSubTransaction();
+				MemoryContextSwitchTo(oldcontext);
+				CurrentResourceOwner = oldowner;
+			}
+			PG_CATCH(2);
+			{
+				RollbackAndReleaseCurrentSubTransaction();
+				MemoryContextSwitchTo(oldcontext);
+				CurrentResourceOwner = oldowner;
+				PG_RE_THROW();
+			}
+			PG_END_TRY(2);
+
+			/*
+			 * The capture window closes with the trial: whatever DDL runs
+			 * from here on -- the replay itself, or the original command in
+			 * the fallback below -- fires the event trigger again, and must
+			 * not be taken for this statement.
+			 */
+			capture_parsetree = NULL;
+
+			if (captured_json == NULL)
+			{
+				Node	   *stmt = pstmt->utilityStmt;
+				bool		noop;
+
+				/*
+				 * The deparser produced nothing.  The benign case is an IF
+				 * NOT EXISTS command that did nothing (the trial execution
+				 * has already emitted the "skipping" notice), so there is
+				 * nothing to replay; anything else is unexpected, so warn and
+				 * run the original command for real rather than silently
+				 * losing it.  The IsA check matters: once more command types
+				 * are supported, if_not_exists must only be consulted on
+				 * statements that actually have the field.
+				 */
+				noop = (IsA(stmt, CreateStmt) &&
+						((CreateStmt *) stmt)->if_not_exists);
+				if (!noop)
+				{
+					ereport(WARNING,
+							(errmsg("could not deparse DDL command, executing the original"),
+							 errdetail("Command: %s", queryString)));
+					call_next_ProcessUtility(pstmt, queryString, readOnlyTree,
+											 context, params, queryEnv, dest, qc);
+				}
+			}
+			else
+			{
+				ErrorContextCallback errcallback;
+				TestDDLDeparseErrorInfo errinfo;
+				char	   *sql;
+
+				sql = deparse_ddl_json_to_string(captured_json);
+
+				errinfo.orig_ddl = queryString;
+				errinfo.deparsed_ddl = sql;
+
+				errcallback.callback = test_ddl_deparse_error_callback;
+				errcallback.previous = error_context_stack;
+				errcallback.arg = &errinfo;
+				error_context_stack = &errcallback;
+
+				in_roundtrip = true;
+				PG_TRY(2);
+				{
+					execute_deparsed_command(sql, context, params, queryEnv,
+											 dest, qc);
+				}
+				PG_FINALLY(2);
+				{
+					in_roundtrip = false;
+				}
+				PG_END_TRY(2);
+
+				error_context_stack = errcallback.previous;
+			}
+		}
+	}
+	PG_FINALLY();
+	{
+		capture_parsetree = NULL;
+		/* don't leave a dangling pointer into transaction-lifetime memory */
+		captured_json = NULL;
+	}
+	PG_END_TRY();
+}
+
+/*
+ * Report a top-level command that the deparser declined.
+ *
+ * Only statement types deparse_ddl_command() has a case for are reported: for
+ * those, a decline is a gap in the deparser, whereas for anything else it
+ * just means the statement is not this deparser's business.  Note that even
+ * so a report is not by itself a defect -- ALTER FUNCTION ... RENAME is
+ * declined because RENAME is only supported for tables and columns, and that
+ * is by design.  What matters is that the set of reports is visible, so that
+ * one that ought not to be there can be noticed.
+ *
+ * This goes to the server log rather than to the client so that enabling it
+ * does not perturb the output of the regression tests being replayed.
+ */
+static void
+report_declined_command(Node *stmt, const char *queryString)
+{
+	if (!log_declined_commands)
+		return;
+
+	switch (nodeTag(stmt))
+	{
+		case T_CreateStmt:
+		case T_AlterTableStmt:
+		case T_RenameStmt:
+		case T_AlterObjectSchemaStmt:
+			break;
+		default:
+			return;
+	}
+
+	ereport(LOG,
+			(errmsg("test_ddl_deparse: declined %s: %s",
+					CreateCommandName(stmt), queryString),
+			 errhidestmt(true),
+			 errhidecontext(true)));
+}
+
+static void
+test_ddl_deparse_error_callback(void *arg)
+{
+	TestDDLDeparseErrorInfo *errinfo = (TestDDLDeparseErrorInfo *) arg;
+
+	errcontext("original DDL \"%s\", deparsed DDL \"%s\"",
+			   errinfo->orig_ddl,
+			   errinfo->deparsed_ddl ? errinfo->deparsed_ddl : "(none)");
+}
 
 /*
  * Return the textual representation of the struct type used to represent a
-- 
2.55.0

