Subject: Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement

From: Tom Lane <[email-masked]>
Date: Wed, 18 Feb 2026 19:10:19 -0500

Haritabh Gupta <[email-masked]> writes:
> Thanks for addressing the comments. I tested v7 and found that 
> type modifiers (typmod) are lost in the base type output.

This report crystallized something that's been bothering me
about not only pg_get_domain_ddl() but all the similar patches
that are in the queue.  They are adding a large amount of new
code that will have to be kept in sync with behavior elsewhere,
and there is basically zero forcing function to ensure that
that happens.  Even the rather-overly-voluminous test cases
proposed for the functions cannot catch errors of omission,
especially not future errors of omission.

I don't really know what to do about this, but I don't like the
implementation approach that's being proposed.  I think it's
loading too much development effort and future maintenance effort
onto us in comparison to the expected benefit of having these
functions.

			regards, tom lane

---

From: Haritabh Gupta <[email-masked]>
Date: Thu, 19 Feb 2026 00:40:20 +0000

Hi,

Tom Lane <[email-masked]> writes:
> They are adding a large amount of new code that will have to be
> kept in sync with behavior elsewhere, and there is basically zero
> forcing function to ensure that that happens.

Agree. For the sake of completeness I did a thorough pass over the
rest of v7 and found a few more issues. Documenting them here so 
they're on the record regardless of where the broader discussion 
about the approach lands.

1) get_formatted_string silently drops large formatted strings

+   va_start(args, fmt);
+   appendStringInfoVA(buf, fmt, args);
+   va_end(args);

appendStringInfoVA returns non-zero when the buffer is too small,
requiring enlargeStringInfo + retry (see appendStringInfo in
stringinfo.c). The return value is ignored here, so large
formatted text is silently lost.

Reproduction -- a domain with a ~2647-char CHECK expression:

  DO $$
  DECLARE long_check text;
  BEGIN
      long_check := 'CHECK (';
      FOR i IN 1..50 LOOP
          IF i > 1 THEN long_check := long_check || ' OR '; END IF;
          long_check := long_check || format(
              'VALUE ~ ''^pattern_%s_[a-zA-Z0-9]{10,20}$''', i);
      END LOOP;
      long_check := long_check || ')';
      EXECUTE format(
          'CREATE DOMAIN huge_domain AS text CONSTRAINT big_check %s',
          long_check);
  END $$;

  select pg_get_domain_ddl('huge_domain');
   CREATE DOMAIN public.huge_domain AS pg_catalog.text CONSTRAINT big_check ;
  (1 row)

The entire CHECK clause (~2647 chars) is silently dropped.

This function was adopted from the pg_get_policy_ddl patch [1].
I checked v8 there and confirmed the same bug exists.

2) Function is VOLATILE PARALLEL UNSAFE

pg_proc.dat is missing provolatile => 's', and system_functions.sql
does not specify STABLE PARALLEL SAFE. Every other pg_get_*def
function is STABLE PARALLEL SAFE:

  select proname, provolatile, proparallel from pg_proc
  where proname in ('pg_get_domain_ddl','pg_get_constraintdef',
    'pg_get_functiondef','pg_get_triggerdef');

   pg_get_constraintdef | s | s
   pg_get_domain_ddl    | v | u   <--
   pg_get_functiondef   | s | s
   pg_get_triggerdef    | s | s

Same issue in pg_get_policy_ddl v8 [1].

3) Internal type names exposed (related to typmod bug)

generate_qualified_type_name also uses the raw pg_type.typname,
so beyond losing modifiers:

  int[]        -> pg_catalog._int4       (should be integer[])
  char(5)      -> pg_catalog.bpchar      (should be character(5))
  timestamp(6) -> pg_catalog."timestamp" (should be timestamp(6) without time zone)

The format_type_extended fix from my earlier message resolves
this too. Several test expectations in object_ddl.out will need
updating once fixed.

Regards,
Haritabh

[1] https://www.postgresql.org/message-id/flat/CANxoLDdJsRJqnjMXV3yjsk07Z5iRWxG-c2hZJC7bAKqf8ZXj_A%40mail.gmail.com

---

From: Andrew Dunstan <[email-masked]>
Date: Fri, 20 Feb 2026 08:24:25 -0500

> onto us in comparison to the expected benefit of having these

Do you have an alternative suggestion? We could create an extension, but 
keeping that in sync might in fact be harder, and we know from 
experience that extensions are not universally available. That would 
make leveraging these functions for something like Matheus Alcantara's 
schema cloning proposal (as I think Alvaro suggested) pretty much 
impossible.

I'm not sure how much maintenance effort you think will be needed. We 
don't change the shape of database objects all that often.

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

---

From: Florin Irion <[email-masked]>
Date: Mon, 2 Mar 2026 17:41:41 +0100

Hi Haritabh, Tom,

Thanks for the thorough review. v8 fixes all reported bugs and adds
round-trip tests to address the forcing-function concern.

Haritabh's bugs — all fixed in attached v8.

On 19/02/26 01:10, Tom Lane wrote:

> code that will have to be kept in sync with behavior elsewhere

v8 adds a PL/pgSQL round-trip harness that captures DDL, drops
the domain, re-executes the DDL, and ASSERTs the regenerated DDL
is identical, any suggestions on how to improve it are welcomed.
This function can be re-used also with other get_<object>_ddl
as it accepts a parameter for the <object_type>, this way we can
use some common code.

> onto us in comparison to the expected benefit of having these

I understand your point that there are multiple implementations
and each have its own way of doing it. I think we should start
somewhere and eventually ask further implementations to adapt to
use common code or make it work with all existing (at that point)
implementations, one at a time.
pg_get_domain_ddl implementation uses mainly common code in ruleutils.c
plus some glue code. We could maybe also create a new separate
module and put all the code for all these features there.

What do you think?

Cheers,
Florin

-- 
*Florin Irion*
www.enterprisedb.com <https://www.enterprisedb.com>
From 8e3683a199595ac6100eabd459ab9cbcc858f0ab Mon Sep 17 00:00:00 2001
From: Florin Irion <[email-masked]>
Date: Thu, 18 Sep 2025 18:52:43 +0200
Subject: [PATCH v8] 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

This patch introduces a new system function pg_get_domain_ddl() that
reconstructs the CREATE DOMAIN statement for a given domain. The function
takes a regtype parameter and returns the complete DDL statement including
the domain name, base type, default value, and all associated constraints.

The function follows the same pattern as other DDL reconstruction functions
like pg_get_functiondef() and pg_get_constraintdef(), providing a
decompiled reconstruction rather than the original command text.

Key features:
* Supports domains with default values
* Includes all domain constraints (CHECK, NOT NULL)
* NOT VALID constraint are handled with an extra ALTER command.
* Properly quotes identifiers and schema names
* Handles complex constraint expressions
* pretty printing support
* warn against conflicting built-in names
* Uses GET_DDL_PRETTY_FLAGS macro for consistent pretty-printing behavior

A new documentation section "Get Object DDL Functions" has been created
to group DDL reconstruction functions, starting with pg_get_domain_ddl().
This provides a foundation for future DDL functions for other object types.

Comprehensive regression tests are included covering various domain
configurations. In a separate file sql/object_ddl.sql where more
"get_object_ddl" functions can be tested in the future.
Round-trip regression tests that drop and recreate every test domain
from its own DDL output, asserting the regenerated DDL is identical.
The reusable harness (regress_verify_ddl_roundtrip) is parameterized
by object type, so future pg_get_*_ddl() functions can reuse it.

Reference: PG-151
Author: Florin Irion <[email-masked]>
Author: Tim Waizenegger <[email-masked]>
Reviewed-by: Álvaro Herrera [email-masked]
Reviewed-by: jian he <[email-masked]>
Reviewed-by: Chao Li <[email-masked]>
Reviewed-by: Neil Chen <[email-masked]>
Reviewed-by: Man Zeng <[email-masked]>
Reviewed-by: Haritabh <Gupta [email-masked]>
---
 doc/src/sgml/func/func-info.sgml         |  53 +++
 src/backend/catalog/system_functions.sql |   7 +
 src/backend/utils/adt/ruleutils.c        | 260 +++++++++++++
 src/include/catalog/pg_proc.dat          |   3 +
 src/test/regress/expected/object_ddl.out | 461 +++++++++++++++++++++++
 src/test/regress/parallel_schedule       |   2 +-
 src/test/regress/sql/object_ddl.sql      | 180 +++++++++
 7 files changed, 965 insertions(+), 1 deletion(-)
 create mode 100644 src/test/regress/expected/object_ddl.out
 create mode 100644 src/test/regress/sql/object_ddl.sql

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 294f45e82a3..eb128fede09 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3845,4 +3845,57 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+    print the DDL statements for various database objects.
+    (This is a decompiled reconstruction, not the original text
+    of the command.)
+   </para>
+
+   <table id="functions-get-object-ddl-table">
+    <title>Get Object DDL Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <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> </optional>)
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the creating command for a domain.
+        The result is a complete <command>CREATE DOMAIN</command> statement.
+       </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>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 69699f8830a..f4829cc7765 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -378,6 +378,13 @@ BEGIN ATOMIC
 END;

+CREATE OR REPLACE FUNCTION
+ pg_get_domain_ddl(domain_name regtype, pretty bool DEFAULT false)
+ RETURNS text
+ LANGUAGE internal
+ STABLE PARALLEL SAFE
+AS 'pg_get_domain_ddl_ext';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index f16f1535785..45e04bcf15b 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -94,6 +94,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
 
@@ -547,6 +552,11 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
 										  deparse_context *context,
 										  bool showimplicit,
 										  bool needcomma);
+static void get_formatted_string(StringInfo buf,
+								 int prettyFlags,
+								 int noOfTabChars,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char *pg_get_domain_ddl_worker(Oid domain_oid, int prettyFlags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13760,3 +13770,253 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - If pretty is true, the output includes tabs (\t) and newlines (\n).
+ * noOfTabChars - indent with specified no of tabs.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, int prettyFlags, int noOfTabChars, const char *fmt,...)
+{
+	int			save_errno = errno;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	for (;;)
+	{
+		va_list		args;
+		int			needed;
+
+		errno = save_errno;
+		va_start(args, fmt);
+		needed = appendStringInfoVA(buf, fmt, args);
+		va_end(args);
+
+		if (needed == 0)
+			break;
+
+		enlargeStringInfo(buf, needed);
+	}
+}
+
+
+/*
+ * Helper function to scan domain constraints
+ */
+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);
+}
+
+/*
+ * Helper function to build CREATE DOMAIN statement
+ */
+static void
+build_create_domain_statement(StringInfo buf, Form_pg_type typForm,
+							  Node *defaultExpr, List *validConstraints, int prettyFlags)
+{
+	HeapTuple	baseTypeTuple;
+	Form_pg_type baseTypeForm;
+	Oid			baseCollation = InvalidOid;
+	ListCell   *lc;
+
+	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));
+
+	/* Add collation if it differs from base type's collation */
+	if (OidIsValid(typForm->typcollation))
+	{
+		/* Get base type's collation for comparison */
+		baseTypeTuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typForm->typbasetype));
+		if (HeapTupleIsValid(baseTypeTuple))
+		{
+			baseTypeForm = (Form_pg_type) GETSTRUCT(baseTypeTuple);
+			baseCollation = baseTypeForm->typcollation;
+			ReleaseSysCache(baseTypeTuple);
+		}
+
+		/* Only add COLLATE if domain's collation differs from base type's */
+		if (typForm->typcollation != baseCollation)
+		{
+			get_formatted_string(buf, prettyFlags, 1, "COLLATE %s",
+								 generate_collation_name(typForm->typcollation));
+		}
+	}
+
+	/* Add default value if present */
+	if (defaultExpr != NULL)
+	{
+		char	   *defaultValue = deparse_expression_pretty(defaultExpr, NIL, false, false, prettyFlags, 0);
+
+		get_formatted_string(buf, prettyFlags, 1, "DEFAULT %s", defaultValue);
+	}
+
+	/* Add valid constraints */
+	foreach(lc, validConstraints)
+	{
+		Oid			constraintOid = lfirst_oid(lc);
+		HeapTuple	constraintTup;
+		Form_pg_constraint con;
+		char	   *constraintDef;
+
+		/* Look up the constraint info */
+		constraintTup = SearchSysCache1(CONSTROID, ObjectIdGetDatum(constraintOid));
+		if (!HeapTupleIsValid(constraintTup))
+			continue;			/* constraint was dropped concurrently */
+
+		con = (Form_pg_constraint) GETSTRUCT(constraintTup);
+		constraintDef = pg_get_constraintdef_worker(constraintOid, false, prettyFlags, true);
+
+		get_formatted_string(buf, prettyFlags, 1, "CONSTRAINT %s",
+							 quote_identifier(NameStr(con->conname)));
+		get_formatted_string(buf, prettyFlags, 2, "%s", constraintDef);
+
+		ReleaseSysCache(constraintTup);
+	}
+
+	appendStringInfoChar(buf, ';');
+}
+
+/*
+ * Helper function to add ALTER DOMAIN statements for invalid constraints
+ */
+static void
+add_alter_domain_statements(StringInfo buf, List *invalidConstraints, int prettyFlags)
+{
+	ListCell   *lc;
+
+	foreach(lc, invalidConstraints)
+	{
+		Oid			constraintOid = lfirst_oid(lc);
+		char	   *alterStmt = pg_get_constraintdef_worker(constraintOid, true, prettyFlags, true);
+
+		if (alterStmt)
+			appendStringInfo(buf, "\n%s;", alterStmt);
+	}
+}
+
+/*
+ * pg_get_domain_ddl_ext - Get CREATE DOMAIN statement for a domain with pretty-print option
+ */
+Datum
+pg_get_domain_ddl_ext(PG_FUNCTION_ARGS)
+{
+	Oid			domain_oid = PG_GETARG_OID(0);
+	bool		pretty = PG_GETARG_BOOL(1);
+	char	   *res;
+	int			prettyFlags;
+
+	prettyFlags = GET_DDL_PRETTY_FLAGS(pretty);
+
+	res = pg_get_domain_ddl_worker(domain_oid, prettyFlags);
+	if (res == NULL)
+		PG_RETURN_NULL();
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+
+
+static char *
+pg_get_domain_ddl_worker(Oid domain_oid, int prettyFlags)
+{
+	StringInfoData buf;
+	HeapTuple	typeTuple;
+	Form_pg_type typForm;
+	Node	   *defaultExpr;
+	List	   *validConstraints;
+	List	   *invalidConstraints;
+
+	/* Look up the domain in pg_type */
+	typeTuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(domain_oid));
+	if (!HeapTupleIsValid(typeTuple))
+		return NULL;
+
+	typForm = (Form_pg_type) GETSTRUCT(typeTuple);
+
+	/* Check that this is actually a domain */
+	if (typForm->typtype != TYPTYPE_DOMAIN)
+		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.")));
+
+	/* Get default expression */
+	defaultExpr = get_typdefault(domain_oid);
+
+	/* Scan for valid and invalid constraints */
+	scan_domain_constraints(domain_oid, &validConstraints, &invalidConstraints);
+
+	/* Build the DDL statement */
+	initStringInfo(&buf);
+	build_create_domain_statement(&buf, typForm, defaultExpr, validConstraints, prettyFlags);
+
+	/* Add ALTER DOMAIN statements for invalid constraints */
+	if (list_length(invalidConstraints) > 0)
+		add_alter_domain_statements(&buf, invalidConstraints, prettyFlags);
+
+	/* Cleanup */
+	list_free(validConstraints);
+	list_free(invalidConstraints);
+	ReleaseSysCache(typeTuple);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dac40992cbc..d1a4fbaf40a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8539,6 +8539,9 @@
 { oid => '2508', descr => 'constraint description with pretty-print option',
   proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
   proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
+{ oid => '8024', descr => 'get CREATE statement for DOMAIN with pretty option',
+  proname => 'pg_get_domain_ddl', provolatile => 's', prorettype => 'text',
+  proargtypes => 'regtype bool', prosrc => 'pg_get_domain_ddl_ext' },
 { oid => '2509',
   descr => 'deparse an encoded expression with pretty-print option',
   proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',
diff --git a/src/test/regress/expected/object_ddl.out b/src/test/regress/expected/object_ddl.out
new file mode 100644
index 00000000000..f8dc8d12dd6
--- /dev/null
+++ b/src/test/regress/expected/object_ddl.out
@@ -0,0 +1,461 @@
+--
+-- Test for the following functions to get object DDL:
+-- - pg_get_domain_ddl
+--
+CREATE DOMAIN regress_us_postal_code AS TEXT
+    DEFAULT '00000'
+    CONSTRAINT regress_us_postal_code_check
+        CHECK (
+            VALUE ~ '^\d{5}$'
+    OR VALUE ~ '^\d{5}-\d{4}$'
+    );
+SELECT pg_get_domain_ddl('regress_us_postal_code');
+                                                                                          pg_get_domain_ddl                                                                                           
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_us_postal_code AS pg_catalog.text DEFAULT '00000'::text CONSTRAINT regress_us_postal_code_check CHECK (((VALUE ~ '^\d{5}$'::text) OR (VALUE ~ '^\d{5}-\d{4}$'::text)));
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_us_postal_code', pretty => false);
+                                                                                          pg_get_domain_ddl                                                                                           
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_us_postal_code AS pg_catalog.text DEFAULT '00000'::text CONSTRAINT regress_us_postal_code_check CHECK (((VALUE ~ '^\d{5}$'::text) OR (VALUE ~ '^\d{5}-\d{4}$'::text)));
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_us_postal_code', false);
+                                                                                          pg_get_domain_ddl                                                                                           
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_us_postal_code AS pg_catalog.text DEFAULT '00000'::text CONSTRAINT regress_us_postal_code_check CHECK (((VALUE ~ '^\d{5}$'::text) OR (VALUE ~ '^\d{5}-\d{4}$'::text)));
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_us_postal_code', pretty => true);
+                                 pg_get_domain_ddl                                 
+-----------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_us_postal_code AS pg_catalog.text                   +
+         DEFAULT '00000'::text                                                    +
+         CONSTRAINT regress_us_postal_code_check                                  +
+                 CHECK (VALUE ~ '^\d{5}$'::text OR VALUE ~ '^\d{5}-\d{4}$'::text);
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_us_postal_code', true);
+                                 pg_get_domain_ddl                                 
+-----------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_us_postal_code AS pg_catalog.text                   +
+         DEFAULT '00000'::text                                                    +
+         CONSTRAINT regress_us_postal_code_check                                  +
+                 CHECK (VALUE ~ '^\d{5}$'::text OR VALUE ~ '^\d{5}-\d{4}$'::text);
+(1 row)
+
+CREATE DOMAIN regress_domain_not_null AS INT NOT NULL;
+SELECT pg_get_domain_ddl('regress_domain_not_null');
+                                               pg_get_domain_ddl                                               
+---------------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_domain_not_null AS integer CONSTRAINT regress_domain_not_null_not_null NOT NULL;
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_domain_not_null', pretty => true);
+                    pg_get_domain_ddl                    
+---------------------------------------------------------
+ CREATE DOMAIN public.regress_domain_not_null AS integer+
+         CONSTRAINT regress_domain_not_null_not_null    +
+                 NOT NULL;
+(1 row)
+
+CREATE DOMAIN regress_domain_check AS INT
+    CONSTRAINT regress_a CHECK (VALUE < 100)
+    CONSTRAINT regress_b CHECK (VALUE > 10);
+SELECT pg_get_domain_ddl('regress_domain_check');
+                                                             pg_get_domain_ddl                                                              
+--------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_domain_check AS integer CONSTRAINT regress_a CHECK ((VALUE < 100)) CONSTRAINT regress_b CHECK ((VALUE > 10));
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_domain_check', pretty => true);
+                  pg_get_domain_ddl                   
+------------------------------------------------------
+ CREATE DOMAIN public.regress_domain_check AS integer+
+         CONSTRAINT regress_a                        +
+                 CHECK (VALUE < 100)                 +
+         CONSTRAINT regress_b                        +
+                 CHECK (VALUE > 10);
+(1 row)
+
+CREATE DOMAIN "regress_domain with space" AS INT
+    CONSTRAINT regress_a CHECK (VALUE < 100)
+    CONSTRAINT "regress_Constraint B" CHECK (VALUE > 10)
+    CONSTRAINT "regress_ConstraintC" CHECK (VALUE != 55);
+SELECT pg_get_domain_ddl('"regress_domain with space"');
+                                                                                                   pg_get_domain_ddl                                                                                                   
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public."regress_domain with space" AS integer CONSTRAINT regress_a CHECK ((VALUE < 100)) CONSTRAINT "regress_Constraint B" CHECK ((VALUE > 10)) CONSTRAINT "regress_ConstraintC" CHECK ((VALUE <> 55));
+(1 row)
+
+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_a                               +
+                 CHECK (VALUE < 100)                        +
+         CONSTRAINT "regress_Constraint B"                  +
+                 CHECK (VALUE > 10)                         +
+         CONSTRAINT "regress_ConstraintC"                   +
+                 CHECK (VALUE <> 55);
+(1 row)
+
+-- Test error cases
+SELECT pg_get_domain_ddl('regress_nonexistent_domain'::regtype);  -- should fail
+ERROR:  type "regress_nonexistent_domain" does not exist
+LINE 1: SELECT pg_get_domain_ddl('regress_nonexistent_domain'::regty...
+                                 ^
+SELECT pg_get_domain_ddl(NULL);  -- should return NULL
+ pg_get_domain_ddl 
+-------------------
+ 
+(1 row)
+
+SELECT pg_get_domain_ddl(NULL, pretty => true);  -- should return NULL
+ pg_get_domain_ddl 
+-------------------
+ 
+(1 row)
+
+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.
+-- Test domains with no constraints
+CREATE DOMAIN regress_simple_domain AS text;
+SELECT pg_get_domain_ddl('regress_simple_domain');
+                       pg_get_domain_ddl                        
+----------------------------------------------------------------
+ CREATE DOMAIN public.regress_simple_domain AS pg_catalog.text;
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_simple_domain', pretty => true);
+                       pg_get_domain_ddl                        
+----------------------------------------------------------------
+ CREATE DOMAIN public.regress_simple_domain AS pg_catalog.text;
+(1 row)
+
+-- Test domain over another domain
+CREATE DOMAIN regress_base_domain AS varchar(10);
+CREATE DOMAIN regress_derived_domain AS regress_base_domain CHECK (LENGTH(VALUE) > 3);
+SELECT pg_get_domain_ddl('regress_derived_domain');
+                                                                   pg_get_domain_ddl                                                                    
+--------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_derived_domain AS public.regress_base_domain CONSTRAINT regress_derived_domain_check CHECK ((length((VALUE)::text) > 3));
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_derived_domain', pretty => true);
+                             pg_get_domain_ddl                             
+---------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_derived_domain AS public.regress_base_domain+
+         CONSTRAINT regress_derived_domain_check                          +
+                 CHECK (length(VALUE::text) > 3);
+(1 row)
+
+-- Test domain with complex default expressions
+CREATE SEQUENCE regress_test_seq;
+CREATE DOMAIN regress_seq_domain AS int DEFAULT nextval('regress_test_seq');
+SELECT pg_get_domain_ddl('regress_seq_domain');
+                                         pg_get_domain_ddl                                         
+---------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_seq_domain AS integer DEFAULT nextval('regress_test_seq'::regclass);
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_seq_domain', pretty => true);
+                   pg_get_domain_ddl                    
+--------------------------------------------------------
+ CREATE DOMAIN public.regress_seq_domain AS integer    +
+         DEFAULT nextval('regress_test_seq'::regclass);
+(1 row)
+
+-- Test domain with a renamed sequence as default expression
+ALTER SEQUENCE regress_test_seq RENAME TO regress_test_seq_renamed;
+SELECT pg_get_domain_ddl('regress_seq_domain');
+                                             pg_get_domain_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_seq_domain AS integer DEFAULT nextval('regress_test_seq_renamed'::regclass);
+(1 row)
+
+-- Test domain with type modifiers
+CREATE DOMAIN regress_precise_numeric AS numeric(10,2) DEFAULT 0.00;
+SELECT pg_get_domain_ddl('regress_precise_numeric');
+                              pg_get_domain_ddl                              
+-----------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_precise_numeric AS numeric(10,2) DEFAULT 0.00;
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_precise_numeric', pretty => true);
+                       pg_get_domain_ddl                       
+---------------------------------------------------------------
+ CREATE DOMAIN public.regress_precise_numeric AS numeric(10,2)+
+         DEFAULT 0.00;
+(1 row)
+
+-- Test domain over array type
+CREATE DOMAIN regress_int_array_domain AS int[] CHECK (array_length(VALUE, 1) <= 5);
+SELECT pg_get_domain_ddl('regress_int_array_domain');
+                                                              pg_get_domain_ddl                                                              
+---------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_int_array_domain AS integer[] CONSTRAINT regress_int_array_domain_check CHECK ((array_length(VALUE, 1) <= 5));
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_int_array_domain', pretty => true);
+                     pg_get_domain_ddl                      
+------------------------------------------------------------
+ CREATE DOMAIN public.regress_int_array_domain AS integer[]+
+         CONSTRAINT regress_int_array_domain_check         +
+                 CHECK (array_length(VALUE, 1) <= 5);
+(1 row)
+
+-- Test domain in non-public schema
+CREATE SCHEMA regress_test_schema;
+CREATE DOMAIN regress_test_schema.regress_schema_domain AS text DEFAULT 'test';
+SELECT pg_get_domain_ddl('regress_test_schema.regress_schema_domain');
+                                        pg_get_domain_ddl                                         
+--------------------------------------------------------------------------------------------------
+ CREATE DOMAIN regress_test_schema.regress_schema_domain AS pg_catalog.text DEFAULT 'test'::text;
+(1 row)
+
+-- Test domain with multiple constraint types combined
+CREATE DOMAIN regress_comprehensive_domain AS varchar(50)
+    NOT NULL
+    DEFAULT 'default_value'
+    CHECK (LENGTH(VALUE) >= 5)
+    CHECK (VALUE !~ '^\s*$');  -- not just whitespace
+SELECT pg_get_domain_ddl('regress_comprehensive_domain');
+                                                                                                                                                                      pg_get_domain_ddl                                                                                                                                                                      
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_comprehensive_domain AS character varying(50) DEFAULT 'default_value'::character varying CONSTRAINT regress_comprehensive_domain_not_null NOT NULL CONSTRAINT regress_comprehensive_domain_check CHECK ((length((VALUE)::text) >= 5)) CONSTRAINT regress_comprehensive_domain_check1 CHECK (((VALUE)::text !~ '^\s*$'::text));
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_comprehensive_domain', pretty => true);
+                             pg_get_domain_ddl                              
+----------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_comprehensive_domain AS character varying(50)+
+         DEFAULT 'default_value'::character varying                        +
+         CONSTRAINT regress_comprehensive_domain_not_null                  +
+                 NOT NULL                                                  +
+         CONSTRAINT regress_comprehensive_domain_check                     +
+                 CHECK (length(VALUE::text) >= 5)                          +
+         CONSTRAINT regress_comprehensive_domain_check1                    +
+                 CHECK (VALUE::text !~ '^\s*$'::text);
+(1 row)
+
+-- Test domain over composite type
+CREATE TYPE regress_address_type AS (street text, city text, zipcode text);
+CREATE DOMAIN regress_address_domain AS regress_address_type CHECK ((VALUE).zipcode ~ '^\d{5}$');
+SELECT pg_get_domain_ddl('regress_address_domain');
+                                                                        pg_get_domain_ddl                                                                        
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_address_domain AS public.regress_address_type CONSTRAINT regress_address_domain_check CHECK (((VALUE).zipcode ~ '^\d{5}$'::text));
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_address_domain', pretty => true);
+                             pg_get_domain_ddl                              
+----------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_address_domain AS public.regress_address_type+
+         CONSTRAINT regress_address_domain_check                           +
+                 CHECK ((VALUE).zipcode ~ '^\d{5}$'::text);
+(1 row)
+
+-- Test domain with NOT VALID constraint
+CREATE DOMAIN regress_domain_not_valid AS int;
+ALTER DOMAIN regress_domain_not_valid ADD CONSTRAINT check_positive CHECK (VALUE > 0) NOT VALID;
+SELECT pg_get_domain_ddl('regress_domain_not_valid');
+                                             pg_get_domain_ddl                                             
+-----------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_domain_not_valid AS integer;                                                +
+ ALTER DOMAIN public.regress_domain_not_valid ADD CONSTRAINT check_positive CHECK ((VALUE > 0)) NOT VALID;
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_domain_not_valid', pretty => true);
+                                            pg_get_domain_ddl                                            
+---------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_domain_not_valid AS integer;                                              +
+ ALTER DOMAIN public.regress_domain_not_valid ADD CONSTRAINT check_positive CHECK (VALUE > 0) NOT VALID;
+(1 row)
+
+-- Test domain with mix of valid and not valid constraints
+CREATE DOMAIN regress_domain_mixed AS int CHECK (VALUE != 0);
+ALTER DOMAIN regress_domain_mixed ADD CONSTRAINT check_range CHECK (VALUE BETWEEN 1 AND 100) NOT VALID;
+SELECT pg_get_domain_ddl('regress_domain_mixed');
+                                                    pg_get_domain_ddl                                                     
+--------------------------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_domain_mixed AS integer CONSTRAINT regress_domain_mixed_check CHECK ((VALUE <> 0));        +
+ ALTER DOMAIN public.regress_domain_mixed ADD CONSTRAINT check_range CHECK (((VALUE >= 1) AND (VALUE <= 100))) NOT VALID;
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_domain_mixed', pretty => true);
+                                                 pg_get_domain_ddl                                                  
+--------------------------------------------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_domain_mixed AS integer                                                              +
+         CONSTRAINT regress_domain_mixed_check                                                                     +
+                 CHECK (VALUE <> 0);                                                                               +
+ ALTER DOMAIN public.regress_domain_mixed ADD CONSTRAINT check_range CHECK (VALUE >= 1 AND VALUE <= 100) NOT VALID;
+(1 row)
+
+-- Test domain with collation
+CREATE DOMAIN regress_domain_with_collate AS text COLLATE "C";
+SELECT pg_get_domain_ddl('regress_domain_with_collate');
+                                pg_get_domain_ddl                                 
+----------------------------------------------------------------------------------
+ CREATE DOMAIN public.regress_domain_with_collate AS pg_catalog.text COLLATE "C";
+(1 row)
+
+SELECT pg_get_domain_ddl('regress_domain_with_collate', pretty => true);
+                          pg_get_domain_ddl                          
+---------------------------------------------------------------------
+ CREATE DOMAIN public.regress_domain_with_collate AS pg_catalog.text+
+         COLLATE "C";
+(1 row)
+
+-- Test domain that shadows a built-in type name (must use schema-qualified name)
+CREATE DOMAIN public.int AS pg_catalog.int4;
+-- This should fail because 'int' resolves to pg_catalog.int4, not public.int
+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.
+-- This should work with schema-qualified name
+SELECT pg_get_domain_ddl('public.int');
+           pg_get_domain_ddl            
+----------------------------------------
+ CREATE DOMAIN public."int" AS integer;
+(1 row)
+
+-- Round-trip tests: verify DDL is syntactically valid and semantically correct
+CREATE FUNCTION regress_verify_ddl_roundtrip(object_type text, object_name text) RETURNS void
+LANGUAGE plpgsql AS $$
+DECLARE
+    original_ddl text;
+    recreated_ddl text;
+BEGIN
+    EXECUTE format('SELECT pg_get_%s_ddl(%L)', object_type, object_name) INTO original_ddl;
+    EXECUTE format('DROP %s %s', object_type, object_name);
+    EXECUTE original_ddl;
+    EXECUTE format('SELECT pg_get_%s_ddl(%L)', object_type, object_name) INTO recreated_ddl;
+    ASSERT original_ddl = recreated_ddl,
+        format(E'round-trip mismatch for %s %s:\n  original:  %s\n  recreated: %s',
+               object_type, object_name, original_ddl, recreated_ddl);
+END;
+$$;
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_us_postal_code');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_domain_not_null');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_domain_check');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', '"regress_domain with space"');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_simple_domain');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_derived_domain');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_seq_domain');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_precise_numeric');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_int_array_domain');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_test_schema.regress_schema_domain');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_comprehensive_domain');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_address_domain');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_domain_not_valid');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_domain_mixed');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_domain_with_collate');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+SELECT regress_verify_ddl_roundtrip('domain', 'public."int"');
+ regress_verify_ddl_roundtrip 
+------------------------------
+ 
+(1 row)
+
+-- Cleanup
+DROP FUNCTION regress_verify_ddl_roundtrip;
+DROP DOMAIN regress_us_postal_code;
+DROP DOMAIN regress_domain_not_null;
+DROP DOMAIN regress_domain_check;
+DROP DOMAIN "regress_domain with space";
+DROP DOMAIN regress_comprehensive_domain;
+DROP DOMAIN regress_test_schema.regress_schema_domain;
+DROP SCHEMA regress_test_schema;
+DROP DOMAIN regress_address_domain;
+DROP TYPE regress_address_type;
+DROP DOMAIN regress_int_array_domain;
+DROP DOMAIN regress_precise_numeric;
+DROP DOMAIN regress_seq_domain;
+DROP SEQUENCE regress_test_seq_renamed;
+DROP DOMAIN regress_derived_domain;
+DROP DOMAIN regress_base_domain;
+DROP DOMAIN regress_simple_domain;
+DROP DOMAIN regress_domain_not_valid;
+DROP DOMAIN regress_domain_mixed;
+DROP DOMAIN regress_domain_with_collate;
+DROP DOMAIN public.int;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 549e9b2d7be..52b24ab806e 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct pg_dependencies oid8 encoding euc_kr
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct pg_dependencies oid8 encoding euc_kr object_ddl
 
 # ----------
 # Load huge amounts of data
diff --git a/src/test/regress/sql/object_ddl.sql b/src/test/regress/sql/object_ddl.sql
new file mode 100644
index 00000000000..f214e3b4611
--- /dev/null
+++ b/src/test/regress/sql/object_ddl.sql
@@ -0,0 +1,180 @@
+--
+-- Test for the following functions to get object DDL:
+-- - pg_get_domain_ddl
+--
+
+CREATE DOMAIN regress_us_postal_code AS TEXT
+    DEFAULT '00000'
+    CONSTRAINT regress_us_postal_code_check
+        CHECK (
+            VALUE ~ '^\d{5}$'
+    OR VALUE ~ '^\d{5}-\d{4}$'
+    );
+
+SELECT pg_get_domain_ddl('regress_us_postal_code');
+SELECT pg_get_domain_ddl('regress_us_postal_code', pretty => false);
+SELECT pg_get_domain_ddl('regress_us_postal_code', false);
+SELECT pg_get_domain_ddl('regress_us_postal_code', pretty => true);
+SELECT pg_get_domain_ddl('regress_us_postal_code', true);
+
+
+CREATE DOMAIN regress_domain_not_null AS INT NOT NULL;
+
+SELECT pg_get_domain_ddl('regress_domain_not_null');
+SELECT pg_get_domain_ddl('regress_domain_not_null', pretty => true);
+
+
+CREATE DOMAIN regress_domain_check AS INT
+    CONSTRAINT regress_a CHECK (VALUE < 100)
+    CONSTRAINT regress_b CHECK (VALUE > 10);
+
+SELECT pg_get_domain_ddl('regress_domain_check');
+SELECT pg_get_domain_ddl('regress_domain_check', pretty => true);
+
+
+CREATE DOMAIN "regress_domain with space" AS INT
+    CONSTRAINT regress_a CHECK (VALUE < 100)
+    CONSTRAINT "regress_Constraint B" CHECK (VALUE > 10)
+    CONSTRAINT "regress_ConstraintC" CHECK (VALUE != 55);
+
+SELECT pg_get_domain_ddl('"regress_domain with space"');
+SELECT pg_get_domain_ddl('"regress_domain with space"', pretty => true);
+
+-- Test error cases
+SELECT pg_get_domain_ddl('regress_nonexistent_domain'::regtype);  -- should fail
+SELECT pg_get_domain_ddl(NULL);  -- should return NULL
+SELECT pg_get_domain_ddl(NULL, pretty => true);  -- should return NULL
+SELECT pg_get_domain_ddl('pg_class');  -- should fail - not a domain
+SELECT pg_get_domain_ddl('integer');  -- should fail - not a domain
+
+-- Test domains with no constraints
+CREATE DOMAIN regress_simple_domain AS text;
+SELECT pg_get_domain_ddl('regress_simple_domain');
+SELECT pg_get_domain_ddl('regress_simple_domain', pretty => true);
+
+-- Test domain over another domain
+CREATE DOMAIN regress_base_domain AS varchar(10);
+CREATE DOMAIN regress_derived_domain AS regress_base_domain CHECK (LENGTH(VALUE) > 3);
+SELECT pg_get_domain_ddl('regress_derived_domain');
+SELECT pg_get_domain_ddl('regress_derived_domain', pretty => true);
+
+-- Test domain with complex default expressions
+CREATE SEQUENCE regress_test_seq;
+CREATE DOMAIN regress_seq_domain AS int DEFAULT nextval('regress_test_seq');
+SELECT pg_get_domain_ddl('regress_seq_domain');
+SELECT pg_get_domain_ddl('regress_seq_domain', pretty => true);
+
+-- Test domain with a renamed sequence as default expression
+ALTER SEQUENCE regress_test_seq RENAME TO regress_test_seq_renamed;
+SELECT pg_get_domain_ddl('regress_seq_domain');
+
+-- Test domain with type modifiers
+CREATE DOMAIN regress_precise_numeric AS numeric(10,2) DEFAULT 0.00;
+SELECT pg_get_domain_ddl('regress_precise_numeric');
+SELECT pg_get_domain_ddl('regress_precise_numeric', pretty => true);
+
+-- Test domain over array type
+CREATE DOMAIN regress_int_array_domain AS int[] CHECK (array_length(VALUE, 1) <= 5);
+SELECT pg_get_domain_ddl('regress_int_array_domain');
+SELECT pg_get_domain_ddl('regress_int_array_domain', pretty => true);
+
+-- Test domain in non-public schema
+CREATE SCHEMA regress_test_schema;
+CREATE DOMAIN regress_test_schema.regress_schema_domain AS text DEFAULT 'test';
+SELECT pg_get_domain_ddl('regress_test_schema.regress_schema_domain');
+
+-- Test domain with multiple constraint types combined
+CREATE DOMAIN regress_comprehensive_domain AS varchar(50)
+    NOT NULL
+    DEFAULT 'default_value'
+    CHECK (LENGTH(VALUE) >= 5)
+    CHECK (VALUE !~ '^\s*$');  -- not just whitespace
+SELECT pg_get_domain_ddl('regress_comprehensive_domain');
+SELECT pg_get_domain_ddl('regress_comprehensive_domain', pretty => true);
+
+-- Test domain over composite type
+CREATE TYPE regress_address_type AS (street text, city text, zipcode text);
+CREATE DOMAIN regress_address_domain AS regress_address_type CHECK ((VALUE).zipcode ~ '^\d{5}$');
+SELECT pg_get_domain_ddl('regress_address_domain');
+SELECT pg_get_domain_ddl('regress_address_domain', pretty => true);
+
+-- Test domain with NOT VALID constraint
+CREATE DOMAIN regress_domain_not_valid AS int;
+ALTER DOMAIN regress_domain_not_valid ADD CONSTRAINT check_positive CHECK (VALUE > 0) NOT VALID;
+SELECT pg_get_domain_ddl('regress_domain_not_valid');
+SELECT pg_get_domain_ddl('regress_domain_not_valid', pretty => true);
+
+-- Test domain with mix of valid and not valid constraints
+CREATE DOMAIN regress_domain_mixed AS int CHECK (VALUE != 0);
+ALTER DOMAIN regress_domain_mixed ADD CONSTRAINT check_range CHECK (VALUE BETWEEN 1 AND 100) NOT VALID;
+SELECT pg_get_domain_ddl('regress_domain_mixed');
+SELECT pg_get_domain_ddl('regress_domain_mixed', pretty => true);
+
+-- Test domain with collation
+CREATE DOMAIN regress_domain_with_collate AS text COLLATE "C";
+SELECT pg_get_domain_ddl('regress_domain_with_collate');
+SELECT pg_get_domain_ddl('regress_domain_with_collate', pretty => true);
+
+-- Test domain that shadows a built-in type name (must use schema-qualified name)
+CREATE DOMAIN public.int AS pg_catalog.int4;
+-- This should fail because 'int' resolves to pg_catalog.int4, not public.int
+SELECT pg_get_domain_ddl('int');  -- should fail
+-- This should work with schema-qualified name
+SELECT pg_get_domain_ddl('public.int');
+
+-- Round-trip tests: verify DDL is syntactically valid and semantically correct
+CREATE FUNCTION regress_verify_ddl_roundtrip(object_type text, object_name text) RETURNS void
+LANGUAGE plpgsql AS $$
+DECLARE
+    original_ddl text;
+    recreated_ddl text;
+BEGIN
+    EXECUTE format('SELECT pg_get_%s_ddl(%L)', object_type, object_name) INTO original_ddl;
+    EXECUTE format('DROP %s %s', object_type, object_name);
+    EXECUTE original_ddl;
+    EXECUTE format('SELECT pg_get_%s_ddl(%L)', object_type, object_name) INTO recreated_ddl;
+    ASSERT original_ddl = recreated_ddl,
+        format(E'round-trip mismatch for %s %s:\n  original:  %s\n  recreated: %s',
+               object_type, object_name, original_ddl, recreated_ddl);
+END;
+$$;
+
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_us_postal_code');
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_domain_not_null');
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_domain_check');
+SELECT regress_verify_ddl_roundtrip('domain', '"regress_domain with space"');
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_simple_domain');
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_derived_domain');
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_seq_domain');
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_precise_numeric');
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_int_array_domain');
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_test_schema.regress_schema_domain');
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_comprehensive_domain');
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_address_domain');
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_domain_not_valid');
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_domain_mixed');
+SELECT regress_verify_ddl_roundtrip('domain', 'regress_domain_with_collate');
+SELECT regress_verify_ddl_roundtrip('domain', 'public."int"');
+
+-- Cleanup
+DROP FUNCTION regress_verify_ddl_roundtrip;
+DROP DOMAIN regress_us_postal_code;
+DROP DOMAIN regress_domain_not_null;
+DROP DOMAIN regress_domain_check;
+DROP DOMAIN "regress_domain with space";
+DROP DOMAIN regress_comprehensive_domain;
+DROP DOMAIN regress_test_schema.regress_schema_domain;
+DROP SCHEMA regress_test_schema;
+DROP DOMAIN regress_address_domain;
+DROP TYPE regress_address_type;
+DROP DOMAIN regress_int_array_domain;
+DROP DOMAIN regress_precise_numeric;
+DROP DOMAIN regress_seq_domain;
+DROP SEQUENCE regress_test_seq_renamed;
+DROP DOMAIN regress_derived_domain;
+DROP DOMAIN regress_base_domain;
+DROP DOMAIN regress_simple_domain;
+DROP DOMAIN regress_domain_not_valid;
+DROP DOMAIN regress_domain_mixed;
+DROP DOMAIN regress_domain_with_collate;
+DROP DOMAIN public.int;
-- 
2.45.1