public inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
21+ messages / 12 participants
[nested] [flat]

* [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
@ 2025-10-16 09:16 Tim Waizenegger <[email protected]>
  2025-10-16 11:04 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement jian he <[email protected]>
  2026-02-19 00:10 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tom Lane <[email protected]>
  0 siblings, 2 replies; 21+ messages in thread

From: Tim Waizenegger @ 2025-10-16 09:16 UTC (permalink / raw)
  To: pgsql-hackers

Hi all,

Following the recent "Retail DDL" discussion [1], we're submitting another
implementation: pg_get_domain_ddl().

This function reconstructs CREATE DOMAIN statements for existing domains,
following what seems to be the agreed pg_get_{objecttype}_ddl naming convention.

## Function

pg_get_domain_ddl(regtype) returns text

Returns a complete CREATE DOMAIN statement including base type, default values,
and all constraints. Uses get_typdefault() for proper expression handling and
supports schema-qualified domains.

## Example

```
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 text DEFAULT
'00000'::text CONSTRAINT regress_us_postal_code_check CHECK (VALUE ~
'^\d{5}$'::text OR VALUE ~ '^\d{5}-\d{4}$'::text);
(1 row)
```

## Implementation

- New "Get Object DDL Functions" documentation section
- Comprehensive regression tests in a separate file where we will add
  tests for the other objects functions.

We're unsure about the place where to add the trigger to the `object_ddl` test.
We added it now in `src/test/regress/parallel_schedule`, please let us know
if there is a better place.

This is part of a coordinated effort where we've divided the DDL functions
among different contributors. Additional patches for other object types
(tables, indexes, etc.) will follow from other team members.
Already submitted are: CREATE TRIGGER [2] and CREATE POLICY [3].

Patch attached. Feedback welcome.

[1] https://www.postgresql.org/message-id/flat/945db7c5-be75-45bf-b55b-cb1e56f2e3e9%40dunslane.net
[2] https://www.postgresql.org/message-id/flat/CAPXBC8K5awmtMoq66DGHe%2BnD7hUf6HPRVHLeGNBRpCDpzusOXQ%40m...
[3] https://www.postgresql.org/message-id/flat/CANxoLDdJsRJqnjMXV3yjsk07Z5iRWxG-c2hZJC7bAKqf8ZXj_A%40mai...

---
Best regards,
Florin Irion
Tim Waizenegger

EDB (EnterpriseDB)


Attachments:

  [application/octet-stream] v1-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch (20.7K, 2-v1-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch)
  download | inline diff:
From 6dbbf85dfe261c15145e857c9ee5535c1e591545 Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Thu, 18 Sep 2025 18:52:43 +0200
Subject: [PATCH v1 1/2] 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)
* Properly quotes identifiers and schema names
* Handles complex constraint expressions

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.

Reference: PG-151
Author: Florin Irion <[email protected]>
Author: Tim Waizenegger <[email protected]>
Reviewed-by: Álvaro Herrera [email protected]
---
 doc/src/sgml/func/func-info.sgml         |  44 +++++++
 src/backend/utils/adt/ruleutils.c        |  75 +++++++++++
 src/include/catalog/pg_proc.dat          |   3 +
 src/test/regress/expected/object_ddl.out | 151 +++++++++++++++++++++++
 src/test/regress/parallel_schedule       |   2 +-
 src/test/regress/sql/object_ddl.sql      | 100 +++++++++++++++
 6 files changed, 374 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 c393832d94c..4602c8eb54e 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,48 @@ 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>text</type> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the creating command for a domain.
+        The result is a complete <command>CREATE DOMAIN</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 050eef97a4c..af79634b44c 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -13738,3 +13738,78 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+
+/*
+ * pg_get_domain_ddl - Get CREATE DOMAIN statement for a domain
+ */
+Datum
+pg_get_domain_ddl(PG_FUNCTION_ARGS)
+{
+	StringInfoData buf;
+	Oid			domain_oid = PG_GETARG_OID(0);
+	HeapTuple	typeTuple;
+	Form_pg_type typForm;
+	Relation	constraintRel;
+	SysScanDesc sscan;
+	ScanKeyData skey;
+	HeapTuple	constraintTup;
+	Node	   *defaultExpr;
+
+	/* Look up the domain in pg_type */
+	typeTuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(domain_oid));
+
+	/* function param is a regtype, so typeoid must be valid */
+	Assert(HeapTupleIsValid(typeTuple));
+
+	typForm = (Form_pg_type) GETSTRUCT(typeTuple);
+	initStringInfo(&buf);
+	appendStringInfo(&buf, "CREATE DOMAIN %s.%s AS %s",
+					 quote_identifier(get_namespace_name(typForm->typnamespace)),
+					 quote_identifier(NameStr(typForm->typname)),
+					 format_type_be(typForm->typbasetype));
+
+	/* Get the default value expression, if any */
+	defaultExpr = get_typdefault(domain_oid);
+
+	if (defaultExpr != NULL)
+	{
+		char	   *defaultValue;
+
+		defaultValue = deparse_expression_pretty(defaultExpr, NIL, false, false,
+												 0, 0);
+		appendStringInfo(&buf, " DEFAULT %s", defaultValue);
+	}
+
+	/* table scan to look up constraints belonging to this domain */
+	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);
+		char	   *val = NULL;
+
+		val = pg_get_constraintdef_worker(con->oid, false, PRETTYFLAG_PAREN, true);
+		appendStringInfo(&buf, " CONSTRAINT %s %s",
+						 quote_identifier(NameStr(con->conname)), val);
+	}
+	systable_endscan(sscan);
+	table_close(constraintRel, AccessShareLock);
+	ReleaseSysCache(typeTuple);
+
+	appendStringInfo(&buf, ";");
+
+	PG_RETURN_TEXT_P(cstring_to_text(buf.data));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b51d2b17379..897bc1f6270 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,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',
+  proname => 'pg_get_domain_ddl', prorettype => 'text',
+  proargtypes => 'regtype', 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/test/regress/expected/object_ddl.out b/src/test/regress/expected/object_ddl.out
new file mode 100644
index 00000000000..a35b0ec19ca
--- /dev/null
+++ b/src/test/regress/expected/object_ddl.out
@@ -0,0 +1,151 @@
+--
+-- 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 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)
+
+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)
+
+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)
+
+-- 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)
+
+-- 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 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 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)
+
+-- 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 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)
+
+-- 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 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 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 regress_address_type CONSTRAINT regress_address_domain_check CHECK ((VALUE).zipcode ~ '^\d{5}$'::text);
+(1 row)
+
+-- Cleanup
+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;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f9450cdc477..70ac529259b 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
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import 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..7182202ad5f
--- /dev/null
+++ b/src/test/regress/sql/object_ddl.sql
@@ -0,0 +1,100 @@
+--
+-- 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');
+
+
+CREATE DOMAIN regress_domain_not_null AS INT NOT NULL;
+
+SELECT pg_get_domain_ddl('regress_domain_not_null');
+
+
+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');
+
+
+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"');
+
+-- Test error cases
+SELECT pg_get_domain_ddl('regress_nonexistent_domain'::regtype);  -- should fail
+SELECT pg_get_domain_ddl(NULL);  -- should return NULL
+
+-- Test domains with no constraints
+CREATE DOMAIN regress_simple_domain AS text;
+SELECT pg_get_domain_ddl('regress_simple_domain');
+
+-- 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');
+
+-- 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');
+
+-- 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');
+
+-- 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');
+
+-- 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');
+
+-- 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');
+
+-- Cleanup
+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;
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
@ 2025-10-16 11:04 ` jian he <[email protected]>
  2025-10-22 09:32   ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  1 sibling, 1 reply; 21+ messages in thread

From: jian he @ 2025-10-16 11:04 UTC (permalink / raw)
  To: Tim Waizenegger <[email protected]>; +Cc: pgsql-hackers

On Thu, Oct 16, 2025 at 5:17 PM Tim Waizenegger
<[email protected]> wrote:
>
> Hi all,
>
> Following the recent "Retail DDL" discussion [1], we're submitting another
> implementation: pg_get_domain_ddl().
>
> This function reconstructs CREATE DOMAIN statements for existing domains,
> following what seems to be the agreed pg_get_{objecttype}_ddl naming convention.
>
> ## Function
>
> pg_get_domain_ddl(regtype) returns text
>
> Returns a complete CREATE DOMAIN statement including base type, default values,
> and all constraints. Uses get_typdefault() for proper expression handling and
> supports schema-qualified domains.
>

        <indexterm>
+         <primary>pg_get_domain_ddl</primary>
+        </indexterm>
+        <function>pg_get_domain_ddl</function> (
<parameter>domain</parameter> <type>text</type> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the creating command for a domain.
+        The result is a complete <command>CREATE DOMAIN</command> statement.
+       </para></entry>

<type>text</type>

should be
<type>regtype</type>

+ Oid domain_oid = PG_GETARG_OID(0);
+ HeapTuple typeTuple;
,....
+
+ /* Look up the domain in pg_type */
+ typeTuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(domain_oid));
+

select pg_get_domain_ddl(-1);
will cause segfault.
see https://www.postgresql.org/message-id/3759807.1711658868%40sss.pgh.pa.us
and pg_get_trigger_ddl thread.


NOT VALID check constraint handling is tricky currently.
create domain x as int;
alter domain x add constraint cc check(value > 2) not valid;

select pg_get_domain_ddl('x'::regtype);
CREATE DOMAIN public.x AS integer CONSTRAINT cc CHECK (VALUE > 2) NOT VALID;
but putting the above to psql would result in syntax error.


https://www.postgresql.org/docs/current/sql-createdomain.html
[ COLLATE collation ]
part not handled?

create domain d0 as text collate "C";
select pg_get_domain_ddl('d0'::regtype);
        pg_get_domain_ddl
----------------------------------
 CREATE DOMAIN public.d0 AS text;
(1 row)

we should expect
CREATE DOMAIN public.d0 AS text COLLATE "C";





^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-16 11:04 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement jian he <[email protected]>
@ 2025-10-22 09:32   ` Tim Waizenegger <[email protected]>
  2025-10-22 10:26     ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Chao Li <[email protected]>
  2025-10-23 04:21     ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement jian he <[email protected]>
  0 siblings, 2 replies; 21+ messages in thread

From: Tim Waizenegger @ 2025-10-22 09:32 UTC (permalink / raw)
  To: jian he <[email protected]>; pgsql-hackers

On Thu, Oct 16, 2025 at 1:05 PM jian he <[email protected]> wrote:
>
> On Thu, Oct 16, 2025 at 5:17 PM Tim Waizenegger
> <[email protected]> wrote:
> >
> > Hi all,
> >
> > Following the recent "Retail DDL" discussion [1], we're submitting another
> > implementation: pg_get_domain_ddl().
> >
>
> select pg_get_domain_ddl(-1);
> will cause segfault.
> see https://www.postgresql.org/message-id/3759807.1711658868%40sss.pgh.pa.us
> and pg_get_trigger_ddl thread.
>
>
> NOT VALID check constraint handling is tricky currently.
> create domain x as int;
> alter domain x add constraint cc check(value > 2) not valid;
>
> select pg_get_domain_ddl('x'::regtype);
> CREATE DOMAIN public.x AS integer CONSTRAINT cc CHECK (VALUE > 2) NOT VALID;
> but putting the above to psql would result in syntax error.
>
>
> https://www.postgresql.org/docs/current/sql-createdomain.html
> [ COLLATE collation ]
> part not handled?
>
> create domain d0 as text collate "C";
> select pg_get_domain_ddl('d0'::regtype);
>         pg_get_domain_ddl
> ----------------------------------
>  CREATE DOMAIN public.d0 AS text;
> (1 row)
>
> we should expect
> CREATE DOMAIN public.d0 AS text COLLATE "C";

Thanks for the feedback! We addressed the issues mentioned above and
also added more extensive test cases:

postgres=# select pg_get_domain_ddl(-1);
 pg_get_domain_ddl
-------------------

(1 row)

postgres=# create domain d0 as text collate "C";
CREATE DOMAIN
postgres=# select pg_get_domain_ddl('d0'::regtype);
              pg_get_domain_ddl
----------------------------------------------
 CREATE DOMAIN public.d0 AS text COLLATE "C";
(1 row)

postgres=# create domain x as int;
CREATE DOMAIN
postgres=# alter domain x add constraint cc check(value > 2) not valid;
ALTER DOMAIN
postgres=# select pg_get_domain_ddl('x'::regtype);
                          pg_get_domain_ddl
----------------------------------------------------------------------
 CREATE DOMAIN public.x AS integer;                                  +
 ALTER DOMAIN public.x ADD CONSTRAINT cc CHECK (VALUE > 2) NOT VALID;
(1 row)


updated patch is attached

---
Best regards,
Florin Irion
Tim Waizenegger

EDB (EnterpriseDB)


Attachments:

  [application/octet-stream] v1-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch (26.1K, 2-v1-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch)
  download | inline diff:
From 41ac9f6f14778d36098caf0a7bba523e2f0f99bb Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Thu, 18 Sep 2025 18:52:43 +0200
Subject: [PATCH v1] 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

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.

Reference: PG-151
Author: Florin Irion <[email protected]>
Author: Tim Waizenegger <[email protected]>
Reviewed-by: Álvaro Herrera [email protected]
---
 doc/src/sgml/func/func-info.sgml         |  44 ++++++
 src/backend/utils/adt/ruleutils.c        | 174 ++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   3 +
 src/test/regress/expected/object_ddl.out | 182 +++++++++++++++++++++++
 src/test/regress/parallel_schedule       |   2 +-
 src/test/regress/sql/object_ddl.sql      | 117 +++++++++++++++
 6 files changed, 521 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 c393832d94c..9a937df960d 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,48 @@ 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>text</type> )
+        <returnvalue>regtype</returnvalue>
+       </para>
+       <para>
+        Reconstructs the creating command for a domain.
+        The result is a complete <command>CREATE DOMAIN</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 050eef97a4c..7b2ce4e460f 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -13738,3 +13738,177 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf->data;
 }
+
+
+/*
+ * 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);
+}
+
+/*
+ * Helper function to build CREATE DOMAIN statement
+ */
+static void
+build_create_domain_statement(StringInfo buf, Form_pg_type typForm,
+							 Node *defaultExpr, List *validConstraints)
+{
+	HeapTuple	baseTypeTuple;
+	Form_pg_type baseTypeForm;
+	Oid			baseCollation = InvalidOid;
+
+	appendStringInfo(buf, "CREATE DOMAIN %s.%s AS %s",
+					 quote_identifier(get_namespace_name(typForm->typnamespace)),
+					 quote_identifier(NameStr(typForm->typname)),
+					 format_type_be(typForm->typbasetype));
+
+	/* 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)
+		{
+			appendStringInfo(buf, " COLLATE %s",
+							 generate_collation_name(typForm->typcollation));
+		}
+	}
+
+	/* Add default value if present */
+	if (defaultExpr != NULL)
+	{
+		char *defaultValue = deparse_expression_pretty(defaultExpr, NIL, false, false, 0, 0);
+		appendStringInfo(buf, " DEFAULT %s", defaultValue);
+	}
+
+	/* Add valid constraints */
+	ListCell *lc;
+	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, PRETTYFLAG_PAREN, true);
+
+		appendStringInfo(buf, " CONSTRAINT %s %s",
+						 quote_identifier(NameStr(con->conname)),
+						 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)
+{
+	ListCell *lc;
+
+	foreach(lc, invalidConstraints)
+	{
+		Oid constraintOid = lfirst_oid(lc);
+		char *alterStmt = pg_get_constraintdef_worker(constraintOid, true, PRETTYFLAG_PAREN, true);
+
+		if (alterStmt)
+			appendStringInfo(buf, "\n%s;", alterStmt);
+	}
+}
+
+/*
+ * pg_get_domain_ddl - Get CREATE DOMAIN statement for a domain
+ */
+Datum
+pg_get_domain_ddl(PG_FUNCTION_ARGS)
+{
+	StringInfoData buf;
+	Oid			domain_oid = PG_GETARG_OID(0);
+	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))
+		PG_RETURN_NULL();
+
+	typForm = (Form_pg_type) GETSTRUCT(typeTuple);
+
+	/* 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);
+
+	/* Add ALTER DOMAIN statements for invalid constraints */
+	if (list_length(invalidConstraints) > 0)
+		add_alter_domain_statements(&buf, invalidConstraints);
+
+	/* Cleanup */
+	list_free(validConstraints);
+	list_free(invalidConstraints);
+	ReleaseSysCache(typeTuple);
+
+	PG_RETURN_TEXT_P(cstring_to_text(buf.data));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b51d2b17379..897bc1f6270 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,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',
+  proname => 'pg_get_domain_ddl', prorettype => 'text',
+  proargtypes => 'regtype', 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/test/regress/expected/object_ddl.out b/src/test/regress/expected/object_ddl.out
new file mode 100644
index 00000000000..3dc1f1bc049
--- /dev/null
+++ b/src/test/regress/expected/object_ddl.out
@@ -0,0 +1,182 @@
+--
+-- 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 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)
+
+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)
+
+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)
+
+-- 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)
+
+-- 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 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 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)
+
+-- 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 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)
+
+-- 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 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 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 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)
+
+-- 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)
+
+-- 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 text COLLATE "C";
+(1 row)
+
+-- Cleanup
+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;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f9450cdc477..70ac529259b 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
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import 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..aa2ab62c193
--- /dev/null
+++ b/src/test/regress/sql/object_ddl.sql
@@ -0,0 +1,117 @@
+--
+-- 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');
+
+
+CREATE DOMAIN regress_domain_not_null AS INT NOT NULL;
+
+SELECT pg_get_domain_ddl('regress_domain_not_null');
+
+
+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');
+
+
+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"');
+
+-- Test error cases
+SELECT pg_get_domain_ddl('regress_nonexistent_domain'::regtype);  -- should fail
+SELECT pg_get_domain_ddl(NULL);  -- should return NULL
+
+-- Test domains with no constraints
+CREATE DOMAIN regress_simple_domain AS text;
+SELECT pg_get_domain_ddl('regress_simple_domain');
+
+-- 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');
+
+-- 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');
+
+-- 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');
+
+-- 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');
+
+-- 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');
+
+-- 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');
+
+-- 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');
+
+-- 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');
+
+-- Test domain with collation
+CREATE DOMAIN regress_domain_with_collate AS text COLLATE "C";
+SELECT pg_get_domain_ddl('regress_domain_with_collate');
+
+-- Cleanup
+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;
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-16 11:04 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement jian he <[email protected]>
  2025-10-22 09:32   ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
@ 2025-10-22 10:26     ` Chao Li <[email protected]>
  2025-10-22 12:00       ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  1 sibling, 1 reply; 21+ messages in thread

From: Chao Li @ 2025-10-22 10:26 UTC (permalink / raw)
  To: Tim Waizenegger <[email protected]>; +Cc: jian he <[email protected]>; pgsql-hackers

Hi Tim,

Thanks for working on this. I haven’t finished reviewing the entire patch. But I got a quick question:

> On Oct 22, 2025, at 17:32, Tim Waizenegger <[email protected]> wrote:
> 
> updated patch is attached
> 
> ---
> Best regards,
> Florin Irion
> Tim Waizenegger
> 
> EDB (EnterpriseDB)
> <v1-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch>

```
+/*
+ * pg_get_domain_ddl - Get CREATE DOMAIN statement for a domain
+ */
+Datum
+pg_get_domain_ddl(PG_FUNCTION_ARGS)
+{
+	StringInfoData buf;
+	Oid			domain_oid = PG_GETARG_OID(0);
+	HeapTuple	typeTuple;
+	Form_pg_type typForm;
+	Node	   *defaultExpr;
```

While reviewing a similar patch of pg_get_policy_ddl(), it take the last parameter as a pretty flag. I wonder why pg_get_domain_ddl() doesn’t support an argument for pretty? 


See the code snippet from the other patch:

```
+/*
+ * pg_get_policy_ddl
+ *
+ * Generate a CREATE POLICY statement for the specified policy.
+ *
+ * tableID - Table ID of the policy.
+ * policyName - Name of the policy for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_policy_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			tableID = PG_GETARG_OID(0);
+	Name		policyName = PG_GETARG_NAME(1);
+	bool		pretty = PG_GETARG_BOOL(2);  # <====== This is the pretty arg
+	bool		attrIsNull;
```

Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/









^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-16 11:04 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement jian he <[email protected]>
  2025-10-22 09:32   ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-22 10:26     ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Chao Li <[email protected]>
@ 2025-10-22 12:00       ` Tim Waizenegger <[email protected]>
  2025-10-23 09:19         ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Tim Waizenegger @ 2025-10-22 12:00 UTC (permalink / raw)
  To: Chao Li <[email protected]>; +Cc: pgsql-hackers

On Wed, Oct 22, 2025 at 12:27 PM Chao Li <[email protected]> wrote:
>
> Hi Tim,
>
> Thanks for working on this. I haven’t finished reviewing the entire patch. But I got a quick question:
>
> While reviewing a similar patch of pg_get_policy_ddl(), it take the last parameter as a pretty flag. I wonder why pg_get_domain_ddl() doesn’t support an argument for pretty?
>
>

That's a good point; we'll add pretty printing support for consistency
with the other functions. I'll send a new patch in the coming days.

Best regards,
Florin Irion
Tim Waizenegger
EDB (EnterpriseDB)





^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-16 11:04 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement jian he <[email protected]>
  2025-10-22 09:32   ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-22 10:26     ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Chao Li <[email protected]>
  2025-10-22 12:00       ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
@ 2025-10-23 09:19         ` Akshay Joshi <[email protected]>
  2025-11-10 12:44           ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Akshay Joshi @ 2025-10-23 09:19 UTC (permalink / raw)
  To: Tim Waizenegger <[email protected]>; +Cc: Chao Li <[email protected]>; pgsql-hackers

On Wed, 22 Oct, 2025, 17:30 Tim Waizenegger, <
[email protected]> wrote:

> On Wed, Oct 22, 2025 at 12:27 PM Chao Li <[email protected]> wrote:
> >
> > Hi Tim,
> >
> > Thanks for working on this. I haven’t finished reviewing the entire
> patch. But I got a quick question:
> >
> > While reviewing a similar patch of pg_get_policy_ddl(), it take the last
> parameter as a pretty flag. I wonder why pg_get_domain_ddl() doesn’t
> support an argument for pretty?
> >
> >
>
> That's a good point; we'll add pretty printing support for consistency
> with the other functions. I'll send a new patch in the coming days.
>

I've already implemented a generic function for pretty-formatted DDL in the
ruleutils.c file as part of my pg_get_policy_ddl patch. I suggest reusing
it once my patch is accepted and committed by the community.

>
> Best regards,
> Florin Irion
> Tim Waizenegger
> EDB (EnterpriseDB)
>
>
>


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-16 11:04 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement jian he <[email protected]>
  2025-10-22 09:32   ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-22 10:26     ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Chao Li <[email protected]>
  2025-10-22 12:00       ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-23 09:19         ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Akshay Joshi <[email protected]>
@ 2025-11-10 12:44           ` Tim Waizenegger <[email protected]>
  2025-11-11 16:14             ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Florin Irion <[email protected]>
  0 siblings, 1 reply; 21+ messages in thread

From: Tim Waizenegger @ 2025-11-10 12:44 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; Chao Li <[email protected]>; jian he <[email protected]>; +Cc: pgsql-hackers; Florin Irion <[email protected]>

>> On Wed, Oct 22, 2025 at 12:27 PM Chao Li <[email protected]> wrote:
>>
>> > While reviewing a similar patch of pg_get_policy_ddl(), it take the last parameter as a pretty flag. I wonder why pg_get_domain_ddl() doesn’t support an argument for pretty?

We have now added pretty printing support in the latest version; see
attached patch. FYI, we tried to stay consistent in the implementation
with pg_get_policy_ddl from
https://www.postgresql.org/message-id/flat/CANxoLDdJsRJqnjMXV3yjsk07Z5iRWxG-c2hZJC7bAKqf8ZXj_A%40mai...
or

On Thu, Oct 23, 2025 at 11:20 AM Akshay Joshi
<[email protected]> wrote:
>
>> I've already implemented a generic function for pretty-formatted DDL in the ruleutils.c file as part of my pg_get_policy_ddl patch. I suggest reusing it once my patch is accepted and committed by the community.

Thanks Akshay, we adopted your "get_formatted_string()" function into
our path and tried to follow similar implementation patterns as well.

On Thu, Oct 23, 2025 at 6:22 AM jian he <[email protected]> wrote:
>
> I’ve done some refactoring, hope it’s now more intuitive to you.
> Since a domain’s base type can itself be another domain, it’s better to use
>
>     appendStringInfo(&buf, "CREATE DOMAIN %s AS %s",
>                      generate_qualified_type_name(domain_oid),
>                      generate_qualified_type_name(typForm->typbasetype));
>
> then the domain's base type is also fully qualified.

Thanks for the feedback and refactoring Jian! We adopted the
"generate_qualified_type_name" into our patch; this is much better.


> I also refactored the logic for printing domain constraints, which should reduce
> syscache lookups or table scans compared to your version.

we did a lot of refactoring as well while integrating the
pretty-printing support and aligning with e.g. the pg_get_policy_ddl
command. Some of this refactoring follows your suggestiong.
There is one change we decided not to adopt: constructing the
ddl-strings _while_ scanning for constraints in order to optimize the
syscache lookups. The reason is this:

the optimization will save one "SearchSysCache1" per constraint in the
domain. But we still call "pg_get_constraintdef_worker" for each
constraint which does a full table scan.
So in that context, saving the cache lookup seems like a minor
improvement. To us it seemed more desirable to leave the code
unoptimized in this location so that constraint scan and constraint
processing can be decoupled into individual single-purpose
functions/blocks.
Let us know what you think.





Best regards,
Florin Irion
Tim Waizenegger
EDB (EnterpriseDB)


Attachments:

  [application/octet-stream] v3-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch (39.0K, 2-v3-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch)
  download | inline diff:
From e8bd8c712308d59bba19f1236b3d691a718a26b6 Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Thu, 18 Sep 2025 18:52:43 +0200
Subject: [PATCH v3] 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

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.

Reference: PG-151
Author: Florin Irion <[email protected]>
Author: Tim Waizenegger <[email protected]>
Reviewed-by: Álvaro Herrera [email protected]
Reviewed-by: jian he <[email protected]>
Reviewed-by: Chao Li <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  45 ++++
 src/backend/catalog/system_functions.sql |   6 +
 src/backend/utils/adt/ruleutils.c        | 227 ++++++++++++++++
 src/include/catalog/pg_proc.dat          |   3 +
 src/test/regress/expected/object_ddl.out | 328 +++++++++++++++++++++++
 src/test/regress/parallel_schedule       |   2 +-
 src/test/regress/sql/object_ddl.sql      | 135 ++++++++++
 7 files changed, 745 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 d4508114a48..55527f468ae 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ 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></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 2d946d6d9e9..5a96ff1efcb 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -657,6 +657,12 @@ LANGUAGE INTERNAL
 STRICT VOLATILE PARALLEL UNSAFE
 AS 'pg_replication_origin_session_setup';
 
+CREATE OR REPLACE FUNCTION
+ pg_get_domain_ddl(domain_name regtype, pretty bool DEFAULT false)
+ RETURNS text
+ LANGUAGE internal
+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 556ab057e5a..a6f59e6776e 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -546,6 +546,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 ")
 
@@ -13743,3 +13748,225 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * pretty - 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,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+}
+
+
+/*
+ * 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);
+}
+
+/*
+ * 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),
+					 generate_qualified_type_name(typForm->typbasetype));
+
+	/* 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 = pretty ? GET_PRETTY_FLAGS(pretty) : 0;
+
+	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);
+
+	/* 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 5cf9e12fcb9..476874d0063 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,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', 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..9aad54347da
--- /dev/null
+++ b/src/test/regress/expected/object_ddl.out
@@ -0,0 +1,328 @@
+--
+-- 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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog.int4+
+         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)
+
+-- 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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog."numeric" 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 pg_catalog."numeric"+
+         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 pg_catalog._int4 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 pg_catalog._int4+
+         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 pg_catalog."varchar" 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 pg_catalog."varchar"+
+         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 pg_catalog.int4;                                        +
+ 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 pg_catalog.int4;                                      +
+ 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 pg_catalog.int4 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 pg_catalog.int4                                                      +
+         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)
+
+-- Cleanup
+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;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f56482fb9f1..8b6881c397f 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
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import 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..98fb20017ea
--- /dev/null
+++ b/src/test/regress/sql/object_ddl.sql
@@ -0,0 +1,135 @@
+--
+-- 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
+
+-- 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);
+
+-- Cleanup
+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;
-- 
2.50.1 (Apple Git-155)



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-16 11:04 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement jian he <[email protected]>
  2025-10-22 09:32   ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-22 10:26     ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Chao Li <[email protected]>
  2025-10-22 12:00       ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-23 09:19         ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Akshay Joshi <[email protected]>
  2025-11-10 12:44           ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
@ 2025-11-11 16:14             ` Florin Irion <[email protected]>
  2025-11-20 06:55               ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Man Zeng <[email protected]>
  2025-11-20 08:47               ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Chao Li <[email protected]>
  2025-11-20 09:44               ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Neil Chen <[email protected]>
  0 siblings, 3 replies; 21+ messages in thread

From: Florin Irion @ 2025-11-11 16:14 UTC (permalink / raw)
  To: [email protected]

Hello, Cirrus-CI was complaining because we don't sort the constraints 
and thus
they were making the test fail because of the random order.
Made it sort with `list_sort`and `list_oid_cmp`not sure if that's the best
thing to sort them.
Check v4 attached.
Cheers,
Florin Irion
Tim Waizenegger
EDB (EnterpriseDB)

From 2aeca53cbaec510ee54145039742e5d484940895 Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Thu, 18 Sep 2025 18:52:43 +0200
Subject: [PATCH v4] 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

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.

Reference: PG-151
Author: Florin Irion <[email protected]>
Author: Tim Waizenegger <[email protected]>
Reviewed-by: Álvaro Herrera [email protected]
Reviewed-by: jian he <[email protected]>
Reviewed-by: Chao Li <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  45 ++++
 src/backend/catalog/system_functions.sql |   6 +
 src/backend/utils/adt/ruleutils.c        | 233 ++++++++++++++++
 src/include/catalog/pg_proc.dat          |   3 +
 src/test/regress/expected/object_ddl.out | 328 +++++++++++++++++++++++
 src/test/regress/parallel_schedule       |   2 +-
 src/test/regress/sql/object_ddl.sql      | 135 ++++++++++
 7 files changed, 751 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 d4508114a48..55527f468ae 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ 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></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 2d946d6d9e9..5a96ff1efcb 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -657,6 +657,12 @@ LANGUAGE INTERNAL
 STRICT VOLATILE PARALLEL UNSAFE
 AS 'pg_replication_origin_session_setup';
 
+CREATE OR REPLACE FUNCTION
+ pg_get_domain_ddl(domain_name regtype, pretty bool DEFAULT false)
+ RETURNS text
+ LANGUAGE internal
+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 556ab057e5a..34d63f2f502 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -546,6 +546,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 ")
 
@@ -13743,3 +13748,231 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * pretty - 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,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+}
+
+
+/*
+ * 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),
+					 generate_qualified_type_name(typForm->typbasetype));
+
+	/* 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 = pretty ? GET_PRETTY_FLAGS(pretty) : 0;
+
+	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);
+
+	/* 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 5cf9e12fcb9..476874d0063 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,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', 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..9aad54347da
--- /dev/null
+++ b/src/test/regress/expected/object_ddl.out
@@ -0,0 +1,328 @@
+--
+-- 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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog.int4+
+         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)
+
+-- 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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog."numeric" 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 pg_catalog."numeric"+
+         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 pg_catalog._int4 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 pg_catalog._int4+
+         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 pg_catalog."varchar" 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 pg_catalog."varchar"+
+         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 pg_catalog.int4;                                        +
+ 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 pg_catalog.int4;                                      +
+ 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 pg_catalog.int4 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 pg_catalog.int4                                                      +
+         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)
+
+-- Cleanup
+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;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f56482fb9f1..8b6881c397f 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
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import 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..98fb20017ea
--- /dev/null
+++ b/src/test/regress/sql/object_ddl.sql
@@ -0,0 +1,135 @@
+--
+-- 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
+
+-- 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);
+
+-- Cleanup
+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;
-- 
2.45.1



Attachments:

  [text/plain] v4-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch (39.2K, 2-v4-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch)
  download | inline diff:
From 2aeca53cbaec510ee54145039742e5d484940895 Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Thu, 18 Sep 2025 18:52:43 +0200
Subject: [PATCH v4] 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

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.

Reference: PG-151
Author: Florin Irion <[email protected]>
Author: Tim Waizenegger <[email protected]>
Reviewed-by: Álvaro Herrera [email protected]
Reviewed-by: jian he <[email protected]>
Reviewed-by: Chao Li <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  45 ++++
 src/backend/catalog/system_functions.sql |   6 +
 src/backend/utils/adt/ruleutils.c        | 233 ++++++++++++++++
 src/include/catalog/pg_proc.dat          |   3 +
 src/test/regress/expected/object_ddl.out | 328 +++++++++++++++++++++++
 src/test/regress/parallel_schedule       |   2 +-
 src/test/regress/sql/object_ddl.sql      | 135 ++++++++++
 7 files changed, 751 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 d4508114a48..55527f468ae 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ 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></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 2d946d6d9e9..5a96ff1efcb 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -657,6 +657,12 @@ LANGUAGE INTERNAL
 STRICT VOLATILE PARALLEL UNSAFE
 AS 'pg_replication_origin_session_setup';
 
+CREATE OR REPLACE FUNCTION
+ pg_get_domain_ddl(domain_name regtype, pretty bool DEFAULT false)
+ RETURNS text
+ LANGUAGE internal
+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 556ab057e5a..34d63f2f502 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -546,6 +546,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 ")
 
@@ -13743,3 +13748,231 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * pretty - 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,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+}
+
+
+/*
+ * 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),
+					 generate_qualified_type_name(typForm->typbasetype));
+
+	/* 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 = pretty ? GET_PRETTY_FLAGS(pretty) : 0;
+
+	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);
+
+	/* 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 5cf9e12fcb9..476874d0063 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,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', 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..9aad54347da
--- /dev/null
+++ b/src/test/regress/expected/object_ddl.out
@@ -0,0 +1,328 @@
+--
+-- 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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog.int4+
+         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)
+
+-- 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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog."numeric" 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 pg_catalog."numeric"+
+         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 pg_catalog._int4 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 pg_catalog._int4+
+         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 pg_catalog."varchar" 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 pg_catalog."varchar"+
+         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 pg_catalog.int4;                                        +
+ 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 pg_catalog.int4;                                      +
+ 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 pg_catalog.int4 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 pg_catalog.int4                                                      +
+         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)
+
+-- Cleanup
+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;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f56482fb9f1..8b6881c397f 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
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import 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..98fb20017ea
--- /dev/null
+++ b/src/test/regress/sql/object_ddl.sql
@@ -0,0 +1,135 @@
+--
+-- 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
+
+-- 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);
+
+-- Cleanup
+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;
-- 
2.45.1



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-16 11:04 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement jian he <[email protected]>
  2025-10-22 09:32   ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-22 10:26     ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Chao Li <[email protected]>
  2025-10-22 12:00       ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-23 09:19         ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Akshay Joshi <[email protected]>
  2025-11-10 12:44           ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-11-11 16:14             ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Florin Irion <[email protected]>
@ 2025-11-20 06:55               ` Man Zeng <[email protected]>
  2 siblings, 0 replies; 21+ messages in thread

From: Man Zeng @ 2025-11-20 06:55 UTC (permalink / raw)
  To: [email protected]; +Cc: Florin Irion <[email protected]>; Tim Waizenegger <[email protected]>

Quick correction with an apology: I accidentally created a new thread (https://www.postgresql.org/message-id/tencent_64301BB7627E58CD256CE15F%40qq.com) and submitted the patch there—my apologies for the mix-up! Let’s just continue the discussion here as planned.

-- 
Regrads,
Man Zeng

^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-16 11:04 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement jian he <[email protected]>
  2025-10-22 09:32   ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-22 10:26     ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Chao Li <[email protected]>
  2025-10-22 12:00       ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-23 09:19         ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Akshay Joshi <[email protected]>
  2025-11-10 12:44           ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-11-11 16:14             ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Florin Irion <[email protected]>
@ 2025-11-20 08:47               ` Chao Li <[email protected]>
  2025-12-02 21:11                 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Florin Irion <[email protected]>
  2 siblings, 1 reply; 21+ messages in thread

From: Chao Li @ 2025-11-20 08:47 UTC (permalink / raw)
  To: Florin Irion <[email protected]>; +Cc: [email protected]



> On Nov 12, 2025, at 00:14, Florin Irion <[email protected]> wrote:
> 
> Hello, Cirrus-CI was complaining because we don't sort the constraints and thus
> they were making the test fail because of the random order.
> Made it sort with `list_sort`and `list_oid_cmp`not sure if that's the best
> thing to sort them.
> Check v4 attached.
> Cheers,
> Florin Irion
> Tim Waizenegger
> EDB (EnterpriseDB)
> <v4-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch>

I just tested v4, and see two problems:

```
evantest=# CREATE DOMAIN public.int AS pg_catalog.int4;
CREATE DOMAIN
evantest=# SELECT pg_get_domain_ddl('int');
ERROR:  cache lookup failed for type 0
evantest=#
evantest=#
evantest=# SELECT pg_get_domain_ddl('pg_class');
ERROR:  cache lookup failed for type 0
evantest=#
evantest=#
evantest=# SELECT pg_get_domain_ddl('public.int');
               pg_get_domain_ddl
------------------------------------------------
 CREATE DOMAIN public."int" AS pg_catalog.int4;
(1 row)

evantest=# show search_path;
   search_path
-----------------
 "$user", public
(1 row)
```

1. The error message "cache lookup failed for type 0” looks not good. At lease saying something like “domain ‘int’ does not exist”.

2. I created a domain “int” in “public”, as you see, “public” is in the search_path, but SELECT pg_get_domain_ddl('int’); failed.

Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/









^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-16 11:04 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement jian he <[email protected]>
  2025-10-22 09:32   ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-22 10:26     ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Chao Li <[email protected]>
  2025-10-22 12:00       ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-23 09:19         ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Akshay Joshi <[email protected]>
  2025-11-10 12:44           ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-11-11 16:14             ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Florin Irion <[email protected]>
  2025-11-20 08:47               ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Chao Li <[email protected]>
@ 2025-12-02 21:11                 ` Florin Irion <[email protected]>
  0 siblings, 0 replies; 21+ messages in thread

From: Florin Irion @ 2025-12-02 21:11 UTC (permalink / raw)
  To: Chao Li <[email protected]>; Neil Chen <[email protected]>; +Cc: [email protected]


Hello,

On 20/11/25 07:55, Man Zeng wrote:
> Quick correction with an apology: I accidentally created a new thread (https://www.postgresql.org/message-id/tencent_64301BB7627E58CD256CE15F%40qq.com) and submitted the patch there—my apologies for the mix-up! Let’s just continue the discussion here as planned.
On 20/11/25 09:47, Chao Li wrote:
> 1. The error message "cache lookup failed for type 0” looks not good. At lease saying something like “domain ‘int’ does not exist”.
>
> 2. I created a domain “int” in “public”, as you see, “public” is in the search_path, but SELECT pg_get_domain_ddl('int’); failed.

Thank you both Man Zeng and Chao Li for checking this. Changes added in v5.
I don't think there is a way to make the path issue work, so we just 
give more info
to the caller. We exit with error when a built-in name is used and we 
throw also a
hint saying that schema-qualified domain name should be used to be sure 
it's not
conflicting with a built in  name.

On 20/11/25 10:44, Neil Chen wrote:
> Hi Florin,
>
>     +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 = pretty ? GET_PRETTY_FLAGS(pretty) : 0;
>
>
> Seems like we should directly use GET_PRETTY_FLAGS here, as it already 
> checks the value of "pretty". For a "display-oriented" result, using 
> PRETTYFLAG_INDENT looks more appropriate.

Well, actually no,
GET_PRETTY_FLAGS(false) returns PRETTYFLAG_INDENT
But we actually want 0 when pretty is false (no indentation, just spaces)

>     + appendStringInfo(buf, "CREATE DOMAIN %s AS %s",
>     + generate_qualified_type_name(typForm->oid),
>     + generate_qualified_type_name(typForm->typbasetype));
>
>
> It might be good to first call get_typtype to check if it is 
> TYPTYPE_DOMAIN.

I added this in `pg_get_domain_ddl_worker`, as we need to make this 
check ASAP.

Cheers,
Florin Irion
Tim Waizenegger
EDB (EnterpriseDB)



From 30738bef278cd0140e9b3030dde62b5f048a0dfc Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Thu, 18 Sep 2025 18:52:43 +0200
Subject: [PATCH v5] 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

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.

Reference: PG-151
Author: Florin Irion <[email protected]>
Author: Tim Waizenegger <[email protected]>
Reviewed-by: Álvaro Herrera [email protected]
Reviewed-by: jian he <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Neil Chen <[email protected]>
Reviewed-by: Man Zeng <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  53 ++++
 src/backend/catalog/system_functions.sql |   6 +
 src/backend/utils/adt/ruleutils.c        | 240 ++++++++++++++++
 src/include/catalog/pg_proc.dat          |   3 +
 src/test/regress/expected/object_ddl.out | 348 +++++++++++++++++++++++
 src/test/regress/parallel_schedule       |   2 +-
 src/test/regress/sql/object_ddl.sql      | 145 ++++++++++
 7 files changed, 796 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 d4508114a48..4bba7551c21 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,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 2d946d6d9e9..5a96ff1efcb 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -657,6 +657,12 @@ LANGUAGE INTERNAL
 STRICT VOLATILE PARALLEL UNSAFE
 AS 'pg_replication_origin_session_setup';
 
+CREATE OR REPLACE FUNCTION
+ pg_get_domain_ddl(domain_name regtype, pretty bool DEFAULT false)
+ RETURNS text
+ LANGUAGE internal
+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 6cf90be40bb..ae676557210 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -546,6 +546,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 ")
 
@@ -13743,3 +13748,238 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * pretty - 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,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+}
+
+
+/*
+ * 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),
+					 generate_qualified_type_name(typForm->typbasetype));
+
+	/* 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 = pretty ? GET_PRETTY_FLAGS(pretty) : 0;
+
+	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 66af2d96d67..2f7869103f3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,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', 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..1241b89a770
--- /dev/null
+++ b/src/test/regress/expected/object_ddl.out
@@ -0,0 +1,348 @@
+--
+-- 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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog."numeric" 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 pg_catalog."numeric"+
+         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 pg_catalog._int4 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 pg_catalog._int4+
+         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 pg_catalog."varchar" 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 pg_catalog."varchar"+
+         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 pg_catalog.int4;                                        +
+ 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 pg_catalog.int4;                                      +
+ 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 pg_catalog.int4 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 pg_catalog.int4                                                      +
+         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 pg_catalog.int4;
+(1 row)
+
+-- Cleanup
+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 cc6d799bcea..5a615806dcf 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
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct pg_dependencies 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..4eb0e5b86d6
--- /dev/null
+++ b/src/test/regress/sql/object_ddl.sql
@@ -0,0 +1,145 @@
+--
+-- 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');
+
+-- Cleanup
+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



Attachments:

  [text/plain] v5-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch (41.7K, 2-v5-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch)
  download | inline diff:
From 30738bef278cd0140e9b3030dde62b5f048a0dfc Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Thu, 18 Sep 2025 18:52:43 +0200
Subject: [PATCH v5] 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

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.

Reference: PG-151
Author: Florin Irion <[email protected]>
Author: Tim Waizenegger <[email protected]>
Reviewed-by: Álvaro Herrera [email protected]
Reviewed-by: jian he <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Neil Chen <[email protected]>
Reviewed-by: Man Zeng <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  53 ++++
 src/backend/catalog/system_functions.sql |   6 +
 src/backend/utils/adt/ruleutils.c        | 240 ++++++++++++++++
 src/include/catalog/pg_proc.dat          |   3 +
 src/test/regress/expected/object_ddl.out | 348 +++++++++++++++++++++++
 src/test/regress/parallel_schedule       |   2 +-
 src/test/regress/sql/object_ddl.sql      | 145 ++++++++++
 7 files changed, 796 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 d4508114a48..4bba7551c21 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,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 2d946d6d9e9..5a96ff1efcb 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -657,6 +657,12 @@ LANGUAGE INTERNAL
 STRICT VOLATILE PARALLEL UNSAFE
 AS 'pg_replication_origin_session_setup';
 
+CREATE OR REPLACE FUNCTION
+ pg_get_domain_ddl(domain_name regtype, pretty bool DEFAULT false)
+ RETURNS text
+ LANGUAGE internal
+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 6cf90be40bb..ae676557210 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -546,6 +546,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 ")
 
@@ -13743,3 +13748,238 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * pretty - 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,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+}
+
+
+/*
+ * 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),
+					 generate_qualified_type_name(typForm->typbasetype));
+
+	/* 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 = pretty ? GET_PRETTY_FLAGS(pretty) : 0;
+
+	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 66af2d96d67..2f7869103f3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,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', 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..1241b89a770
--- /dev/null
+++ b/src/test/regress/expected/object_ddl.out
@@ -0,0 +1,348 @@
+--
+-- 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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog."numeric" 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 pg_catalog."numeric"+
+         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 pg_catalog._int4 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 pg_catalog._int4+
+         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 pg_catalog."varchar" 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 pg_catalog."varchar"+
+         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 pg_catalog.int4;                                        +
+ 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 pg_catalog.int4;                                      +
+ 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 pg_catalog.int4 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 pg_catalog.int4                                                      +
+         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 pg_catalog.int4;
+(1 row)
+
+-- Cleanup
+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 cc6d799bcea..5a615806dcf 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
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import pg_ndistinct pg_dependencies 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..4eb0e5b86d6
--- /dev/null
+++ b/src/test/regress/sql/object_ddl.sql
@@ -0,0 +1,145 @@
+--
+-- 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');
+
+-- Cleanup
+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



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-16 11:04 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement jian he <[email protected]>
  2025-10-22 09:32   ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-22 10:26     ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Chao Li <[email protected]>
  2025-10-22 12:00       ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-23 09:19         ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Akshay Joshi <[email protected]>
  2025-11-10 12:44           ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-11-11 16:14             ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Florin Irion <[email protected]>
@ 2025-11-20 09:44               ` Neil Chen <[email protected]>
  2 siblings, 0 replies; 21+ messages in thread

From: Neil Chen @ 2025-11-20 09:44 UTC (permalink / raw)
  To: Florin Irion <[email protected]>; +Cc: [email protected]

Hi Florin,

+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 = pretty ? GET_PRETTY_FLAGS(pretty) : 0;


Seems like we should directly use GET_PRETTY_FLAGS here, as it already
checks the value of "pretty". For a "display-oriented" result, using
PRETTYFLAG_INDENT looks more appropriate.

+ appendStringInfo(buf, "CREATE DOMAIN %s AS %s",
> + generate_qualified_type_name(typForm->oid),
> + generate_qualified_type_name(typForm->typbasetype));


It might be good to first call get_typtype to check if it is TYPTYPE_DOMAIN.


^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2025-10-16 11:04 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement jian he <[email protected]>
  2025-10-22 09:32   ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
@ 2025-10-23 04:21     ` jian he <[email protected]>
  1 sibling, 0 replies; 21+ messages in thread

From: jian he @ 2025-10-23 04:21 UTC (permalink / raw)
  To: Tim Waizenegger <[email protected]>; +Cc: pgsql-hackers

On Wed, Oct 22, 2025 at 5:32 PM Tim Waizenegger
<[email protected]> wrote:
>
> updated patch is attached
>

I’ve done some refactoring, hope it’s now more intuitive to you.
Since a domain’s base type can itself be another domain, it’s better to use

    appendStringInfo(&buf, "CREATE DOMAIN %s AS %s",
                     generate_qualified_type_name(domain_oid),
                     generate_qualified_type_name(typForm->typbasetype));

then the domain's base type is also fully qualified.

I also refactored the logic for printing domain constraints, which should reduce
syscache lookups or table scans compared to your version.

please check the attached.


Attachments:

  [application/octet-stream] v2-0001-refactor-pg_get_domain_ddl.no-cfbot (25.4K, 2-v2-0001-refactor-pg_get_domain_ddl.no-cfbot)
  download

^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
@ 2026-02-19 00:10 ` Tom Lane <[email protected]>
  2026-02-19 00:40   ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Haritabh Gupta <[email protected]>
  2026-02-20 13:24   ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Andrew Dunstan <[email protected]>
  2026-03-02 16:41   ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Florin Irion <[email protected]>
  1 sibling, 3 replies; 21+ messages in thread

From: Tom Lane @ 2026-02-19 00:10 UTC (permalink / raw)
  To: Haritabh Gupta <[email protected]>; +Cc: [email protected]; Florin Irion <[email protected]>; Tim Waizenegger <[email protected]>

Haritabh Gupta <[email protected]> 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






^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2026-02-19 00:10 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tom Lane <[email protected]>
@ 2026-02-19 00:40   ` Haritabh Gupta <[email protected]>
  2 siblings, 0 replies; 21+ messages in thread

From: Haritabh Gupta @ 2026-02-19 00:40 UTC (permalink / raw)
  To: [email protected]; +Cc: Florin Irion <[email protected]>; Tim Waizenegger <[email protected]>

Hi,

Tom Lane <[email protected]> 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%40mai...

^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2026-02-19 00:10 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tom Lane <[email protected]>
@ 2026-02-20 13:24   ` Andrew Dunstan <[email protected]>
  2 siblings, 0 replies; 21+ messages in thread

From: Andrew Dunstan @ 2026-02-20 13:24 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; Haritabh Gupta <[email protected]>; +Cc: [email protected]; Florin Irion <[email protected]>; Tim Waizenegger <[email protected]>


On 2026-02-18 We 7:10 PM, Tom Lane wrote:
> Haritabh Gupta <[email protected]> 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.



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







^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
  2026-02-19 00:10 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tom Lane <[email protected]>
@ 2026-03-02 16:41   ` Florin Irion <[email protected]>
  2 siblings, 0 replies; 21+ messages in thread

From: Florin Irion @ 2026-03-02 16:41 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; Haritabh Gupta <[email protected]>; +Cc: [email protected]

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:

> 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.

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.

> 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.

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 protected]>
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 protected]>
Author: Tim Waizenegger <[email protected]>
Reviewed-by: Álvaro Herrera [email protected]
Reviewed-by: jian he <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Neil Chen <[email protected]>
Reviewed-by: Man Zeng <[email protected]>
Reviewed-by: Haritabh <Gupta [email protected]>
---
 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



Attachments:

  [text/plain] v8-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch (47.6K, 2-v8-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch)
  download | inline diff:
From 8e3683a199595ac6100eabd459ab9cbcc858f0ab Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
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 protected]>
Author: Tim Waizenegger <[email protected]>
Reviewed-by: Álvaro Herrera [email protected]
Reviewed-by: jian he <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Neil Chen <[email protected]>
Reviewed-by: Man Zeng <[email protected]>
Reviewed-by: Haritabh <Gupta [email protected]>
---
 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



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
@ 2025-11-20 06:42 =?ISO-8859-1?B?emVuZ21hbg==?= <[email protected]>
  0 siblings, 0 replies; 21+ messages in thread

From: =?ISO-8859-1?B?emVuZ21hbg==?= @ 2025-11-20 06:42 UTC (permalink / raw)
  To: =?ISO-8859-1?B?dGltLndhaXplbmVnZ2Vy?= <[email protected]>; =?ISO-8859-1?B?ZmxvcmluLmlyaW9u?= <[email protected]>; +Cc: =?ISO-8859-1?B?cGdzcWwtaGFja2Vycw==?= <[email protected]>

Hello everyone, 

I have tested the v4 patch and observed that the following scenario requires support. Accordingly, I have prepared version v5: v5-0001 is fully consistent with the v4 patch, and v5-0002 restricts input to domains exclusively, with an additional simple test case attached.

```sql
postgres=# create table test_table(a int);
CREATE TABLE
postgres=# SELECT pg_get_domain_ddl('test_table');
ERROR:  cache lookup failed for type 0
```

-- 
Regrads,
Man Zeng

Attachments:

  [application/octet-stream] v5-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch (39.2K, 2-v5-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch)
  download | inline diff:
From 19361cbd9833a460d142bc5ccfea95885c954e90 Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Thu, 18 Sep 2025 18:52:43 +0200
Subject: [PATCH v5 1/2] 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

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.

Reference: PG-151
Author: Florin Irion <[email protected]>
Author: Tim Waizenegger <[email protected]>
Reviewed-by: Ã�lvaro Herrera [email protected]
Reviewed-by: jian he <[email protected]>
Reviewed-by: Chao Li <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  45 ++++
 src/backend/catalog/system_functions.sql |   6 +
 src/backend/utils/adt/ruleutils.c        | 233 ++++++++++++++++
 src/include/catalog/pg_proc.dat          |   3 +
 src/test/regress/expected/object_ddl.out | 328 +++++++++++++++++++++++
 src/test/regress/parallel_schedule       |   2 +-
 src/test/regress/sql/object_ddl.sql      | 135 ++++++++++
 7 files changed, 751 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 d4508114a48..55527f468ae 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,49 @@ 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></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 2d946d6d9e9..5a96ff1efcb 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -657,6 +657,12 @@ LANGUAGE INTERNAL
 STRICT VOLATILE PARALLEL UNSAFE
 AS 'pg_replication_origin_session_setup';
 
+CREATE OR REPLACE FUNCTION
+ pg_get_domain_ddl(domain_name regtype, pretty bool DEFAULT false)
+ RETURNS text
+ LANGUAGE internal
+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 556ab057e5a..34d63f2f502 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -546,6 +546,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 ")
 
@@ -13743,3 +13748,231 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * pretty - 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,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with tabs */
+		for (int i = 0; i < noOfTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+}
+
+
+/*
+ * 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),
+					 generate_qualified_type_name(typForm->typbasetype));
+
+	/* 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 = pretty ? GET_PRETTY_FLAGS(pretty) : 0;
+
+	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);
+
+	/* 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 aaadfd8c748..b0f3e7229ca 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8515,6 +8515,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', 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..9aad54347da
--- /dev/null
+++ b/src/test/regress/expected/object_ddl.out
@@ -0,0 +1,328 @@
+--
+-- 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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog.int4+
+         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)
+
+-- 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 pg_catalog.int4 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 pg_catalog.int4+
+         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 pg_catalog.int4 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 pg_catalog."numeric" 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 pg_catalog."numeric"+
+         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 pg_catalog._int4 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 pg_catalog._int4+
+         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 pg_catalog."varchar" 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 pg_catalog."varchar"+
+         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 pg_catalog.int4;                                        +
+ 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 pg_catalog.int4;                                      +
+ 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 pg_catalog.int4 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 pg_catalog.int4                                                      +
+         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)
+
+-- Cleanup
+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;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index f56482fb9f1..8b6881c397f 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
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import 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..98fb20017ea
--- /dev/null
+++ b/src/test/regress/sql/object_ddl.sql
@@ -0,0 +1,135 @@
+--
+-- 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
+
+-- 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);
+
+-- Cleanup
+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;
-- 
2.43.0



  [application/octet-stream] v5-0002-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch (2.7K, 3-v5-0002-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch)
  download | inline diff:
From d000fabf78f875d484a747859fc9a101735f7d7e Mon Sep 17 00:00:00 2001
From: zengman <[email protected]>
Date: Thu, 20 Nov 2025 14:29:27 +0800
Subject: [PATCH v5 2/2] Add pg_get_domain_ddl() function to reconstruct CREATE
  DOMAIN statements

---
 src/backend/utils/adt/ruleutils.c        |  5 +++++
 src/test/regress/expected/object_ddl.out | 15 +++++++++++++++
 src/test/regress/sql/object_ddl.sql      |  6 ++++++
 3 files changed, 26 insertions(+)

diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 34d63f2f502..bcd6d69672d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -13954,6 +13954,11 @@ pg_get_domain_ddl_worker(Oid domain_oid, int prettyFlags)
 		return NULL;
 
 	typForm = (Form_pg_type) GETSTRUCT(typeTuple);
+	if (typForm->typtype != TYPTYPE_DOMAIN)
+	{
+		ReleaseSysCache(typeTuple);
+		return NULL;
+	}
 
 	/* Get default expression */
 	defaultExpr = get_typdefault(domain_oid);
diff --git a/src/test/regress/expected/object_ddl.out b/src/test/regress/expected/object_ddl.out
index 9aad54347da..8ea86bcfc96 100644
--- a/src/test/regress/expected/object_ddl.out
+++ b/src/test/regress/expected/object_ddl.out
@@ -306,6 +306,20 @@ SELECT pg_get_domain_ddl('regress_domain_with_collate', pretty => true);
          COLLATE "C";
 (1 row)
 
+-- Test input is not DOMAIN
+CREATE TABLE domain_test (a int, b int);
+SELECT pg_get_domain_ddl('domain_test');
+ pg_get_domain_ddl 
+-------------------
+ 
+(1 row)
+
+SELECT pg_get_domain_ddl('domain_test', pretty => true);
+ pg_get_domain_ddl 
+-------------------
+ 
+(1 row)
+
 -- Cleanup
 DROP DOMAIN regress_us_postal_code;
 DROP DOMAIN regress_domain_not_null;
@@ -326,3 +340,4 @@ DROP DOMAIN regress_simple_domain;
 DROP DOMAIN regress_domain_not_valid;
 DROP DOMAIN regress_domain_mixed;
 DROP DOMAIN regress_domain_with_collate;
+DROP TABLE domain_test;
diff --git a/src/test/regress/sql/object_ddl.sql b/src/test/regress/sql/object_ddl.sql
index 98fb20017ea..f18d1c4322e 100644
--- a/src/test/regress/sql/object_ddl.sql
+++ b/src/test/regress/sql/object_ddl.sql
@@ -113,6 +113,11 @@ 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 input is not DOMAIN
+CREATE TABLE domain_test (a int, b int);
+SELECT pg_get_domain_ddl('domain_test');
+SELECT pg_get_domain_ddl('domain_test', pretty => true);
+
 -- Cleanup
 DROP DOMAIN regress_us_postal_code;
 DROP DOMAIN regress_domain_not_null;
@@ -133,3 +138,4 @@ DROP DOMAIN regress_simple_domain;
 DROP DOMAIN regress_domain_not_valid;
 DROP DOMAIN regress_domain_mixed;
 DROP DOMAIN regress_domain_with_collate;
+DROP TABLE domain_test;
-- 
2.43.0



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
@ 2026-03-02 18:56 Tom Lane <[email protected]>
  2026-03-03 17:35 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Florin Irion <[email protected]>
  2026-03-12 01:20 ` Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Japin Li <[email protected]>
  0 siblings, 2 replies; 21+ messages in thread

From: Tom Lane @ 2026-03-02 18:56 UTC (permalink / raw)
  To: Álvaro Herrera <[email protected]>; +Cc: Florin Irion <[email protected]>; Haritabh Gupta <[email protected]>; [email protected]

=?utf-8?Q?=C3=81lvaro?= Herrera <[email protected]> writes:
> I think it would be more helpful to have a test module that

> 1. installs an event trigger on ddl_command_end for CREATE for
>    object being created
> 2. runs all the tests in parallel_schedule
> 3. do [... something ...] with the event trigger to generate the DDL
>    using the new functions, and compare with the object created
>    originally.  (There's a lot of handwaving here.  Maybe pg_dump both
>    and compare?)

While I agree that automating this might be helpful, please please
please do not create yet another execution of the core regression
tests.  There is far too much stuff in there that is not DDL and
will only be useless cycles for this purpose.

I wonder if it'd be practical to extract just the DDL commands from
the core scripts, and then run just those through a process like
you suggest?

I agree that the "handwaving" part is trickier than it looks.
If memory serves, we've had bugs-of-omission where somebody
forgot to update pg_dump for some new feature, and it wasn't
obvious because comparing pg_dump output against pg_dump
output didn't show that the relevant object property wasn't
copied correctly.  In this context, forgetting to update both
pg_dump and the DDL-dumping function would mask both omissions.
Maybe that's unlikely, but ...

> Another possibility is to use the pg_dump/t/002_pg_dump.pl database
> instead of the stock regression one, which is perhaps richer in object
> type diversity.

I think that test script also suffers from the out-of-sight,
out-of-mind problem.  Not to mention that you need a lot of study
to figure out how to modify it at all.  I certainly avoid doing so.

			regards, tom lane





^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2026-03-02 18:56 Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tom Lane <[email protected]>
@ 2026-03-03 17:35 ` Florin Irion <[email protected]>
  1 sibling, 0 replies; 21+ messages in thread

From: Florin Irion @ 2026-03-03 17:35 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; Álvaro Herrera <[email protected]>; +Cc: [email protected]

Tom, Álvaro, thanks for the direction — I think the v9 patch addresses
both of your concerns nicely.
   
No extra test run. The event trigger is installed once in
test_setup.sql, so it piggybacks on the existing regression suite.
Every CREATE that fires ddl_command_end is checked inline —
there is no separate execution of the core tests.

Developers don't have to remember anything. The trigger inspects
command_tag to derive the object type, then probes pg_catalog for a
matching pg_get_<type>_ddl() function. If one exists it round-trips
the object right there; if not it silently moves on. Adding a new
pg_get_type_ddl() or pg_get_sequence_ddl() in the future
automatically extends coverage to every CREATE of that type across
the entire suite — zero changes to the trigger or to existing tests.

Inline verification at creation time. The trigger does:
get DDL → DROP → CREATE from DDL → get DDL → ASSERT match
Because it runs at creation time, nothing yet depends on the new object,
so the drop/recreate is safe. A session-local GUC guards against
recursion (the recreate fires the trigger again).

Bugs of omission. Tom raised the concern that comparing DDL output
against DDL output could mask a missing property. The key thing here is
that the test suite continues running with the recreated object. If
the DDL function omits, say, a CHECK constraint, the recreated domain
silently loses it — and any subsequent test that exercises that
constraint will fail. So omissions surface as unexpected failures
elsewhere in the suite, not just in the DDL comparison itself.

With the current patch, 160 domains across 33 test files are
automatically round-tripped. The dedicated object_ddl.sql file is
gone — a small set of pg_get_domain_ddl() output-format tests (pretty
printing, quoted identifiers, NOT VALID rendering, built-in type name
shadowing, error cases) now lives in domain.sql alongside the rest
of the domain coverage.

v9 attached.

While working on this I bumped into an unrelated crash and started a
new thread [1] for it.

[1] https://www.postgresql.org/message-id/c6fff161-9aee-4290-9ada-71e21e4d84de%40gmail.com


--

Cheers,
Florin

EDB -- www.enterprisedb.com

From c65f5a1d7bc6893cf703e44db1f5d4267341dae2 Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Thu, 18 Sep 2025 18:52:43 +0200
Subject: [PATCH v9] 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 constraints are handled with an extra ALTER command
* Properly quotes identifiers and schema names
* Handles complex constraint expressions
* Pretty printing support
* Warns 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.

Regression tests for pg_get_domain_ddl() output are included in domain.sql,
covering pretty-printing, quoted identifiers, NOT VALID constraints, domain
shadowing built-in type names, and error cases.

A global event trigger installed in test_setup.sql automatically round-trips
every CREATE command whose object type has a matching pg_get_<type>_ddl()
function in pg_catalog. On each CREATE, the trigger extracts the DDL, drops
the object, recreates it from the DDL, and asserts the output is identical.
Because it runs inline at creation time, even objects that are later dropped
get verified. A session-local GUC guards against recursion. This gives
automatic round-trip coverage to every domain across the entire regression
suite — and extends to future pg_get_<type>_ddl() functions with zero
additional effort.

Reference: PG-151
Author: Florin Irion <[email protected]>
Author: Tim Waizenegger <[email protected]>
Reviewed-by: Álvaro Herrera [email protected]
Reviewed-by: jian he <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Neil Chen <[email protected]>
Reviewed-by: Man Zeng <[email protected]>
Reviewed-by: Haritabh <Gupta [email protected]>
---
 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/domain.out        |  78 ++++++
 src/test/regress/expected/event_trigger.out |  13 +-
 src/test/regress/expected/test_setup.out    |  50 ++++
 src/test/regress/sql/domain.sql             |  37 +++
 src/test/regress/sql/test_setup.sql         |  52 ++++
 9 files changed, 547 insertions(+), 6 deletions(-)

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/domain.out b/src/test/regress/expected/domain.out
index 62a48a523a2..6dd6408fd75 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -1378,6 +1378,84 @@ 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 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;
+(1 row)
+
+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(NULL);  -- 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.
+--
 -- 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 16e4530708c..8c7085e3b2c 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -785,12 +785,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..0d58cbb628b 100644
--- a/src/test/regress/expected/test_setup.out
+++ b/src/test/regress/expected/test_setup.out
@@ -235,3 +235,53 @@ 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;
+
+    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);
+
+                EXECUTE format('SELECT pg_get_%s_ddl(%L)', obj_type, r.object_identity)
+                    INTO original;
+                EXECUTE format('DROP %s %s', obj_type, r.object_identity);
+                EXECUTE original;
+                EXECUTE format('SELECT pg_get_%s_ddl(%L)', 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;
+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..dd0993a0c84 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -894,6 +894,43 @@ 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 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(NULL);  -- 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
+
 --
 -- Information schema
 --
diff --git a/src/test/regress/sql/test_setup.sql b/src/test/regress/sql/test_setup.sql
index 5854399a028..16bfc689348 100644
--- a/src/test/regress/sql/test_setup.sql
+++ b/src/test/regress/sql/test_setup.sql
@@ -289,3 +289,55 @@ 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;
+
+    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);
+
+                EXECUTE format('SELECT pg_get_%s_ddl(%L)', obj_type, r.object_identity)
+                    INTO original;
+                EXECUTE format('DROP %s %s', obj_type, r.object_identity);
+                EXECUTE original;
+                EXECUTE format('SELECT pg_get_%s_ddl(%L)', 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;
+END;
+$$;
+
+CREATE EVENT TRIGGER regress_ddl_roundtrip_trigger ON ddl_command_end
+    EXECUTE FUNCTION regress_ddl_roundtrip_trigger_func();
-- 
2.45.1



Attachments:

  [text/plain] v9-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch (28.1K, 2-v9-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CRE.patch)
  download | inline diff:
From c65f5a1d7bc6893cf703e44db1f5d4267341dae2 Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Thu, 18 Sep 2025 18:52:43 +0200
Subject: [PATCH v9] 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 constraints are handled with an extra ALTER command
* Properly quotes identifiers and schema names
* Handles complex constraint expressions
* Pretty printing support
* Warns 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.

Regression tests for pg_get_domain_ddl() output are included in domain.sql,
covering pretty-printing, quoted identifiers, NOT VALID constraints, domain
shadowing built-in type names, and error cases.

A global event trigger installed in test_setup.sql automatically round-trips
every CREATE command whose object type has a matching pg_get_<type>_ddl()
function in pg_catalog. On each CREATE, the trigger extracts the DDL, drops
the object, recreates it from the DDL, and asserts the output is identical.
Because it runs inline at creation time, even objects that are later dropped
get verified. A session-local GUC guards against recursion. This gives
automatic round-trip coverage to every domain across the entire regression
suite — and extends to future pg_get_<type>_ddl() functions with zero
additional effort.

Reference: PG-151
Author: Florin Irion <[email protected]>
Author: Tim Waizenegger <[email protected]>
Reviewed-by: Álvaro Herrera [email protected]
Reviewed-by: jian he <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Neil Chen <[email protected]>
Reviewed-by: Man Zeng <[email protected]>
Reviewed-by: Haritabh <Gupta [email protected]>
---
 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/domain.out        |  78 ++++++
 src/test/regress/expected/event_trigger.out |  13 +-
 src/test/regress/expected/test_setup.out    |  50 ++++
 src/test/regress/sql/domain.sql             |  37 +++
 src/test/regress/sql/test_setup.sql         |  52 ++++
 9 files changed, 547 insertions(+), 6 deletions(-)

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/domain.out b/src/test/regress/expected/domain.out
index 62a48a523a2..6dd6408fd75 100644
--- a/src/test/regress/expected/domain.out
+++ b/src/test/regress/expected/domain.out
@@ -1378,6 +1378,84 @@ 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 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;
+(1 row)
+
+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(NULL);  -- 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.
+--
 -- 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 16e4530708c..8c7085e3b2c 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -785,12 +785,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..0d58cbb628b 100644
--- a/src/test/regress/expected/test_setup.out
+++ b/src/test/regress/expected/test_setup.out
@@ -235,3 +235,53 @@ 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;
+
+    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);
+
+                EXECUTE format('SELECT pg_get_%s_ddl(%L)', obj_type, r.object_identity)
+                    INTO original;
+                EXECUTE format('DROP %s %s', obj_type, r.object_identity);
+                EXECUTE original;
+                EXECUTE format('SELECT pg_get_%s_ddl(%L)', 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;
+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..dd0993a0c84 100644
--- a/src/test/regress/sql/domain.sql
+++ b/src/test/regress/sql/domain.sql
@@ -894,6 +894,43 @@ 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 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(NULL);  -- 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
+
 --
 -- Information schema
 --
diff --git a/src/test/regress/sql/test_setup.sql b/src/test/regress/sql/test_setup.sql
index 5854399a028..16bfc689348 100644
--- a/src/test/regress/sql/test_setup.sql
+++ b/src/test/regress/sql/test_setup.sql
@@ -289,3 +289,55 @@ 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;
+
+    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);
+
+                EXECUTE format('SELECT pg_get_%s_ddl(%L)', obj_type, r.object_identity)
+                    INTO original;
+                EXECUTE format('DROP %s %s', obj_type, r.object_identity);
+                EXECUTE original;
+                EXECUTE format('SELECT pg_get_%s_ddl(%L)', 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;
+END;
+$$;
+
+CREATE EVENT TRIGGER regress_ddl_roundtrip_trigger ON ddl_command_end
+    EXECUTE FUNCTION regress_ddl_roundtrip_trigger_func();
-- 
2.45.1



^ permalink  raw  reply  [nested|flat] 21+ messages in thread

* Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement
  2026-03-02 18:56 Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tom Lane <[email protected]>
@ 2026-03-12 01:20 ` Japin Li <[email protected]>
  1 sibling, 0 replies; 21+ messages in thread

From: Japin Li @ 2026-03-12 01:20 UTC (permalink / raw)
  To: Florin Irion <[email protected]>; +Cc: Tom Lane <[email protected]>; Álvaro Herrera <[email protected]>; Haritabh Gupta <[email protected]>; [email protected]


Hi, Florin

On Tue, 10 Mar 2026 at 16:01, Florin Irion <[email protected]> wrote:
> Hello,
>
> v10 rebased on latest master attached.
>

Thanks for updating the patch. When I reviewed [1], I noticed that
pg_get_database_ddl() uses spaces for indentation instead of tabs.
What do you think?

1.
+    /* Add valid constraints */
+    foreach(lc, validConstraints)
+    {
+        Oid         constraintOid = lfirst_oid(lc);

IMO, I think we can replace foreach() with foreach_oid().

2.
+    foreach(lc, invalidConstraints)
+    {
+        Oid         constraintOid = lfirst_oid(lc);

Same as above.

[1] https://www.postgresql.org/message-id/CANxoLDc6FHBYJvcgOnZyS%2BjF0NUo3Lq_83-rttBuJgs9id_UDg%40mail.g...

> Cheers,
> Florin
>
>
> www.enterprisedb.com
>
> [2. text/plain; v10-0001-Add-pg_get_domain_ddl-function-to-reconstruct-CR.patch]...

-- 
Regards,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.





^ permalink  raw  reply  [nested|flat] 21+ messages in thread


end of thread, other threads:[~2026-03-12 01:20 UTC | newest]

Thread overview: 21+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2025-10-16 09:16 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tim Waizenegger <[email protected]>
2025-10-16 11:04 ` jian he <[email protected]>
2025-10-22 09:32   ` Tim Waizenegger <[email protected]>
2025-10-22 10:26     ` Chao Li <[email protected]>
2025-10-22 12:00       ` Tim Waizenegger <[email protected]>
2025-10-23 09:19         ` Akshay Joshi <[email protected]>
2025-11-10 12:44           ` Tim Waizenegger <[email protected]>
2025-11-11 16:14             ` Florin Irion <[email protected]>
2025-11-20 06:55               ` Man Zeng <[email protected]>
2025-11-20 08:47               ` Chao Li <[email protected]>
2025-12-02 21:11                 ` Florin Irion <[email protected]>
2025-11-20 09:44               ` Neil Chen <[email protected]>
2025-10-23 04:21     ` jian he <[email protected]>
2026-02-19 00:10 ` Tom Lane <[email protected]>
2026-02-19 00:40   ` Haritabh Gupta <[email protected]>
2026-02-20 13:24   ` Andrew Dunstan <[email protected]>
2026-03-02 16:41   ` Florin Irion <[email protected]>
2025-11-20 06:42 [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement =?ISO-8859-1?B?emVuZ21hbg==?= <[email protected]>
2026-03-02 18:56 Re: [PATCH] pg_get_domain_ddl: DDL reconstruction function for CREATE DOMAIN statement Tom Lane <[email protected]>
2026-03-03 17:35 ` Florin Irion <[email protected]>
2026-03-12 01:20 ` Japin Li <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox