v11-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CR.patch

text/plain

Filename: v11-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CR.patch
Type: text/plain
Part: 0
Message: Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement

Patch

Format: format-patch
Series: patch v11-0001
Subject: Add pg_get_domain_ddl() function to reconstruct CREATE DOMAIN statements
File+
doc/src/sgml/func/func-info.sgml 30 1
src/backend/utils/adt/ddlutils.c 232 0
src/backend/utils/adt/ruleutils.c 26 3
src/include/catalog/pg_proc.dat 6 0
src/include/utils/ruleutils.h 4 0
src/test/regress/expected/create_property_graph.out 1 0
src/test/regress/expected/create_schema.out 1 1
src/test/regress/expected/domain.out 80 0
src/test/regress/expected/event_trigger.out 7 6
src/test/regress/expected/test_setup.out 73 0
src/test/regress/sql/domain.sql 39 0
src/test/regress/sql/test_setup.sql 75 0
From f990fe27af72695580bbb0c9d284487385143202 Mon Sep 17 00:00:00 2001
From: Florin Irion <florin.irion@enterprisedb.com>
Date: Thu, 18 Sep 2025 18:52:43 +0200
Subject: [PATCH v11] Add pg_get_domain_ddl() function to reconstruct CREATE
 DOMAIN statements
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Reconstructs the CREATE DOMAIN statement for a given domain, including
base type, default, and constraints. NOT VALID constraints are rendered
as separate ALTER DOMAIN statements.

While this patch was in flight, master gained sibling functions
(pg_get_role_ddl(), pg_get_tablespace_ddl(), pg_get_database_ddl()) in
a new ddlutils.c with their own conventions. This version follows suit:
moved into ddlutils.c, converted to SETOF text (one row per statement),
and given named pg_proc.dat arguments instead of a SQL wrapper.

Also fixes two bugs found while aligning with the siblings: the DDL
round-trip test trigger silently dropped all but the first row of a
multi-row result, and an undefined domain OID returned NULL instead of
erroring.

Author: Florin Irion <florin.irion@enterprisedb.com>
Author: Tim Waizenegger <tim.waizenegger@enterprisedb.com>
Reviewed-by: Álvaro Herrera alvherre@alvh.no-ip.org
Reviewed-by: jian he <jian.universality@gmail.com>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: Neil Chen <carpenter.nail.cz@gmail.com>
Reviewed-by: Man Zeng <zengman@halodbtech.com>
Reviewed-by: Haritabh <Gupta haritabh1992@gmail.com>
Reviewed-by: Japin Li <japinli@hotmail.com
Reference: PG-151
---
 doc/src/sgml/func/func-info.sgml              |  31 ++-
 src/backend/utils/adt/ddlutils.c              | 232 ++++++++++++++++++
 src/backend/utils/adt/ruleutils.c             |  29 ++-
 src/include/catalog/pg_proc.dat               |   6 +
 src/include/utils/ruleutils.h                 |   4 +
 .../expected/create_property_graph.out        |   1 +
 src/test/regress/expected/create_schema.out   |   2 +-
 src/test/regress/expected/domain.out          |  80 ++++++
 src/test/regress/expected/event_trigger.out   |  13 +-
 src/test/regress/expected/test_setup.out      |  73 ++++++
 src/test/regress/sql/domain.sql               |  39 +++
 src/test/regress/sql/test_setup.sql           |  75 ++++++
 12 files changed, 574 insertions(+), 11 deletions(-)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 69ef3857cfa..672e681b3ab 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3853,7 +3853,7 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
    <para>
     The functions shown in <xref linkend="functions-get-object-ddl-table"/>
-    reconstruct DDL statements for various global database objects.
+    reconstruct DDL statements for various database objects.
     Each function returns a set of text rows, one SQL statement per row.
     (This is a decompiled reconstruction, not the original text of the
     command.)
@@ -3960,6 +3960,35 @@ 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_domain_ddl</primary>
+        </indexterm>
+        <function>pg_get_domain_ddl</function>
+        ( <parameter>domain</parameter> <type>regtype</type>
+        <optional>, <parameter>pretty</parameter> <type>boolean</type>
+        <literal>DEFAULT</literal> false</optional> )
+        <returnvalue>setof text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <link linkend="sql-createdomain"><command>CREATE
+        DOMAIN</command></link> statement for the specified domain, followed
+        by <link linkend="sql-alterdomain"><command>ALTER DOMAIN ... ADD
+        CONSTRAINT ... NOT VALID</command></link> statements for any
+        constraints that have not been validated.  Each statement is
+        returned as a separate row.  When <parameter>pretty</parameter> is
+        true, the output is pretty-printed.
+       </para>
+       <para>
+        The <parameter>domain</parameter> parameter uses type <type>regtype</type>,
+        which follows the standard <varname>search_path</varname> for type name
+        resolution. If a domain name conflicts with a built-in type name
+        (for example, a domain named <literal>int</literal>), you must use a
+        schema-qualified name (for example, <literal>'public.int'::regtype</literal>)
+        to reference the domain.
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c
index a70f1c28655..0e35ceb7905 100644
--- a/src/backend/utils/adt/ddlutils.c
+++ b/src/backend/utils/adt/ddlutils.c
@@ -24,9 +24,11 @@
 #include "catalog/pg_auth_members.h"
 #include "catalog/pg_authid.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_tablespace.h"
+#include "catalog/pg_type.h"
 #include "commands/tablespace.h"
 #include "common/relpath.h"
 #include "funcapi.h"
@@ -56,6 +58,9 @@ static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner
 static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid);
 static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
 										  bool no_owner, bool no_tablespace);
+static void scan_domain_constraints(Oid domain_oid, List **validcons,
+									List **invalidcons);
+static List *pg_get_domain_ddl_internal(Oid domain_oid, bool pretty);
 
 
 /*
@@ -975,3 +980,230 @@ pg_get_database_ddl(PG_FUNCTION_ARGS)
 		SRF_RETURN_DONE(funcctx);
 	}
 }
+
+/*
+ * scan_domain_constraints
+ *		Split a domain's pg_constraint rows into validated and NOT VALID
+ *		lists, each sorted by OID for stable, deterministic output.
+ */
+static void
+scan_domain_constraints(Oid domain_oid, List **validcons, List **invalidcons)
+{
+	Relation	constraintRel;
+	SysScanDesc sscan;
+	ScanKeyData skey;
+	HeapTuple	constraintTup;
+
+	*validcons = NIL;
+	*invalidcons = NIL;
+
+	constraintRel = table_open(ConstraintRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey,
+				Anum_pg_constraint_contypid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(domain_oid));
+
+	sscan = systable_beginscan(constraintRel,
+							   ConstraintTypidIndexId,
+							   true,
+							   NULL,
+							   1,
+							   &skey);
+
+	while (HeapTupleIsValid(constraintTup = systable_getnext(sscan)))
+	{
+		Form_pg_constraint con = (Form_pg_constraint) GETSTRUCT(constraintTup);
+
+		if (con->convalidated)
+			*validcons = lappend_oid(*validcons, con->oid);
+		else
+			*invalidcons = lappend_oid(*invalidcons, con->oid);
+	}
+
+	systable_endscan(sscan);
+	table_close(constraintRel, AccessShareLock);
+
+	/* Sort constraints by OID for stable output */
+	if (list_length(*validcons) > 1)
+		list_sort(*validcons, list_oid_cmp);
+	if (list_length(*invalidcons) > 1)
+		list_sort(*invalidcons, list_oid_cmp);
+}
+
+/*
+ * pg_get_domain_ddl_internal
+ *		Generate DDL statements to recreate a domain.
+ *
+ * Returns a List of palloc'd strings, each a complete SQL statement.  The
+ * first element is always the CREATE DOMAIN statement, including any
+ * COLLATE/DEFAULT clauses and already-validated CHECK/NOT NULL constraints;
+ * subsequent elements, if any, are one ALTER DOMAIN ... ADD CONSTRAINT ...
+ * NOT VALID statement per constraint that has not yet been validated.
+ */
+static List *
+pg_get_domain_ddl_internal(Oid domain_oid, bool pretty)
+{
+	HeapTuple	typeTuple;
+	Form_pg_type typForm;
+	Node	   *defaultExpr;
+	List	   *validConstraints;
+	List	   *invalidConstraints;
+	StringInfoData buf;
+	List	   *statements = NIL;
+
+	typeTuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(domain_oid));
+	if (!HeapTupleIsValid(typeTuple))
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("domain with OID %u does not exist", domain_oid)));
+
+	typForm = (Form_pg_type) GETSTRUCT(typeTuple);
+
+	if (typForm->typtype != TYPTYPE_DOMAIN)
+	{
+		ReleaseSysCache(typeTuple);
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("\"%s\" is not a domain", format_type_be(domain_oid)),
+				 errhint("Use a schema-qualified name if the domain name conflicts with a built-in name.")));
+	}
+
+	defaultExpr = get_typdefault(domain_oid);
+	scan_domain_constraints(domain_oid, &validConstraints, &invalidConstraints);
+
+	initStringInfo(&buf);
+
+	appendStringInfo(&buf, "CREATE DOMAIN %s AS %s",
+					 generate_qualified_type_name(typForm->oid),
+					 format_type_extended(typForm->typbasetype,
+										  typForm->typtypmod,
+										  FORMAT_TYPE_TYPEMOD_GIVEN |
+										  FORMAT_TYPE_FORCE_QUALIFY));
+
+	/* only show COLLATE if it differs from the base type's collation */
+	if (OidIsValid(typForm->typcollation))
+	{
+		HeapTuple	baseTypeTuple = SearchSysCache1(TYPEOID,
+													ObjectIdGetDatum(typForm->typbasetype));
+		Oid			baseCollation = InvalidOid;
+
+		if (HeapTupleIsValid(baseTypeTuple))
+		{
+			baseCollation = ((Form_pg_type) GETSTRUCT(baseTypeTuple))->typcollation;
+			ReleaseSysCache(baseTypeTuple);
+		}
+
+		if (typForm->typcollation != baseCollation)
+			append_ddl_option(&buf, pretty, 4, "COLLATE %s",
+							  generate_collation_name(typForm->typcollation));
+	}
+
+	if (defaultExpr != NULL)
+	{
+		char	   *defaultValue = deparse_expression_for_ddl(defaultExpr, pretty);
+
+		append_ddl_option(&buf, pretty, 4, "DEFAULT %s", defaultValue);
+	}
+
+	foreach_oid(constraintOid, validConstraints)
+	{
+		HeapTuple	constraintTup;
+		Form_pg_constraint con;
+		char	   *constraintDef;
+
+		constraintTup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constraintOid));
+		if (!HeapTupleIsValid(constraintTup))
+			continue;			/* constraint was dropped concurrently */
+
+		con = (Form_pg_constraint) GETSTRUCT(constraintTup);
+		constraintDef = pg_get_constraintdef_for_ddl(constraintOid, false,
+													 pretty, true);
+
+		if (constraintDef == NULL)
+		{
+			ReleaseSysCache(constraintTup);
+			continue;			/* constraint was dropped concurrently */
+		}
+
+		append_ddl_option(&buf, pretty, 4, "CONSTRAINT %s",
+						  quote_identifier(NameStr(con->conname)));
+		append_ddl_option(&buf, pretty, 8, "%s", constraintDef);
+
+		ReleaseSysCache(constraintTup);
+	}
+
+	appendStringInfoChar(&buf, ';');
+	statements = lappend(statements, pstrdup(buf.data));
+
+	/* each NOT VALID constraint is its own ALTER DOMAIN row */
+	foreach_oid(constraintOid, invalidConstraints)
+	{
+		char	   *alterStmt = pg_get_constraintdef_for_ddl(constraintOid, true,
+															 pretty, true);
+
+		if (alterStmt == NULL)
+			continue;			/* constraint was dropped concurrently */
+
+		resetStringInfo(&buf);
+		appendStringInfo(&buf, "%s;", alterStmt);
+		statements = lappend(statements, pstrdup(buf.data));
+	}
+
+	list_free(validConstraints);
+	list_free(invalidConstraints);
+	pfree(buf.data);
+	ReleaseSysCache(typeTuple);
+
+	return statements;
+}
+
+/*
+ * pg_get_domain_ddl
+ *		Return DDL to recreate a domain as a set of text rows.
+ *
+ * Each row is a complete SQL statement.  The first row is always the
+ * CREATE DOMAIN statement; subsequent rows, if any, are ALTER DOMAIN ...
+ * ADD CONSTRAINT ... NOT VALID statements for constraints that have not
+ * been validated.
+ */
+Datum
+pg_get_domain_ddl(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	List	   *statements;
+
+	if (SRF_IS_FIRSTCALL())
+	{
+		MemoryContext oldcontext;
+		Oid			domain_oid;
+		bool		pretty;
+
+		funcctx = SRF_FIRSTCALL_INIT();
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+		domain_oid = PG_GETARG_OID(0);
+		pretty = PG_GETARG_BOOL(1);
+
+		statements = pg_get_domain_ddl_internal(domain_oid, pretty);
+		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 = (char *) list_nth(statements, funcctx->call_cntr);
+
+		SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
+	}
+	else
+	{
+		list_free_deep(statements);
+		SRF_RETURN_DONE(funcctx);
+	}
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 88de5c0481c..6630bf48f54 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -99,6 +99,11 @@
 	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
 	 : PRETTYFLAG_INDENT)
 
+/* Conversion of "bool pretty" option for DDL statements (0 when false) */
+#define GET_DDL_PRETTY_FLAGS(pretty) \
+	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
+	 : 0)
+
 /* Default line length for pretty-print wrapping: 0 means wrap always */
 #define WRAP_COLUMN_DEFAULT		0
 
@@ -544,7 +549,6 @@ static char *generate_function_name(Oid funcid, int nargs,
 									bool inGroupBy);
 static char *generate_operator_name(Oid operid, Oid arg1, Oid arg2);
 static void add_cast_to(StringInfo buf, Oid typid);
-static char *generate_qualified_type_name(Oid typid);
 static text *string_to_text(char *str);
 static char *flatten_reloptions(Oid relid);
 void		get_reloptions(StringInfo buf, Datum reloptions);
@@ -557,7 +561,6 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
 										  deparse_context *context,
 										  bool showimplicit,
 										  bool needcomma);
-
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
 
@@ -14162,7 +14165,7 @@ add_cast_to(StringInfo buf, Oid typid)
  * SQL-standard type names ... although in current usage, this should
  * only get used for domains, so such cases wouldn't occur anyway.
  */
-static char *
+char *
 generate_qualified_type_name(Oid typid)
 {
 	HeapTuple	tp;
@@ -14364,3 +14367,23 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * Cross-file wrappers exposing this file's PRETTYFLAG_*-based deparsing to
+ * external callers that only know a plain "pretty" bool.
+ */
+char *
+pg_get_constraintdef_for_ddl(Oid constraintId, bool fullCommand,
+							 bool pretty, bool missing_ok)
+{
+	return pg_get_constraintdef_worker(constraintId, fullCommand,
+									   GET_DDL_PRETTY_FLAGS(pretty),
+									   missing_ok);
+}
+
+char *
+deparse_expression_for_ddl(Node *expr, bool pretty)
+{
+	return deparse_expression_pretty(expr, NIL, false, false,
+									 GET_DDL_PRETTY_FLAGS(pretty), 0);
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3cb84359adf..9468f7eb830 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8634,6 +8634,12 @@
   proargtypes => 'regdatabase bool bool bool',
   proargnames => '{database,pretty,owner,tablespace}',
   proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' },
+{ oid => '8024', descr => 'get DDL to recreate a domain',
+  proname => 'pg_get_domain_ddl', prorows => '10', proretset => 't',
+  provolatile => 's', pronargdefaults => '1', prorettype => 'text',
+  proargtypes => 'regtype bool',
+  proargnames => '{domain,pretty}', proargdefaults => '{false}',
+  prosrc => 'pg_get_domain_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/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 25c05e2f649..2bc0f3495c1 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -34,8 +34,11 @@ 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_for_ddl(Oid constraintId, bool fullCommand,
+										  bool pretty, bool missing_ok);
 extern char *deparse_expression(Node *expr, List *dpcontext,
 								bool forceprefix, bool showimplicit);
+extern char *deparse_expression_for_ddl(Node *expr, bool pretty);
 extern List *deparse_context_for(const char *aliasname, Oid relid);
 extern List *deparse_context_for_plan_tree(PlannedStmt *pstmt,
 										   List *rtable_names);
@@ -50,6 +53,7 @@ extern char *get_window_frame_options_for_explain(int frameOptions,
 												  bool forceprefix);
 extern char *generate_collation_name(Oid collid);
 extern char *generate_opclass_name(Oid opclass);
+extern char *generate_qualified_type_name(Oid typid);
 extern char *get_range_partbound_string(List *bound_datums);
 extern void get_reloptions(StringInfo buf, Datum reloptions);
 
diff --git a/src/test/regress/expected/create_property_graph.out b/src/test/regress/expected/create_property_graph.out
index 2f06c7ce5a8..77599f00331 100644
--- a/src/test/regress/expected/create_property_graph.out
+++ b/src/test/regress/expected/create_property_graph.out
@@ -220,6 +220,7 @@ ERROR:  mismatching number of properties in definition of label "t3l1"
 ALTER PROPERTY GRAPH g1 OWNER TO regress_graph_user1;
 SET ROLE regress_graph_user1;
 GRANT SELECT ON PROPERTY GRAPH g1 TO regress_graph_user2;
+WARNING:  regress_ddl_roundtrip_trigger_func: skipping DDL round-trip check (XX000): unsupported object type: 30
 GRANT UPDATE ON PROPERTY GRAPH g1 TO regress_graph_user2;  -- fail
 ERROR:  invalid privilege type UPDATE for property graph
 RESET ROLE;
diff --git a/src/test/regress/expected/create_schema.out b/src/test/regress/expected/create_schema.out
index b9ae4c402fd..f45845c3249 100644
--- a/src/test/regress/expected/create_schema.out
+++ b/src/test/regress/expected/create_schema.out
@@ -307,7 +307,6 @@ DROP SCHEMA regress_schema_misc CASCADE;
 NOTICE:  drop cascades to 17 other objects
 DETAIL:  drop cascades to function regress_schema_misc.cs_sum(integer)
 drop cascades to collation regress_schema_misc.cs_builtin_c
-drop cascades to type regress_schema_misc.cs_positive
 drop cascades to function regress_schema_misc.cs_add(integer,integer)
 drop cascades to operator regress_schema_misc.+(integer,integer)
 drop cascades to function regress_schema_misc.cs_proc(integer,integer)
@@ -322,5 +321,6 @@ drop cascades to type regress_schema_misc.cs_range
 drop cascades to function regress_schema_misc.cs_type_out(regress_schema_misc.cs_type)
 drop cascades to type regress_schema_misc.cs_type
 drop cascades to function regress_schema_misc.cs_type_in(cstring)
+drop cascades to type regress_schema_misc.cs_positive
 -- Clean up
 DROP ROLE regress_create_schema_role;
diff --git a/src/test/regress/expected/domain.out b/src/test/regress/expected/domain.out
index 62a48a523a2..0b169244827 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -1378,6 +1378,86 @@ LINE 1: ...m ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORC...
                                                              ^
 DROP DOMAIN constraint_enforced_dom;
 --
+-- pg_get_domain_ddl
+--
+-- Pretty output for a comprehensive domain (DEFAULT + NOT NULL + multiple CHECKs)
+CREATE DOMAIN regress_ddl_comprehensive AS varchar(50)
+    NOT NULL
+    DEFAULT 'hello'
+    CHECK (LENGTH(VALUE) >= 3)
+    CHECK (VALUE !~ '^\s*$');
+SELECT pg_get_domain_ddl('regress_ddl_comprehensive', pretty => true);
+                            pg_get_domain_ddl                            
+-------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_ddl_comprehensive AS character varying(50)+
+     DEFAULT 'hello'::character varying                                 +
+     CONSTRAINT regress_ddl_comprehensive_not_null                      +
+         NOT NULL                                                       +
+     CONSTRAINT regress_ddl_comprehensive_check                         +
+         CHECK (length(VALUE::text) >= 3)                               +
+     CONSTRAINT regress_ddl_comprehensive_check1                        +
+         CHECK (VALUE::text !~ '^\s*$'::text);
+(1 row)
+
+DROP DOMAIN regress_ddl_comprehensive;
+-- Quoted and special identifiers
+CREATE DOMAIN "regress_domain with space" AS int
+    CONSTRAINT "regress_Constraint A" CHECK (VALUE < 100)
+    CONSTRAINT "regress_Constraint B" CHECK (VALUE > 10);
+SELECT pg_get_domain_ddl('"regress_domain with space"', pretty => true);
+                      pg_get_domain_ddl                      
+-------------------------------------------------------------
+ CREATE DOMAIN public."regress_domain with space" AS integer+
+     CONSTRAINT "regress_Constraint A"                      +
+         CHECK (VALUE < 100)                                +
+     CONSTRAINT "regress_Constraint B"                      +
+         CHECK (VALUE > 10);
+(1 row)
+
+DROP DOMAIN "regress_domain with space";
+-- NOT VALID constraint rendering (requires ALTER DOMAIN, not CREATE:
+-- CREATE DOMAIN constraints are always validated)
+CREATE DOMAIN regress_ddl_notvalid AS int;
+ALTER DOMAIN regress_ddl_notvalid ADD CONSTRAINT check_positive CHECK (VALUE > 0) NOT VALID;
+SELECT pg_get_domain_ddl('regress_ddl_notvalid', pretty => true);
+                                          pg_get_domain_ddl                                          
+-----------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_ddl_notvalid AS integer;
+ ALTER DOMAIN public.regress_ddl_notvalid ADD CONSTRAINT check_positive CHECK (VALUE > 0) NOT VALID;
+(2 rows)
+
+DROP DOMAIN regress_ddl_notvalid;
+-- Domain shadowing a built-in type name
+CREATE DOMAIN public.int AS pg_catalog.int4;
+SELECT pg_get_domain_ddl('int');  -- should fail
+ERROR:  "integer" is not a domain
+HINT:  Use a schema-qualified name if the domain name conflicts with a built-in name.
+SELECT pg_get_domain_ddl('public.int');
+           pg_get_domain_ddl            
+----------------------------------------
+ CREATE DOMAIN public."int" AS integer;
+(1 row)
+
+DROP DOMAIN public.int;
+-- Error cases
+SELECT pg_get_domain_ddl('nonexistent_domain_type'::regtype);  -- should fail
+ERROR:  type "nonexistent_domain_type" does not exist
+LINE 1: SELECT pg_get_domain_ddl('nonexistent_domain_type'::regtype)...
+                                 ^
+SELECT pg_get_domain_ddl(999999::regtype);  -- should fail - undefined OID
+ERROR:  domain with OID 999999 does not exist
+SELECT pg_get_domain_ddl(NULL);  -- strict SRF: NULL argument yields no rows
+ pg_get_domain_ddl 
+-------------------
+(0 rows)
+
+SELECT pg_get_domain_ddl('pg_class');  -- should fail - not a domain
+ERROR:  "pg_class" is not a domain
+HINT:  Use a schema-qualified name if the domain name conflicts with a built-in name.
+SELECT pg_get_domain_ddl('integer');  -- should fail - not a domain
+ERROR:  "integer" is not a domain
+HINT:  Use a schema-qualified name if the domain name conflicts with a built-in name.
+--
 -- Information schema
 --
 SELECT * FROM information_schema.column_domain_usage
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 86ae50ce531..e96fe6f45b7 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -815,12 +815,13 @@ SELECT
     LATERAL pg_identify_object_as_address('pg_event_trigger'::regclass, e.oid, 0) as b,
     LATERAL pg_get_object_address(b.type, b.object_names, b.object_args) as a
   ORDER BY e.evtname;
-      evtname      |              descr              |     type      |    object_names     | object_args |                         ident                          
--------------------+---------------------------------+---------------+---------------------+-------------+--------------------------------------------------------
- end_rls_command   | event trigger end_rls_command   | event trigger | {end_rls_command}   | {}          | ("event trigger",,end_rls_command,end_rls_command)
- sql_drop_command  | event trigger sql_drop_command  | event trigger | {sql_drop_command}  | {}          | ("event trigger",,sql_drop_command,sql_drop_command)
- start_rls_command | event trigger start_rls_command | event trigger | {start_rls_command} | {}          | ("event trigger",,start_rls_command,start_rls_command)
-(3 rows)
+            evtname            |                    descr                    |     type      |          object_names           | object_args |                                     ident                                      
+-------------------------------+---------------------------------------------+---------------+---------------------------------+-------------+--------------------------------------------------------------------------------
+ end_rls_command               | event trigger end_rls_command               | event trigger | {end_rls_command}               | {}          | ("event trigger",,end_rls_command,end_rls_command)
+ regress_ddl_roundtrip_trigger | event trigger regress_ddl_roundtrip_trigger | event trigger | {regress_ddl_roundtrip_trigger} | {}          | ("event trigger",,regress_ddl_roundtrip_trigger,regress_ddl_roundtrip_trigger)
+ sql_drop_command              | event trigger sql_drop_command              | event trigger | {sql_drop_command}              | {}          | ("event trigger",,sql_drop_command,sql_drop_command)
+ start_rls_command             | event trigger start_rls_command             | event trigger | {start_rls_command}             | {}          | ("event trigger",,start_rls_command,start_rls_command)
+(4 rows)
 
 DROP EVENT TRIGGER start_rls_command;
 DROP EVENT TRIGGER end_rls_command;
diff --git a/src/test/regress/expected/test_setup.out b/src/test/regress/expected/test_setup.out
index 93a4c2691c1..9aa9aea3cad 100644
--- a/src/test/regress/expected/test_setup.out
+++ b/src/test/regress/expected/test_setup.out
@@ -235,3 +235,76 @@ create function fipshash(text)
     returns text
     strict immutable parallel safe leakproof
     return substr(encode(sha256($1::bytea), 'hex'), 1, 32);
+--
+-- DDL round-trip verification infrastructure.
+-- An event trigger that automatically verifies pg_get_<type>_ddl() for every
+-- CREATE command that has a matching reconstruction function.  Runs inline at
+-- creation time so even objects that are later dropped get tested.
+--
+CREATE FUNCTION regress_ddl_roundtrip_trigger_func() RETURNS event_trigger
+LANGUAGE plpgsql AS $$
+DECLARE
+    r           RECORD;
+    obj_type    text;
+    original    text;
+    recreated   text;
+BEGIN
+    -- Recursion guard: the recreate step fires this trigger again.
+    IF current_setting('regress.ddl_roundtrip_in_progress', true) = 'true' THEN
+        RETURN;
+    END IF;
+
+    -- pg_event_trigger_ddl_commands() can raise before yielding a row (e.g.
+    -- an unsupported object type), so the whole loop -- not just each row --
+    -- must be inside the handler; a gap on one row of a multi-row event
+    -- therefore also discards verification already done for earlier rows in
+    -- that event. Accepted trade-off for this test convenience. A genuine
+    -- round-trip mismatch (SQLSTATE P0004) still propagates.
+    BEGIN
+        FOR r IN SELECT * FROM pg_event_trigger_ddl_commands()
+        LOOP
+            IF r.command_tag LIKE 'CREATE %' THEN
+                obj_type := lower(substring(r.command_tag from 'CREATE (.*)'));
+
+                IF EXISTS (
+                    SELECT 1 FROM pg_proc
+                    WHERE proname  = format('pg_get_%s_ddl', obj_type)
+                      AND pronamespace = 'pg_catalog'::regnamespace
+                ) THEN
+                    PERFORM set_config('regress.ddl_roundtrip_in_progress', 'true', true);
+
+                    -- pg_get_<type>_ddl() is SETOF text; aggregate all rows
+                    -- in emission order instead of only capturing the first.
+                    EXECUTE format(
+                        'SELECT string_agg(ddl, E''\n'' ORDER BY ord)
+                           FROM pg_get_%s_ddl(%L) WITH ORDINALITY AS t(ddl, ord)',
+                        obj_type, r.object_identity)
+                        INTO original;
+                    EXECUTE format('DROP %s %s', obj_type, r.object_identity);
+                    EXECUTE original;
+                    EXECUTE format(
+                        'SELECT string_agg(ddl, E''\n'' ORDER BY ord)
+                           FROM pg_get_%s_ddl(%L) WITH ORDINALITY AS t(ddl, ord)',
+                        obj_type, r.object_identity)
+                        INTO recreated;
+
+                    ASSERT original = recreated,
+                        format(E'DDL round-trip mismatch for %s %s:\n  original:  %s\n  recreated: %s',
+                               obj_type, r.object_identity, original, recreated);
+
+                    PERFORM set_config('regress.ddl_roundtrip_in_progress', 'false', true);
+                END IF;
+            END IF;
+        END LOOP;
+    EXCEPTION
+        WHEN OTHERS THEN
+            IF SQLSTATE = 'P0004' THEN
+                RAISE;
+            END IF;
+            RAISE WARNING 'regress_ddl_roundtrip_trigger_func: skipping DDL round-trip check (%): %',
+                SQLSTATE, SQLERRM;
+    END;
+END;
+$$;
+CREATE EVENT TRIGGER regress_ddl_roundtrip_trigger ON ddl_command_end
+    EXECUTE FUNCTION regress_ddl_roundtrip_trigger_func();
diff --git a/src/test/regress/sql/domain.sql b/src/test/regress/sql/domain.sql
index b8f5a639712..89231828ef5 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -894,6 +894,45 @@ ALTER DOMAIN constraint_enforced_dom ADD CONSTRAINT the_constraint CHECK (value
 ALTER DOMAIN constraint_enforced_dom ADD CONSTRAINT the_constraint CHECK (value > 0) NOT ENFORCED;
 DROP DOMAIN constraint_enforced_dom;
 
+--
+-- pg_get_domain_ddl
+--
+-- Pretty output for a comprehensive domain (DEFAULT + NOT NULL + multiple CHECKs)
+CREATE DOMAIN regress_ddl_comprehensive AS varchar(50)
+    NOT NULL
+    DEFAULT 'hello'
+    CHECK (LENGTH(VALUE) >= 3)
+    CHECK (VALUE !~ '^\s*$');
+SELECT pg_get_domain_ddl('regress_ddl_comprehensive', pretty => true);
+DROP DOMAIN regress_ddl_comprehensive;
+
+-- Quoted and special identifiers
+CREATE DOMAIN "regress_domain with space" AS int
+    CONSTRAINT "regress_Constraint A" CHECK (VALUE < 100)
+    CONSTRAINT "regress_Constraint B" CHECK (VALUE > 10);
+SELECT pg_get_domain_ddl('"regress_domain with space"', pretty => true);
+DROP DOMAIN "regress_domain with space";
+
+-- NOT VALID constraint rendering (requires ALTER DOMAIN, not CREATE:
+-- CREATE DOMAIN constraints are always validated)
+CREATE DOMAIN regress_ddl_notvalid AS int;
+ALTER DOMAIN regress_ddl_notvalid ADD CONSTRAINT check_positive CHECK (VALUE > 0) NOT VALID;
+SELECT pg_get_domain_ddl('regress_ddl_notvalid', pretty => true);
+DROP DOMAIN regress_ddl_notvalid;
+
+-- Domain shadowing a built-in type name
+CREATE DOMAIN public.int AS pg_catalog.int4;
+SELECT pg_get_domain_ddl('int');  -- should fail
+SELECT pg_get_domain_ddl('public.int');
+DROP DOMAIN public.int;
+
+-- Error cases
+SELECT pg_get_domain_ddl('nonexistent_domain_type'::regtype);  -- should fail
+SELECT pg_get_domain_ddl(999999::regtype);  -- should fail - undefined OID
+SELECT pg_get_domain_ddl(NULL);  -- strict SRF: NULL argument yields no rows
+SELECT pg_get_domain_ddl('pg_class');  -- should fail - not a domain
+SELECT pg_get_domain_ddl('integer');  -- should fail - not a domain
+
 --
 -- Information schema
 --
diff --git a/src/test/regress/sql/test_setup.sql b/src/test/regress/sql/test_setup.sql
index 5854399a028..741f1a0ce68 100644
--- a/src/test/regress/sql/test_setup.sql
+++ b/src/test/regress/sql/test_setup.sql
@@ -289,3 +289,78 @@ create function fipshash(text)
     returns text
     strict immutable parallel safe leakproof
     return substr(encode(sha256($1::bytea), 'hex'), 1, 32);
+
+--
+-- DDL round-trip verification infrastructure.
+-- An event trigger that automatically verifies pg_get_<type>_ddl() for every
+-- CREATE command that has a matching reconstruction function.  Runs inline at
+-- creation time so even objects that are later dropped get tested.
+--
+CREATE FUNCTION regress_ddl_roundtrip_trigger_func() RETURNS event_trigger
+LANGUAGE plpgsql AS $$
+DECLARE
+    r           RECORD;
+    obj_type    text;
+    original    text;
+    recreated   text;
+BEGIN
+    -- Recursion guard: the recreate step fires this trigger again.
+    IF current_setting('regress.ddl_roundtrip_in_progress', true) = 'true' THEN
+        RETURN;
+    END IF;
+
+    -- pg_event_trigger_ddl_commands() can raise before yielding a row (e.g.
+    -- an unsupported object type), so the whole loop -- not just each row --
+    -- must be inside the handler; a gap on one row of a multi-row event
+    -- therefore also discards verification already done for earlier rows in
+    -- that event. Accepted trade-off for this test convenience. A genuine
+    -- round-trip mismatch (SQLSTATE P0004) still propagates.
+    BEGIN
+        FOR r IN SELECT * FROM pg_event_trigger_ddl_commands()
+        LOOP
+            IF r.command_tag LIKE 'CREATE %' THEN
+                obj_type := lower(substring(r.command_tag from 'CREATE (.*)'));
+
+                IF EXISTS (
+                    SELECT 1 FROM pg_proc
+                    WHERE proname  = format('pg_get_%s_ddl', obj_type)
+                      AND pronamespace = 'pg_catalog'::regnamespace
+                ) THEN
+                    PERFORM set_config('regress.ddl_roundtrip_in_progress', 'true', true);
+
+                    -- pg_get_<type>_ddl() is SETOF text; aggregate all rows
+                    -- in emission order instead of only capturing the first.
+                    EXECUTE format(
+                        'SELECT string_agg(ddl, E''\n'' ORDER BY ord)
+                           FROM pg_get_%s_ddl(%L) WITH ORDINALITY AS t(ddl, ord)',
+                        obj_type, r.object_identity)
+                        INTO original;
+                    EXECUTE format('DROP %s %s', obj_type, r.object_identity);
+                    EXECUTE original;
+                    EXECUTE format(
+                        'SELECT string_agg(ddl, E''\n'' ORDER BY ord)
+                           FROM pg_get_%s_ddl(%L) WITH ORDINALITY AS t(ddl, ord)',
+                        obj_type, r.object_identity)
+                        INTO recreated;
+
+                    ASSERT original = recreated,
+                        format(E'DDL round-trip mismatch for %s %s:\n  original:  %s\n  recreated: %s',
+                               obj_type, r.object_identity, original, recreated);
+
+                    PERFORM set_config('regress.ddl_roundtrip_in_progress', 'false', true);
+                END IF;
+            END IF;
+        END LOOP;
+    EXCEPTION
+        WHEN OTHERS THEN
+            IF SQLSTATE = 'P0004' THEN
+                RAISE;
+            END IF;
+            RAISE WARNING 'regress_ddl_roundtrip_trigger_func: skipping DDL round-trip check (%): %',
+                SQLSTATE, SQLERRM;
+    END;
+END;
+$$;
+
+CREATE EVENT TRIGGER regress_ddl_roundtrip_trigger ON ddl_command_end
+    EXECUTE FUNCTION regress_ddl_roundtrip_trigger_func();
-- 
2.45.1