From 6dbbf85dfe261c15145e857c9ee5535c1e591545 Mon Sep 17 00:00:00 2001
From: Florin Irion <florin.irion@enterprisedb.com>
Date: Thu, 18 Sep 2025 18:52:43 +0200
Subject: [PATCH 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 <florin.irion@enterprisedb.com>
Author: Tim Waizenegger <tim.waizenegger@enterprisedb.com>
Reviewed-by: Álvaro Herrera alvherre@alvh.no-ip.org
---
 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)

