public inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
38+ messages / 9 participants
[nested] [flat]

* [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-12 12:04  Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Akshay Joshi @ 2025-11-12 12:04 UTC (permalink / raw)
  To: pgsql-hackers

Hi Hackers,

I’m submitting a patch as part of the broader Retail DDL Functions project
described by Andrew Dunstan
https://www.postgresql.org/message-id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9%40dunslane.net

This patch adds a new system function
pg_get_database_ddl(database_name/database_oid, pretty), which reconstructs
the CREATE DATABASE statement for a given database name or database oid.
When the pretty flag is set to true, the function returns a neatly
formatted, multi-line DDL statement instead of a single-line statement.

*Usage examples:*

1) SELECT pg_get_database_ddl('test_get_database_ddl_builtin');  -- *non-pretty
formatted DDL*

                                                        pg_get_database_ddl


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  CREATE DATABASE test_get_database_ddl_builtin WITH OWNER =
regress_ddl_database ENCODING = "UTF8" LC_COLLATE = "C" LC_CTYPE = "C"
BUILTIN_LOCALE = "C.UTF-8" COLLATION_VERSION = "1" LOCALE_PROVIDER =
'builtin' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT
= -1;


2) SELECT pg_get_database_ddl('test_get_database_ddl_builtin', true);
 -- *pretty
formatted DDL*

CREATE DATABASE test_get_database_ddl_builtin
         WITH
         OWNER = regress_ddl_database
         ENCODING = "UTF8"
         LC_COLLATE = "C"
         LC_CTYPE = "C"
         BUILTIN_LOCALE = "C.UTF-8"
         COLLATION_VERSION = "1"
         LOCALE_PROVIDER = 'builtin'
         TABLESPACE = pg_default
         ALLOW_CONNECTIONS = true
         CONNECTION LIMIT = -1;

3) SELECT pg_get_database_ddl(16835);      -- *non-pretty formatted DDL for
OID*
4) SELECT pg_get_database_ddl(16835, true);  -- *pretty formatted DDL for
OID*

The patch includes documentation, in-code comments, and regression tests,
all of which pass successfully.

*Note:* To run the regression tests, particularly the pg_upgrade tests
successfully, I had to add a helper function, ddl_filter (in database.sql),
which removes locale and collation-related information from the
pg_get_database_ddl output.

-----
Regards,
Akshay Joshi
EDB (EnterpriseDB)


Attachments:

  [application/octet-stream] 0001-Add-pg_get_database_ddl-function-to-reconstruct-CREATE.patch (18.3K, 3-0001-Add-pg_get_database_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From 7a0f292c4883c2b21de318702f3de4df0dcc42ab Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Wed, 24 Sep 2025 17:47:59 +0530
Subject: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE
 DATABASE statements.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This adds a new system function, pg_get_database_ddl(database_name/database_oid, pretty),
which reconstructs the CREATE DATABASE statement for a given database name or database oid.

Usage:
  SELECT pg_get_database_ddl('postgres'); // Non pretty-formatted DDL
  SELECT pg_get_database_ddl(16835); // Non pretty-formatted DDL
  SELECT pg_get_database_ddl('postgres', true); // pretty-formatted DDL
  SELECT pg_get_database_ddl(16835, true); // pretty-formatted DDL

Reference: PG-150
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  70 ++++++++
 src/backend/catalog/system_functions.sql |  12 ++
 src/backend/utils/adt/ruleutils.c        | 204 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   6 +
 src/test/regress/expected/database.out   |  76 +++++++++
 src/test/regress/sql/database.sql        |  62 +++++++
 6 files changed, 430 insertions(+)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..88fe04e649d 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,74 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </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_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>database_name</parameter> <type>name</type>, <optional> <parameter>pretty</parameter> <type>boolean</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE DATABASE</command> statement from the
+        system catalogs for a specified database name. The result is a
+        comprehensive <command>CREATE DATABASE</command> statement.
+       </para></entry>
+      </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>database_oid</parameter> <type>oid</type>, <optional> <parameter>pretty</parameter> <type>boolean</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE DATABASE</command> statement from the
+        system catalogs for a specified database oid. The result is a
+        comprehensive <command>CREATE DATABASE</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+   Most of the functions that reconstruct (decompile) database objects have an
+   optional <parameter>pretty</parameter> flag, which if
+   <literal>true</literal> causes the result to be
+   <quote>pretty-printed</quote>. Pretty-printing adds tab character and new
+   line character for legibility. Passing <literal>false</literal> for the
+   <parameter>pretty</parameter> parameter yields the same result as omitting
+   the parameter.
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 2d946d6d9e9..2db9d3bbcfc 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -657,6 +657,18 @@ LANGUAGE INTERNAL
 STRICT VOLATILE PARALLEL UNSAFE
 AS 'pg_replication_origin_session_setup';
 
+CREATE OR REPLACE FUNCTION
+  pg_get_database_ddl(database_name name, pretty bool DEFAULT false)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl_name';
+
+CREATE OR REPLACE FUNCTION
+  pg_get_database_ddl(database_oid oid, pretty bool DEFAULT false)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl_oid';
+
 --
 -- 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..fd245e3fb45 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -28,6 +28,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_depend.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_opclass.h"
@@ -94,6 +95,10 @@
 	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
 	 : PRETTYFLAG_INDENT)
 
+#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
 
@@ -546,6 +551,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_database_ddl_worker(Oid dbOid, int prettyFlags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13743,3 +13753,197 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - Based on prettyFlags 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);
+}
+
+/*
+ * pg_get_database_ddl_name
+ *
+ * Generate a CREATE DATABASE statement for the specified database name.
+ *
+ * dbName - Name of the database for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_database_ddl_name(PG_FUNCTION_ARGS)
+{
+	Name		dbName = PG_GETARG_NAME(0);
+	bool		pretty = PG_GETARG_BOOL(1);
+	int			prettyFlags;
+	char	   *res;
+
+	/* Get the database oid respective to the given database name */
+	Oid			dbOid = get_database_oid(NameStr(*dbName), false);
+
+	prettyFlags = GET_DDL_PRETTY_FLAGS(pretty);
+	res = pg_get_database_ddl_worker(dbOid, prettyFlags);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+/*
+ * pg_get_database_ddl_oid
+ *
+ * Generate a CREATE DATABASE statement for the specified database oid.
+ *
+ * dbName - Name of the database for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_database_ddl_oid(PG_FUNCTION_ARGS)
+{
+	Oid			dbOid = PG_GETARG_OID(0);
+	bool		pretty = PG_GETARG_BOOL(1);
+	int			prettyFlags;
+	char	   *res;
+
+	prettyFlags = GET_DDL_PRETTY_FLAGS(pretty);
+	res = pg_get_database_ddl_worker(dbOid, prettyFlags);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_database_ddl_worker(Oid dbOid, int prettyFlags)
+{
+	char	   *dbOwner = NULL;
+	char	   *dbTablespace = NULL;
+	bool		attrIsNull;
+	Datum		dbValue;
+	HeapTuple	tupleDatabase;
+	Form_pg_database dbForm;
+	StringInfoData buf;
+
+	/* Look up the database in pg_database */
+	tupleDatabase = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbOid));
+	if (!HeapTupleIsValid(tupleDatabase))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("database with oid %d does not exist", dbOid));
+
+	dbForm = (Form_pg_database) GETSTRUCT(tupleDatabase);
+
+	initStringInfo(&buf);
+
+	/* Look up the owner in the system catalog */
+	if (OidIsValid(dbForm->datdba))
+		dbOwner = GetUserNameFromId(dbForm->datdba, false);
+
+	/* Build the CREATE DATABASE statement */
+	appendStringInfo(&buf, "CREATE DATABASE %s",
+					 quote_identifier(dbForm->datname.data));
+	get_formatted_string(&buf, prettyFlags, 1, "WITH");
+	get_formatted_string(&buf, prettyFlags, 1, "OWNER = %s",
+						 quote_identifier(dbOwner));
+
+	if (dbForm->encoding != 0)
+		get_formatted_string(&buf, prettyFlags, 1, "ENCODING = %s",
+							 quote_identifier(pg_encoding_to_char(dbForm->encoding)));
+
+	/* Fetch the value of LC_COLLATE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datcollate, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 1, "LC_COLLATE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of LC_CTYPE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datctype, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 1, "LC_CTYPE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of LOCALE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datlocale, &attrIsNull);
+	if (!attrIsNull && dbForm->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, prettyFlags, 1, "BUILTIN_LOCALE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+	else if (!attrIsNull && dbForm->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, prettyFlags, 1, "ICU_LOCALE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of ICU_RULES */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_daticurules, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 1, "ICU_RULES = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of COLLATION_VERSION */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datcollversion, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 1, "COLLATION_VERSION = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Set the appropriate LOCALE_PROVIDER */
+	if (dbForm->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, prettyFlags, 1, "LOCALE_PROVIDER = 'builtin'");
+	else if (dbForm->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, prettyFlags, 1, "LOCALE_PROVIDER = 'icu'");
+	else
+		get_formatted_string(&buf, prettyFlags, 1, "LOCALE_PROVIDER = 'libc'");
+
+	/* Get the tablespace name respective to the given tablespace oid */
+	if (OidIsValid(dbForm->dattablespace))
+	{
+		dbTablespace = get_tablespace_name(dbForm->dattablespace);
+		get_formatted_string(&buf, prettyFlags, 1, "TABLESPACE = %s",
+							 quote_identifier(dbTablespace));
+	}
+
+	get_formatted_string(&buf, prettyFlags, 1, "ALLOW_CONNECTIONS = %s",
+						 dbForm->datallowconn ? "true" : "false");
+
+	if (dbForm->datconnlimit != 0)
+		get_formatted_string(&buf, prettyFlags, 1, "CONNECTION LIMIT = %d",
+							 dbForm->datconnlimit);
+
+	if (dbForm->datistemplate)
+		get_formatted_string(&buf, prettyFlags, 1, "IS_TEMPLATE = %s",
+							 dbForm->datistemplate ? "true" : "false");
+
+	appendStringInfoChar(&buf, ';');
+
+	ReleaseSysCache(tupleDatabase);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5cf9e12fcb9..27fbb71297f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4021,6 +4021,12 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '9492', descr => 'get CREATE statement for database name',
+  proname => 'pg_get_database_ddl', prorettype => 'text',
+  proargtypes => 'name bool', prosrc => 'pg_get_database_ddl_name' },
+{ oid => '9493', descr => 'get CREATE statement for database oid',
+  proname => 'pg_get_database_ddl', prorettype => 'text',
+  proargtypes => 'oid bool', prosrc => 'pg_get_database_ddl_oid' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/test/regress/expected/database.out b/src/test/regress/expected/database.out
index 6b879b0f62a..df90fded42c 100644
--- a/src/test/regress/expected/database.out
+++ b/src/test/regress/expected/database.out
@@ -1,3 +1,49 @@
+--
+-- Reconsturct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+	-- Remove LC_COLLATE assignments 
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -16,6 +62,36 @@ CREATE ROLE regress_datdba_before;
 CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database', false);
+ERROR:  database "regression_database" does not exist
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+ pg_get_database_ddl 
+---------------------
+ 
+(1 row)
+
+-- Without pretty
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+                                                                          ddl_filter                                                                          
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after ENCODING = "UTF8" TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123;
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+	WITH
+	OWNER = regress_datdba_after
+	ENCODING = "UTF8"
+	TABLESPACE = pg_default
+	ALLOW_CONNECTIONS = true
+	CONNECTION LIMIT = 123;
+(1 row)
 DROP DATABASE regression_utf8;
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
diff --git a/src/test/regress/sql/database.sql b/src/test/regress/sql/database.sql
index 4ef36127291..392a4d96bb5 100644
--- a/src/test/regress/sql/database.sql
+++ b/src/test/regress/sql/database.sql
@@ -1,3 +1,51 @@
+--
+-- Reconsturct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+	-- Remove LC_COLLATE assignments 
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
+
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -19,6 +67,20 @@ CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
 
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database', false);
+
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+
+-- Without pretty
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', true));
+
 DROP DATABASE regression_utf8;
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
-- 
2.51.0



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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-13 04:17  Quan Zongliang <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 2 replies; 38+ messages in thread

From: Quan Zongliang @ 2025-11-13 04:17 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; pgsql-hackers



On 11/12/25 8:04 PM, Akshay Joshi wrote:
> Hi Hackers,
> 
> I’m submitting a patch as part of the broader Retail DDL Functions 
> project described by Andrew Dunstan https://www.postgresql.org/message- 
> id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9%40dunslane.net <https:// 
> www.postgresql.org/message-id/945db7c5-be75-45bf-b55b- 
> cb1e56f2e3e9%40dunslane.net>
> 
> This patch adds a new system function pg_get_database_ddl(database_name/ 
> database_oid, pretty), which reconstructs the CREATE DATABASE statement 
> for a given database name or database oid. When the pretty flag is set 
> to true, the function returns a neatly formatted, multi-line DDL 
> statement instead of a single-line statement.
> 
> *Usage examples:*
> 
> 1) SELECT pg_get_database_ddl('test_get_database_ddl_builtin');  -- 
> *non-pretty formatted DDL*
>                                                                          
>                                                              
> pg_get_database_ddl
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>    CREATE DATABASE test_get_database_ddl_builtin WITH OWNER = 
> regress_ddl_database ENCODING = "UTF8" LC_COLLATE = "C" LC_CTYPE = "C" 
> BUILTIN_LOCALE = "C.UTF-8" COLLATION_VERSION = "1" LOCALE_PROVIDER = 
> 'builtin' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION 
> LIMIT = -1;
> 
> 
> 2) SELECT pg_get_database_ddl('test_get_database_ddl_builtin', true);   
> -- *pretty formatted DDL*
> 
> CREATE DATABASE test_get_database_ddl_builtin
>           WITH
>           OWNER = regress_ddl_database
>           ENCODING = "UTF8"
>           LC_COLLATE = "C"
>           LC_CTYPE = "C"
>           BUILTIN_LOCALE = "C.UTF-8"
>           COLLATION_VERSION = "1"
>           LOCALE_PROVIDER = 'builtin'
>           TABLESPACE = pg_default
>           ALLOW_CONNECTIONS = true
>           CONNECTION LIMIT = -1;
> 
> 3) SELECT pg_get_database_ddl(16835);      -- *non-pretty formatted DDL 
> for OID*
> 4) SELECT pg_get_database_ddl(16835, true);  -- *pretty formatted DDL 
> for OID*
> 
> The patch includes documentation, in-code comments, and regression 
> tests, all of which pass successfully.
> *
> **Note:* To run the regression tests, particularly the pg_upgrade tests 
> successfully, I had to add a helper function, ddl_filter (in 
> database.sql), which removes locale and collation-related information 
> from the pg_get_database_ddl output.
> 
I think we should check the connection permissions here. Otherwise:

postgres=> SELECT pg_database_size('testdb');
ERROR:  permission denied for database testdb
postgres=> SELECT pg_get_database_ddl('testdb');
  
                         pg_get_database_ddl
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  CREATE DATABASE testdb WITH OWNER = quanzl ENCODING = "UTF8" 
LC_COLLATE = "zh_CN.UTF-8" LC_CTYPE = "zh_CN.UTF-8" LOCALE_PROVIDER = 
'libc' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT 
= -1;
(1 row)

Users without connection permissions should not generate DDL.

Regards,
Quan Zongliang

> -----
> Regards,
> Akshay Joshi
> EDB (EnterpriseDB)
> 
> 
> 






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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-13 04:48  Quan Zongliang <[email protected]>
  parent: Quan Zongliang <[email protected]>
  1 sibling, 1 reply; 38+ messages in thread

From: Quan Zongliang @ 2025-11-13 04:48 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; pgsql-hackers



On 11/13/25 12:17 PM, Quan Zongliang wrote:
> 
> 
> On 11/12/25 8:04 PM, Akshay Joshi wrote:
>> Hi Hackers,
>>
>> I’m submitting a patch as part of the broader Retail DDL Functions 
>> project described by Andrew Dunstan https://www.postgresql.org/ 
>> message- id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9%40dunslane.net 
>> <https:// www.postgresql.org/message-id/945db7c5-be75-45bf-b55b- 
>> cb1e56f2e3e9%40dunslane.net>
>>
>> This patch adds a new system function 
>> pg_get_database_ddl(database_name/ database_oid, pretty), which 
>> reconstructs the CREATE DATABASE statement for a given database name 
>> or database oid. When the pretty flag is set to true, the function 
>> returns a neatly formatted, multi-line DDL statement instead of a 
>> single-line statement.
>>
>> *Usage examples:*
>>
>> 1) SELECT pg_get_database_ddl('test_get_database_ddl_builtin');  -- 
>> *non-pretty formatted DDL*
>> pg_get_database_ddl
>> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>>    CREATE DATABASE test_get_database_ddl_builtin WITH OWNER = 
>> regress_ddl_database ENCODING = "UTF8" LC_COLLATE = "C" LC_CTYPE = "C" 
>> BUILTIN_LOCALE = "C.UTF-8" COLLATION_VERSION = "1" LOCALE_PROVIDER = 
>> 'builtin' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION 
>> LIMIT = -1;
>>
>>
>> 2) SELECT pg_get_database_ddl('test_get_database_ddl_builtin', true); 
>> -- *pretty formatted DDL*
>>
>> CREATE DATABASE test_get_database_ddl_builtin
>>           WITH
>>           OWNER = regress_ddl_database
>>           ENCODING = "UTF8"
>>           LC_COLLATE = "C"
>>           LC_CTYPE = "C"
>>           BUILTIN_LOCALE = "C.UTF-8"
>>           COLLATION_VERSION = "1"
>>           LOCALE_PROVIDER = 'builtin'
>>           TABLESPACE = pg_default
>>           ALLOW_CONNECTIONS = true
>>           CONNECTION LIMIT = -1;
>>
>> 3) SELECT pg_get_database_ddl(16835);      -- *non-pretty formatted 
>> DDL for OID*
>> 4) SELECT pg_get_database_ddl(16835, true);  -- *pretty formatted DDL 
>> for OID*
>>
>> The patch includes documentation, in-code comments, and regression 
>> tests, all of which pass successfully.
>> *
>> **Note:* To run the regression tests, particularly the pg_upgrade 
>> tests successfully, I had to add a helper function, ddl_filter (in 
>> database.sql), which removes locale and collation-related information 
>> from the pg_get_database_ddl output.
>>
> I think we should check the connection permissions here. Otherwise:
> 
> postgres=> SELECT pg_database_size('testdb');
> ERROR:  permission denied for database testdb
> postgres=> SELECT pg_get_database_ddl('testdb');
> 
>                          pg_get_database_ddl
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>   CREATE DATABASE testdb WITH OWNER = quanzl ENCODING = "UTF8" 
> LC_COLLATE = "zh_CN.UTF-8" LC_CTYPE = "zh_CN.UTF-8" LOCALE_PROVIDER = 
> 'libc' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT 
> = -1;
> (1 row)
> 
> Users without connection permissions should not generate DDL.
> 

The "dbOwner" is defined as a null pointer.
char	   *dbOwner = NULL;

Later, there might be a risk of it not being assigned a value.
    if (OidIsValid(dbForm->datdba))
       dbOwner = GetUserNameFromId(dbForm->datdba, false);

Although there is no problem in normal circumstances here. Many parts of 
the existing code have not been checked either. Since this possibility 
exists, it should be checked before using it. Just like the function 
roles_is_member_of (acl.c).

if (dbOwner)
   get_formatted_string(&buf, prettyFlags, 1, "OWNER = %s",
			 quote_identifier(dbOwner));

> Regards,
> Quan Zongliang
> 
>> -----
>> Regards,
>> Akshay Joshi
>> EDB (EnterpriseDB)
>>
>>
>>
> 
> 






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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-13 08:30  Akshay Joshi <[email protected]>
  parent: Quan Zongliang <[email protected]>
  1 sibling, 1 reply; 38+ messages in thread

From: Akshay Joshi @ 2025-11-13 08:30 UTC (permalink / raw)
  To: Quan Zongliang <[email protected]>; +Cc: pgsql-hackers

On Thu, Nov 13, 2025 at 9:47 AM Quan Zongliang <[email protected]>
wrote:

>
>
> On 11/12/25 8:04 PM, Akshay Joshi wrote:
> > Hi Hackers,
> >
> > I’m submitting a patch as part of the broader Retail DDL Functions
> > project described by Andrew Dunstan https://www.postgresql.org/message-
> > id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9%40dunslane.net <https://
> > www.postgresql.org/message-id/945db7c5-be75-45bf-b55b-
> > cb1e56f2e3e9%40dunslane.net>
> >
> > This patch adds a new system function pg_get_database_ddl(database_name/
> > database_oid, pretty), which reconstructs the CREATE DATABASE statement
> > for a given database name or database oid. When the pretty flag is set
> > to true, the function returns a neatly formatted, multi-line DDL
> > statement instead of a single-line statement.
> >
> > *Usage examples:*
> >
> > 1) SELECT pg_get_database_ddl('test_get_database_ddl_builtin');  --
> > *non-pretty formatted DDL*
> >
> >
> > pg_get_database_ddl
> >
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
> >    CREATE DATABASE test_get_database_ddl_builtin WITH OWNER =
> > regress_ddl_database ENCODING = "UTF8" LC_COLLATE = "C" LC_CTYPE = "C"
> > BUILTIN_LOCALE = "C.UTF-8" COLLATION_VERSION = "1" LOCALE_PROVIDER =
> > 'builtin' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION
> > LIMIT = -1;
> >
> >
> > 2) SELECT pg_get_database_ddl('test_get_database_ddl_builtin', true);
> > -- *pretty formatted DDL*
> >
> > CREATE DATABASE test_get_database_ddl_builtin
> >           WITH
> >           OWNER = regress_ddl_database
> >           ENCODING = "UTF8"
> >           LC_COLLATE = "C"
> >           LC_CTYPE = "C"
> >           BUILTIN_LOCALE = "C.UTF-8"
> >           COLLATION_VERSION = "1"
> >           LOCALE_PROVIDER = 'builtin'
> >           TABLESPACE = pg_default
> >           ALLOW_CONNECTIONS = true
> >           CONNECTION LIMIT = -1;
> >
> > 3) SELECT pg_get_database_ddl(16835);      -- *non-pretty formatted DDL
> > for OID*
> > 4) SELECT pg_get_database_ddl(16835, true);  -- *pretty formatted DDL
> > for OID*
> >
> > The patch includes documentation, in-code comments, and regression
> > tests, all of which pass successfully.
> > *
> > **Note:* To run the regression tests, particularly the pg_upgrade tests
> > successfully, I had to add a helper function, ddl_filter (in
> > database.sql), which removes locale and collation-related information
> > from the pg_get_database_ddl output.
> >
> I think we should check the connection permissions here. Otherwise:
>
> postgres=> SELECT pg_database_size('testdb');
> ERROR:  permission denied for database testdb
> postgres=> SELECT pg_get_database_ddl('testdb');
>
>                          pg_get_database_ddl
>
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>   CREATE DATABASE testdb WITH OWNER = quanzl ENCODING = "UTF8"
> LC_COLLATE = "zh_CN.UTF-8" LC_CTYPE = "zh_CN.UTF-8" LOCALE_PROVIDER =
> 'libc' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT
> = -1;
> (1 row)
>
> Users without connection permissions should not generate DDL.
>

pg_database_size() requires CONNECT or pg_read_all_stats privileges since
it accesses on-disk storage details of a database, which are treated as
sensitive information. In contrast, other system functions might not need
such privileges because they operate within the connected database or
reveal less sensitive data.

In my view, the pg_get_database_ddl() function *should not* require CONNECT
or pg_read_all_stats privileges for consistency and security.

>
> Regards,
> Quan Zongliang
>
> > -----
> > Regards,
> > Akshay Joshi
> > EDB (EnterpriseDB)
> >
> >
> >
>
>


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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-13 08:32  Akshay Joshi <[email protected]>
  parent: Quan Zongliang <[email protected]>
  0 siblings, 2 replies; 38+ messages in thread

From: Akshay Joshi @ 2025-11-13 08:32 UTC (permalink / raw)
  To: Quan Zongliang <[email protected]>; +Cc: pgsql-hackers

On Thu, Nov 13, 2025 at 10:18 AM Quan Zongliang <[email protected]>
wrote:

>
>
> On 11/13/25 12:17 PM, Quan Zongliang wrote:
> >
> >
> > On 11/12/25 8:04 PM, Akshay Joshi wrote:
> >> Hi Hackers,
> >>
> >> I’m submitting a patch as part of the broader Retail DDL Functions
> >> project described by Andrew Dunstan https://www.postgresql.org/
> >> message- id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9%40dunslane.net
> >> <https:// www.postgresql.org/message-id/945db7c5-be75-45bf-b55b-
> >> cb1e56f2e3e9%40dunslane.net>
> >>
> >> This patch adds a new system function
> >> pg_get_database_ddl(database_name/ database_oid, pretty), which
> >> reconstructs the CREATE DATABASE statement for a given database name
> >> or database oid. When the pretty flag is set to true, the function
> >> returns a neatly formatted, multi-line DDL statement instead of a
> >> single-line statement.
> >>
> >> *Usage examples:*
> >>
> >> 1) SELECT pg_get_database_ddl('test_get_database_ddl_builtin');  --
> >> *non-pretty formatted DDL*
> >> pg_get_database_ddl
> >>
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
> >>    CREATE DATABASE test_get_database_ddl_builtin WITH OWNER =
> >> regress_ddl_database ENCODING = "UTF8" LC_COLLATE = "C" LC_CTYPE = "C"
> >> BUILTIN_LOCALE = "C.UTF-8" COLLATION_VERSION = "1" LOCALE_PROVIDER =
> >> 'builtin' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION
> >> LIMIT = -1;
> >>
> >>
> >> 2) SELECT pg_get_database_ddl('test_get_database_ddl_builtin', true);
> >> -- *pretty formatted DDL*
> >>
> >> CREATE DATABASE test_get_database_ddl_builtin
> >>           WITH
> >>           OWNER = regress_ddl_database
> >>           ENCODING = "UTF8"
> >>           LC_COLLATE = "C"
> >>           LC_CTYPE = "C"
> >>           BUILTIN_LOCALE = "C.UTF-8"
> >>           COLLATION_VERSION = "1"
> >>           LOCALE_PROVIDER = 'builtin'
> >>           TABLESPACE = pg_default
> >>           ALLOW_CONNECTIONS = true
> >>           CONNECTION LIMIT = -1;
> >>
> >> 3) SELECT pg_get_database_ddl(16835);      -- *non-pretty formatted
> >> DDL for OID*
> >> 4) SELECT pg_get_database_ddl(16835, true);  -- *pretty formatted DDL
> >> for OID*
> >>
> >> The patch includes documentation, in-code comments, and regression
> >> tests, all of which pass successfully.
> >> *
> >> **Note:* To run the regression tests, particularly the pg_upgrade
> >> tests successfully, I had to add a helper function, ddl_filter (in
> >> database.sql), which removes locale and collation-related information
> >> from the pg_get_database_ddl output.
> >>
> > I think we should check the connection permissions here. Otherwise:
> >
> > postgres=> SELECT pg_database_size('testdb');
> > ERROR:  permission denied for database testdb
> > postgres=> SELECT pg_get_database_ddl('testdb');
> >
> >                          pg_get_database_ddl
> >
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
> >   CREATE DATABASE testdb WITH OWNER = quanzl ENCODING = "UTF8"
> > LC_COLLATE = "zh_CN.UTF-8" LC_CTYPE = "zh_CN.UTF-8" LOCALE_PROVIDER =
> > 'libc' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT
> > = -1;
> > (1 row)
> >
> > Users without connection permissions should not generate DDL.
> >
>
> The "dbOwner" is defined as a null pointer.
> char       *dbOwner = NULL;
>
> Later, there might be a risk of it not being assigned a value.
>     if (OidIsValid(dbForm->datdba))
>        dbOwner = GetUserNameFromId(dbForm->datdba, false);
>
> Although there is no problem in normal circumstances here. Many parts of
> the existing code have not been checked either. Since this possibility
> exists, it should be checked before using it. Just like the function
> roles_is_member_of (acl.c).
>
> if (dbOwner)
>    get_formatted_string(&buf, prettyFlags, 1, "OWNER = %s",
>                          quote_identifier(dbOwner));
>

 Fixed the given review comment. I've attached the v2 patch ready for
review.

>
> > Regards,
> > Quan Zongliang
> >
> >> -----
> >> Regards,
> >> Akshay Joshi
> >> EDB (EnterpriseDB)
> >>
> >>
> >>
> >
> >
>
>


Attachments:

  [application/octet-stream] v2-0001-Add-pg_get_database_ddl-function-to-reconstruct-CREATE.patch (18.3K, 3-v2-0001-Add-pg_get_database_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From 25bd4ed143c5dae77c85ade087dfee10356bd562 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Wed, 24 Sep 2025 17:47:59 +0530
Subject: [PATCH v2] Add pg_get_database_ddl() function to reconstruct CREATE
 DATABASE statements.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This adds a new system function, pg_get_database_ddl(database_name/database_oid, pretty),
which reconstructs the CREATE DATABASE statement for a given database name or database oid.

Usage:
  SELECT pg_get_database_ddl('postgres'); // Non pretty-formatted DDL
  SELECT pg_get_database_ddl(16835); // Non pretty-formatted DDL
  SELECT pg_get_database_ddl('postgres', true); // pretty-formatted DDL
  SELECT pg_get_database_ddl(16835, true); // pretty-formatted DDL

Reference: PG-150
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  70 ++++++++
 src/backend/catalog/system_functions.sql |  12 ++
 src/backend/utils/adt/ruleutils.c        | 206 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   6 +
 src/test/regress/expected/database.out   |  76 +++++++++
 src/test/regress/sql/database.sql        |  62 +++++++
 6 files changed, 432 insertions(+)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..88fe04e649d 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,74 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </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_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>database_name</parameter> <type>name</type>, <optional> <parameter>pretty</parameter> <type>boolean</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE DATABASE</command> statement from the
+        system catalogs for a specified database name. The result is a
+        comprehensive <command>CREATE DATABASE</command> statement.
+       </para></entry>
+      </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_get_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>database_oid</parameter> <type>oid</type>, <optional> <parameter>pretty</parameter> <type>boolean</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE DATABASE</command> statement from the
+        system catalogs for a specified database oid. The result is a
+        comprehensive <command>CREATE DATABASE</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+   Most of the functions that reconstruct (decompile) database objects have an
+   optional <parameter>pretty</parameter> flag, which if
+   <literal>true</literal> causes the result to be
+   <quote>pretty-printed</quote>. Pretty-printing adds tab character and new
+   line character for legibility. Passing <literal>false</literal> for the
+   <parameter>pretty</parameter> parameter yields the same result as omitting
+   the parameter.
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 2d946d6d9e9..2db9d3bbcfc 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -657,6 +657,18 @@ LANGUAGE INTERNAL
 STRICT VOLATILE PARALLEL UNSAFE
 AS 'pg_replication_origin_session_setup';
 
+CREATE OR REPLACE FUNCTION
+  pg_get_database_ddl(database_name name, pretty bool DEFAULT false)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl_name';
+
+CREATE OR REPLACE FUNCTION
+  pg_get_database_ddl(database_oid oid, pretty bool DEFAULT false)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl_oid';
+
 --
 -- 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..2b622bcc66d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -28,6 +28,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_depend.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_opclass.h"
@@ -94,6 +95,10 @@
 	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
 	 : PRETTYFLAG_INDENT)
 
+#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
 
@@ -546,6 +551,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_database_ddl_worker(Oid dbOid, int prettyFlags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13743,3 +13753,199 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - Based on prettyFlags 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);
+}
+
+/*
+ * pg_get_database_ddl_name
+ *
+ * Generate a CREATE DATABASE statement for the specified database name.
+ *
+ * dbName - Name of the database for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_database_ddl_name(PG_FUNCTION_ARGS)
+{
+	Name		dbName = PG_GETARG_NAME(0);
+	bool		pretty = PG_GETARG_BOOL(1);
+	int			prettyFlags;
+	char	   *res;
+
+	/* Get the database oid respective to the given database name */
+	Oid			dbOid = get_database_oid(NameStr(*dbName), false);
+
+	prettyFlags = GET_DDL_PRETTY_FLAGS(pretty);
+	res = pg_get_database_ddl_worker(dbOid, prettyFlags);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+/*
+ * pg_get_database_ddl_oid
+ *
+ * Generate a CREATE DATABASE statement for the specified database oid.
+ *
+ * dbName - Name of the database for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_database_ddl_oid(PG_FUNCTION_ARGS)
+{
+	Oid			dbOid = PG_GETARG_OID(0);
+	bool		pretty = PG_GETARG_BOOL(1);
+	int			prettyFlags;
+	char	   *res;
+
+	prettyFlags = GET_DDL_PRETTY_FLAGS(pretty);
+	res = pg_get_database_ddl_worker(dbOid, prettyFlags);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_database_ddl_worker(Oid dbOid, int prettyFlags)
+{
+	char	   *dbOwner = NULL;
+	char	   *dbTablespace = NULL;
+	bool		attrIsNull;
+	Datum		dbValue;
+	HeapTuple	tupleDatabase;
+	Form_pg_database dbForm;
+	StringInfoData buf;
+
+	/* Look up the database in pg_database */
+	tupleDatabase = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbOid));
+	if (!HeapTupleIsValid(tupleDatabase))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("database with oid %d does not exist", dbOid));
+
+	dbForm = (Form_pg_database) GETSTRUCT(tupleDatabase);
+
+	initStringInfo(&buf);
+
+	/* Look up the owner in the system catalog */
+	if (OidIsValid(dbForm->datdba))
+		dbOwner = GetUserNameFromId(dbForm->datdba, false);
+
+	/* Build the CREATE DATABASE statement */
+	appendStringInfo(&buf, "CREATE DATABASE %s",
+					 quote_identifier(dbForm->datname.data));
+	get_formatted_string(&buf, prettyFlags, 1, "WITH");
+	if (dbOwner)
+		get_formatted_string(&buf, prettyFlags, 1, "OWNER = %s",
+							 quote_identifier(dbOwner));
+
+	if (dbForm->encoding != 0)
+		get_formatted_string(&buf, prettyFlags, 1, "ENCODING = %s",
+							 quote_identifier(pg_encoding_to_char(dbForm->encoding)));
+
+	/* Fetch the value of LC_COLLATE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datcollate, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 1, "LC_COLLATE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of LC_CTYPE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datctype, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 1, "LC_CTYPE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of LOCALE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datlocale, &attrIsNull);
+	if (!attrIsNull && dbForm->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, prettyFlags, 1, "BUILTIN_LOCALE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+	else if (!attrIsNull && dbForm->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, prettyFlags, 1, "ICU_LOCALE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of ICU_RULES */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_daticurules, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 1, "ICU_RULES = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of COLLATION_VERSION */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datcollversion, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 1, "COLLATION_VERSION = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Set the appropriate LOCALE_PROVIDER */
+	if (dbForm->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, prettyFlags, 1, "LOCALE_PROVIDER = 'builtin'");
+	else if (dbForm->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, prettyFlags, 1, "LOCALE_PROVIDER = 'icu'");
+	else
+		get_formatted_string(&buf, prettyFlags, 1, "LOCALE_PROVIDER = 'libc'");
+
+	/* Get the tablespace name respective to the given tablespace oid */
+	if (OidIsValid(dbForm->dattablespace))
+	{
+		dbTablespace = get_tablespace_name(dbForm->dattablespace);
+		if (dbTablespace)
+			get_formatted_string(&buf, prettyFlags, 1, "TABLESPACE = %s",
+								 quote_identifier(dbTablespace));
+	}
+
+	get_formatted_string(&buf, prettyFlags, 1, "ALLOW_CONNECTIONS = %s",
+						 dbForm->datallowconn ? "true" : "false");
+
+	if (dbForm->datconnlimit != 0)
+		get_formatted_string(&buf, prettyFlags, 1, "CONNECTION LIMIT = %d",
+							 dbForm->datconnlimit);
+
+	if (dbForm->datistemplate)
+		get_formatted_string(&buf, prettyFlags, 1, "IS_TEMPLATE = %s",
+							 dbForm->datistemplate ? "true" : "false");
+
+	appendStringInfoChar(&buf, ';');
+
+	ReleaseSysCache(tupleDatabase);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5cf9e12fcb9..27fbb71297f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4021,6 +4021,12 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '9492', descr => 'get CREATE statement for database name',
+  proname => 'pg_get_database_ddl', prorettype => 'text',
+  proargtypes => 'name bool', prosrc => 'pg_get_database_ddl_name' },
+{ oid => '9493', descr => 'get CREATE statement for database oid',
+  proname => 'pg_get_database_ddl', prorettype => 'text',
+  proargtypes => 'oid bool', prosrc => 'pg_get_database_ddl_oid' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/test/regress/expected/database.out b/src/test/regress/expected/database.out
index 6b879b0f62a..df90fded42c 100644
--- a/src/test/regress/expected/database.out
+++ b/src/test/regress/expected/database.out
@@ -1,3 +1,49 @@
+--
+-- Reconsturct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+	-- Remove LC_COLLATE assignments 
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -16,6 +62,36 @@ CREATE ROLE regress_datdba_before;
 CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database', false);
+ERROR:  database "regression_database" does not exist
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+ pg_get_database_ddl 
+---------------------
+ 
+(1 row)
+
+-- Without pretty
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+                                                                          ddl_filter                                                                          
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after ENCODING = "UTF8" TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123;
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+	WITH
+	OWNER = regress_datdba_after
+	ENCODING = "UTF8"
+	TABLESPACE = pg_default
+	ALLOW_CONNECTIONS = true
+	CONNECTION LIMIT = 123;
+(1 row)
 DROP DATABASE regression_utf8;
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
diff --git a/src/test/regress/sql/database.sql b/src/test/regress/sql/database.sql
index 4ef36127291..392a4d96bb5 100644
--- a/src/test/regress/sql/database.sql
+++ b/src/test/regress/sql/database.sql
@@ -1,3 +1,51 @@
+--
+-- Reconsturct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+	-- Remove LC_COLLATE assignments 
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
+
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -19,6 +67,20 @@ CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
 
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database', false);
+
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+
+-- Without pretty
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', true));
+
 DROP DATABASE regression_utf8;
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
-- 
2.51.0



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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-13 09:36  Quan Zongliang <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 0 replies; 38+ messages in thread

From: Quan Zongliang @ 2025-11-13 09:36 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: pgsql-hackers



On 11/13/25 4:30 PM, Akshay Joshi wrote:
> 
> On Thu, Nov 13, 2025 at 9:47 AM Quan Zongliang <[email protected] 
> <mailto:[email protected]>> wrote:
> 
> 
> 
>     On 11/12/25 8:04 PM, Akshay Joshi wrote:
>      > Hi Hackers,
>      >
>      > I’m submitting a patch as part of the broader Retail DDL Functions
>      > project described by Andrew Dunstan https://www.postgresql.org/
>     message- <https://www.postgresql.org/message-;
>      > id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9%40dunslane.net
>     <http://40dunslane.net; <https://
>      > www.postgresql.org/message-id/945db7c5-be75-45bf-b55b- <http://
>     www.postgresql.org/message-id/945db7c5-be75-45bf-b55b->
>      > cb1e56f2e3e9%40dunslane.net <http://40dunslane.net>;
>      >
>      > This patch adds a new system function
>     pg_get_database_ddl(database_name/
>      > database_oid, pretty), which reconstructs the CREATE DATABASE
>     statement
>      > for a given database name or database oid. When the pretty flag
>     is set
>      > to true, the function returns a neatly formatted, multi-line DDL
>      > statement instead of a single-line statement.
>      >
>      > *Usage examples:*
>      >
>      > 1) SELECT pg_get_database_ddl('test_get_database_ddl_builtin');  --
>      > *non-pretty formatted DDL*
>      >
>      >
>      > pg_get_database_ddl
>      >
>     -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>      >    CREATE DATABASE test_get_database_ddl_builtin WITH OWNER =
>      > regress_ddl_database ENCODING = "UTF8" LC_COLLATE = "C" LC_CTYPE
>     = "C"
>      > BUILTIN_LOCALE = "C.UTF-8" COLLATION_VERSION = "1" LOCALE_PROVIDER =
>      > 'builtin' TABLESPACE = pg_default ALLOW_CONNECTIONS = true
>     CONNECTION
>      > LIMIT = -1;
>      >
>      >
>      > 2) SELECT pg_get_database_ddl('test_get_database_ddl_builtin',
>     true);
>      > -- *pretty formatted DDL*
>      >
>      > CREATE DATABASE test_get_database_ddl_builtin
>      >           WITH
>      >           OWNER = regress_ddl_database
>      >           ENCODING = "UTF8"
>      >           LC_COLLATE = "C"
>      >           LC_CTYPE = "C"
>      >           BUILTIN_LOCALE = "C.UTF-8"
>      >           COLLATION_VERSION = "1"
>      >           LOCALE_PROVIDER = 'builtin'
>      >           TABLESPACE = pg_default
>      >           ALLOW_CONNECTIONS = true
>      >           CONNECTION LIMIT = -1;
>      >
>      > 3) SELECT pg_get_database_ddl(16835);      -- *non-pretty
>     formatted DDL
>      > for OID*
>      > 4) SELECT pg_get_database_ddl(16835, true);  -- *pretty formatted
>     DDL
>      > for OID*
>      >
>      > The patch includes documentation, in-code comments, and regression
>      > tests, all of which pass successfully.
>      > *
>      > **Note:* To run the regression tests, particularly the pg_upgrade
>     tests
>      > successfully, I had to add a helper function, ddl_filter (in
>      > database.sql), which removes locale and collation-related
>     information
>      > from the pg_get_database_ddl output.
>      >
>     I think we should check the connection permissions here. Otherwise:
> 
>     postgres=> SELECT pg_database_size('testdb');
>     ERROR:  permission denied for database testdb
>     postgres=> SELECT pg_get_database_ddl('testdb');
> 
>                               pg_get_database_ddl
>     -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>        CREATE DATABASE testdb WITH OWNER = quanzl ENCODING = "UTF8"
>     LC_COLLATE = "zh_CN.UTF-8" LC_CTYPE = "zh_CN.UTF-8" LOCALE_PROVIDER =
>     'libc' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION
>     LIMIT
>     = -1;
>     (1 row)
> 
>     Users without connection permissions should not generate DDL.
> 
> 
> pg_database_size() requires CONNECT or pg_read_all_stats privileges 
> since it accesses on-disk storage details of a database, which are 
> treated as sensitive information. In contrast, other system functions 
> might not need such privileges because they operate within the connected 
> database or reveal less sensitive data.
> 
> In my view, the pg_get_database_ddl() function *should not* require 
> CONNECT or pg_read_all_stats privileges for consistency and security.
>
Agree.

But what about the following scenario? If there is no permission to 
access pg_database. Shouldn't the DDL be returned?

postgres=> SELECT * FROM pg_database;
ERROR:  permission denied for table pg_database
postgres=> SELECT pg_get_database_ddl('testdb');

> 
>     Regards,
>     Quan Zongliang
> 
>      > -----
>      > Regards,
>      > Akshay Joshi
>      > EDB (EnterpriseDB)
>      >
>      >
>      >
> 






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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-14 01:13  Quan Zongliang <[email protected]>
  0 siblings, 0 replies; 38+ messages in thread

From: Quan Zongliang @ 2025-11-14 01:13 UTC (permalink / raw)
  To: pgsql-hackers



On 11/13/25 8:28 PM, Álvaro Herrera wrote:

>> But what about the following scenario? If there is no permission to access
>> pg_database. Shouldn't the DDL be returned?
>>
>> postgres=> SELECT * FROM pg_database;
>> ERROR:  permission denied for table pg_database
>> postgres=> SELECT pg_get_database_ddl('testdb');
> 
> Hmm, what scenario is this?  Did you purposefully REVOKE the SELECT
> privileges from pg_database somehow?
> 
Yes. I revoked the access permission using the REVOKE command.

The pg_get_xxx_ddl function is actually revealing system information to 
the users. This is equivalent to accessing the corresponding system 
table. So I think we should consider this issue.

The access permission to the system tables has been revoked. This user 
is unable to directly view the contents of the system tables from the 
client side via SQL. However, it is still possible to obtain the object 
definitions (which was previously inaccessible) through pg_get_xxx_ddl. 
This is more like a security flaw.

A more specific example. Originally, it was impossible to obtain the 
definition of "testdb" by accessing pg_database:

   postgres=> SELECT * FROM pg_database WHERE datname='testdb';
   ERROR:  permission denied for table pg_database

And after having this function. However, users can view these in another 
format.

   postgres=> SELECT pg_get_database_ddl('testdb');
   ------------- ...
   CREATE DATABASE testdb WITH OWNER = quanzl ENCODING = "UTF8" ...

Perhaps it's just that I'm overthinking things. What do you think about it?






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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-14 05:49  Japin Li <[email protected]>
  parent: Akshay Joshi <[email protected]>
  1 sibling, 1 reply; 38+ messages in thread

From: Japin Li @ 2025-11-14 05:49 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Quan Zongliang <[email protected]>; pgsql-hackers

On Thu, Nov 13, 2025 at 02:02:30PM +0530, Akshay Joshi wrote:
> On Thu, Nov 13, 2025 at 10:18 AM Quan Zongliang <[email protected]>
> wrote:
> 
> >
> >
> > On 11/13/25 12:17 PM, Quan Zongliang wrote:
> > >
> > >
> > > On 11/12/25 8:04 PM, Akshay Joshi wrote:
> > >> Hi Hackers,
> > >>
> > >> I’m submitting a patch as part of the broader Retail DDL Functions
> > >> project described by Andrew Dunstan https://www.postgresql.org/
> > >> message- id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9%40dunslane.net
> > >> <https:// www.postgresql.org/message-id/945db7c5-be75-45bf-b55b-
> > >> cb1e56f2e3e9%40dunslane.net>
> > >>
> > >> This patch adds a new system function
> > >> pg_get_database_ddl(database_name/ database_oid, pretty), which
> > >> reconstructs the CREATE DATABASE statement for a given database name
> > >> or database oid. When the pretty flag is set to true, the function
> > >> returns a neatly formatted, multi-line DDL statement instead of a
> > >> single-line statement.
> > >>
> > >> *Usage examples:*
> > >>
> > >> 1) SELECT pg_get_database_ddl('test_get_database_ddl_builtin');  --
> > >> *non-pretty formatted DDL*
> > >> pg_get_database_ddl
> > >>
> > -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
> > >>    CREATE DATABASE test_get_database_ddl_builtin WITH OWNER =
> > >> regress_ddl_database ENCODING = "UTF8" LC_COLLATE = "C" LC_CTYPE = "C"
> > >> BUILTIN_LOCALE = "C.UTF-8" COLLATION_VERSION = "1" LOCALE_PROVIDER =
> > >> 'builtin' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION
> > >> LIMIT = -1;
> > >>
> > >>
> > >> 2) SELECT pg_get_database_ddl('test_get_database_ddl_builtin', true);
> > >> -- *pretty formatted DDL*
> > >>
> > >> CREATE DATABASE test_get_database_ddl_builtin
> > >>           WITH
> > >>           OWNER = regress_ddl_database
> > >>           ENCODING = "UTF8"
> > >>           LC_COLLATE = "C"
> > >>           LC_CTYPE = "C"
> > >>           BUILTIN_LOCALE = "C.UTF-8"
> > >>           COLLATION_VERSION = "1"
> > >>           LOCALE_PROVIDER = 'builtin'
> > >>           TABLESPACE = pg_default
> > >>           ALLOW_CONNECTIONS = true
> > >>           CONNECTION LIMIT = -1;
> > >>
> > >> 3) SELECT pg_get_database_ddl(16835);      -- *non-pretty formatted
> > >> DDL for OID*
> > >> 4) SELECT pg_get_database_ddl(16835, true);  -- *pretty formatted DDL
> > >> for OID*
> > >>
> > >> The patch includes documentation, in-code comments, and regression
> > >> tests, all of which pass successfully.
> > >> *
> > >> **Note:* To run the regression tests, particularly the pg_upgrade
> > >> tests successfully, I had to add a helper function, ddl_filter (in
> > >> database.sql), which removes locale and collation-related information
> > >> from the pg_get_database_ddl output.
> > >>
> > > I think we should check the connection permissions here. Otherwise:
> > >
> > > postgres=> SELECT pg_database_size('testdb');
> > > ERROR:  permission denied for database testdb
> > > postgres=> SELECT pg_get_database_ddl('testdb');
> > >
> > >                          pg_get_database_ddl
> > >
> > -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
> > >   CREATE DATABASE testdb WITH OWNER = quanzl ENCODING = "UTF8"
> > > LC_COLLATE = "zh_CN.UTF-8" LC_CTYPE = "zh_CN.UTF-8" LOCALE_PROVIDER =
> > > 'libc' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT
> > > = -1;
> > > (1 row)
> > >
> > > Users without connection permissions should not generate DDL.
> > >
> >
> > The "dbOwner" is defined as a null pointer.
> > char       *dbOwner = NULL;
> >
> > Later, there might be a risk of it not being assigned a value.
> >     if (OidIsValid(dbForm->datdba))
> >        dbOwner = GetUserNameFromId(dbForm->datdba, false);
> >
> > Although there is no problem in normal circumstances here. Many parts of
> > the existing code have not been checked either. Since this possibility
> > exists, it should be checked before using it. Just like the function
> > roles_is_member_of (acl.c).
> >
> > if (dbOwner)
> >    get_formatted_string(&buf, prettyFlags, 1, "OWNER = %s",
> >                          quote_identifier(dbOwner));
> >
> 
>  Fixed the given review comment. I've attached the v2 patch ready for
> review.
> 

Thanks for updating the patch, some comments on v2.

1.
Should we merge the functions pg_get_database_ddl(oid, [boolean]) and
pg_get_database_ddl(name, [boolean]) in documentation, following the
precedent set by pg_database_size in func-admin.sgml.

2.
+ * noOfTabChars - indent with specified no of tabs.

How about using 'indent with specified number of tab characters'?
And for variable noOfTabChars, I'd like use nTabs or nTabChars.

3.
+/*
+ * pg_get_database_ddl_oid
+ *
+ * Generate a CREATE DATABASE statement for the specified database oid.
+ *
+ * dbName - Name of the database for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */

A copy-paste error resulted in an incorrect comments (dbName).

-- 
Best regards,
Japin Li
ChengDu WenWu Information Technology Co., LTD.





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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-14 07:03  Chao Li <[email protected]>
  parent: Akshay Joshi <[email protected]>
  1 sibling, 0 replies; 38+ messages in thread

From: Chao Li @ 2025-11-14 07:03 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Quan Zongliang <[email protected]>; pgsql-hackers

Hi Akshay,

I quick went through the patch, I do see some problems, but I need some time to wrap up, so I will do a deep review next week. In the meantime, I want to first ask that why there is no privilege check? I think that’s a serious issue.

> On Nov 13, 2025, at 16:32, Akshay Joshi <[email protected]> wrote:
> 
> 
> <v2-0001-Add-pg_get_database_ddl-function-to-reconstruct-CREATE.patch>

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









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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-14 11:12  Álvaro Herrera <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Álvaro Herrera @ 2025-11-14 11:12 UTC (permalink / raw)
  To: Quan Zongliang <[email protected]>; +Cc: Akshay Joshi <[email protected]>; pgsql-hackers

On 2025-Nov-13, Quan Zongliang wrote:

> A more specific example. Originally, it was impossible to obtain the
> definition of "testdb" by accessing pg_database:
> 
>   postgres=> SELECT * FROM pg_database WHERE datname='testdb';
>   ERROR:  permission denied for table pg_database

Hmm.  So I was thinking that running things in this mode (where catalog
access is restricted) has never been supported.  But you're right that
we would be opening a hole that we don't have today, because if the
admin closes down permissions on pg_database, then this new function
would be a way to obtain information that the user can't currently
obtain.

My further point was to be that you still need to obtain a list of
database names or OIDs in order to do anything of value.  But it turns
out that this is extremely easy and quick to do, with something like

SELECT i, pg_describe_object('pg_database'::regclass, i, 0)
FROM generate_series(1, 1_000_000) i
WHERE pg_describe_object('pg_database'::regclass, i, 0) IS NOT NULL;

... and with this function, the user could again obtain everything about
the database even when they can't read the catalog directly.

Maybe checking privs for the database being dumped is enough protection
against this -- the equivalent of has_database_privilege( ..., 'CONNECT') 
I suppose.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"¿Qué importan los años?  Lo que realmente importa es comprobar que
a fin de cuentas la mejor edad de la vida es estar vivo"  (Mafalda)





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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-17 14:34  Akshay Joshi <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Akshay Joshi @ 2025-11-17 14:34 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Quan Zongliang <[email protected]>; pgsql-hackers

On Fri, Nov 14, 2025 at 11:19 AM Japin Li <[email protected]> wrote:

> On Thu, Nov 13, 2025 at 02:02:30PM +0530, Akshay Joshi wrote:
> > On Thu, Nov 13, 2025 at 10:18 AM Quan Zongliang <[email protected]>
> > wrote:
> >
> > >
> > >
> > > On 11/13/25 12:17 PM, Quan Zongliang wrote:
> > > >
> > > >
> > > > On 11/12/25 8:04 PM, Akshay Joshi wrote:
> > > >> Hi Hackers,
> > > >>
> > > >> I’m submitting a patch as part of the broader Retail DDL Functions
> > > >> project described by Andrew Dunstan https://www.postgresql.org/
> > > >> message- id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9%40dunslane.net
> > > >> <https:// www.postgresql.org/message-id/945db7c5-be75-45bf-b55b-
> > > >> cb1e56f2e3e9%40dunslane.net>
> > > >>
> > > >> This patch adds a new system function
> > > >> pg_get_database_ddl(database_name/ database_oid, pretty), which
> > > >> reconstructs the CREATE DATABASE statement for a given database name
> > > >> or database oid. When the pretty flag is set to true, the function
> > > >> returns a neatly formatted, multi-line DDL statement instead of a
> > > >> single-line statement.
> > > >>
> > > >> *Usage examples:*
> > > >>
> > > >> 1) SELECT pg_get_database_ddl('test_get_database_ddl_builtin');  --
> > > >> *non-pretty formatted DDL*
> > > >> pg_get_database_ddl
> > > >>
> > >
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
> > > >>    CREATE DATABASE test_get_database_ddl_builtin WITH OWNER =
> > > >> regress_ddl_database ENCODING = "UTF8" LC_COLLATE = "C" LC_CTYPE =
> "C"
> > > >> BUILTIN_LOCALE = "C.UTF-8" COLLATION_VERSION = "1" LOCALE_PROVIDER =
> > > >> 'builtin' TABLESPACE = pg_default ALLOW_CONNECTIONS = true
> CONNECTION
> > > >> LIMIT = -1;
> > > >>
> > > >>
> > > >> 2) SELECT pg_get_database_ddl('test_get_database_ddl_builtin',
> true);
> > > >> -- *pretty formatted DDL*
> > > >>
> > > >> CREATE DATABASE test_get_database_ddl_builtin
> > > >>           WITH
> > > >>           OWNER = regress_ddl_database
> > > >>           ENCODING = "UTF8"
> > > >>           LC_COLLATE = "C"
> > > >>           LC_CTYPE = "C"
> > > >>           BUILTIN_LOCALE = "C.UTF-8"
> > > >>           COLLATION_VERSION = "1"
> > > >>           LOCALE_PROVIDER = 'builtin'
> > > >>           TABLESPACE = pg_default
> > > >>           ALLOW_CONNECTIONS = true
> > > >>           CONNECTION LIMIT = -1;
> > > >>
> > > >> 3) SELECT pg_get_database_ddl(16835);      -- *non-pretty formatted
> > > >> DDL for OID*
> > > >> 4) SELECT pg_get_database_ddl(16835, true);  -- *pretty formatted
> DDL
> > > >> for OID*
> > > >>
> > > >> The patch includes documentation, in-code comments, and regression
> > > >> tests, all of which pass successfully.
> > > >> *
> > > >> **Note:* To run the regression tests, particularly the pg_upgrade
> > > >> tests successfully, I had to add a helper function, ddl_filter (in
> > > >> database.sql), which removes locale and collation-related
> information
> > > >> from the pg_get_database_ddl output.
> > > >>
> > > > I think we should check the connection permissions here. Otherwise:
> > > >
> > > > postgres=> SELECT pg_database_size('testdb');
> > > > ERROR:  permission denied for database testdb
> > > > postgres=> SELECT pg_get_database_ddl('testdb');
> > > >
> > > >                          pg_get_database_ddl
> > > >
> > >
> -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
> > > >   CREATE DATABASE testdb WITH OWNER = quanzl ENCODING = "UTF8"
> > > > LC_COLLATE = "zh_CN.UTF-8" LC_CTYPE = "zh_CN.UTF-8" LOCALE_PROVIDER =
> > > > 'libc' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION
> LIMIT
> > > > = -1;
> > > > (1 row)
> > > >
> > > > Users without connection permissions should not generate DDL.
> > > >
> > >
> > > The "dbOwner" is defined as a null pointer.
> > > char       *dbOwner = NULL;
> > >
> > > Later, there might be a risk of it not being assigned a value.
> > >     if (OidIsValid(dbForm->datdba))
> > >        dbOwner = GetUserNameFromId(dbForm->datdba, false);
> > >
> > > Although there is no problem in normal circumstances here. Many parts
> of
> > > the existing code have not been checked either. Since this possibility
> > > exists, it should be checked before using it. Just like the function
> > > roles_is_member_of (acl.c).
> > >
> > > if (dbOwner)
> > >    get_formatted_string(&buf, prettyFlags, 1, "OWNER = %s",
> > >                          quote_identifier(dbOwner));
> > >
> >
> >  Fixed the given review comment. I've attached the v2 patch ready for
> > review.
> >
>
> Thanks for updating the patch, some comments on v2.
>
> 1.
> Should we merge the functions pg_get_database_ddl(oid, [boolean]) and
> pg_get_database_ddl(name, [boolean]) in documentation, following the
> precedent set by pg_database_size in func-admin.sgml.
>
> 2.
> + * noOfTabChars - indent with specified no of tabs.
>
> How about using 'indent with specified number of tab characters'?
> And for variable noOfTabChars, I'd like use nTabs or nTabChars.
>
> 3.
> +/*
> + * pg_get_database_ddl_oid
> + *
> + * Generate a CREATE DATABASE statement for the specified database oid.
> + *
> + * dbName - Name of the database for which to generate the DDL.
> + * pretty - If true, format the DDL with indentation and line breaks.
> + */
>
> A copy-paste error resulted in an incorrect comments (dbName).
>
>
 All the review comments have been addressed in v3 patch.

> --
> Best regards,
> Japin Li
> ChengDu WenWu Information Technology Co., LTD.
>


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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-17 14:39  Akshay Joshi <[email protected]>
  parent: Álvaro Herrera <[email protected]>
  0 siblings, 0 replies; 38+ messages in thread

From: Akshay Joshi @ 2025-11-17 14:39 UTC (permalink / raw)
  To: Álvaro Herrera <[email protected]>; +Cc: Quan Zongliang <[email protected]>; pgsql-hackers

The v3 patch adds a check for the CONNECT privilege on the target database
for pg_get_database_ddl(). This aligns its security model with functions
like pg_database_size(). Note that revoking permissions on the *pg_database*
table alone is insufficient to restrict DDL access; users must manually
revoke permission on the pg_get_database_ddl() function itself if
restriction is desired.

Attached is the v3 patch ready for review.

On Fri, Nov 14, 2025 at 4:42 PM Álvaro Herrera <[email protected]> wrote:

> On 2025-Nov-13, Quan Zongliang wrote:
>
> > A more specific example. Originally, it was impossible to obtain the
> > definition of "testdb" by accessing pg_database:
> >
> >   postgres=> SELECT * FROM pg_database WHERE datname='testdb';
> >   ERROR:  permission denied for table pg_database
>
> Hmm.  So I was thinking that running things in this mode (where catalog
> access is restricted) has never been supported.  But you're right that
> we would be opening a hole that we don't have today, because if the
> admin closes down permissions on pg_database, then this new function
> would be a way to obtain information that the user can't currently
> obtain.
>
> My further point was to be that you still need to obtain a list of
> database names or OIDs in order to do anything of value.  But it turns
> out that this is extremely easy and quick to do, with something like
>
> SELECT i, pg_describe_object('pg_database'::regclass, i, 0)
> FROM generate_series(1, 1_000_000) i
> WHERE pg_describe_object('pg_database'::regclass, i, 0) IS NOT NULL;
>
> ... and with this function, the user could again obtain everything about
> the database even when they can't read the catalog directly.
>
> Maybe checking privs for the database being dumped is enough protection
> against this -- the equivalent of has_database_privilege( ..., 'CONNECT')
> I suppose.
>
> --
> Álvaro Herrera         PostgreSQL Developer  —
> https://www.EnterpriseDB.com/
> "¿Qué importan los años?  Lo que realmente importa es comprobar que
> a fin de cuentas la mejor edad de la vida es estar vivo"  (Mafalda)
>


Attachments:

  [application/octet-stream] v3-0001-Add-pg_get_database_ddl-function-to-reconstruct-C.patch (18.5K, 3-v3-0001-Add-pg_get_database_ddl-function-to-reconstruct-C.patch)
  download | inline diff:
From 65dd35ef5f3a91443d493f58dbc4cface31c8350 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Wed, 24 Sep 2025 17:47:59 +0530
Subject: [PATCH v3] Add pg_get_database_ddl() function to reconstruct CREATE
 DATABASE statements.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This adds a new system function, pg_get_database_ddl(database_name/database_oid, pretty),
which reconstructs the CREATE DATABASE statement for a given database name or database oid.

Usage:
  SELECT pg_get_database_ddl('postgres'); // Non pretty-formatted DDL
  SELECT pg_get_database_ddl(16835); // Non pretty-formatted DDL
  SELECT pg_get_database_ddl('postgres', true); // pretty-formatted DDL
  SELECT pg_get_database_ddl(16835, true); // pretty-formatted DDL

Reference: PG-150
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  63 +++++++
 src/backend/catalog/system_functions.sql |  12 ++
 src/backend/utils/adt/ruleutils.c        | 219 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   6 +
 src/test/regress/expected/database.out   |  76 ++++++++
 src/test/regress/sql/database.sql        |  62 +++++++
 6 files changed, 438 insertions(+)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..a918a1694c4 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,67 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </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_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>database_name</parameter> <type>name</type>, <optional> <parameter>pretty</parameter> <type>boolean</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para role="func_signature">
+        <indexterm>
+         <primary>pg_get_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>database_oid</parameter> <type>oid</type>, <optional> <parameter>pretty</parameter> <type>boolean</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE DATABASE</command> statement from the
+        system catalogs for a specified database name or database oid. The
+        result is a comprehensive <command>CREATE DATABASE</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+   Most of the functions that reconstruct (decompile) database objects have an
+   optional <parameter>pretty</parameter> flag, which if
+   <literal>true</literal> causes the result to be
+   <quote>pretty-printed</quote>. Pretty-printing adds tab character and new
+   line character for legibility. Passing <literal>false</literal> for the
+   <parameter>pretty</parameter> parameter yields the same result as omitting
+   the parameter.
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 2d946d6d9e9..2db9d3bbcfc 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -657,6 +657,18 @@ LANGUAGE INTERNAL
 STRICT VOLATILE PARALLEL UNSAFE
 AS 'pg_replication_origin_session_setup';
 
+CREATE OR REPLACE FUNCTION
+  pg_get_database_ddl(database_name name, pretty bool DEFAULT false)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl_name';
+
+CREATE OR REPLACE FUNCTION
+  pg_get_database_ddl(database_oid oid, pretty bool DEFAULT false)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl_oid';
+
 --
 -- 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..714bc037067 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -28,6 +28,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_depend.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_opclass.h"
@@ -57,6 +58,7 @@
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
 #include "rewrite/rewriteSupport.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
@@ -94,6 +96,10 @@
 	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
 	 : PRETTYFLAG_INDENT)
 
+#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
 
@@ -546,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 nTabChars,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char *pg_get_database_ddl_worker(Oid dbOid, int prettyFlags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13743,3 +13754,211 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - Based on prettyFlags the output includes tabs (\t) and
+ *               newlines (\n).
+ * nTabChars - indent with specified number of tab characters.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, int prettyFlags, int nTabChars, const char *fmt,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with tabs */
+		for (int i = 0; i < nTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+}
+
+/*
+ * pg_get_database_ddl_name
+ *
+ * Generate a CREATE DATABASE statement for the specified database name.
+ *
+ * dbName - Name of the database for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_database_ddl_name(PG_FUNCTION_ARGS)
+{
+	Name		dbName = PG_GETARG_NAME(0);
+	bool		pretty = PG_GETARG_BOOL(1);
+	int			prettyFlags;
+	char	   *res;
+
+	/* Get the database oid respective to the given database name */
+	Oid			dbOid = get_database_oid(NameStr(*dbName), false);
+
+	prettyFlags = GET_DDL_PRETTY_FLAGS(pretty);
+	res = pg_get_database_ddl_worker(dbOid, prettyFlags);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+/*
+ * pg_get_database_ddl_oid
+ *
+ * Generate a CREATE DATABASE statement for the specified database oid.
+ *
+ * dbOid - OID of the database for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_database_ddl_oid(PG_FUNCTION_ARGS)
+{
+	Oid			dbOid = PG_GETARG_OID(0);
+	bool		pretty = PG_GETARG_BOOL(1);
+	int			prettyFlags;
+	char	   *res;
+
+	prettyFlags = GET_DDL_PRETTY_FLAGS(pretty);
+	res = pg_get_database_ddl_worker(dbOid, prettyFlags);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_database_ddl_worker(Oid dbOid, int prettyFlags)
+{
+	char	   *dbOwner = NULL;
+	char	   *dbTablespace = NULL;
+	bool		attrIsNull;
+	Datum		dbValue;
+	HeapTuple	tupleDatabase;
+	Form_pg_database dbForm;
+	StringInfoData buf;
+	AclResult	aclresult;
+
+	/*
+	 * User must have connect privilege for target database.
+	 */
+	aclresult = object_aclcheck(DatabaseRelationId, dbOid, GetUserId(),
+								ACL_CONNECT);
+	if (aclresult != ACLCHECK_OK)
+	{
+		aclcheck_error(aclresult, OBJECT_DATABASE,
+					   get_database_name(dbOid));
+	}
+
+	/* Look up the database in pg_database */
+	tupleDatabase = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbOid));
+	if (!HeapTupleIsValid(tupleDatabase))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("database with oid %d does not exist", dbOid));
+
+	dbForm = (Form_pg_database) GETSTRUCT(tupleDatabase);
+
+	initStringInfo(&buf);
+
+	/* Look up the owner in the system catalog */
+	if (OidIsValid(dbForm->datdba))
+		dbOwner = GetUserNameFromId(dbForm->datdba, false);
+
+	/* Build the CREATE DATABASE statement */
+	appendStringInfo(&buf, "CREATE DATABASE %s",
+					 quote_identifier(dbForm->datname.data));
+	get_formatted_string(&buf, prettyFlags, 1, "WITH");
+	if (dbOwner)
+		get_formatted_string(&buf, prettyFlags, 1, "OWNER = %s",
+							 quote_identifier(dbOwner));
+
+	if (dbForm->encoding != 0)
+		get_formatted_string(&buf, prettyFlags, 1, "ENCODING = %s",
+							 quote_identifier(pg_encoding_to_char(dbForm->encoding)));
+
+	/* Fetch the value of LC_COLLATE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datcollate, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 1, "LC_COLLATE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of LC_CTYPE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datctype, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 1, "LC_CTYPE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of LOCALE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datlocale, &attrIsNull);
+	if (!attrIsNull && dbForm->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, prettyFlags, 1, "BUILTIN_LOCALE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+	else if (!attrIsNull && dbForm->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, prettyFlags, 1, "ICU_LOCALE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of ICU_RULES */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_daticurules, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 1, "ICU_RULES = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of COLLATION_VERSION */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datcollversion, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 1, "COLLATION_VERSION = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Set the appropriate LOCALE_PROVIDER */
+	if (dbForm->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, prettyFlags, 1, "LOCALE_PROVIDER = 'builtin'");
+	else if (dbForm->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, prettyFlags, 1, "LOCALE_PROVIDER = 'icu'");
+	else
+		get_formatted_string(&buf, prettyFlags, 1, "LOCALE_PROVIDER = 'libc'");
+
+	/* Get the tablespace name respective to the given tablespace oid */
+	if (OidIsValid(dbForm->dattablespace))
+	{
+		dbTablespace = get_tablespace_name(dbForm->dattablespace);
+		if (dbTablespace)
+			get_formatted_string(&buf, prettyFlags, 1, "TABLESPACE = %s",
+								 quote_identifier(dbTablespace));
+	}
+
+	get_formatted_string(&buf, prettyFlags, 1, "ALLOW_CONNECTIONS = %s",
+						 dbForm->datallowconn ? "true" : "false");
+
+	if (dbForm->datconnlimit != 0)
+		get_formatted_string(&buf, prettyFlags, 1, "CONNECTION LIMIT = %d",
+							 dbForm->datconnlimit);
+
+	if (dbForm->datistemplate)
+		get_formatted_string(&buf, prettyFlags, 1, "IS_TEMPLATE = %s",
+							 dbForm->datistemplate ? "true" : "false");
+
+	appendStringInfoChar(&buf, ';');
+
+	ReleaseSysCache(tupleDatabase);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5cf9e12fcb9..27fbb71297f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4021,6 +4021,12 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '9492', descr => 'get CREATE statement for database name',
+  proname => 'pg_get_database_ddl', prorettype => 'text',
+  proargtypes => 'name bool', prosrc => 'pg_get_database_ddl_name' },
+{ oid => '9493', descr => 'get CREATE statement for database oid',
+  proname => 'pg_get_database_ddl', prorettype => 'text',
+  proargtypes => 'oid bool', prosrc => 'pg_get_database_ddl_oid' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/test/regress/expected/database.out b/src/test/regress/expected/database.out
index 6b879b0f62a..df90fded42c 100644
--- a/src/test/regress/expected/database.out
+++ b/src/test/regress/expected/database.out
@@ -1,3 +1,49 @@
+--
+-- Reconsturct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+	-- Remove LC_COLLATE assignments 
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -16,6 +62,36 @@ CREATE ROLE regress_datdba_before;
 CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database', false);
+ERROR:  database "regression_database" does not exist
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+ pg_get_database_ddl 
+---------------------
+ 
+(1 row)
+
+-- Without pretty
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+                                                                          ddl_filter                                                                          
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after ENCODING = "UTF8" TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123;
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+	WITH
+	OWNER = regress_datdba_after
+	ENCODING = "UTF8"
+	TABLESPACE = pg_default
+	ALLOW_CONNECTIONS = true
+	CONNECTION LIMIT = 123;
+(1 row)
 DROP DATABASE regression_utf8;
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
diff --git a/src/test/regress/sql/database.sql b/src/test/regress/sql/database.sql
index 4ef36127291..392a4d96bb5 100644
--- a/src/test/regress/sql/database.sql
+++ b/src/test/regress/sql/database.sql
@@ -1,3 +1,51 @@
+--
+-- Reconsturct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+	-- Remove LC_COLLATE assignments 
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
+
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -19,6 +67,20 @@ CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
 
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database', false);
+
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+
+-- Without pretty
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', true));
+
 DROP DATABASE regression_utf8;
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
-- 
2.51.0



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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-18 00:28  Chao Li <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Chao Li @ 2025-11-18 00:28 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; Álvaro Herrera <[email protected]>; +Cc: Japin Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

Hi Akshay,

I just reviewed v3 and got some comments:

> On Nov 17, 2025, at 22:34, Akshay Joshi <[email protected]> wrote:
> 
> All the review comments have been addressed in v3 patch.


1 - ruleutils.c
```
+	if (dbForm->datconnlimit != 0)
+		get_formatted_string(&buf, prettyFlags, 1, "CONNECTION LIMIT = %d",
+							 dbForm->datconnlimit);
```

I think this is wrong. Default value of CONNECTION_LIMIT is -1 rather than 0. 0 means no connection is allowed, users should intentionally set the value, thus 0 should be printed.

2 - ruleutils.c
```
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 1, "ICU_RULES = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
```

ICU_RULES should be omitted if provider is not icu.

3 - ruleutils.c
```
+	if (!HeapTupleIsValid(tupleDatabase))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("database with oid %d does not exist", dbOid));
```

I believe all existing code use %u to format oid. I ever raised the same comment to the other get_xxx_ddl patch.

4 - ruleutils.c
```
+	/*
+	 * User must have connect privilege for target database.
+	 */
+	aclresult = object_aclcheck(DatabaseRelationId, dbOid, GetUserId(),
+								ACL_CONNECT);
+	if (aclresult != ACLCHECK_OK)
+	{
+		aclcheck_error(aclresult, OBJECT_DATABASE,
+					   get_database_name(dbOid));
+	}
```

I don’t think CONNECT privilege is good enough. By default, a new user gets CONNECT privilege via the PUBLIC role. I just did a quick test to confirm that.

```
# Create a new cluster
% initdb .
% pg_ctl -D . start
% createdb evantest
% createdb evan

# connect to the db
% psql -d evantest -U evan
psql (19devel)
Type "help" for help. # Got into the database successfully 

# Without any privilege grant, the user can get ddl of the system database, which seems not good
evantest=> select pg_get_database_ddl('postgres', true);
        pg_get_database_ddl
------------------------------------
 CREATE DATABASE postgres          +
         WITH                      +
         OWNER = chaol             +
         ENCODING = "UTF8"         +
         LC_COLLATE = "en_US.UTF-8"+
         LC_CTYPE = "en_US.UTF-8"  +
         LOCALE_PROVIDER = 'libc'  +
         TABLESPACE = pg_default   +
         ALLOW_CONNECTIONS = true  +
         CONNECTION LIMIT = -1;
(1 row)
```

IMO, only super user and database owner should be to get ddl of the database.

5 - as you can see from the above test output, “+” in the end of every line is weird.

6 - “WITH” has the same indent as the parameters, which doesn’t good look. If we look at the doc https://www.postgresql.org/docs/18/sql-createdatabase.html, “WITH” takes the first level of indent, and parameters take the second level of indent.

7 - For those parameters that have default values should be omitted. For example:
```
evantest=> select pg_get_database_ddl('evantest', true);
        pg_get_database_ddl
------------------------------------
 CREATE DATABASE evantest          +
         WITH                      +
         OWNER = chaol             +
         ENCODING = "UTF8"         +
         LC_COLLATE = "en_US.UTF-8"+
         LC_CTYPE = "en_US.UTF-8"  +
         LOCALE_PROVIDER = 'libc'  +
         TABLESPACE = pg_default   +
         ALLOW_CONNECTIONS = true  +
         CONNECTION LIMIT = -1;
(1 row)
```

I created the database “evantest” without providing any parameter. I think at least OWNER, TABLESPACE and ALLOW_CNONNECTIONS should be omitted. For CONNECTION LIMIT, you already have a logic to omit it but the logic has some problem as comment 1.

8 - ruleutils.c
```
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - Based on prettyFlags the output includes tabs (\t) and
+ *               newlines (\n).
+ * nTabChars - indent with specified number of tab characters.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, int prettyFlags, int nTabChars, const char *fmt,...)
```

I don’t feel good with this function, but not because of the implementation of the function.

I have reviewed a bunch of get_xxx_ddl patches submitted by different persons. All of them are under a big project, however, looks like to me that all authors work independently without properly coordinated. A function like this one should be common for all those patches. Maybe Alvaro can help here, pushing a common function that is used to format DDL and requesting all patches to use the function.

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









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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-18 08:03  Akshay Joshi <[email protected]>
  parent: Chao Li <[email protected]>
  0 siblings, 2 replies; 38+ messages in thread

From: Akshay Joshi @ 2025-11-18 08:03 UTC (permalink / raw)
  To: Chao Li <[email protected]>; +Cc: Álvaro Herrera <[email protected]>; Japin Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

Hi Chao

Thanks for reviewing my patch.

On Tue, Nov 18, 2025 at 5:59 AM Chao Li <[email protected]> wrote:

> Hi Akshay,
>
> I just reviewed v3 and got some comments:
>
> > On Nov 17, 2025, at 22:34, Akshay Joshi <[email protected]>
> wrote:
> >
> > All the review comments have been addressed in v3 patch.
>
>
> 1 - ruleutils.c
> ```
> +       if (dbForm->datconnlimit != 0)
> +               get_formatted_string(&buf, prettyFlags, 1, "CONNECTION
> LIMIT = %d",
> +
> dbForm->datconnlimit);
> ```
>
> I think this is wrong. Default value of CONNECTION_LIMIT is -1 rather than
> 0. 0 means no connection is allowed, users should intentionally set the
> value, thus 0 should be printed.
>
> 2 - ruleutils.c
> ```
> +       if (!attrIsNull)
> +               get_formatted_string(&buf, prettyFlags, 1, "ICU_RULES =
> %s",
> +
> quote_identifier(TextDatumGetCString(dbValue)));
> ```
>
> ICU_RULES should be omitted if provider is not icu.
>
> 3 - ruleutils.c
> ```
> +       if (!HeapTupleIsValid(tupleDatabase))
> +               ereport(ERROR,
> +                               errcode(ERRCODE_UNDEFINED_OBJECT),
> +                               errmsg("database with oid %d does not
> exist", dbOid));
> ```
>
> I believe all existing code use %u to format oid. I ever raised the same
> comment to the other get_xxx_ddl patch.
>

 Fixed all above in the attached v4 patch.

>
> 4 - ruleutils.c
> ```
> +       /*
> +        * User must have connect privilege for target database.
> +        */
> +       aclresult = object_aclcheck(DatabaseRelationId, dbOid, GetUserId(),
> +
>  ACL_CONNECT);
> +       if (aclresult != ACLCHECK_OK)
> +       {
> +               aclcheck_error(aclresult, OBJECT_DATABASE,
> +                                          get_database_name(dbOid));
> +       }
> ```
>
> I don’t think CONNECT privilege is good enough. By default, a new user
> gets CONNECT privilege via the PUBLIC role. I just did a quick test to
> confirm that.
>
> ```
> # Create a new cluster
> % initdb .
> % pg_ctl -D . start
> % createdb evantest
> % createdb evan
>
> # connect to the db
> % psql -d evantest -U evan
> psql (19devel)
> Type "help" for help. # Got into the database successfully
>
> # Without any privilege grant, the user can get ddl of the system
> database, which seems not good
> evantest=> select pg_get_database_ddl('postgres', true);
>         pg_get_database_ddl
> ------------------------------------
>  CREATE DATABASE postgres          +
>          WITH                      +
>          OWNER = chaol             +
>          ENCODING = "UTF8"         +
>          LC_COLLATE = "en_US.UTF-8"+
>          LC_CTYPE = "en_US.UTF-8"  +
>          LOCALE_PROVIDER = 'libc'  +
>          TABLESPACE = pg_default   +
>          ALLOW_CONNECTIONS = true  +
>          CONNECTION LIMIT = -1;
> (1 row)
> ```
>
> IMO, only super user and database owner should be to get ddl of the
> database.
>

 I wasn't entirely sure, but after reviewing the *pg_database_size*()
function, I've concluded that its usage extends beyond the Superuser and
Database Owner. Specifically, other roles can view the database size if
they have the *CONNECT* privilege or are Members of the *pg_read_all_stats*
role.

>
> 5 - as you can see from the above test output, “+” in the end of every
> line is weird.
>

The plus sign (+) is merely an artifact of *psql's* output formatting when
a result cell contains a newline character (\n). It serves as a visual cue
to the user that the data continues on the next line. This is confirmed by
the absence of the + sign when viewing the same data in a different client,
such as *pgAdmin*."  To suppress this visual cue in psql, you can use the
command: \pset format unaligned

>
> 6 - “WITH” has the same indent as the parameters, which doesn’t good look.
> If we look at the doc
> https://www.postgresql.org/docs/18/sql-createdatabase.html, “WITH” takes
> the first level of indent, and parameters take the second level of indent.
>

Fixed in the v4 patch and followed the docs.

>
> 7 - For those parameters that have default values should be omitted. For
> example:
> ```
> evantest=> select pg_get_database_ddl('evantest', true);
>         pg_get_database_ddl
> ------------------------------------
>  CREATE DATABASE evantest          +
>          WITH                      +
>          OWNER = chaol             +
>          ENCODING = "UTF8"         +
>          LC_COLLATE = "en_US.UTF-8"+
>          LC_CTYPE = "en_US.UTF-8"  +
>          LOCALE_PROVIDER = 'libc'  +
>          TABLESPACE = pg_default   +
>          ALLOW_CONNECTIONS = true  +
>          CONNECTION LIMIT = -1;
> (1 row)
> ```
>
> I created the database “evantest” without providing any parameter. I think
> at least OWNER, TABLESPACE and ALLOW_CNONNECTIONS should be omitted. For
> CONNECTION LIMIT, you already have a logic to omit it but the logic has
> some problem as comment 1.
>

IMHO, parameters with default values *should not* be omitted from the
output of the pg_get_xxx_ddl functions. The primary purpose of these
functions is to accurately reconstruct the DDL. Including all parameters
ensures clarity, as not everyone is familiar with the default value of
every single parameter.

>
> 8 - ruleutils.c
> ```
> +/*
> + * get_formatted_string
> + *
> + * Return a formatted version of the string.
> + *
> + * prettyFlags - Based on prettyFlags the output includes tabs (\t) and
> + *               newlines (\n).
> + * nTabChars - indent with specified number of tab characters.
> + * fmt - printf-style format string used by appendStringInfoVA.
> + */
> +static void
> +get_formatted_string(StringInfo buf, int prettyFlags, int nTabChars,
> const char *fmt,...)
> ```
>
> I don’t feel good with this function, but not because of the
> implementation of the function.
>
> I have reviewed a bunch of get_xxx_ddl patches submitted by different
> persons. All of them are under a big project, however, looks like to me
> that all authors work independently without properly coordinated. A
> function like this one should be common for all those patches. Maybe Alvaro
> can help here, pushing a common function that is used to format DDL and
> requesting all patches to use the function.
>

Yes, all pg_get_xxx_ddl functions are part of a larger project, and
different authors have implemented output formatting in different ways;
some implementations may lack formatting altogether. Yes I agree one common
function should be developed and committed so that all other authors can
reuse it, ensuring consistency across the entire suite of DDL functions."

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


Attachments:

  [application/octet-stream] v4-0001-Add-pg_get_database_ddl-function-to-reconstruct-CREATE.patch (18.5K, 3-v4-0001-Add-pg_get_database_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From 664f14d030232607da1c644fbb808731f8648a2d Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Wed, 24 Sep 2025 17:47:59 +0530
Subject: [PATCH v4] Add pg_get_database_ddl() function to reconstruct CREATE
 DATABASE statements.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This adds a new system function, pg_get_database_ddl(database_name/database_oid, pretty),
which reconstructs the CREATE DATABASE statement for a given database name or database oid.

Usage:
  SELECT pg_get_database_ddl('postgres'); // Non pretty-formatted DDL
  SELECT pg_get_database_ddl(16835); // Non pretty-formatted DDL
  SELECT pg_get_database_ddl('postgres', true); // pretty-formatted DDL
  SELECT pg_get_database_ddl(16835, true); // pretty-formatted DDL

Reference: PG-150
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  63 +++++++
 src/backend/catalog/system_functions.sql |  12 ++
 src/backend/utils/adt/ruleutils.c        | 217 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   6 +
 src/test/regress/expected/database.out   |  75 ++++++++
 src/test/regress/sql/database.sql        |  62 +++++++
 6 files changed, 435 insertions(+)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..a918a1694c4 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,67 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </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_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>database_name</parameter> <type>name</type>, <optional> <parameter>pretty</parameter> <type>boolean</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para role="func_signature">
+        <indexterm>
+         <primary>pg_get_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>database_oid</parameter> <type>oid</type>, <optional> <parameter>pretty</parameter> <type>boolean</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE DATABASE</command> statement from the
+        system catalogs for a specified database name or database oid. The
+        result is a comprehensive <command>CREATE DATABASE</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+   Most of the functions that reconstruct (decompile) database objects have an
+   optional <parameter>pretty</parameter> flag, which if
+   <literal>true</literal> causes the result to be
+   <quote>pretty-printed</quote>. Pretty-printing adds tab character and new
+   line character for legibility. Passing <literal>false</literal> for the
+   <parameter>pretty</parameter> parameter yields the same result as omitting
+   the parameter.
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 2d946d6d9e9..2db9d3bbcfc 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -657,6 +657,18 @@ LANGUAGE INTERNAL
 STRICT VOLATILE PARALLEL UNSAFE
 AS 'pg_replication_origin_session_setup';
 
+CREATE OR REPLACE FUNCTION
+  pg_get_database_ddl(database_name name, pretty bool DEFAULT false)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl_name';
+
+CREATE OR REPLACE FUNCTION
+  pg_get_database_ddl(database_oid oid, pretty bool DEFAULT false)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl_oid';
+
 --
 -- 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..b39723b0855 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -28,6 +28,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_depend.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_opclass.h"
@@ -57,6 +58,7 @@
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
 #include "rewrite/rewriteSupport.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
@@ -94,6 +96,10 @@
 	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
 	 : PRETTYFLAG_INDENT)
 
+#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
 
@@ -546,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 nTabChars,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char *pg_get_database_ddl_worker(Oid dbOid, int prettyFlags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13743,3 +13754,209 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - Based on prettyFlags the output includes tabs (\t) and
+ *               newlines (\n).
+ * nTabChars - indent with specified number of tab characters.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, int prettyFlags, int nTabChars, const char *fmt,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with tabs */
+		for (int i = 0; i < nTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+}
+
+/*
+ * pg_get_database_ddl_name
+ *
+ * Generate a CREATE DATABASE statement for the specified database name.
+ *
+ * dbName - Name of the database for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_database_ddl_name(PG_FUNCTION_ARGS)
+{
+	Name		dbName = PG_GETARG_NAME(0);
+	bool		pretty = PG_GETARG_BOOL(1);
+	int			prettyFlags;
+	char	   *res;
+
+	/* Get the database oid respective to the given database name */
+	Oid			dbOid = get_database_oid(NameStr(*dbName), false);
+
+	prettyFlags = GET_DDL_PRETTY_FLAGS(pretty);
+	res = pg_get_database_ddl_worker(dbOid, prettyFlags);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+/*
+ * pg_get_database_ddl_oid
+ *
+ * Generate a CREATE DATABASE statement for the specified database oid.
+ *
+ * dbOid - OID of the database for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_database_ddl_oid(PG_FUNCTION_ARGS)
+{
+	Oid			dbOid = PG_GETARG_OID(0);
+	bool		pretty = PG_GETARG_BOOL(1);
+	int			prettyFlags;
+	char	   *res;
+
+	prettyFlags = GET_DDL_PRETTY_FLAGS(pretty);
+	res = pg_get_database_ddl_worker(dbOid, prettyFlags);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_database_ddl_worker(Oid dbOid, int prettyFlags)
+{
+	char	   *dbOwner = NULL;
+	char	   *dbTablespace = NULL;
+	bool		attrIsNull;
+	Datum		dbValue;
+	HeapTuple	tupleDatabase;
+	Form_pg_database dbForm;
+	StringInfoData buf;
+	AclResult	aclresult;
+
+	/*
+	 * User must have connect privilege for target database.
+	 */
+	aclresult = object_aclcheck(DatabaseRelationId, dbOid, GetUserId(),
+								ACL_CONNECT);
+	if (aclresult != ACLCHECK_OK &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclcheck_error(aclresult, OBJECT_DATABASE,
+					   get_database_name(dbOid));
+	}
+
+	/* Look up the database in pg_database */
+	tupleDatabase = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbOid));
+	if (!HeapTupleIsValid(tupleDatabase))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("database with oid %u does not exist", dbOid));
+
+	dbForm = (Form_pg_database) GETSTRUCT(tupleDatabase);
+
+	initStringInfo(&buf);
+
+	/* Look up the owner in the system catalog */
+	if (OidIsValid(dbForm->datdba))
+		dbOwner = GetUserNameFromId(dbForm->datdba, false);
+
+	/* Build the CREATE DATABASE statement */
+	appendStringInfo(&buf, "CREATE DATABASE %s",
+					 quote_identifier(dbForm->datname.data));
+	get_formatted_string(&buf, prettyFlags, 1, "WITH OWNER = %s",
+						 quote_identifier(dbOwner));
+
+	if (dbForm->encoding != 0)
+		get_formatted_string(&buf, prettyFlags, 2, "ENCODING = %s",
+							 quote_identifier(pg_encoding_to_char(dbForm->encoding)));
+
+	/* Fetch the value of LC_COLLATE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datcollate, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 2, "LC_COLLATE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of LC_CTYPE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datctype, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 2, "LC_CTYPE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of LOCALE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datlocale, &attrIsNull);
+	if (!attrIsNull && dbForm->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, prettyFlags, 2, "BUILTIN_LOCALE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+	else if (!attrIsNull && dbForm->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, prettyFlags, 2, "ICU_LOCALE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of ICU_RULES */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_daticurules, &attrIsNull);
+	if (!attrIsNull && dbForm->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, prettyFlags, 2, "ICU_RULES = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of COLLATION_VERSION */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datcollversion, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 2, "COLLATION_VERSION = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Set the appropriate LOCALE_PROVIDER */
+	if (dbForm->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, prettyFlags, 2, "LOCALE_PROVIDER = 'builtin'");
+	else if (dbForm->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, prettyFlags, 2, "LOCALE_PROVIDER = 'icu'");
+	else
+		get_formatted_string(&buf, prettyFlags, 2, "LOCALE_PROVIDER = 'libc'");
+
+	/* Get the tablespace name respective to the given tablespace oid */
+	if (OidIsValid(dbForm->dattablespace))
+	{
+		dbTablespace = get_tablespace_name(dbForm->dattablespace);
+		if (dbTablespace)
+			get_formatted_string(&buf, prettyFlags, 2, "TABLESPACE = %s",
+								 quote_identifier(dbTablespace));
+	}
+
+	get_formatted_string(&buf, prettyFlags, 2, "ALLOW_CONNECTIONS = %s",
+						 dbForm->datallowconn ? "true" : "false");
+
+	get_formatted_string(&buf, prettyFlags, 2, "CONNECTION LIMIT = %d",
+						 dbForm->datconnlimit);
+
+	if (dbForm->datistemplate)
+		get_formatted_string(&buf, prettyFlags, 2, "IS_TEMPLATE = %s",
+							 dbForm->datistemplate ? "true" : "false");
+
+	appendStringInfoChar(&buf, ';');
+
+	ReleaseSysCache(tupleDatabase);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5cf9e12fcb9..27fbb71297f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4021,6 +4021,12 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '9492', descr => 'get CREATE statement for database name',
+  proname => 'pg_get_database_ddl', prorettype => 'text',
+  proargtypes => 'name bool', prosrc => 'pg_get_database_ddl_name' },
+{ oid => '9493', descr => 'get CREATE statement for database oid',
+  proname => 'pg_get_database_ddl', prorettype => 'text',
+  proargtypes => 'oid bool', prosrc => 'pg_get_database_ddl_oid' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/test/regress/expected/database.out b/src/test/regress/expected/database.out
index 6b879b0f62a..fa0d1b01e67 100644
--- a/src/test/regress/expected/database.out
+++ b/src/test/regress/expected/database.out
@@ -1,3 +1,49 @@
+--
+-- Reconsturct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+	-- Remove LC_COLLATE assignments 
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -16,6 +62,35 @@ CREATE ROLE regress_datdba_before;
 CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database', false);
+ERROR:  database "regression_database" does not exist
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+ pg_get_database_ddl 
+---------------------
+ 
+(1 row)
+
+-- Without pretty
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+                                                                          ddl_filter                                                                          
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after ENCODING = "UTF8" TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123;
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+	WITH OWNER = regress_datdba_after
+		ENCODING = "UTF8"
+		TABLESPACE = pg_default
+		ALLOW_CONNECTIONS = true
+		CONNECTION LIMIT = 123;
+(1 row)
 DROP DATABASE regression_utf8;
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
diff --git a/src/test/regress/sql/database.sql b/src/test/regress/sql/database.sql
index 4ef36127291..392a4d96bb5 100644
--- a/src/test/regress/sql/database.sql
+++ b/src/test/regress/sql/database.sql
@@ -1,3 +1,51 @@
+--
+-- Reconsturct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+	-- Remove LC_COLLATE assignments 
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
+
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -19,6 +67,20 @@ CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
 
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database', false);
+
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+
+-- Without pretty
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', true));
+
 DROP DATABASE regression_utf8;
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
-- 
2.51.0



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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-19 10:17  Japin Li <[email protected]>
  parent: Akshay Joshi <[email protected]>
  1 sibling, 1 reply; 38+ messages in thread

From: Japin Li @ 2025-11-19 10:17 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Chao Li <[email protected]>; Álvaro Herrera <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers


Hi Akshay,

Thanks for updating the patch.

On Tue, 18 Nov 2025 at 13:33, Akshay Joshi <[email protected]> wrote:
> Hi Chao
>
> Thanks for reviewing my patch.
>
> On Tue, Nov 18, 2025 at 5:59 AM Chao Li <[email protected]> wrote:
>
>  Hi Akshay,
>
>  I just reviewed v3 and got some comments:
>
>  > On Nov 17, 2025, at 22:34, Akshay Joshi <[email protected]> wrote:
>  > 
>  > All the review comments have been addressed in v3 patch.
>
>  1 - ruleutils.c
>  ```
>  +       if (dbForm->datconnlimit != 0)
>  +               get_formatted_string(&buf, prettyFlags, 1, "CONNECTION LIMIT = %d",
>  +                                                        dbForm->datconnlimit);
>  ```
>
>  I think this is wrong. Default value of CONNECTION_LIMIT is -1 rather than 0. 0 means no connection is allowed, users
>  should intentionally set the value, thus 0 should be printed.
>
>  2 - ruleutils.c
>  ```
>  +       if (!attrIsNull)
>  +               get_formatted_string(&buf, prettyFlags, 1, "ICU_RULES = %s",
>  +                                                        quote_identifier(TextDatumGetCString(dbValue)));
>  ```
>
>  ICU_RULES should be omitted if provider is not icu.
>
>  3 - ruleutils.c
>  ```
>  +       if (!HeapTupleIsValid(tupleDatabase))
>  +               ereport(ERROR,
>  +                               errcode(ERRCODE_UNDEFINED_OBJECT),
>  +                               errmsg("database with oid %d does not exist", dbOid));
>  ```
>
>  I believe all existing code use %u to format oid. I ever raised the same comment to the other get_xxx_ddl patch.
>
>  Fixed all above in the attached v4 patch. 

1.
Since the STRATEGY and OID parameters are not being reconstructed, should we
document this behavior?

postgres=# CREATE DATABASE mydb WITH STRATEGY file_copy OID 19876 IS_TEMPLATE true;
CREATE DATABASE
postgres=# SELECT pg_get_database_ddl('mydb', true);
            pg_get_database_ddl
--------------------------------------------
 CREATE DATABASE mydb                      +
         WITH OWNER = japin                +
                 ENCODING = "UTF8"         +
                 LC_COLLATE = "en_US.UTF-8"+
                 LC_CTYPE = "en_US.UTF-8"  +
                 COLLATION_VERSION = "2.39"+
                 LOCALE_PROVIDER = 'libc'  +
                 TABLESPACE = pg_default   +
                 ALLOW_CONNECTIONS = true  +
                 CONNECTION LIMIT = -1     +
                 IS_TEMPLATE = true;
(1 row)

2.
We can restrict the scope of the dbTablespace variable as follows:

+	if (OidIsValid(dbForm->dattablespace))
+	{
+		char *dbTablespace = get_tablespace_name(dbForm->dattablespace);
+		if (dbTablespace)
+			get_formatted_string(&buf, prettyFlags, 2, "TABLESPACE = %s",
+								 quote_identifier(dbTablespace));
+	}

> >
> > 7 - For those parameters that have default values should be omitted. For
> > example:
> > ```
> > evantest=> select pg_get_database_ddl('evantest', true);
> >         pg_get_database_ddl
> > ------------------------------------
> >  CREATE DATABASE evantest          +
> >          WITH                      +
> >          OWNER = chaol             +
> >          ENCODING = "UTF8"         +
> >          LC_COLLATE = "en_US.UTF-8"+
> >          LC_CTYPE = "en_US.UTF-8"  +
> >          LOCALE_PROVIDER = 'libc'  +
> >          TABLESPACE = pg_default   +
> >          ALLOW_CONNECTIONS = true  +
> >          CONNECTION LIMIT = -1;
> > (1 row)
> > ```
> >
> > I created the database “evantest” without providing any parameter. I think
> > at least OWNER, TABLESPACE and ALLOW_CNONNECTIONS should be omitted. For
> > CONNECTION LIMIT, you already have a logic to omit it but the logic has
> > some problem as comment 1.
> >
> 
> 
> 
> IMHO, parameters with default values *should not* be omitted from the
> output of the pg_get_xxx_ddl functions. The primary purpose of these
> functions is to accurately reconstruct the DDL. Including all parameters
> ensures clarity, as not everyone is familiar with the default value of
> every single parameter.

+1

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





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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-19 10:47  Álvaro Herrera <[email protected]>
  parent: Akshay Joshi <[email protected]>
  1 sibling, 2 replies; 38+ messages in thread

From: Álvaro Herrera @ 2025-11-19 10:47 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Chao Li <[email protected]>; Japin Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

Hello,

One thing I realized a few days ago is that since commit bd09f024a1bb we
have type regdatabase, so instead of having two functions (one taking
name and one taking Oid), we should have just one, taking regdatabase,
just like the functions for producing DDL for other object types that
have corresponding reg* type.

Regards,

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/





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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-19 11:06  Akshay Joshi <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Akshay Joshi @ 2025-11-19 11:06 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Chao Li <[email protected]>; Álvaro Herrera <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

On Wed, Nov 19, 2025 at 3:48 PM Japin Li <[email protected]> wrote:

>
> Hi Akshay,
>
> Thanks for updating the patch.
>
> On Tue, 18 Nov 2025 at 13:33, Akshay Joshi <[email protected]>
> wrote:
> > Hi Chao
> >
> > Thanks for reviewing my patch.
> >
> > On Tue, Nov 18, 2025 at 5:59 AM Chao Li <[email protected]> wrote:
> >
> >  Hi Akshay,
> >
> >  I just reviewed v3 and got some comments:
> >
> >  > On Nov 17, 2025, at 22:34, Akshay Joshi <
> [email protected]> wrote:
> >  >
> >  > All the review comments have been addressed in v3 patch.
> >
> >  1 - ruleutils.c
> >  ```
> >  +       if (dbForm->datconnlimit != 0)
> >  +               get_formatted_string(&buf, prettyFlags, 1, "CONNECTION
> LIMIT = %d",
> >  +
> dbForm->datconnlimit);
> >  ```
> >
> >  I think this is wrong. Default value of CONNECTION_LIMIT is -1 rather
> than 0. 0 means no connection is allowed, users
> >  should intentionally set the value, thus 0 should be printed.
> >
> >  2 - ruleutils.c
> >  ```
> >  +       if (!attrIsNull)
> >  +               get_formatted_string(&buf, prettyFlags, 1, "ICU_RULES =
> %s",
> >  +
> quote_identifier(TextDatumGetCString(dbValue)));
> >  ```
> >
> >  ICU_RULES should be omitted if provider is not icu.
> >
> >  3 - ruleutils.c
> >  ```
> >  +       if (!HeapTupleIsValid(tupleDatabase))
> >  +               ereport(ERROR,
> >  +                               errcode(ERRCODE_UNDEFINED_OBJECT),
> >  +                               errmsg("database with oid %d does not
> exist", dbOid));
> >  ```
> >
> >  I believe all existing code use %u to format oid. I ever raised the
> same comment to the other get_xxx_ddl patch.
> >
> >  Fixed all above in the attached v4 patch.
>
> 1.
> Since the STRATEGY and OID parameters are not being reconstructed, should
> we
> document this behavior?
>
> postgres=# CREATE DATABASE mydb WITH STRATEGY file_copy OID 19876
> IS_TEMPLATE true;
> CREATE DATABASE
> postgres=# SELECT pg_get_database_ddl('mydb', true);
>             pg_get_database_ddl
> --------------------------------------------
>  CREATE DATABASE mydb                      +
>          WITH OWNER = japin                +
>                  ENCODING = "UTF8"         +
>                  LC_COLLATE = "en_US.UTF-8"+
>                  LC_CTYPE = "en_US.UTF-8"  +
>                  COLLATION_VERSION = "2.39"+
>                  LOCALE_PROVIDER = 'libc'  +
>                  TABLESPACE = pg_default   +
>                  ALLOW_CONNECTIONS = true  +
>                  CONNECTION LIMIT = -1     +
>                  IS_TEMPLATE = true;
> (1 row)
>

The FormData_pg_database structure does not expose the database *STRATEGY*,
and reconstructing the *OID* serves no practical purpose. If the majority
agrees, I can extend the DDL to include OID.

>
> 2.
> We can restrict the scope of the dbTablespace variable as follows:
>
> +       if (OidIsValid(dbForm->dattablespace))
> +       {
> +               char *dbTablespace =
> get_tablespace_name(dbForm->dattablespace);
> +               if (dbTablespace)
> +                       get_formatted_string(&buf, prettyFlags, 2,
> "TABLESPACE = %s",
> +
> quote_identifier(dbTablespace));
> +       }
>

   Will fix this in the next patch.

>
> > >
> > > 7 - For those parameters that have default values should be omitted.
> For
> > > example:
> > > ```
> > > evantest=> select pg_get_database_ddl('evantest', true);
> > >         pg_get_database_ddl
> > > ------------------------------------
> > >  CREATE DATABASE evantest          +
> > >          WITH                      +
> > >          OWNER = chaol             +
> > >          ENCODING = "UTF8"         +
> > >          LC_COLLATE = "en_US.UTF-8"+
> > >          LC_CTYPE = "en_US.UTF-8"  +
> > >          LOCALE_PROVIDER = 'libc'  +
> > >          TABLESPACE = pg_default   +
> > >          ALLOW_CONNECTIONS = true  +
> > >          CONNECTION LIMIT = -1;
> > > (1 row)
> > > ```
> > >
> > > I created the database “evantest” without providing any parameter. I
> think
> > > at least OWNER, TABLESPACE and ALLOW_CNONNECTIONS should be omitted.
> For
> > > CONNECTION LIMIT, you already have a logic to omit it but the logic has
> > > some problem as comment 1.
> > >
> >
> >
> >
> > IMHO, parameters with default values *should not* be omitted from the
> > output of the pg_get_xxx_ddl functions. The primary purpose of these
> > functions is to accurately reconstruct the DDL. Including all parameters
> > ensures clarity, as not everyone is familiar with the default value of
> > every single parameter.
>
> +1
>
> --
> Regards,
> Japin Li
> ChengDu WenWu Information Technology Co., Ltd.
>


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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-19 11:07  Akshay Joshi <[email protected]>
  parent: Álvaro Herrera <[email protected]>
  1 sibling, 0 replies; 38+ messages in thread

From: Akshay Joshi @ 2025-11-19 11:07 UTC (permalink / raw)
  To: Álvaro Herrera <[email protected]>; +Cc: Chao Li <[email protected]>; Japin Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

Thanks Álvaro
Will work on it and send the updated patch.

On Wed, Nov 19, 2025 at 4:17 PM Álvaro Herrera <[email protected]> wrote:

> Hello,
>
> One thing I realized a few days ago is that since commit bd09f024a1bb we
> have type regdatabase, so instead of having two functions (one taking
> name and one taking Oid), we should have just one, taking regdatabase,
> just like the functions for producing DDL for other object types that
> have corresponding reg* type.
>
> Regards,
>
> --
> Álvaro Herrera         PostgreSQL Developer  —
> https://www.EnterpriseDB.com/
>


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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-20 01:39  Japin Li <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 0 replies; 38+ messages in thread

From: Japin Li @ 2025-11-20 01:39 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Chao Li <[email protected]>; Álvaro Herrera <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

On Wed, 19 Nov 2025 at 16:36, Akshay Joshi <[email protected]> wrote:
> On Wed, Nov 19, 2025 at 3:48 PM Japin Li <[email protected]> wrote:
>
>  Hi Akshay,
>
>  Thanks for updating the patch.
>
>  On Tue, 18 Nov 2025 at 13:33, Akshay Joshi <[email protected]> wrote:
>  > Hi Chao
>  >
>  > Thanks for reviewing my patch.
>  >
>  > On Tue, Nov 18, 2025 at 5:59 AM Chao Li <[email protected]> wrote:
>  >
>  >  Hi Akshay,
>  >
>  >  I just reviewed v3 and got some comments:
>  >
>  >  > On Nov 17, 2025, at 22:34, Akshay Joshi <[email protected]> wrote:
>  >  > 
>  >  > All the review comments have been addressed in v3 patch.
>  >
>  >  1 - ruleutils.c
>  >  ```
>  >  +       if (dbForm->datconnlimit != 0)
>  >  +               get_formatted_string(&buf, prettyFlags, 1, "CONNECTION LIMIT = %d",
>  >  +                                                        dbForm->datconnlimit);
>  >  ```
>  >
>  >  I think this is wrong. Default value of CONNECTION_LIMIT is -1 rather than 0. 0 means no connection is allowed,
>  users
>  >  should intentionally set the value, thus 0 should be printed.
>  >
>  >  2 - ruleutils.c
>  >  ```
>  >  +       if (!attrIsNull)
>  >  +               get_formatted_string(&buf, prettyFlags, 1, "ICU_RULES = %s",
>  >  +                                                        quote_identifier(TextDatumGetCString(dbValue)));
>  >  ```
>  >
>  >  ICU_RULES should be omitted if provider is not icu.
>  >
>  >  3 - ruleutils.c
>  >  ```
>  >  +       if (!HeapTupleIsValid(tupleDatabase))
>  >  +               ereport(ERROR,
>  >  +                               errcode(ERRCODE_UNDEFINED_OBJECT),
>  >  +                               errmsg("database with oid %d does not exist", dbOid));
>  >  ```
>  >
>  >  I believe all existing code use %u to format oid. I ever raised the same comment to the other get_xxx_ddl patch.
>  >
>  >  Fixed all above in the attached v4 patch. 
>
>  1.
>  Since the STRATEGY and OID parameters are not being reconstructed, should we
>  document this behavior?
>
>  postgres=# CREATE DATABASE mydb WITH STRATEGY file_copy OID 19876 IS_TEMPLATE true;
>  CREATE DATABASE
>  postgres=# SELECT pg_get_database_ddl('mydb', true);
>              pg_get_database_ddl
>  --------------------------------------------
>   CREATE DATABASE mydb                      +
>           WITH OWNER = japin                +
>                   ENCODING = "UTF8"         +
>                   LC_COLLATE = "en_US.UTF-8"+
>                   LC_CTYPE = "en_US.UTF-8"  +
>                   COLLATION_VERSION = "2.39"+
>                   LOCALE_PROVIDER = 'libc'  +
>                   TABLESPACE = pg_default   +
>                   ALLOW_CONNECTIONS = true  +
>                   CONNECTION LIMIT = -1     +
>                   IS_TEMPLATE = true;
>  (1 row)
>
> The FormData_pg_database structure does not expose the database STRATEGY,

Yes, it's not exposed.

> and reconstructing the OID serves no practical
> purpose. If the majority agrees, I can extend the DDL to include OID. 

I'm not insisting on reconstructing those parameters; I mean, we can provide
a sentence to describe this behavior.

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





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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-11-20 09:18  Akshay Joshi <[email protected]>
  parent: Álvaro Herrera <[email protected]>
  1 sibling, 1 reply; 38+ messages in thread

From: Akshay Joshi @ 2025-11-20 09:18 UTC (permalink / raw)
  To: Álvaro Herrera <[email protected]>; +Cc: Chao Li <[email protected]>; Japin Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

Hi Álvaro,

On Wed, Nov 19, 2025 at 4:17 PM Álvaro Herrera <[email protected]> wrote:

> Hello,
>
> One thing I realized a few days ago is that since commit bd09f024a1bb we
> have type regdatabase, so instead of having two functions (one taking
> name and one taking Oid), we should have just one, taking regdatabase,
> just like the functions for producing DDL for other object types that
> have corresponding reg* type.
>

 Implemented in the suggested solution. Attached is the v5 patch for review.

>
> Regards,
>
> --
> Álvaro Herrera         PostgreSQL Developer  —
> https://www.EnterpriseDB.com/
>


Attachments:

  [application/octet-stream] v5-0001-Add-pg_get_database_ddl-function-to-reconstruct-CREATE.patch (17.2K, 3-v5-0001-Add-pg_get_database_ddl-function-to-reconstruct-CREATE.patch)
  download | inline diff:
From 8ea45bf50d6dd521bedaa40eaad88bcb35103aaa Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Wed, 24 Sep 2025 17:47:59 +0530
Subject: [PATCH v5] Add pg_get_database_ddl() function to reconstruct CREATE
 DATABASE statements.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This adds a new system function, pg_get_database_ddl(database_name/database_oid, pretty),
which reconstructs the CREATE DATABASE statement for a given database name or database oid.

Usage:
  SELECT pg_get_database_ddl('postgres'); // Non pretty-formatted DDL
  SELECT pg_get_database_ddl(16835); // Non pretty-formatted DDL
  SELECT pg_get_database_ddl('postgres', true); // pretty-formatted DDL
  SELECT pg_get_database_ddl(16835, true); // pretty-formatted DDL

Reference: PG-150
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  55 +++++++
 src/backend/catalog/system_functions.sql |   6 +
 src/backend/utils/adt/ruleutils.c        | 189 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   3 +
 src/test/regress/expected/database.out   |  77 +++++++++
 src/test/regress/sql/database.sql        |  62 ++++++++
 6 files changed, 392 insertions(+)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..115724616cd 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3797,4 +3797,59 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </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_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>databaseID</parameter> <type>regdatabase</type>, <optional> <parameter>pretty</parameter> <type>boolean</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE DATABASE</command> statement from the
+        system catalogs for a specified database name or database oid. The
+        result is a comprehensive <command>CREATE DATABASE</command> statement.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+   Most of the functions that reconstruct (decompile) database objects have an
+   optional <parameter>pretty</parameter> flag, which if
+   <literal>true</literal> causes the result to be
+   <quote>pretty-printed</quote>. Pretty-printing adds tab character and new
+   line character for legibility. Passing <literal>false</literal> for the
+   <parameter>pretty</parameter> parameter yields the same result as omitting
+   the parameter.
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 2d946d6d9e9..9fb02a2017d 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_database_ddl(databaseID regdatabase, pretty bool DEFAULT false)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl';
+
 --
 -- 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..ded8247212a 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -28,6 +28,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_depend.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_opclass.h"
@@ -57,6 +58,7 @@
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
 #include "rewrite/rewriteSupport.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
@@ -94,6 +96,10 @@
 	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
 	 : PRETTYFLAG_INDENT)
 
+#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
 
@@ -546,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 nTabChars,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char *pg_get_database_ddl_worker(Oid dbOid, int prettyFlags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13743,3 +13754,181 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - Based on prettyFlags the output includes tabs (\t) and
+ *               newlines (\n).
+ * nTabChars - indent with specified number of tab characters.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, int prettyFlags, int nTabChars, const char *fmt,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with tabs */
+		for (int i = 0; i < nTabChars; i++)
+		{
+			appendStringInfoChar(buf, '\t');
+		}
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+}
+
+/*
+ * pg_get_database_ddl
+ *
+ * Generate a CREATE DATABASE statement for the specified database name or oid.
+ *
+ * databaseID - OID/Name of the database for which to generate the DDL.
+ * pretty - If true, format the DDL with indentation and line breaks.
+ */
+Datum
+pg_get_database_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			dbOid = PG_GETARG_OID(0);
+	bool		pretty = PG_GETARG_BOOL(1);
+	int			prettyFlags;
+	char	   *res;
+
+	prettyFlags = GET_DDL_PRETTY_FLAGS(pretty);
+	res = pg_get_database_ddl_worker(dbOid, prettyFlags);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_database_ddl_worker(Oid dbOid, int prettyFlags)
+{
+	char	   *dbOwner = NULL;
+	bool		attrIsNull;
+	Datum		dbValue;
+	HeapTuple	tupleDatabase;
+	Form_pg_database dbForm;
+	StringInfoData buf;
+	AclResult	aclresult;
+
+	/*
+	 * User must have connect privilege for target database.
+	 */
+	aclresult = object_aclcheck(DatabaseRelationId, dbOid, GetUserId(),
+								ACL_CONNECT);
+	if (aclresult != ACLCHECK_OK &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclcheck_error(aclresult, OBJECT_DATABASE,
+					   get_database_name(dbOid));
+	}
+
+	/* Look up the database in pg_database */
+	tupleDatabase = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbOid));
+	if (!HeapTupleIsValid(tupleDatabase))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("database with oid %u does not exist", dbOid));
+
+	dbForm = (Form_pg_database) GETSTRUCT(tupleDatabase);
+
+	initStringInfo(&buf);
+
+	/* Look up the owner in the system catalog */
+	if (OidIsValid(dbForm->datdba))
+		dbOwner = GetUserNameFromId(dbForm->datdba, false);
+
+	/* Build the CREATE DATABASE statement */
+	appendStringInfo(&buf, "CREATE DATABASE %s",
+					 quote_identifier(dbForm->datname.data));
+	get_formatted_string(&buf, prettyFlags, 1, "WITH OWNER = %s",
+						 quote_identifier(dbOwner));
+
+	if (dbForm->encoding != 0)
+		get_formatted_string(&buf, prettyFlags, 2, "ENCODING = %s",
+							 quote_identifier(pg_encoding_to_char(dbForm->encoding)));
+
+	/* Fetch the value of LC_COLLATE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datcollate, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 2, "LC_COLLATE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of LC_CTYPE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datctype, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 2, "LC_CTYPE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of LOCALE */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datlocale, &attrIsNull);
+	if (!attrIsNull && dbForm->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, prettyFlags, 2, "BUILTIN_LOCALE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+	else if (!attrIsNull && dbForm->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, prettyFlags, 2, "ICU_LOCALE = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of ICU_RULES */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_daticurules, &attrIsNull);
+	if (!attrIsNull && dbForm->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, prettyFlags, 2, "ICU_RULES = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Fetch the value of COLLATION_VERSION */
+	dbValue = SysCacheGetAttr(DATABASEOID, tupleDatabase,
+							  Anum_pg_database_datcollversion, &attrIsNull);
+	if (!attrIsNull)
+		get_formatted_string(&buf, prettyFlags, 2, "COLLATION_VERSION = %s",
+							 quote_identifier(TextDatumGetCString(dbValue)));
+
+	/* Set the appropriate LOCALE_PROVIDER */
+	if (dbForm->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, prettyFlags, 2, "LOCALE_PROVIDER = 'builtin'");
+	else if (dbForm->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, prettyFlags, 2, "LOCALE_PROVIDER = 'icu'");
+	else
+		get_formatted_string(&buf, prettyFlags, 2, "LOCALE_PROVIDER = 'libc'");
+
+	/* Get the tablespace name respective to the given tablespace oid */
+	if (OidIsValid(dbForm->dattablespace))
+	{
+		char	   *dbTablespace = get_tablespace_name(dbForm->dattablespace);
+
+		if (dbTablespace)
+			get_formatted_string(&buf, prettyFlags, 2, "TABLESPACE = %s",
+								 quote_identifier(dbTablespace));
+	}
+
+	get_formatted_string(&buf, prettyFlags, 2, "ALLOW_CONNECTIONS = %s",
+						 dbForm->datallowconn ? "true" : "false");
+
+	get_formatted_string(&buf, prettyFlags, 2, "CONNECTION LIMIT = %d",
+						 dbForm->datconnlimit);
+
+	if (dbForm->datistemplate)
+		get_formatted_string(&buf, prettyFlags, 2, "IS_TEMPLATE = %s",
+							 dbForm->datistemplate ? "true" : "false");
+
+	appendStringInfoChar(&buf, ';');
+
+	ReleaseSysCache(tupleDatabase);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index aaadfd8c748..8ae49a0d35c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4021,6 +4021,9 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '9492', descr => 'get CREATE statement for database name and oid',
+  proname => 'pg_get_database_ddl', prorettype => 'text',
+  proargtypes => 'regdatabase bool', prosrc => 'pg_get_database_ddl' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/test/regress/expected/database.out b/src/test/regress/expected/database.out
index 6b879b0f62a..e0dac8d89e2 100644
--- a/src/test/regress/expected/database.out
+++ b/src/test/regress/expected/database.out
@@ -1,3 +1,49 @@
+--
+-- Reconsturct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+	-- Remove LC_COLLATE assignments 
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -16,6 +62,37 @@ CREATE ROLE regress_datdba_before;
 CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database', false);
+ERROR:  database "regression_database" does not exist
+LINE 1: SELECT pg_get_database_ddl('regression_database', false);
+                                   ^
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+ pg_get_database_ddl 
+---------------------
+ 
+(1 row)
+
+-- Without pretty
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+                                                                          ddl_filter                                                                          
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after ENCODING = "UTF8" TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123;
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+	WITH OWNER = regress_datdba_after
+		ENCODING = "UTF8"
+		TABLESPACE = pg_default
+		ALLOW_CONNECTIONS = true
+		CONNECTION LIMIT = 123;
+(1 row)
 DROP DATABASE regression_utf8;
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
diff --git a/src/test/regress/sql/database.sql b/src/test/regress/sql/database.sql
index 4ef36127291..392a4d96bb5 100644
--- a/src/test/regress/sql/database.sql
+++ b/src/test/regress/sql/database.sql
@@ -1,3 +1,51 @@
+--
+-- Reconsturct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+	-- Remove LC_COLLATE assignments 
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
+
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -19,6 +67,20 @@ CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
 
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database', false);
+
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+
+-- Without pretty
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', true));
+
 DROP DATABASE regression_utf8;
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
-- 
2.51.0



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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-12-11 13:59  Euler Taveira <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Euler Taveira @ 2025-12-11 13:59 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; Álvaro Herrera <[email protected]>; +Cc: Chao Li <[email protected]>; japin <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

On Thu, Nov 20, 2025, at 6:18 AM, Akshay Joshi wrote:
>
>  Implemented in the suggested solution. Attached is the v5 patch for review.
>

I reviewed your patch and have some suggestions for this patch.

* You shouldn't include the property if the value is default. A long command
  adds nothing. Clarity? Tell someone that needs to select, copy and paste a
  long statement. It is a good goal to provide a short command to reconstruct
  the object. If you don't know why it didn't include CONNECTION LIMIT, it is
  time to check the manual again.

$ psql -AtqX -c "SELECT pg_get_database_ddl('postgres')" -d postgres
CREATE DATABASE postgres WITH OWNER = euler ENCODING = "UTF8" LC_COLLATE = "pt_BR.UTF-8" LC_CTYPE = "pt_BR.UTF-8" COLLATION_VERSION = "2.36" LOCALE_PROVIDER = 'libc' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = -1;

* Use single quotes. The encoding, locale, lc_collate, lc_type,
  collation_version and some other properties should use single quotes. The
  locale_provider doesn't need a single quote because it is an enum. See how
  pg_dumpall constructs the command. Use simple_quote_literal.

$ pg_dumpall --binary-upgrade | grep 'p5'
-- Database "p5" dump
-- Name: p5; Type: DATABASE; Schema: -; Owner: euler
CREATE DATABASE p5 WITH TEMPLATE = template0 OID = 16392 STRATEGY = FILE_COPY ENCODING = 'UTF8' LOCALE_PROVIDER = libc LOCALE = 'en_US.UTF-8' COLLATION_VERSION = '2.36';
ALTER DATABASE p5 OWNER TO euler;
\connect p5

* OWNER. There is no guarantee that the owner exists in the cluster you will
  use this output. That's something that pg_dumpall treats separately (see
  above). Does it mean we should include the owner? No. We can make it an
  option.

* LOCALE. Why didn't you include it? I know there are some combinations that
  does not work together but this function can provide a minimal set of
  properties related to locale.

postgres=# CREATE DATABASE p6 LOCALE_PROVIDER builtin LOCALE 'C' TEMPLATE template0;
CREATE DATABASE

* STRATEGY. Although this is a runtime property, it should be an option.

* TEMPLATE. Ditto.

* options. Since I mentioned options for some properties (owner, strategy,
  template), these properties can be accommodated as a VARIADIC argument. The
  function signature can be something like

pg_get_database_ddl(oid, VARIADIC options text[])

I would include the pretty print into options too.

* Tabs. I don't think we use tabs to format output. Use spaces. A good practice
  is to use EXPLAIN style (2 spaces)and depending on the nesting, 4 spaces are
  fine too.

$ psql -AtqX -c "SELECT pg_get_database_ddl('postgres', true)" -d postgres
CREATE DATABASE postgres
	WITH OWNER = euler
		ENCODING = "UTF8"
		LC_COLLATE = "pt_BR.UTF-8"
		LC_CTYPE = "pt_BR.UTF-8"
		COLLATION_VERSION = "2.36"
		LOCALE_PROVIDER = 'libc'
		TABLESPACE = pg_default
		ALLOW_CONNECTIONS = true
		CONNECTION LIMIT = -1;

* permission. I don't think you need to check for permissions inside the
  function. I wouldn't want a different behavior than pg_dump(all). You can
  always adjust it in system_functions.sql.

* typo.

+--                                                                             
+-- Reconsturct DDL                                                             
+--

s/Reconsturct/Reconstruct/


-- 
Euler Taveira
EDB   https://www.enterprisedb.com/





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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-12-12 10:52  Akshay Joshi <[email protected]>
  parent: Euler Taveira <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Akshay Joshi @ 2025-12-12 10:52 UTC (permalink / raw)
  To: Euler Taveira <[email protected]>; +Cc: Álvaro Herrera <[email protected]>; Chao Li <[email protected]>; japin <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

On Thu, Dec 11, 2025 at 7:29 PM Euler Taveira <[email protected]> wrote:
>
> On Thu, Nov 20, 2025, at 6:18 AM, Akshay Joshi wrote:
> >
> >  Implemented in the suggested solution. Attached is the v5 patch for review.
> >
>
> I reviewed your patch and have some suggestions for this patch.
>
> * You shouldn't include the property if the value is default. A long command
>   adds nothing. Clarity? Tell someone that needs to select, copy and paste a
>   long statement. It is a good goal to provide a short command to reconstruct
>   the object. If you don't know why it didn't include CONNECTION LIMIT, it is
>   time to check the manual again.
>
> $ psql -AtqX -c "SELECT pg_get_database_ddl('postgres')" -d postgres
> CREATE DATABASE postgres WITH OWNER = euler ENCODING = "UTF8" LC_COLLATE = "pt_BR.UTF-8" LC_CTYPE = "pt_BR.UTF-8" COLLATION_VERSION = "2.36" LOCALE_PROVIDER = 'libc' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = -1;
>
Is there any way to obtain the default values directly from the source
code itself, or do I need to refer to the documentation? If we rely on
the documentation and compare against that, then in the future, if the
default values change, we would also need to update our logic
accordingly.

Constantly having to check the documentation for default values may
feel annoying to some users. Some users run queries with parameters
such as encoding, connection limit, and locale using their default
values. When they call the pg_get_database_ddl function, it
reconstructs the short command based on those defaults.

I am still open to updating my code.

> * Use single quotes. The encoding, locale, lc_collate, lc_type,
>   collation_version and some other properties should use single quotes. The
>   locale_provider doesn't need a single quote because it is an enum. See how
>   pg_dumpall constructs the command. Use simple_quote_literal.
>
I’ll update this in my next patch.

> $ pg_dumpall --binary-upgrade | grep 'p5'
> -- Database "p5" dump
> -- Name: p5; Type: DATABASE; Schema: -; Owner: euler
> CREATE DATABASE p5 WITH TEMPLATE = template0 OID = 16392 STRATEGY = FILE_COPY ENCODING = 'UTF8' LOCALE_PROVIDER = libc LOCALE = 'en_US.UTF-8' COLLATION_VERSION = '2.36';
> ALTER DATABASE p5 OWNER TO euler;
> \connect p5
>
> * OWNER. There is no guarantee that the owner exists in the cluster you will
>   use this output. That's something that pg_dumpall treats separately (see
>   above). Does it mean we should include the owner? No. We can make it an
>   option.
>
If I understand correctly, the owner should be an option provided by
the caller of the function, and we reconstruct the Database DDL using
that specified owner. Is that right?
If so, then in my humble opinion, this is not truly a reconstruction
of the existing database object.

> * LOCALE. Why didn't you include it? I know there are some combinations that
>   does not work together but this function can provide a minimal set of
>   properties related to locale.
>
> postgres=# CREATE DATABASE p6 LOCALE_PROVIDER builtin LOCALE 'C' TEMPLATE template0;
> CREATE DATABASE
>
For LOCALE where the provider is libc, datlocale is NULL. For builtin
and icu, I have already added the following condition in the code:
if (!attrIsNull && dbForm->datlocprovider == COLLPROVIDER_BUILTIN)
    get_formatted_string(&buf, prettyFlags, 2, "BUILTIN_LOCALE = %s",
                         quote_identifier(TextDatumGetCString(dbValue)));
else if (!attrIsNull && dbForm->datlocprovider == COLLPROVIDER_ICU)
    get_formatted_string(&buf, prettyFlags, 2, "ICU_LOCALE = %s",
                         quote_identifier(TextDatumGetCString(dbValue)));

> * STRATEGY. Although this is a runtime property, it should be an option.
>
> * TEMPLATE. Ditto.
>
> * options. Since I mentioned options for some properties (owner, strategy,
>   template), these properties can be accommodated as a VARIADIC argument. The
>   function signature can be something like
>
> pg_get_database_ddl(oid, VARIADIC options text[])
>
> I would include the pretty print into options too.
>
Same comment as the one I gave for the Owner, if you are referring to
these as options to the function.

> * Tabs. I don't think we use tabs to format output. Use spaces. A good practice
>   is to use EXPLAIN style (2 spaces)and depending on the nesting, 4 spaces are
>   fine too.
>
> $ psql -AtqX -c "SELECT pg_get_database_ddl('postgres', true)" -d postgres
> CREATE DATABASE postgres
>         WITH OWNER = euler
>                 ENCODING = "UTF8"
>                 LC_COLLATE = "pt_BR.UTF-8"
>                 LC_CTYPE = "pt_BR.UTF-8"
>                 COLLATION_VERSION = "2.36"
>                 LOCALE_PROVIDER = 'libc'
>                 TABLESPACE = pg_default
>                 ALLOW_CONNECTIONS = true
>                 CONNECTION LIMIT = -1;
>
I received a review comment suggesting the use of tabs. I also looked
up PostgreSQL best practices on google, which recommend using tabs for
indentation and spaces for alignment. I’m open to updating my code
accordingly.

> * permission. I don't think you need to check for permissions inside the
>   function. I wouldn't want a different behavior than pg_dump(all). You can
>   always adjust it in system_functions.sql.
>
We’ve already had extensive discussions on this topic in the same
email thread, and ultimately we decided to add the permission check.

> * typo.
>
> +--
> +-- Reconsturct DDL
> +--
>
> s/Reconsturct/Reconstruct/
>
I’ll update this in my next patch.
>
> --
> Euler Taveira
> EDB   https://www.enterprisedb.com/





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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2025-12-12 15:19  Euler Taveira <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 0 replies; 38+ messages in thread

From: Euler Taveira @ 2025-12-12 15:19 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Álvaro Herrera <[email protected]>; Chao Li <[email protected]>; japin <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

On Fri, Dec 12, 2025, at 7:52 AM, Akshay Joshi wrote:
> On Thu, Dec 11, 2025 at 7:29 PM Euler Taveira <[email protected]> wrote:
>>
> Is there any way to obtain the default values directly from the source
> code itself, or do I need to refer to the documentation? If we rely on
> the documentation and compare against that, then in the future, if the
> default values change, we would also need to update our logic
> accordingly.
>

No, you need to check the documentation. If you are changing the default value,
you are breaking compatibility; that rarely happens. If we are really concern
about this fact, you can add a test case that creates the object without
properties (all default values) and another with all default properties and
then compare the output.

> Constantly having to check the documentation for default values may
> feel annoying to some users. Some users run queries with parameters
> such as encoding, connection limit, and locale using their default
> values. When they call the pg_get_database_ddl function, it
> reconstructs the short command based on those defaults.
>

Encoding and locale, ok but I doubt about connection limit.

postgres=# SELECT current_user;
 current_user
--------------
 euler
(1 row)

postgres=# CREATE DATABASE foo;
CREATE DATABASE
postgres=# CREATE DATABASE bar OWNER euler;
CREATE DATABASE

When you are learning a new command, you generally don't set the default value
for a property just to be correct. I'm not saying this function shouldn't
include OWNER. I'm just suggesting it to be optional.  See some arguments
below.

>> * OWNER. There is no guarantee that the owner exists in the cluster you will
>>   use this output. That's something that pg_dumpall treats separately (see
>>   above). Does it mean we should include the owner? No. We can make it an
>>   option.
>>
> If I understand correctly, the owner should be an option provided by
> the caller of the function, and we reconstruct the Database DDL using
> that specified owner. Is that right?
> If so, then in my humble opinion, this is not truly a reconstruction
> of the existing database object.
>

No. My idea is to have something like the pg_dump --no-owner option. This is
important if you are transporting the objects from one cluster to another one.
Owner might be different. That's why I'm suggesting it should be optional. It
means flexibility. See pg_dump output format that always apply the OWNER as a
separate ALTER command.

>> * options. Since I mentioned options for some properties (owner, strategy,
>>   template), these properties can be accommodated as a VARIADIC argument. The
>>   function signature can be something like
>>
>> pg_get_database_ddl(oid, VARIADIC options text[])
>>
>> I would include the pretty print into options too.
>>
> Same comment as the one I gave for the Owner, if you are referring to
> these as options to the function.
>

Let me elaborate a bit. As I suggested you can control the output with options.
Why? Flexibility.

Why am I suggesting such a general purpose implementation? See some of the use
cases.

1. object DDL. Check DDL to recreate the object. It is not the exact DDL that
the user informed but it produces the same result.
2. clone tool. Clone the objects to recreate the environment for another
customer. These objects can be created in the same cluster or in another one.
(Of course, global objects don't apply for the same cluster.)
3. dump tool. Dump the commands to recreate the existing objects.
4. diff tool. There are tools like pgquarrel [1] that queries the catalog and
compare the results to create commands to turn the target database into the
source database. The general purpose functions can be used if the object
doesn't exist in the target database. (Of course, it doesn't apply for global
objects but again it is a good UI to have all of these pg_get_OBJECT_ddl
functions using the same approach.)
5. logical replication. These pg_get_OBJECT_ddl functions can be good
candidates to be used in the initial schema replication and even in the DDL
replication (if the object doesn't exist in the target database).

The "options" parameter is to get the DDL command to serve any of these use
cases. There are some properties in a certain object that you *don't* want for
whatever reason. See some --no-OBJECT options in pg_dump. Let's say you don't
want the TABLESPACE or the table access method while getting the CREATE TABLE
DDL because it is different in the other database.

> I received a review comment suggesting the use of tabs. I also looked
> up PostgreSQL best practices on google, which recommend using tabs for
> indentation and spaces for alignment. I’m open to updating my code
> accordingly.
>

I didn't check all of the possible output but the majority uses space instead
of tabs. Check psql. If you check the git history (git log --grep=tabs), you
will notice that tabs are removed from source code.

>> * permission. I don't think you need to check for permissions inside the
>>   function. I wouldn't want a different behavior than pg_dump(all). You can
>>   always adjust it in system_functions.sql.
>>
> We’ve already had extensive discussions on this topic in the same
> email thread, and ultimately we decided to add the permission check.
>

That's fair. Again, I expect that all of these pg_get_OBJECT_ddl functions use
the same approach. We can always relax this restriction in the future.


-- 
Euler Taveira
EDB   https://www.enterprisedb.com/





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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-04 08:39  Japin Li <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Japin Li @ 2026-03-04 08:39 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Euler Taveira <[email protected]>; Álvaro Herrera <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

On Fri, 27 Feb 2026 at 17:52, Akshay Joshi <[email protected]> wrote:
> Following suggestions from Alvaro and Mark, I have re-implemented this feature using variadic
> function argument pairs.
> This patch incorporates Mark Wong’s documentation updates, Amul’s one review comment, and the
> majority of Euler’s comments.
>
> New changes:
> - Supports flexible DDL options as key-value pairs:
>     - pretty - Format output for readability
>     - owner - Include OWNER clause
>     - tablespace - Include TABLESPACE clause
>     - defaults - Include parameters even if it is set to default value.
> - Option values accept: 'yes'/'on'/true/'1' (or their negatives)
>
> Usage Examples:
>   -- Basic usage with options
>   SELECT pg_get_database_ddl('mydb', 'owner', 'yes', 'defaults', 'yes');
>   -- Pretty-printed output without owner and tablespace
>   SELECT pg_get_database_ddl('mydb', 'owner', 'no', 'tablespace', 'no', 'pretty', 'on');
>   -- Using boolean values
>   SELECT pg_get_database_ddl('mydb', 'owner', false, 'defaults', true);
>   -- Using OID
>   SELECT pg_get_database_ddl(16384, 'pretty', 'yes');
>
> Note: To keep things clean, I’ve moved the logic into two generic functions (get_formatted_string
> and parse_ddl_options) and a common DDLOptionDef struct. This should simplify the work for the
> rest of the pg_get_<object>_ddl patches.
>
> Attached is the v9 patch which is ready for review.
>
> On Thu, Feb 26, 2026 at 2:49 AM Euler Taveira <[email protected]> wrote:
>
>  On Wed, Feb 25, 2026, at 8:53 AM, Álvaro Herrera wrote:
>  >
>  > I'm surprised to not have seen an update on this topic following the
>  > discovery by Mark Wong that commit d32d1463995c (in branch 18) already
>  > established a convention for passing arguments to functions: use argument
>  > pairs to variadic functions, the way pg_restore_relation_stats() and
>  > pg_restore_attribute_stats() work.  While I like my previous suggestion
>  > of using DefElems better, I think it's more sensible to follow this
>  > established precedent and not innovate on this.
>  >
>
>  This convention is much older than the referred commit. It predates from the
>  logical decoding (commit b89e151054a0). See pg_logical_slot_get_changes_guts()
>  that is an internal function for pg_logical_slot_FOO_changes().  It seems a
>  good idea to have a central function to validate the variadic parameter for all
>  of these functions.
>

Thanks for updating the patch, here are some comments on v9.

1.
+	uint64		flag;			/* Flag to set */

Do we actually need 64 bits for this flag field?

2.
+		/* Indent with spaces */
+		for (int i = 0; i < nSpaces; i++)
+		{
+			appendStringInfoChar(buf, ' ');
+		}

How about using appendStringInfoSpaces(buf, nSpaces) instead?

3.
+	/* If no options provided (VARIADIC NULL), return the empty bitmask */
+	if (nargs < 0)
+		return flags;
+
+	...
+
+	/* No arguments provided */
+	if (nargs == 0)
+		return flags;

These two conditions are identical — how about just `if (nargs <= 0)`?

4.
+	/* Arguments must come in name/value pairs */
+	if (nargs % 2 != 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("argument list must have even number of elements"),
+				 errhint("The arguments of %s must consist of alternating keys and values.",
+						 "pg_get_database_ddl()")));

Should we align this with stats_fill_fcinfo_from_arg_pairs()?

Suggested wording:
    errmsg("variadic arguments must be name/value pairs")
    
5.
+		/* Key must not be null */
+		if (nulls[i])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("argument %d: key must not be null", i + 1)));
+

Suggested wording:

    errmsg("name at variadic position %d is null")

6.
+		/* Key must be text type */
+		if (types[i] != TEXTOID)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("argument %d: key must be text type", i + 1)));

Suggested wording:

    errmsg("name at variadic position %d has type %s, expected type %s")

7.
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("argument %d: value for key \"%s\" must be boolean or text type",
+							i + 2, name)));

Suggested wording:

    errmsg("argument \"%s\" has type %s, execpted type boolean or text")

See stats_check_arg_type().

8.
+	for (i = 0; i < nargs; i += 2)
+	{

We can narrow the scope of `i` by declaring it in the for initializer.

9.
+        {
+            bool        found = false;
+            int         j;
+
+            for (j = 0; j < lengthof(ddl_option_defs); j++)
+            {

Minor style improvements:

- We can (and should) declare `j` inside its for-loop initializer, just like `i`.
- Move the declaration of `found` up to the top of the outer for-loop scope.  
  This allows us to remove the unnecessary braces around the loop body.

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





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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-04 09:30  Japin Li <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Japin Li @ 2026-03-04 09:30 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Euler Taveira <[email protected]>; Álvaro Herrera <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

On Wed, 04 Mar 2026 at 16:39, Japin Li <[email protected]> wrote:
> On Fri, 27 Feb 2026 at 17:52, Akshay Joshi <[email protected]> wrote:
>> Following suggestions from Alvaro and Mark, I have re-implemented this feature using variadic
>> function argument pairs.
>> This patch incorporates Mark Wong’s documentation updates, Amul’s one review comment, and the
>> majority of Euler’s comments.
>>
>> New changes:
>> - Supports flexible DDL options as key-value pairs:
>>     - pretty - Format output for readability
>>     - owner - Include OWNER clause
>>     - tablespace - Include TABLESPACE clause
>>     - defaults - Include parameters even if it is set to default value.
>> - Option values accept: 'yes'/'on'/true/'1' (or their negatives)
>>
>> Usage Examples:
>>   -- Basic usage with options
>>   SELECT pg_get_database_ddl('mydb', 'owner', 'yes', 'defaults', 'yes');
>>   -- Pretty-printed output without owner and tablespace
>>   SELECT pg_get_database_ddl('mydb', 'owner', 'no', 'tablespace', 'no', 'pretty', 'on');
>>   -- Using boolean values
>>   SELECT pg_get_database_ddl('mydb', 'owner', false, 'defaults', true);
>>   -- Using OID
>>   SELECT pg_get_database_ddl(16384, 'pretty', 'yes');
>>
>> Note: To keep things clean, I’ve moved the logic into two generic functions (get_formatted_string
>> and parse_ddl_options) and a common DDLOptionDef struct. This should simplify the work for the
>> rest of the pg_get_<object>_ddl patches.
>>
>> Attached is the v9 patch which is ready for review.
>>
>> On Thu, Feb 26, 2026 at 2:49 AM Euler Taveira <[email protected]> wrote:
>>
>>  On Wed, Feb 25, 2026, at 8:53 AM, Álvaro Herrera wrote:
>>  >
>>  > I'm surprised to not have seen an update on this topic following the
>>  > discovery by Mark Wong that commit d32d1463995c (in branch 18) already
>>  > established a convention for passing arguments to functions: use argument
>>  > pairs to variadic functions, the way pg_restore_relation_stats() and
>>  > pg_restore_attribute_stats() work.  While I like my previous suggestion
>>  > of using DefElems better, I think it's more sensible to follow this
>>  > established precedent and not innovate on this.
>>  >
>>
>>  This convention is much older than the referred commit. It predates from the
>>  logical decoding (commit b89e151054a0). See pg_logical_slot_get_changes_guts()
>>  that is an internal function for pg_logical_slot_FOO_changes().  It seems a
>>  good idea to have a central function to validate the variadic parameter for all
>>  of these functions.
>>
>
> Thanks for updating the patch, here are some comments on v9.
>
> 1.
> +	uint64		flag;			/* Flag to set */
>
> Do we actually need 64 bits for this flag field?
>
> 2.
> +		/* Indent with spaces */
> +		for (int i = 0; i < nSpaces; i++)
> +		{
> +			appendStringInfoChar(buf, ' ');
> +		}
>
> How about using appendStringInfoSpaces(buf, nSpaces) instead?
>
> 3.
> +	/* If no options provided (VARIADIC NULL), return the empty bitmask */
> +	if (nargs < 0)
> +		return flags;
> +
> +	...
> +
> +	/* No arguments provided */
> +	if (nargs == 0)
> +		return flags;
>
> These two conditions are identical — how about just `if (nargs <= 0)`?
>
> 4.
> +	/* Arguments must come in name/value pairs */
> +	if (nargs % 2 != 0)
> +		ereport(ERROR,
> +				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> +				 errmsg("argument list must have even number of elements"),
> +				 errhint("The arguments of %s must consist of alternating keys and values.",
> +						 "pg_get_database_ddl()")));
>
> Should we align this with stats_fill_fcinfo_from_arg_pairs()?
>
> Suggested wording:
>     errmsg("variadic arguments must be name/value pairs")
>     
> 5.
> +		/* Key must not be null */
> +		if (nulls[i])
> +			ereport(ERROR,
> +					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> +					 errmsg("argument %d: key must not be null", i + 1)));
> +
>
> Suggested wording:
>
>     errmsg("name at variadic position %d is null")
>
> 6.
> +		/* Key must be text type */
> +		if (types[i] != TEXTOID)
> +			ereport(ERROR,
> +					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> +					 errmsg("argument %d: key must be text type", i + 1)));
>
> Suggested wording:
>
>     errmsg("name at variadic position %d has type %s, expected type %s")
>
> 7.
> +			ereport(ERROR,
> +					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> +					 errmsg("argument %d: value for key \"%s\" must be boolean or text type",
> +							i + 2, name)));
>
> Suggested wording:
>
>     errmsg("argument \"%s\" has type %s, execpted type boolean or text")
>
> See stats_check_arg_type().
>
> 8.
> +	for (i = 0; i < nargs; i += 2)
> +	{
>
> We can narrow the scope of `i` by declaring it in the for initializer.
>
> 9.
> +        {
> +            bool        found = false;
> +            int         j;
> +
> +            for (j = 0; j < lengthof(ddl_option_defs); j++)
> +            {
>
> Minor style improvements:
>
> - We can (and should) declare `j` inside its for-loop initializer, just like `i`.
> - Move the declaration of `found` up to the top of the outer for-loop scope.  
>   This allows us to remove the unnecessary braces around the loop body.
>

After playing with this patch, I’m seeing the following output:

# select pg_get_database_ddl('postgres'::regdatabase, 'defaults', true, 'pretty', true, 'pretty', false);
        pg_get_database_ddl
------------------------------------
 CREATE DATABASE postgres          +
     WITH                          +
         OWNER = japin             +
         ENCODING = 'UTF8'         +
         LC_COLLATE = 'en_US.UTF-8'+
         LC_CTYPE = 'en_US.UTF-8'  +
         COLLATION_VERSION = '2.39'+
         LOCALE_PROVIDER = libc    +
         TABLESPACE = pg_default   +
         ALLOW_CONNECTIONS = true  +
         CONNECTION LIMIT = -1;
(1 row)

Is this the expected behavior?

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





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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-04 12:59  Akshay Joshi <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Akshay Joshi @ 2026-03-04 12:59 UTC (permalink / raw)
  To: Álvaro Herrera <[email protected]>; +Cc: Japin Li <[email protected]>; Euler Taveira <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

Thanks for the review, Japin. I’ve addressed all of your comments. I also
added a check to throw an error if an option appears more than once.

Attached is the *v10 patch*, now ready for further review.

On Wed, Mar 4, 2026 at 3:15 PM Álvaro Herrera <[email protected]> wrote:

> On 2026-Mar-04, Japin Li wrote:
>
> > After playing with this patch, I’m seeing the following output:
> >
> > # select pg_get_database_ddl('postgres'::regdatabase, 'defaults', true,
> 'pretty', true, 'pretty', false);
>
> I think this should throw an error that 'pretty' was contradictorily
> specified twice.  (Maybe also an error if an option appears twice,
> period.)
>
> --
> Álvaro Herrera               48°01'N 7°57'E  —
> https://www.EnterpriseDB.com/
>


Attachments:

  [application/octet-stream] v10-0001-Add-pg_get_database_ddl-function-to-reconstruct-ddl.patch (33.3K, 3-v10-0001-Add-pg_get_database_ddl-function-to-reconstruct-ddl.patch)
  download | inline diff:
From 0a3b5111bcf19509807719aaa1bedf72d4d56670 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Wed, 24 Sep 2025 17:47:59 +0530
Subject: [PATCH v10] Add pg_get_database_ddl() function to reconstruct CREATE
 DATABASE statements.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This adds a new system function, pg_get_database_ddl(database_name/database_oid, ddl_options),
which reconstructs the CREATE DATABASE statement for a given database name or OID.

Supported ddl_options are 'pretty', 'owner', 'tablespace' and 'defaults' and respective
values could be 'yes'/'on'/true/'1'.

Usage:
SELECT pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes');
SELECT pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1');
SELECT pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on');
SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no');

Reference: PG-150
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
Reviewed-by: Euler Taveira <[email protected]>
Reviewed-by: Quan Zongliang <[email protected]>
Reviewed-by: Japin Li <[email protected]>
Reviewed-by: Chao Li <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  86 +++++
 src/backend/catalog/system_functions.sql |   6 +
 src/backend/utils/adt/ruleutils.c        | 395 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 +
 src/include/utils/ddl_defaults.h         |  39 +++
 src/test/regress/expected/database.out   | 173 ++++++++++
 src/test/regress/sql/database.sql        | 111 +++++++
 7 files changed, 817 insertions(+)
 create mode 100644 src/include/utils/ddl_defaults.h

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 294f45e82a3..c346b8d1fcf 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3845,4 +3845,90 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </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_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>database_id</parameter> <type>regdatabase</type>
+        <optional>, <literal>VARIADIC</literal> <parameter>options</parameter>
+        <type>"any"</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE DATABASE</command> statement from the
+        system catalogs for a specified database by name or OID. The optional
+        variadic arguments are name/value pairs to control the output
+        formatting and content (e.g., <literal>'pretty', true, 'owner', false</literal>).
+        Supported options are explained below.
+        </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+    The <parameter>options</parameter> for <function>pg_get_database_ddl</function>
+    provide fine-grained control over the generated SQL. Options are passed as
+    alternating key/value pairs where the key is a text string and the
+    value is either a boolean or a text string representing a boolean
+    (<literal>true</literal>, <literal>false</literal>, <literal>yes</literal>,
+    <literal>no</literal>, <literal>1</literal>, <literal>0</literal>,
+    <literal>on</literal>, <literal>off</literal>):
+    <itemizedlist>
+    <listitem>
+      <para>
+      <literal>'pretty', true</literal> (or <literal>'pretty', 'yes'</literal>):
+      Adds newlines and indentation for better readability.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'owner', false</literal> (or <literal>'owner', 'no'</literal>):
+      Omits the <literal>OWNER</literal> clause from the reconstructed statement.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'tablespace', false</literal> (or <literal>'tablespace', '0'</literal>):
+      Omits the <literal>TABLESPACE</literal> clause from the reconstructed statement.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'defaults', true</literal> (or <literal>'defaults', '1'</literal>):
+      Includes clauses for parameters that are currently at their default values
+      (e.g., <literal>CONNECTION LIMIT -1</literal>), which are normally omitted for brevity.
+      </para>
+    </listitem>
+    </itemizedlist>
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 69699f8830a..ae573e2fb2c 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -378,6 +378,12 @@ BEGIN ATOMIC
 END;
 
 
+CREATE OR REPLACE FUNCTION
+  pg_get_database_ddl(database_id regdatabase, VARIADIC options "any" DEFAULT NULL)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl';
+
 --
 -- 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..d473ae75298 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -28,6 +28,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_depend.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_opclass.h"
@@ -57,8 +58,10 @@
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
 #include "rewrite/rewriteSupport.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/ddl_defaults.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
 #include "utils/hsearch.h"
@@ -89,11 +92,45 @@
 #define PRETTYFLAG_INDENT		0x0002
 #define PRETTYFLAG_SCHEMA		0x0004
 
+/* DDL Options flags */
+#define PG_DDL_PRETTY_INDENT	0x00000001
+#define PG_DDL_WITH_DEFAULTS	0x00000002
+#define PG_DDL_NO_OWNER			0x00000004
+#define PG_DDL_NO_TABLESPACE	0x00000008
+
+/*
+ * Structure to define DDL options for parse_ddl_options().
+ * This allows easy addition of new options in the future.
+ */
+typedef struct DDLOptionDef
+{
+	const char *name;			/* Option name (case-insensitive) */
+	uint32		flag;			/* Flag to set */
+	bool		set_on_true;	/* If true, set flag when value is true; if
+								 * false, set flag when value is false */
+}			DDLOptionDef;
+
+/*
+ * Array of supported DDL options.
+ * To add a new option, simply add an entry to this array.
+ */
+static const DDLOptionDef ddl_option_defs[] = {
+	{"pretty", PG_DDL_PRETTY_INDENT, true},
+	{"defaults", PG_DDL_WITH_DEFAULTS, true},
+	{"owner", PG_DDL_NO_OWNER, false},
+	{"tablespace", PG_DDL_NO_TABLESPACE, false},
+};
+
+
 /* Standard conversion of a "bool pretty" option to detailed flags */
 #define GET_PRETTY_FLAGS(pretty) \
 	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
 	 : PRETTYFLAG_INDENT)
 
+#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 +584,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 nSpaces,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char *pg_get_database_ddl_worker(Oid db_oid, uint32 ddl_flags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13760,3 +13802,356 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - Based on prettyFlags the output includes spaces and
+ *               newlines (\n).
+ * nSpaces - indent with specified number of space characters.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, int prettyFlags, int nSpaces, const char *fmt,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with spaces */
+		appendStringInfoSpaces(buf, nSpaces);
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	va_start(args, fmt);
+	appendStringInfoVA(buf, fmt, args);
+	va_end(args);
+}
+
+/**
+ * parse_ddl_options - Generic helper to parse variadic name/value options
+ * fcinfo: The FunctionCallInfo from the calling function
+ * variadic_start: The argument position where variadic arguments start
+ *
+ * Returns: Bitmask of flags based on the parsed options.
+ *
+ * Options are passed as name/value pairs.
+ * For example: pg_get_database_ddl('mydb', 'owner', false, 'pretty', true)
+ */
+static uint32
+parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
+{
+	uint32		flags = 0;
+	uint32		seen_flags = 0;
+	Datum	   *args;
+	bool	   *nulls;
+	Oid		   *types;
+	int			nargs;
+	bool		found = false;
+
+	/* Extract variadic arguments */
+	nargs = extract_variadic_args(fcinfo, variadic_start, true,
+								  &args, &types, &nulls);
+
+	/* If no options provided (VARIADIC NULL), return the empty bitmask */
+	if (nargs <= 0)
+		return flags;
+
+	/*
+	 * Handle the case where DEFAULT NULL was used and no explicit variadic
+	 * arguments were provided. In this case, we get a single NULL argument.
+	 */
+	if (nargs == 1 && nulls[0])
+		return flags;
+
+	/* Arguments must come in name/value pairs */
+	if (nargs % 2 != 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("variadic arguments must be name/value pairs"),
+				 errhint("Provide an even number of variadic arguments that can be divided into pairs.")));
+
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *name;
+		bool		bval;
+
+		/* Key must not be null */
+		if (nulls[i])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("name at variadic position %d is null", i + 1)));
+
+		/* Key must be text type */
+		if (types[i] != TEXTOID)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("name at variadic position %d has type %s, expected type %s",
+							i + 1, format_type_be(types[i]),
+							format_type_be(TEXTOID))));
+
+		name = TextDatumGetCString(args[i]);
+
+		/* Value must not be null */
+		if (nulls[i + 1])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("argument \"%s\" has type %s, expected type boolean or text",
+							name, format_type_be(types[i + 1]))));
+
+		/* Value must be boolean or text type */
+		if (types[i + 1] == BOOLOID)
+		{
+			bval = DatumGetBool(args[i + 1]);
+		}
+		else if (types[i + 1] == TEXTOID)
+		{
+			char	   *valstr = TextDatumGetCString(args[i + 1]);
+
+			if (!parse_bool(valstr, &bval))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("argument %d: invalid value \"%s\" for key \"%s\"",
+								i + 2, valstr, name),
+						 errhint("Valid values are: true, false, yes, no, 1, 0, on, off.")));
+			pfree(valstr);
+		}
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("argument %d: value for key \"%s\" must be boolean or text type",
+							i + 2, name)));
+		}
+
+		/*
+		 * Look up the option in the ddl_option_defs array and set the
+		 * appropriate flag based on the value.
+		 */
+		for (int j = 0; j < lengthof(ddl_option_defs); j++)
+		{
+			const		DDLOptionDef *opt = &ddl_option_defs[j];
+
+			if (pg_strcasecmp(name, opt->name) == 0)
+			{
+				/* Error if this option was already specified */
+				if (seen_flags & opt->flag)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								errmsg("option \"%s\" is specified more than once", name)));
+
+				seen_flags |= opt->flag;
+
+				/*
+					* Set the flag if the value matches the set_on_true
+					* condition: if set_on_true is true, set flag when bval
+					* is true; if set_on_true is false, set flag when bval is
+					* false.
+					*/
+				if (bval == opt->set_on_true)
+					flags |= opt->flag;
+
+				found = true;
+				break;
+			}
+		}
+
+		if (!found)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						errmsg("unrecognized option: \"%s\"", name)));
+						 
+		pfree(name);
+	}
+
+	return flags;
+}
+
+/*
+ * pg_get_database_ddl
+ *
+ * Generate a CREATE DATABASE statement for the specified database name or oid.
+ *
+ * db_oid - OID/Name of the database for which to generate the DDL.
+ * options - Variadic name/value pairs to modify the output.
+ */
+Datum
+pg_get_database_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			db_oid = PG_GETARG_OID(0);
+	uint32		ddl_flags;
+	char	   *res;
+
+	/* Parse variadic options starting from argument 1 */
+	ddl_flags = parse_ddl_options(fcinfo, 1);
+
+	res = pg_get_database_ddl_worker(db_oid, ddl_flags);
+
+	if (res == NULL)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_database_ddl_worker(Oid db_oid, uint32 ddl_flags)
+{
+	char	   *dbowner = NULL;
+	bool		attr_isnull;
+	Datum		dbvalue;
+	HeapTuple	tuple_database;
+	Form_pg_database dbform;
+	StringInfoData buf;
+	AclResult	aclresult;
+
+	/* Variables for ddl_options parsing */
+	int			pretty_flags = 0;
+	bool		is_with_defaults = false;
+
+	/* Set the appropriate flags */
+	if (ddl_flags & PG_DDL_PRETTY_INDENT)
+		pretty_flags = GET_DDL_PRETTY_FLAGS(1);
+
+	is_with_defaults = (ddl_flags & PG_DDL_WITH_DEFAULTS) ? true : false;
+
+	/*
+	 * User must have connect privilege for target database.
+	 */
+	aclresult = object_aclcheck(DatabaseRelationId, db_oid, GetUserId(),
+								ACL_CONNECT);
+	if (aclresult != ACLCHECK_OK &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclcheck_error(aclresult, OBJECT_DATABASE,
+					   get_database_name(db_oid));
+	}
+
+	/* Look up the database in pg_database */
+	tuple_database = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(db_oid));
+	if (!HeapTupleIsValid(tuple_database))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("database with oid %u does not exist", db_oid));
+
+	dbform = (Form_pg_database) GETSTRUCT(tuple_database);
+
+	initStringInfo(&buf);
+
+	/* Look up the owner in the system catalog */
+	if (OidIsValid(dbform->datdba))
+		dbowner = GetUserNameFromId(dbform->datdba, false);
+
+	/* Build the CREATE DATABASE statement */
+	appendStringInfo(&buf, "CREATE DATABASE %s",
+					 quote_identifier(dbform->datname.data));
+	get_formatted_string(&buf, pretty_flags, 4, "WITH");
+
+	/* Set the OWNER in the DDL if owner is not omitted */
+	if (OidIsValid(dbform->datdba) && !(ddl_flags & PG_DDL_NO_OWNER))
+	{
+		get_formatted_string(&buf, pretty_flags, 8, "OWNER = %s",
+							 quote_identifier(dbowner));
+	}
+
+	/* Set the ENCODING in the DDL */
+	if (dbform->encoding != 0)
+	{
+		/* If is_with_defaults is true, then we skip default encoding check */
+		if (is_with_defaults ||
+			(pg_strcasecmp(pg_encoding_to_char(dbform->encoding),
+						   DDL_DEFAULTS.DATABASE.ENCODING) != 0))
+		{
+			get_formatted_string(&buf, pretty_flags, 8, "ENCODING = %s",
+								 quote_literal_cstr(
+													pg_encoding_to_char(dbform->encoding)));
+		}
+	}
+
+	/* Fetch the value of LC_COLLATE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datcollate, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LC_COLLATE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	/* Fetch the value of LC_CTYPE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datctype, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LC_CTYPE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	/* Fetch the value of LOCALE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datlocale, &attr_isnull);
+	if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, pretty_flags, 8, "BUILTIN_LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	else if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "ICU_LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	else if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Fetch the value of ICU_RULES */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_daticurules, &attr_isnull);
+	if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "ICU_RULES = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Fetch the value of COLLATION_VERSION */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datcollversion, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "COLLATION_VERSION = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Set the appropriate LOCALE_PROVIDER */
+	if (dbform->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = builtin");
+	else if (dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = icu");
+	else if (is_with_defaults)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = libc");
+
+	/* Set the TABLESPACE in the DDL if tablespace is not omitted */
+	if (OidIsValid(dbform->dattablespace) && !(ddl_flags & PG_DDL_NO_TABLESPACE))
+	{
+		/* Get the tablespace name respective to the given tablespace oid */
+		char	   *dbTablespace = get_tablespace_name(dbform->dattablespace);
+
+		/* If it's with defaults, we skip default tablespace check */
+		if (is_with_defaults ||
+			(pg_strcasecmp(dbTablespace, DDL_DEFAULTS.DATABASE.TABLESPACE) != 0))
+			get_formatted_string(&buf, pretty_flags, 8, "TABLESPACE = %s",
+								 quote_identifier(dbTablespace));
+	}
+
+	if (is_with_defaults ||
+		(dbform->datallowconn != DDL_DEFAULTS.DATABASE.ALLOW_CONN))
+	{
+		get_formatted_string(&buf, pretty_flags, 8, "ALLOW_CONNECTIONS = %s",
+							 dbform->datallowconn ? "true" : "false");
+	}
+
+	if (is_with_defaults ||
+		(dbform->datconnlimit != DDL_DEFAULTS.DATABASE.CONN_LIMIT))
+	{
+		get_formatted_string(&buf, pretty_flags, 8, "CONNECTION LIMIT = %d",
+							 dbform->datconnlimit);
+	}
+
+	if (dbform->datistemplate)
+		get_formatted_string(&buf, pretty_flags, 8, "IS_TEMPLATE = %s",
+							 dbform->datistemplate ? "true" : "false");
+
+	appendStringInfoChar(&buf, ';');
+
+	ReleaseSysCache(tuple_database);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dac40992cbc..5c43eee32ec 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4031,6 +4031,13 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '9492', descr => 'get CREATE statement for database name and oid',
+  proname => 'pg_get_database_ddl', provariadic => 'any', proisstrict => 'f',
+  prorettype => 'text',
+  proargtypes => 'regdatabase any',
+  proargmodes => '{i,v}',
+  proallargtypes => '{regdatabase,any}',
+  prosrc => 'pg_get_database_ddl' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/include/utils/ddl_defaults.h b/src/include/utils/ddl_defaults.h
new file mode 100644
index 00000000000..d17e843fe09
--- /dev/null
+++ b/src/include/utils/ddl_defaults.h
@@ -0,0 +1,39 @@
+/*-------------------------------------------------------------------------
+ *
+ * ddl_defaults.h
+ *	  Declarations for DDL defaults.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/ddl_defaults.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DDL_DEFAULTS_H
+#define DDL_DEFAULTS_H
+
+#include <stdbool.h>
+
+static const struct
+{
+	struct
+	{
+		const char *ENCODING;
+		const char *TABLESPACE;
+		int			CONN_LIMIT;
+		bool		ALLOW_CONN;
+	}			DATABASE;
+
+	/* Add more object types as needed */
+}			DDL_DEFAULTS = {
+
+	.DATABASE = {
+		.ENCODING = "UTF8",
+		.TABLESPACE = "pg_default",
+		.CONN_LIMIT = -1,
+		.ALLOW_CONN = true,
+	}
+};
+
+#endif							/* DDL_DEFAULTS_H */
diff --git a/src/test/regress/expected/database.out b/src/test/regress/expected/database.out
index 6b879b0f62a..77235949f66 100644
--- a/src/test/regress/expected/database.out
+++ b/src/test/regress/expected/database.out
@@ -1,3 +1,57 @@
+--
+-- Reconstruct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+    -- Remove %LOCALE_PROVIDER% placeholders
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)',
+        '',
+        'gi'
+    );
+
+	-- Remove LC_COLLATE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -16,6 +70,125 @@ CREATE ROLE regress_datdba_before;
 CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database');
+ERROR:  database "regression_database" does not exist
+LINE 1: SELECT pg_get_database_ddl('regression_database');
+                                   ^
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+ERROR:  database with oid 0 does not exist
+-- Without options
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+                                        ddl_filter                                         
+-------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after CONNECTION LIMIT = 123;
+(1 row)
+
+-- With No Owner
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false));
+                          ddl_filter                          
+--------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH CONNECTION LIMIT = 123;
+(1 row)
+
+-- With No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true, 'tablespace', false));
+                                                              ddl_filter                                                              
+--------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123;
+(1 row)
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true));
+                                                                          ddl_filter                                                                          
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after ENCODING = 'UTF8' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123;
+(1 row)
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'tablespace', false, 'defaults', true));
+                                               ddl_filter                                                
+---------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123;
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        OWNER = regress_datdba_after
+        CONNECTION LIMIT = 123;
+(1 row)
+-- With No Owner and No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        CONNECTION LIMIT = 123;
+(1 row)
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'defaults', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        OWNER = regress_datdba_after
+        ENCODING = 'UTF8'
+        TABLESPACE = pg_default
+        ALLOW_CONNECTIONS = true
+        CONNECTION LIMIT = 123;
+(1 row)
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false, 'defaults', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        ENCODING = 'UTF8'
+        ALLOW_CONNECTIONS = true
+        CONNECTION LIMIT = 123;
+(1 row)
+-- Test with text values: 'yes', 'no', '1', '0', 'on', 'off'
+\pset format aligned
+-- Using 'yes' and 'no'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes'));
+                                                           ddl_filter                                                            
+---------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ENCODING = 'UTF8' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123;
+(1 row)
+
+-- Using '1' and '0'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1'));
+                                               ddl_filter                                                
+---------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123;
+(1 row)
+
+-- Using 'on' and 'off'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on'));
+           ddl_filter            
+---------------------------------
+ CREATE DATABASE regression_utf8+
+     WITH                       +
+         CONNECTION LIMIT = 123;
+(1 row)
+
+-- Mixed boolean and text values
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no'));
+                                               ddl_filter                                                
+---------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123;
+(1 row)
+
+-- Test duplicate option (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'owner', true);
+ERROR:  option "owner" is specified more than once
+-- Test invalid text value (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', 'invalid');
+ERROR:  argument 2: invalid value "invalid" for key "owner"
+HINT:  Valid values are: true, false, yes, no, 1, 0, on, off.
 DROP DATABASE regression_utf8;
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
diff --git a/src/test/regress/sql/database.sql b/src/test/regress/sql/database.sql
index 4ef36127291..05fd94ab9b4 100644
--- a/src/test/regress/sql/database.sql
+++ b/src/test/regress/sql/database.sql
@@ -1,3 +1,59 @@
+--
+-- Reconstruct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+    -- Remove %LOCALE_PROVIDER% placeholders
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)',
+        '',
+        'gi'
+    );
+
+	-- Remove LC_COLLATE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
+
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -19,6 +75,61 @@ CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
 
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database');
+
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+
+-- Without options
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+
+-- With No Owner
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false));
+
+-- With No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true, 'tablespace', false));
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true));
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'tablespace', false, 'defaults', true));
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true));
+
+-- With No Owner and No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false));
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'defaults', true));
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false, 'defaults', true));
+
+-- Test with text values: 'yes', 'no', '1', '0', 'on', 'off'
+\pset format aligned
+-- Using 'yes' and 'no'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes'));
+
+-- Using '1' and '0'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1'));
+
+-- Using 'on' and 'off'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on'));
+
+-- Mixed boolean and text values
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no'));
+
+-- Test duplicate option (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'owner', true);
+
+-- Test invalid text value (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', 'invalid');
+
 DROP DATABASE regression_utf8;
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
-- 
2.51.0



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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-04 14:01  Japin Li <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Japin Li @ 2026-03-04 14:01 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Álvaro Herrera <[email protected]>; Euler Taveira <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

On Wed, 04 Mar 2026 at 18:29, Akshay Joshi <[email protected]> wrote:
> Thanks for the review, Japin. I’ve addressed all of your comments. I also added a check to throw an error if an option
> appears more than once.
>
> Attached is the v10 patch, now ready for further review.
>

Thanks for updating the patch.  Here are some comments on v10.

1.
+ * db_oid - OID/Name of the database for which to generate the DDL.

Should the comment be updated? The code only accepts an OID for `db_oid`,
database names are not supported.

2.
+       /* Set the OWNER in the DDL if owner is not omitted */
+       if (OidIsValid(dbform->datdba) && !(ddl_flags & PG_DDL_NO_OWNER))
+       {
+               get_formatted_string(&buf, pretty_flags, 8, "OWNER = %s",
+                                                        quote_identifier(dbowner));
+       }

`dbowner` is only needed inside this `if` — how about declaring it there to
reduce its scope?

3.
+               /* If is_with_defaults is true, then we skip default encoding check */
+               if (is_with_defaults ||
+                       (pg_strcasecmp(pg_encoding_to_char(dbform->encoding),
+                                                  DDL_DEFAULTS.DATABASE.ENCODING) != 0))
+               {
+                       get_formatted_string(&buf, pretty_flags, 8, "ENCODING = %s",
+                                                                quote_literal_cstr(
+                                                                                                       pg_encoding_to_char(dbform->encoding)));
+               }

How about cache the result of `pg_encoding_to_char()` in a local variable to
avoid calling it twice?

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





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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-05 09:50  Akshay Joshi <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Akshay Joshi @ 2026-03-05 09:50 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Álvaro Herrera <[email protected]>; Euler Taveira <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

I’ve addressed all of your comments.

Attached is the *v11 patch*, now ready for further review.

On Wed, Mar 4, 2026 at 7:31 PM Japin Li <[email protected]> wrote:

> On Wed, 04 Mar 2026 at 18:29, Akshay Joshi <[email protected]>
> wrote:
> > Thanks for the review, Japin. I’ve addressed all of your comments. I
> also added a check to throw an error if an option
> > appears more than once.
> >
> > Attached is the v10 patch, now ready for further review.
> >
>
> Thanks for updating the patch.  Here are some comments on v10.
>
> 1.
> + * db_oid - OID/Name of the database for which to generate the DDL.
>
> Should the comment be updated? The code only accepts an OID for `db_oid`,
> database names are not supported.
>
> 2.
> +       /* Set the OWNER in the DDL if owner is not omitted */
> +       if (OidIsValid(dbform->datdba) && !(ddl_flags & PG_DDL_NO_OWNER))
> +       {
> +               get_formatted_string(&buf, pretty_flags, 8, "OWNER = %s",
> +
> quote_identifier(dbowner));
> +       }
>
> `dbowner` is only needed inside this `if` — how about declaring it there to
> reduce its scope?
>
> 3.
> +               /* If is_with_defaults is true, then we skip default
> encoding check */
> +               if (is_with_defaults ||
> +
>  (pg_strcasecmp(pg_encoding_to_char(dbform->encoding),
> +
> DDL_DEFAULTS.DATABASE.ENCODING) != 0))
> +               {
> +                       get_formatted_string(&buf, pretty_flags, 8,
> "ENCODING = %s",
> +
> quote_literal_cstr(
> +
>                              pg_encoding_to_char(dbform->encoding)));
> +               }
>
> How about cache the result of `pg_encoding_to_char()` in a local variable
> to
> avoid calling it twice?
>
> --
> Regards,
> Japin Li
> ChengDu WenWu Information Technology Co., Ltd.
>


Attachments:

  [application/octet-stream] v11-0001-Add-pg_get_database_ddl-function-to-reconstruct-ddl.patch (33.7K, 3-v11-0001-Add-pg_get_database_ddl-function-to-reconstruct-ddl.patch)
  download | inline diff:
From 90c521d74cfa5e45d5174c4ea6f2d192f061d1c3 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Wed, 24 Sep 2025 17:47:59 +0530
Subject: [PATCH v11] Add pg_get_database_ddl() function to reconstruct CREATE
 DATABASE statements.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This adds a new system function, pg_get_database_ddl(database_name/database_oid, ddl_options),
which reconstructs the CREATE DATABASE statement for a given database name or OID.

Supported ddl_options are 'pretty', 'owner', 'tablespace' and 'defaults' and respective
values could be 'yes'/'on'/true/'1'.

Usage:
SELECT pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes');
SELECT pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1');
SELECT pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on');
SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no');

Reference: PG-150
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
Reviewed-by: Euler Taveira <[email protected]>
Reviewed-by: Quan Zongliang <[email protected]>
Reviewed-by: Japin Li <[email protected]>
Reviewed-by: Chao Li <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  86 +++++
 src/backend/catalog/system_functions.sql |   6 +
 src/backend/utils/adt/ruleutils.c        | 403 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 +
 src/include/utils/ddl_defaults.h         |  37 +++
 src/test/regress/expected/database.out   | 180 ++++++++++
 src/test/regress/sql/database.sql        | 112 +++++++
 7 files changed, 831 insertions(+)
 create mode 100644 src/include/utils/ddl_defaults.h

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 294f45e82a3..c346b8d1fcf 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3845,4 +3845,90 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions for each object type.
+   </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_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>database_id</parameter> <type>regdatabase</type>
+        <optional>, <literal>VARIADIC</literal> <parameter>options</parameter>
+        <type>"any"</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE DATABASE</command> statement from the
+        system catalogs for a specified database by name or OID. The optional
+        variadic arguments are name/value pairs to control the output
+        formatting and content (e.g., <literal>'pretty', true, 'owner', false</literal>).
+        Supported options are explained below.
+        </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+    The <parameter>options</parameter> for <function>pg_get_database_ddl</function>
+    provide fine-grained control over the generated SQL. Options are passed as
+    alternating key/value pairs where the key is a text string and the
+    value is either a boolean or a text string representing a boolean
+    (<literal>true</literal>, <literal>false</literal>, <literal>yes</literal>,
+    <literal>no</literal>, <literal>1</literal>, <literal>0</literal>,
+    <literal>on</literal>, <literal>off</literal>):
+    <itemizedlist>
+    <listitem>
+      <para>
+      <literal>'pretty', true</literal> (or <literal>'pretty', 'yes'</literal>):
+      Adds newlines and indentation for better readability.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'owner', false</literal> (or <literal>'owner', 'no'</literal>):
+      Omits the <literal>OWNER</literal> clause from the reconstructed statement.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'tablespace', false</literal> (or <literal>'tablespace', '0'</literal>):
+      Omits the <literal>TABLESPACE</literal> clause from the reconstructed statement.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'defaults', true</literal> (or <literal>'defaults', '1'</literal>):
+      Includes clauses for parameters that are currently at their default values
+      (e.g., <literal>CONNECTION LIMIT -1</literal>), which are normally omitted for brevity.
+      </para>
+    </listitem>
+    </itemizedlist>
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 69699f8830a..ae573e2fb2c 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -378,6 +378,12 @@ BEGIN ATOMIC
 END;
 
 
+CREATE OR REPLACE FUNCTION
+  pg_get_database_ddl(database_id regdatabase, VARIADIC options "any" DEFAULT NULL)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl';
+
 --
 -- 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..bbf82c1d6c0 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -28,6 +28,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_depend.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_opclass.h"
@@ -57,8 +58,10 @@
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
 #include "rewrite/rewriteSupport.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/ddl_defaults.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
 #include "utils/hsearch.h"
@@ -89,11 +92,45 @@
 #define PRETTYFLAG_INDENT		0x0002
 #define PRETTYFLAG_SCHEMA		0x0004
 
+/* DDL Options flags */
+#define PG_DDL_PRETTY_INDENT	0x00000001
+#define PG_DDL_WITH_DEFAULTS	0x00000002
+#define PG_DDL_NO_OWNER			0x00000004
+#define PG_DDL_NO_TABLESPACE	0x00000008
+
+/*
+ * Structure to define DDL options for parse_ddl_options().
+ * This allows easy addition of new options in the future.
+ */
+typedef struct DDLOptionDef
+{
+	const char *name;			/* Option name (case-insensitive) */
+	uint32		flag;			/* Flag to set */
+	bool		set_on_true;	/* If true, set flag when value is true; if
+								 * false, set flag when value is false */
+}			DDLOptionDef;
+
+/*
+ * Array of supported DDL options.
+ * To add a new option, simply add an entry to this array.
+ */
+static const DDLOptionDef ddl_option_defs[] = {
+	{"pretty", PG_DDL_PRETTY_INDENT, true},
+	{"defaults", PG_DDL_WITH_DEFAULTS, true},
+	{"owner", PG_DDL_NO_OWNER, false},
+	{"tablespace", PG_DDL_NO_TABLESPACE, false},
+};
+
+
 /* Standard conversion of a "bool pretty" option to detailed flags */
 #define GET_PRETTY_FLAGS(pretty) \
 	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
 	 : PRETTYFLAG_INDENT)
 
+#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 +584,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 nSpaces,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char *pg_get_database_ddl_worker(Oid db_oid, uint32 ddl_flags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13760,3 +13802,364 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - Based on prettyFlags the output includes spaces and
+ *               newlines (\n).
+ * nSpaces - indent with specified number of space characters.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, int prettyFlags, int nSpaces, const char *fmt,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with spaces */
+		appendStringInfoSpaces(buf, nSpaces);
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	for (;;)
+	{
+		int			needed;
+
+		va_start(args, fmt);
+		needed = appendStringInfoVA(buf, fmt, args);
+		va_end(args);
+		if (needed == 0)
+			break;
+		enlargeStringInfo(buf, needed);
+	}
+}
+
+/**
+ * parse_ddl_options - Generic helper to parse variadic name/value options
+ * fcinfo: The FunctionCallInfo from the calling function
+ * variadic_start: The argument position where variadic arguments start
+ *
+ * Returns: Bitmask of flags based on the parsed options.
+ *
+ * Options are passed as name/value pairs.
+ * For example: pg_get_database_ddl('mydb', 'owner', false, 'pretty', true)
+ */
+static uint32
+parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
+{
+	uint32		flags = 0;
+	uint32		seen_flags = 0;
+	Datum	   *args;
+	bool	   *nulls;
+	Oid		   *types;
+	int			nargs;
+	bool		found = false;
+
+	/* Extract variadic arguments */
+	nargs = extract_variadic_args(fcinfo, variadic_start, true,
+								  &args, &types, &nulls);
+
+	/* If no options provided (VARIADIC NULL), return the empty bitmask */
+	if (nargs <= 0)
+		return flags;
+
+	/*
+	 * Handle the case where DEFAULT NULL was used and no explicit variadic
+	 * arguments were provided. In this case, we get a single NULL argument.
+	 */
+	if (nargs == 1 && nulls[0])
+		return flags;
+
+	/* Arguments must come in name/value pairs */
+	if (nargs % 2 != 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("variadic arguments must be name/value pairs"),
+				 errhint("Provide an even number of variadic arguments that can be divided into pairs.")));
+
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *name;
+		bool		bval;
+
+		found = false;
+
+		/* Key must not be null */
+		if (nulls[i])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("name at variadic position %d is null", i + 1)));
+
+		/* Key must be text type */
+		if (types[i] != TEXTOID)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("name at variadic position %d has type %s, expected type %s",
+							i + 1, format_type_be(types[i]),
+							format_type_be(TEXTOID))));
+
+		name = TextDatumGetCString(args[i]);
+
+		/* Value must not be null */
+		if (nulls[i + 1])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("value for option \"%s\" must not be null",
+							name)));
+
+		/* Value must be boolean or text type */
+		if (types[i + 1] == BOOLOID)
+		{
+			bval = DatumGetBool(args[i + 1]);
+		}
+		else if (types[i + 1] == TEXTOID)
+		{
+			char	   *valstr = TextDatumGetCString(args[i + 1]);
+
+			if (!parse_bool(valstr, &bval))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("argument %d: invalid value \"%s\" for key \"%s\"",
+								i + 2, valstr, name),
+						 errhint("Valid values are: true, false, yes, no, 1, 0, on, off.")));
+			pfree(valstr);
+		}
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("argument %d: value for key \"%s\" must be boolean or text type",
+							i + 2, name)));
+		}
+
+		/*
+		 * Look up the option in the ddl_option_defs array and set the
+		 * appropriate flag based on the value.
+		 */
+		for (int j = 0; j < lengthof(ddl_option_defs); j++)
+		{
+			const		DDLOptionDef *opt = &ddl_option_defs[j];
+
+			if (pg_strcasecmp(name, opt->name) == 0)
+			{
+				/* Error if this option was already specified */
+				if (seen_flags & opt->flag)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("option \"%s\" is specified more than once", name)));
+
+				seen_flags |= opt->flag;
+
+				/*
+				 * Set the flag if the value matches the set_on_true
+				 * condition: if set_on_true is true, set flag when bval is
+				 * true; if set_on_true is false, set flag when bval is false.
+				 */
+				if (bval == opt->set_on_true)
+					flags |= opt->flag;
+
+				found = true;
+				break;
+			}
+		}
+
+		if (!found)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unrecognized option: \"%s\"", name)));
+
+		pfree(name);
+	}
+
+	return flags;
+}
+
+/*
+ * pg_get_database_ddl
+ *
+ * Generate a CREATE DATABASE statement for the specified database name or oid.
+ *
+ * db_oid - OID of the database for which to generate the DDL.
+ * options - Variadic name/value pairs to modify the output.
+ */
+Datum
+pg_get_database_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			db_oid;
+	uint32		ddl_flags;
+	char	   *res;
+
+	if (PG_ARGISNULL(0))
+		PG_RETURN_NULL();
+
+	db_oid = PG_GETARG_OID(0);
+
+	/* Parse variadic options starting from argument 1 */
+	ddl_flags = parse_ddl_options(fcinfo, 1);
+
+	res = pg_get_database_ddl_worker(db_oid, ddl_flags);
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_database_ddl_worker(Oid db_oid, uint32 ddl_flags)
+{
+	const char *encoding = NULL;
+	bool		attr_isnull;
+	Datum		dbvalue;
+	HeapTuple	tuple_database;
+	Form_pg_database dbform;
+	StringInfoData buf;
+	AclResult	aclresult;
+
+	/* Variables for ddl_options parsing */
+	int			pretty_flags = 0;
+	bool		is_with_defaults = false;
+
+	/* Set the appropriate flags */
+	if (ddl_flags & PG_DDL_PRETTY_INDENT)
+		pretty_flags = GET_DDL_PRETTY_FLAGS(1);
+
+	is_with_defaults = (ddl_flags & PG_DDL_WITH_DEFAULTS) ? true : false;
+
+	/*
+	 * User must have connect privilege for target database.
+	 */
+	aclresult = object_aclcheck(DatabaseRelationId, db_oid, GetUserId(),
+								ACL_CONNECT);
+	if (aclresult != ACLCHECK_OK &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclcheck_error(aclresult, OBJECT_DATABASE,
+					   get_database_name(db_oid));
+	}
+
+	/* Look up the database in pg_database */
+	tuple_database = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(db_oid));
+	if (!HeapTupleIsValid(tuple_database))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("database with oid %u does not exist", db_oid));
+
+	dbform = (Form_pg_database) GETSTRUCT(tuple_database);
+
+	initStringInfo(&buf);
+
+	/* Build the CREATE DATABASE statement */
+	appendStringInfo(&buf, "CREATE DATABASE %s",
+					 quote_identifier(dbform->datname.data));
+	get_formatted_string(&buf, pretty_flags, 4, "WITH");
+
+	/* Set the OWNER in the DDL if owner is not omitted */
+	if (OidIsValid(dbform->datdba) && !(ddl_flags & PG_DDL_NO_OWNER))
+	{
+		char	   *dbowner = GetUserNameFromId(dbform->datdba, false);
+
+		if (dbowner != NULL)
+		{
+			get_formatted_string(&buf, pretty_flags, 8, "OWNER = %s",
+								 quote_identifier(dbowner));
+		}
+	}
+
+	/* Set the ENCODING in the DDL */
+	encoding = pg_encoding_to_char(dbform->encoding);
+
+	if (is_with_defaults ||
+		pg_strcasecmp(encoding, DDL_DEFAULTS.DATABASE.ENCODING) != 0)
+	{
+		get_formatted_string(&buf, pretty_flags, 8, "ENCODING = %s",
+							 quote_literal_cstr(encoding));
+	}
+
+	/* Fetch the value of LC_COLLATE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datcollate, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LC_COLLATE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	/* Fetch the value of LC_CTYPE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datctype, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LC_CTYPE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	/* Fetch the value of LOCALE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datlocale, &attr_isnull);
+	if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, pretty_flags, 8, "BUILTIN_LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	else if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "ICU_LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	else if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Fetch the value of ICU_RULES */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_daticurules, &attr_isnull);
+	if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "ICU_RULES = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Fetch the value of COLLATION_VERSION */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datcollversion, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "COLLATION_VERSION = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Set the appropriate LOCALE_PROVIDER */
+	if (dbform->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = builtin");
+	else if (dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = icu");
+	else if (is_with_defaults)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = libc");
+
+	/* Set the TABLESPACE in the DDL if tablespace is not omitted */
+	if (OidIsValid(dbform->dattablespace) && !(ddl_flags & PG_DDL_NO_TABLESPACE))
+	{
+		/* Get the tablespace name respective to the given tablespace oid */
+		char	   *dbTablespace = get_tablespace_name(dbform->dattablespace);
+
+		/* If it's with defaults, we skip default tablespace check */
+		if (is_with_defaults ||
+			(pg_strcasecmp(dbTablespace, DDL_DEFAULTS.DATABASE.TABLESPACE) != 0))
+			get_formatted_string(&buf, pretty_flags, 8, "TABLESPACE = %s",
+								 quote_identifier(dbTablespace));
+	}
+
+	if (is_with_defaults ||
+		(dbform->datallowconn != DDL_DEFAULTS.DATABASE.ALLOW_CONN))
+	{
+		get_formatted_string(&buf, pretty_flags, 8, "ALLOW_CONNECTIONS = %s",
+							 dbform->datallowconn ? "true" : "false");
+	}
+
+	if (is_with_defaults ||
+		(dbform->datconnlimit != DDL_DEFAULTS.DATABASE.CONN_LIMIT))
+	{
+		get_formatted_string(&buf, pretty_flags, 8, "CONNECTION LIMIT = %d",
+							 dbform->datconnlimit);
+	}
+
+	if (is_with_defaults || dbform->datistemplate)
+		get_formatted_string(&buf, pretty_flags, 8, "IS_TEMPLATE = %s",
+							 dbform->datistemplate ? "true" : "false");
+
+	appendStringInfoChar(&buf, ';');
+
+	ReleaseSysCache(tuple_database);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dac40992cbc..c8f3736392d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4031,6 +4031,13 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '9492', descr => 'get CREATE statement for database name and oid',
+  proname => 'pg_get_database_ddl', provariadic => 'any', proisstrict => 'f',
+  provolatile => 's', prorettype => 'text',
+  proargtypes => 'regdatabase any',
+  proargmodes => '{i,v}',
+  proallargtypes => '{regdatabase,any}',
+  prosrc => 'pg_get_database_ddl' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/include/utils/ddl_defaults.h b/src/include/utils/ddl_defaults.h
new file mode 100644
index 00000000000..9f21c42c05a
--- /dev/null
+++ b/src/include/utils/ddl_defaults.h
@@ -0,0 +1,37 @@
+/*-------------------------------------------------------------------------
+ *
+ * ddl_defaults.h
+ *	  Declarations for DDL defaults.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/ddl_defaults.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DDL_DEFAULTS_H
+#define DDL_DEFAULTS_H
+
+static const struct
+{
+	struct
+	{
+		const char *ENCODING;
+		const char *TABLESPACE;
+		int			CONN_LIMIT;
+		bool		ALLOW_CONN;
+	}			DATABASE;
+
+	/* Add more object types as needed */
+}			DDL_DEFAULTS = {
+
+	.DATABASE = {
+		.ENCODING = "UTF8",
+		.TABLESPACE = "pg_default",
+		.CONN_LIMIT = -1,
+		.ALLOW_CONN = true,
+	}
+};
+
+#endif							/* DDL_DEFAULTS_H */
diff --git a/src/test/regress/expected/database.out b/src/test/regress/expected/database.out
index 6b879b0f62a..5d2834771ac 100644
--- a/src/test/regress/expected/database.out
+++ b/src/test/regress/expected/database.out
@@ -1,3 +1,57 @@
+--
+-- Reconstruct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+    -- Remove %LOCALE_PROVIDER% placeholders
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)',
+        '',
+        'gi'
+    );
+
+	-- Remove LC_COLLATE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -16,6 +70,132 @@ CREATE ROLE regress_datdba_before;
 CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database');
+ERROR:  database "regression_database" does not exist
+LINE 1: SELECT pg_get_database_ddl('regression_database');
+                                   ^
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+ pg_get_database_ddl 
+---------------------
+ 
+(1 row)
+
+-- Without options
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+                                        ddl_filter                                         
+-------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after CONNECTION LIMIT = 123;
+(1 row)
+
+-- With No Owner
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false));
+                          ddl_filter                          
+--------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH CONNECTION LIMIT = 123;
+(1 row)
+
+-- With No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true, 'tablespace', false));
+                                                                        ddl_filter                                                                        
+----------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true));
+                                                                                    ddl_filter                                                                                    
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after ENCODING = 'UTF8' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'tablespace', false, 'defaults', true));
+                                                         ddl_filter                                                          
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        OWNER = regress_datdba_after
+        CONNECTION LIMIT = 123;
+(1 row)
+-- With No Owner and No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        CONNECTION LIMIT = 123;
+(1 row)
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'defaults', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        OWNER = regress_datdba_after
+        ENCODING = 'UTF8'
+        TABLESPACE = pg_default
+        ALLOW_CONNECTIONS = true
+        CONNECTION LIMIT = 123
+        IS_TEMPLATE = false;
+(1 row)
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false, 'defaults', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        ENCODING = 'UTF8'
+        ALLOW_CONNECTIONS = true
+        CONNECTION LIMIT = 123
+        IS_TEMPLATE = false;
+(1 row)
+-- Test with text values: 'yes', 'no', '1', '0', 'on', 'off'
+\pset format aligned
+-- Using 'yes' and 'no'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes'));
+                                                                     ddl_filter                                                                      
+-----------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ENCODING = 'UTF8' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Using '1' and '0'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1'));
+                                                         ddl_filter                                                          
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Using 'on' and 'off'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on'));
+           ddl_filter            
+---------------------------------
+ CREATE DATABASE regression_utf8+
+     WITH                       +
+         CONNECTION LIMIT = 123;
+(1 row)
+
+-- Mixed boolean and text values
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no'));
+                                                         ddl_filter                                                          
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Test duplicate option (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'owner', true);
+ERROR:  option "owner" is specified more than once
+-- Test invalid text value (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', 'invalid');
+ERROR:  argument 2: invalid value "invalid" for key "owner"
+HINT:  Valid values are: true, false, yes, no, 1, 0, on, off.
 DROP DATABASE regression_utf8;
+DROP FUNCTION ddl_filter(text);
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
diff --git a/src/test/regress/sql/database.sql b/src/test/regress/sql/database.sql
index 4ef36127291..b67b56006d8 100644
--- a/src/test/regress/sql/database.sql
+++ b/src/test/regress/sql/database.sql
@@ -1,3 +1,59 @@
+--
+-- Reconstruct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+    -- Remove %LOCALE_PROVIDER% placeholders
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)',
+        '',
+        'gi'
+    );
+
+	-- Remove LC_COLLATE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
+
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -19,6 +75,62 @@ CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
 
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database');
+
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+
+-- Without options
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+
+-- With No Owner
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false));
+
+-- With No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true, 'tablespace', false));
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true));
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'tablespace', false, 'defaults', true));
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true));
+
+-- With No Owner and No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false));
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'defaults', true));
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false, 'defaults', true));
+
+-- Test with text values: 'yes', 'no', '1', '0', 'on', 'off'
+\pset format aligned
+-- Using 'yes' and 'no'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes'));
+
+-- Using '1' and '0'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1'));
+
+-- Using 'on' and 'off'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on'));
+
+-- Mixed boolean and text values
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no'));
+
+-- Test duplicate option (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'owner', true);
+
+-- Test invalid text value (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', 'invalid');
+
 DROP DATABASE regression_utf8;
+DROP FUNCTION ddl_filter(text);
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
-- 
2.51.0



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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-05 14:45  Rafia Sabih <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Rafia Sabih @ 2026-03-05 14:45 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Japin Li <[email protected]>; Álvaro Herrera <[email protected]>; Euler Taveira <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

I like the idea for this patch and find this useful.

I have few comments for the patch, looking at the test cases added in the
patch, it is not clear to me what is the default value for each of the
options. For example, in
+-- With No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true,
'tablespace', false));
+
 ddl_filter

+----------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after
ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123
IS_TEMPLATE = false;
+(1 row)
+
owner option is omitted but the output contains the owner information. So
does it mean that the default value for each option is true?
But that doesn't seem so, because omitting pretty option, doesn't give
output in pretty, rather pretty is used only when provided.
I find this rather confusing, it is worth documenting what is the default
value for each of these options.

Similarly, what is the expected behaviour when defaults option is not
provided,
+-- With No Owner
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false));
+                          ddl_filter
+--------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH CONNECTION LIMIT = 123;
+(1 row)
+
+-- With No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true,
'tablespace', false));
+
 ddl_filter

+----------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after
ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123
IS_TEMPLATE = false;
+(1 row)
Why is connection_limit present in both of these cases...?

Another thing, what is the significance of having defaults option, because
I think that knowing non-default values could be more useful, or atleast
there should be a way to know  the non-default options for the database.
Also, the option strategy is missing in the output, is it deliberate? If
yes, why?

On Thu, 5 Mar 2026 at 01:50, Akshay Joshi <[email protected]>
wrote:

> I’ve addressed all of your comments.
>
> Attached is the *v11 patch*, now ready for further review.
>
> On Wed, Mar 4, 2026 at 7:31 PM Japin Li <[email protected]> wrote:
>
>> On Wed, 04 Mar 2026 at 18:29, Akshay Joshi <[email protected]>
>> wrote:
>> > Thanks for the review, Japin. I’ve addressed all of your comments. I
>> also added a check to throw an error if an option
>> > appears more than once.
>> >
>> > Attached is the v10 patch, now ready for further review.
>> >
>>
>> Thanks for updating the patch.  Here are some comments on v10.
>>
>> 1.
>> + * db_oid - OID/Name of the database for which to generate the DDL.
>>
>> Should the comment be updated? The code only accepts an OID for `db_oid`,
>> database names are not supported.
>>
>> 2.
>> +       /* Set the OWNER in the DDL if owner is not omitted */
>> +       if (OidIsValid(dbform->datdba) && !(ddl_flags & PG_DDL_NO_OWNER))
>> +       {
>> +               get_formatted_string(&buf, pretty_flags, 8, "OWNER = %s",
>> +
>> quote_identifier(dbowner));
>> +       }
>>
>> `dbowner` is only needed inside this `if` — how about declaring it there
>> to
>> reduce its scope?
>>
>> 3.
>> +               /* If is_with_defaults is true, then we skip default
>> encoding check */
>> +               if (is_with_defaults ||
>> +
>>  (pg_strcasecmp(pg_encoding_to_char(dbform->encoding),
>> +
>> DDL_DEFAULTS.DATABASE.ENCODING) != 0))
>> +               {
>> +                       get_formatted_string(&buf, pretty_flags, 8,
>> "ENCODING = %s",
>> +
>> quote_literal_cstr(
>> +
>>                                pg_encoding_to_char(dbform->encoding)));
>> +               }
>>
>> How about cache the result of `pg_encoding_to_char()` in a local variable
>> to
>> avoid calling it twice?
>>
>> --
>> Regards,
>> Japin Li
>> ChengDu WenWu Information Technology Co., Ltd.
>>
>

-- 
Regards,
Rafia Sabih
CYBERTEC PostgreSQL International GmbH


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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-05 16:15  Akshay Joshi <[email protected]>
  parent: Rafia Sabih <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Akshay Joshi @ 2026-03-05 16:15 UTC (permalink / raw)
  To: Rafia Sabih <[email protected]>; +Cc: Japin Li <[email protected]>; Álvaro Herrera <[email protected]>; Euler Taveira <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

On Thu, Mar 5, 2026 at 8:15 PM Rafia Sabih <[email protected]>
wrote:

> I like the idea for this patch and find this useful.
>
> I have few comments for the patch, looking at the test cases added in the
> patch, it is not clear to me what is the default value for each of the
> options. For example, in
>

   Following the postgresql documentation, I created ddl_defaults.h to
handle default options. This will be used across all database objects in
future pg_get_<object>_ddl patches.


> +-- With No Tablespace
> +SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults',
> true, 'tablespace', false));
> +
>  ddl_filter
>
>
> +----------------------------------------------------------------------------------------------------------------------------------------------------------
> + CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after
> ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123
> IS_TEMPLATE = false;
> +(1 row)
> +
> owner option is omitted but the output contains the owner information. So
> does it mean that the default value for each option is true?
> But that doesn't seem so, because omitting pretty option, doesn't give
> output in pretty, rather pretty is used only when provided.
> I find this rather confusing, it is worth documenting what is the default
> value for each of these options.
>

    In my view, every database has an owner; it's a required attribute, not
an optional one with a "default" value. The OWNER clause is correctly
controlled only by the PG_DDL_NO_OWNER flag. Users must explicitly
provide (*'owner',
false/'no'/'off')* to opt out of including ownership info in the DDL. When
comparing the Owner and Pretty options: by default, the Owner is included
in the DDL reconstruction, whereas the Pretty option is set to false by
default.

    In my initial patches, I used flags like --no-owner and --no-tablespace
because they are very intuitive for users. However, reviewers noted that
this style is unique to pg_dump and not used elsewhere. Consequently, I
switched to the existing name-value pair format using VARIADIC arguments.

>
> Similarly, what is the expected behaviour when defaults option is not
> provided,
> +-- With No Owner
> +SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false));
> +                          ddl_filter
> +--------------------------------------------------------------
> + CREATE DATABASE regression_utf8 WITH CONNECTION LIMIT = 123;
> +(1 row)
> +
> +-- With No Tablespace
> +SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults',
> true, 'tablespace', false));
> +
>  ddl_filter
>
>
> +----------------------------------------------------------------------------------------------------------------------------------------------------------
> + CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after
> ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123
> IS_TEMPLATE = false;
> +(1 row)
> Why is connection_limit present in both of these cases...?
>
> Another thing, what is the significance of having defaults option, because
> I think that knowing non-default values could be more useful, or atleast
> there should be a way to know  the non-default options for the database.
>

    The defaults option is false by default; if a user does not specify it,
the reconstructed DDL only includes non-default parameters. In my opinion,
we should retain this flag for users who wish to see all available
parameters for easier manual editing later. While simple database objects
have fewer parameters, this flag is essential for complex TABLE or FUNCTION
syntax where the parameter list is extensive.


> Also, the option strategy is missing in the output, is it deliberate? If
> yes, why?
>

> On Thu, 5 Mar 2026 at 01:50, Akshay Joshi <[email protected]>
> wrote:
>
>> I’ve addressed all of your comments.
>>
>> Attached is the *v11 patch*, now ready for further review.
>>
>> On Wed, Mar 4, 2026 at 7:31 PM Japin Li <[email protected]> wrote:
>>
>>> On Wed, 04 Mar 2026 at 18:29, Akshay Joshi <
>>> [email protected]> wrote:
>>> > Thanks for the review, Japin. I’ve addressed all of your comments. I
>>> also added a check to throw an error if an option
>>> > appears more than once.
>>> >
>>> > Attached is the v10 patch, now ready for further review.
>>> >
>>>
>>> Thanks for updating the patch.  Here are some comments on v10.
>>>
>>> 1.
>>> + * db_oid - OID/Name of the database for which to generate the DDL.
>>>
>>> Should the comment be updated? The code only accepts an OID for `db_oid`,
>>> database names are not supported.
>>>
>>> 2.
>>> +       /* Set the OWNER in the DDL if owner is not omitted */
>>> +       if (OidIsValid(dbform->datdba) && !(ddl_flags & PG_DDL_NO_OWNER))
>>> +       {
>>> +               get_formatted_string(&buf, pretty_flags, 8, "OWNER = %s",
>>> +
>>> quote_identifier(dbowner));
>>> +       }
>>>
>>> `dbowner` is only needed inside this `if` — how about declaring it there
>>> to
>>> reduce its scope?
>>>
>>> 3.
>>> +               /* If is_with_defaults is true, then we skip default
>>> encoding check */
>>> +               if (is_with_defaults ||
>>> +
>>>  (pg_strcasecmp(pg_encoding_to_char(dbform->encoding),
>>> +
>>> DDL_DEFAULTS.DATABASE.ENCODING) != 0))
>>> +               {
>>> +                       get_formatted_string(&buf, pretty_flags, 8,
>>> "ENCODING = %s",
>>> +
>>> quote_literal_cstr(
>>> +
>>>                                pg_encoding_to_char(dbform->encoding)));
>>> +               }
>>>
>>> How about cache the result of `pg_encoding_to_char()` in a local
>>> variable to
>>> avoid calling it twice?
>>>
>>> --
>>> Regards,
>>> Japin Li
>>> ChengDu WenWu Information Technology Co., Ltd.
>>>
>>
>
> --
> Regards,
> Rafia Sabih
> CYBERTEC PostgreSQL International GmbH
>


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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-06 07:37  Rafia Sabih <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Rafia Sabih @ 2026-03-06 07:37 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Japin Li <[email protected]>; Álvaro Herrera <[email protected]>; Euler Taveira <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

On Thu, 5 Mar 2026 at 08:16, Akshay Joshi <[email protected]>
wrote:

>
>
> On Thu, Mar 5, 2026 at 8:15 PM Rafia Sabih <[email protected]>
> wrote:
>
>> I like the idea for this patch and find this useful.
>>
>> I have few comments for the patch, looking at the test cases added in the
>> patch, it is not clear to me what is the default value for each of the
>> options. For example, in
>>
>
>    Following the postgresql documentation, I created ddl_defaults.h to
> handle default options. This will be used across all database objects in
> future pg_get_<object>_ddl patches.
>
>
>> +-- With No Tablespace
>> +SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults',
>> true, 'tablespace', false));
>> +
>>  ddl_filter
>>
>>
>> +----------------------------------------------------------------------------------------------------------------------------------------------------------
>> + CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after
>> ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123
>> IS_TEMPLATE = false;
>> +(1 row)
>> +
>> owner option is omitted but the output contains the owner information. So
>> does it mean that the default value for each option is true?
>> But that doesn't seem so, because omitting pretty option, doesn't give
>> output in pretty, rather pretty is used only when provided.
>> I find this rather confusing, it is worth documenting what is the default
>> value for each of these options.
>>
>
>     In my view, every database has an owner; it's a required attribute,
> not an optional one with a "default" value. The OWNER clause is correctly
> controlled only by the PG_DDL_NO_OWNER flag. Users must explicitly provide (*'owner',
> false/'no'/'off')* to opt out of including ownership info in the
> DDL. When comparing the Owner and Pretty options: by default, the Owner is
> included in the DDL reconstruction, whereas the Pretty option is set to
> false by default.
>
> Right, I think this needs to be added in the documentation as part of this
patch. Basically, here
<para>
+      <literal>'pretty', true</literal> (or <literal>'pretty',
'yes'</literal>):
+      Adds newlines and indentation for better readability.
+      </para>
it needs to have a line more saying, by default this option is off.
Similarly for other options, like for the owner it is by default true, etc.

>     In my initial patches, I used flags like --no-owner and
> --no-tablespace because they are very intuitive for users. However,
> reviewers noted that this style is unique to pg_dump and not used
> elsewhere. Consequently, I switched to the existing name-value pair format
> using VARIADIC arguments.
>
>>
>> Similarly, what is the expected behaviour when defaults option is not
>> provided,
>> +-- With No Owner
>> +SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner',
>> false));
>> +                          ddl_filter
>> +--------------------------------------------------------------
>> + CREATE DATABASE regression_utf8 WITH CONNECTION LIMIT = 123;
>> +(1 row)
>> +
>> +-- With No Tablespace
>> +SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults',
>> true, 'tablespace', false));
>> +
>>  ddl_filter
>>
>>
>> +----------------------------------------------------------------------------------------------------------------------------------------------------------
>> + CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after
>> ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123
>> IS_TEMPLATE = false;
>> +(1 row)
>> Why is connection_limit present in both of these cases...?
>>
>> Another thing, what is the significance of having defaults option,
>> because I think that knowing non-default values could be more useful, or
>> atleast there should be a way to know  the non-default options for the
>> database.
>>
>
>     The defaults option is false by default; if a user does not specify
> it, the reconstructed DDL only includes non-default parameters. In my
> opinion, we should retain this flag for users who wish to see all available
> parameters for easier manual editing later. While simple database objects
> have fewer parameters, this flag is essential for complex TABLE or FUNCTION
> syntax where the parameter list is extensive.
>
This information should also be included in the documentation.

>
>
>> Also, the option strategy is missing in the output, is it deliberate? If
>> yes, why?
>>
>
>> On Thu, 5 Mar 2026 at 01:50, Akshay Joshi <[email protected]>
>> wrote:
>>
>>> I’ve addressed all of your comments.
>>>
>>> Attached is the *v11 patch*, now ready for further review.
>>>
>>> On Wed, Mar 4, 2026 at 7:31 PM Japin Li <[email protected]> wrote:
>>>
>>>> On Wed, 04 Mar 2026 at 18:29, Akshay Joshi <
>>>> [email protected]> wrote:
>>>> > Thanks for the review, Japin. I’ve addressed all of your comments. I
>>>> also added a check to throw an error if an option
>>>> > appears more than once.
>>>> >
>>>> > Attached is the v10 patch, now ready for further review.
>>>> >
>>>>
>>>> Thanks for updating the patch.  Here are some comments on v10.
>>>>
>>>> 1.
>>>> + * db_oid - OID/Name of the database for which to generate the DDL.
>>>>
>>>> Should the comment be updated? The code only accepts an OID for
>>>> `db_oid`,
>>>> database names are not supported.
>>>>
>>>> 2.
>>>> +       /* Set the OWNER in the DDL if owner is not omitted */
>>>> +       if (OidIsValid(dbform->datdba) && !(ddl_flags &
>>>> PG_DDL_NO_OWNER))
>>>> +       {
>>>> +               get_formatted_string(&buf, pretty_flags, 8, "OWNER =
>>>> %s",
>>>> +
>>>> quote_identifier(dbowner));
>>>> +       }
>>>>
>>>> `dbowner` is only needed inside this `if` — how about declaring it
>>>> there to
>>>> reduce its scope?
>>>>
>>>> 3.
>>>> +               /* If is_with_defaults is true, then we skip default
>>>> encoding check */
>>>> +               if (is_with_defaults ||
>>>> +
>>>>  (pg_strcasecmp(pg_encoding_to_char(dbform->encoding),
>>>> +
>>>> DDL_DEFAULTS.DATABASE.ENCODING) != 0))
>>>> +               {
>>>> +                       get_formatted_string(&buf, pretty_flags, 8,
>>>> "ENCODING = %s",
>>>> +
>>>> quote_literal_cstr(
>>>> +
>>>>                                  pg_encoding_to_char(dbform->encoding)));
>>>> +               }
>>>>
>>>> How about cache the result of `pg_encoding_to_char()` in a local
>>>> variable to
>>>> avoid calling it twice?
>>>>
>>>> --
>>>> Regards,
>>>> Japin Li
>>>> ChengDu WenWu Information Technology Co., Ltd.
>>>>
>>>
>>
>> --
>> Regards,
>> Rafia Sabih
>> CYBERTEC PostgreSQL International GmbH
>>
>

-- 
Regards,
Rafia Sabih
CYBERTEC PostgreSQL International GmbH


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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-06 11:51  Akshay Joshi <[email protected]>
  parent: Rafia Sabih <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Akshay Joshi @ 2026-03-06 11:51 UTC (permalink / raw)
  To: Rafia Sabih <[email protected]>; +Cc: Japin Li <[email protected]>; Álvaro Herrera <[email protected]>; Euler Taveira <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

I have updated the documentation.
Attached is the *v12 patch*, now ready for further review.

On Fri, Mar 6, 2026 at 1:07 PM Rafia Sabih <[email protected]>
wrote:

>
>
> On Thu, 5 Mar 2026 at 08:16, Akshay Joshi <[email protected]>
> wrote:
>
>>
>>
>> On Thu, Mar 5, 2026 at 8:15 PM Rafia Sabih <[email protected]>
>> wrote:
>>
>>> I like the idea for this patch and find this useful.
>>>
>>> I have few comments for the patch, looking at the test cases added in
>>> the patch, it is not clear to me what is the default value for each of the
>>> options. For example, in
>>>
>>
>>    Following the postgresql documentation, I created ddl_defaults.h to
>> handle default options. This will be used across all database objects in
>> future pg_get_<object>_ddl patches.
>>
>>
>>> +-- With No Tablespace
>>> +SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults',
>>> true, 'tablespace', false));
>>> +
>>>  ddl_filter
>>>
>>>
>>> +----------------------------------------------------------------------------------------------------------------------------------------------------------
>>> + CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after
>>> ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123
>>> IS_TEMPLATE = false;
>>> +(1 row)
>>> +
>>> owner option is omitted but the output contains the owner information.
>>> So does it mean that the default value for each option is true?
>>> But that doesn't seem so, because omitting pretty option, doesn't give
>>> output in pretty, rather pretty is used only when provided.
>>> I find this rather confusing, it is worth documenting what is the
>>> default value for each of these options.
>>>
>>
>>     In my view, every database has an owner; it's a required attribute,
>> not an optional one with a "default" value. The OWNER clause is correctly
>> controlled only by the PG_DDL_NO_OWNER flag. Users must explicitly provide (*'owner',
>> false/'no'/'off')* to opt out of including ownership info in the
>> DDL. When comparing the Owner and Pretty options: by default, the Owner is
>> included in the DDL reconstruction, whereas the Pretty option is set to
>> false by default.
>>
>> Right, I think this needs to be added in the documentation as part of
> this patch. Basically, here
> <para>
> +      <literal>'pretty', true</literal> (or <literal>'pretty',
> 'yes'</literal>):
> +      Adds newlines and indentation for better readability.
> +      </para>
> it needs to have a line more saying, by default this option is off.
> Similarly for other options, like for the owner it is by default true, etc.
>
>>     In my initial patches, I used flags like --no-owner and
>> --no-tablespace because they are very intuitive for users. However,
>> reviewers noted that this style is unique to pg_dump and not used
>> elsewhere. Consequently, I switched to the existing name-value pair format
>> using VARIADIC arguments.
>>
>>>
>>> Similarly, what is the expected behaviour when defaults option is not
>>> provided,
>>> +-- With No Owner
>>> +SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner',
>>> false));
>>> +                          ddl_filter
>>> +--------------------------------------------------------------
>>> + CREATE DATABASE regression_utf8 WITH CONNECTION LIMIT = 123;
>>> +(1 row)
>>> +
>>> +-- With No Tablespace
>>> +SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults',
>>> true, 'tablespace', false));
>>> +
>>>  ddl_filter
>>>
>>>
>>> +----------------------------------------------------------------------------------------------------------------------------------------------------------
>>> + CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after
>>> ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123
>>> IS_TEMPLATE = false;
>>> +(1 row)
>>> Why is connection_limit present in both of these cases...?
>>>
>>> Another thing, what is the significance of having defaults option,
>>> because I think that knowing non-default values could be more useful, or
>>> atleast there should be a way to know  the non-default options for the
>>> database.
>>>
>>
>>     The defaults option is false by default; if a user does not specify
>> it, the reconstructed DDL only includes non-default parameters. In my
>> opinion, we should retain this flag for users who wish to see all available
>> parameters for easier manual editing later. While simple database objects
>> have fewer parameters, this flag is essential for complex TABLE or FUNCTION
>> syntax where the parameter list is extensive.
>>
> This information should also be included in the documentation.
>
>>
>>
>>> Also, the option strategy is missing in the output, is it deliberate? If
>>> yes, why?
>>>
>>
>>> On Thu, 5 Mar 2026 at 01:50, Akshay Joshi <[email protected]>
>>> wrote:
>>>
>>>> I’ve addressed all of your comments.
>>>>
>>>> Attached is the *v11 patch*, now ready for further review.
>>>>
>>>> On Wed, Mar 4, 2026 at 7:31 PM Japin Li <[email protected]> wrote:
>>>>
>>>>> On Wed, 04 Mar 2026 at 18:29, Akshay Joshi <
>>>>> [email protected]> wrote:
>>>>> > Thanks for the review, Japin. I’ve addressed all of your comments. I
>>>>> also added a check to throw an error if an option
>>>>> > appears more than once.
>>>>> >
>>>>> > Attached is the v10 patch, now ready for further review.
>>>>> >
>>>>>
>>>>> Thanks for updating the patch.  Here are some comments on v10.
>>>>>
>>>>> 1.
>>>>> + * db_oid - OID/Name of the database for which to generate the DDL.
>>>>>
>>>>> Should the comment be updated? The code only accepts an OID for
>>>>> `db_oid`,
>>>>> database names are not supported.
>>>>>
>>>>> 2.
>>>>> +       /* Set the OWNER in the DDL if owner is not omitted */
>>>>> +       if (OidIsValid(dbform->datdba) && !(ddl_flags &
>>>>> PG_DDL_NO_OWNER))
>>>>> +       {
>>>>> +               get_formatted_string(&buf, pretty_flags, 8, "OWNER =
>>>>> %s",
>>>>> +
>>>>> quote_identifier(dbowner));
>>>>> +       }
>>>>>
>>>>> `dbowner` is only needed inside this `if` — how about declaring it
>>>>> there to
>>>>> reduce its scope?
>>>>>
>>>>> 3.
>>>>> +               /* If is_with_defaults is true, then we skip default
>>>>> encoding check */
>>>>> +               if (is_with_defaults ||
>>>>> +
>>>>>  (pg_strcasecmp(pg_encoding_to_char(dbform->encoding),
>>>>> +
>>>>> DDL_DEFAULTS.DATABASE.ENCODING) != 0))
>>>>> +               {
>>>>> +                       get_formatted_string(&buf, pretty_flags, 8,
>>>>> "ENCODING = %s",
>>>>> +
>>>>> quote_literal_cstr(
>>>>> +
>>>>>                                  pg_encoding_to_char(dbform->encoding)));
>>>>> +               }
>>>>>
>>>>> How about cache the result of `pg_encoding_to_char()` in a local
>>>>> variable to
>>>>> avoid calling it twice?
>>>>>
>>>>> --
>>>>> Regards,
>>>>> Japin Li
>>>>> ChengDu WenWu Information Technology Co., Ltd.
>>>>>
>>>>
>>>
>>> --
>>> Regards,
>>> Rafia Sabih
>>> CYBERTEC PostgreSQL International GmbH
>>>
>>
>
> --
> Regards,
> Rafia Sabih
> CYBERTEC PostgreSQL International GmbH
>


Attachments:

  [application/octet-stream] v12-0001-Add-pg_get_database_ddl-function-to-reconstruct-ddl.patch (33.9K, 3-v12-0001-Add-pg_get_database_ddl-function-to-reconstruct-ddl.patch)
  download | inline diff:
From 89cfa436e7cd815f56fdf7a44981d9b60200dd0c Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 6 Mar 2026 16:46:02 +0530
Subject: [PATCH v12] Add pg_get_database_ddl() function to reconstruct CREATE
 DATABASE statements.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This adds a new system function, pg_get_database_ddl(database_name/database_oid, ddl_options),
which reconstructs the CREATE DATABASE statement for a given database name or OID.

Supported ddl_options are 'pretty', 'owner', 'tablespace' and 'defaults' and respective
values could be 'yes'/'on'/true/'1' or 'no'/'off'/false/'0'.

Usage:
 SELECT pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes');
 SELECT pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1');
 SELECT pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on');
 SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no');

Reference: PG-150
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
Reviewed-by: Euler Taveira <[email protected]>
Reviewed-by: Quan Zongliang <[email protected]>
Reviewed-by: Japin Li <[email protected]>
Reviewed-by: Chao Li <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  91 +++++
 src/backend/catalog/system_functions.sql |   6 +
 src/backend/utils/adt/ruleutils.c        | 403 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 +
 src/include/utils/ddl_defaults.h         |  37 +++
 src/test/regress/expected/database.out   | 180 ++++++++++
 src/test/regress/sql/database.sql        | 112 +++++++
 7 files changed, 836 insertions(+)
 create mode 100644 src/include/utils/ddl_defaults.h

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 294f45e82a3..6915408ae30 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3845,4 +3845,95 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions, one for each object type.
+   </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_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>database_id</parameter> <type>regdatabase</type>
+        <optional>, <literal>VARIADIC</literal> <parameter>options</parameter>
+        <type>"any"</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE DATABASE</command> statement for the
+        specified database (identified by name or OID) from the system
+        catalogs. The optional variadic arguments are name/value pairs that
+        control the output
+        formatting and content (e.g., <literal>'pretty', true, 'owner', false</literal>).
+        Supported options are explained below.
+        </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+    The <parameter>options</parameter> for <function>pg_get_database_ddl</function>
+    provide fine-grained control over the generated SQL. Options are passed as
+    alternating key/value pairs where the key is a text string and the
+    value is either a boolean or a text string representing a boolean
+    (<literal>true</literal>, <literal>false</literal>, <literal>yes</literal>,
+    <literal>no</literal>, <literal>1</literal>, <literal>0</literal>,
+    <literal>on</literal>, <literal>off</literal>):
+    <itemizedlist>
+    <listitem>
+      <para>
+      <literal>'pretty', true</literal> (or <literal>'pretty', 'yes'</literal>):
+      Formats the output with newlines and indentation for better readability.
+      This option defaults to false.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'owner', false</literal> (or <literal>'owner', 'no'</literal>):
+      Omits the <literal>OWNER</literal> clause from the reconstructed statement.
+      This option defaults to true.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'tablespace', false</literal> (or <literal>'tablespace', '0'</literal>):
+      Omits the <literal>TABLESPACE</literal> clause from the reconstructed statement.
+      This option defaults to true.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'defaults', true</literal> (or <literal>'defaults', '1'</literal>):
+      Includes clauses for parameters that are currently at their default values
+      (e.g., <literal>CONNECTION LIMIT -1</literal>), which are normally omitted for brevity.
+      This option defaults to false.
+      </para>
+    </listitem>
+    </itemizedlist>
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 1c5b6d6df05..fa48e2f0775 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -372,3 +372,9 @@ CREATE OR REPLACE FUNCTION ts_debug(document text,
 BEGIN ATOMIC
     SELECT * FROM ts_debug(get_current_ts_config(), $1);
 END;
+
+CREATE OR REPLACE FUNCTION
+  pg_get_database_ddl(database_id regdatabase, VARIADIC options "any" DEFAULT NULL)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl';
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index f16f1535785..bbf82c1d6c0 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -28,6 +28,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_depend.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_opclass.h"
@@ -57,8 +58,10 @@
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
 #include "rewrite/rewriteSupport.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/ddl_defaults.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
 #include "utils/hsearch.h"
@@ -89,11 +92,45 @@
 #define PRETTYFLAG_INDENT		0x0002
 #define PRETTYFLAG_SCHEMA		0x0004
 
+/* DDL Options flags */
+#define PG_DDL_PRETTY_INDENT	0x00000001
+#define PG_DDL_WITH_DEFAULTS	0x00000002
+#define PG_DDL_NO_OWNER			0x00000004
+#define PG_DDL_NO_TABLESPACE	0x00000008
+
+/*
+ * Structure to define DDL options for parse_ddl_options().
+ * This allows easy addition of new options in the future.
+ */
+typedef struct DDLOptionDef
+{
+	const char *name;			/* Option name (case-insensitive) */
+	uint32		flag;			/* Flag to set */
+	bool		set_on_true;	/* If true, set flag when value is true; if
+								 * false, set flag when value is false */
+}			DDLOptionDef;
+
+/*
+ * Array of supported DDL options.
+ * To add a new option, simply add an entry to this array.
+ */
+static const DDLOptionDef ddl_option_defs[] = {
+	{"pretty", PG_DDL_PRETTY_INDENT, true},
+	{"defaults", PG_DDL_WITH_DEFAULTS, true},
+	{"owner", PG_DDL_NO_OWNER, false},
+	{"tablespace", PG_DDL_NO_TABLESPACE, false},
+};
+
+
 /* Standard conversion of a "bool pretty" option to detailed flags */
 #define GET_PRETTY_FLAGS(pretty) \
 	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
 	 : PRETTYFLAG_INDENT)
 
+#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 +584,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 nSpaces,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char *pg_get_database_ddl_worker(Oid db_oid, uint32 ddl_flags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13760,3 +13802,364 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - Based on prettyFlags the output includes spaces and
+ *               newlines (\n).
+ * nSpaces - indent with specified number of space characters.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, int prettyFlags, int nSpaces, const char *fmt,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with spaces */
+		appendStringInfoSpaces(buf, nSpaces);
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	for (;;)
+	{
+		int			needed;
+
+		va_start(args, fmt);
+		needed = appendStringInfoVA(buf, fmt, args);
+		va_end(args);
+		if (needed == 0)
+			break;
+		enlargeStringInfo(buf, needed);
+	}
+}
+
+/**
+ * parse_ddl_options - Generic helper to parse variadic name/value options
+ * fcinfo: The FunctionCallInfo from the calling function
+ * variadic_start: The argument position where variadic arguments start
+ *
+ * Returns: Bitmask of flags based on the parsed options.
+ *
+ * Options are passed as name/value pairs.
+ * For example: pg_get_database_ddl('mydb', 'owner', false, 'pretty', true)
+ */
+static uint32
+parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
+{
+	uint32		flags = 0;
+	uint32		seen_flags = 0;
+	Datum	   *args;
+	bool	   *nulls;
+	Oid		   *types;
+	int			nargs;
+	bool		found = false;
+
+	/* Extract variadic arguments */
+	nargs = extract_variadic_args(fcinfo, variadic_start, true,
+								  &args, &types, &nulls);
+
+	/* If no options provided (VARIADIC NULL), return the empty bitmask */
+	if (nargs <= 0)
+		return flags;
+
+	/*
+	 * Handle the case where DEFAULT NULL was used and no explicit variadic
+	 * arguments were provided. In this case, we get a single NULL argument.
+	 */
+	if (nargs == 1 && nulls[0])
+		return flags;
+
+	/* Arguments must come in name/value pairs */
+	if (nargs % 2 != 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("variadic arguments must be name/value pairs"),
+				 errhint("Provide an even number of variadic arguments that can be divided into pairs.")));
+
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *name;
+		bool		bval;
+
+		found = false;
+
+		/* Key must not be null */
+		if (nulls[i])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("name at variadic position %d is null", i + 1)));
+
+		/* Key must be text type */
+		if (types[i] != TEXTOID)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("name at variadic position %d has type %s, expected type %s",
+							i + 1, format_type_be(types[i]),
+							format_type_be(TEXTOID))));
+
+		name = TextDatumGetCString(args[i]);
+
+		/* Value must not be null */
+		if (nulls[i + 1])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("value for option \"%s\" must not be null",
+							name)));
+
+		/* Value must be boolean or text type */
+		if (types[i + 1] == BOOLOID)
+		{
+			bval = DatumGetBool(args[i + 1]);
+		}
+		else if (types[i + 1] == TEXTOID)
+		{
+			char	   *valstr = TextDatumGetCString(args[i + 1]);
+
+			if (!parse_bool(valstr, &bval))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("argument %d: invalid value \"%s\" for key \"%s\"",
+								i + 2, valstr, name),
+						 errhint("Valid values are: true, false, yes, no, 1, 0, on, off.")));
+			pfree(valstr);
+		}
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("argument %d: value for key \"%s\" must be boolean or text type",
+							i + 2, name)));
+		}
+
+		/*
+		 * Look up the option in the ddl_option_defs array and set the
+		 * appropriate flag based on the value.
+		 */
+		for (int j = 0; j < lengthof(ddl_option_defs); j++)
+		{
+			const		DDLOptionDef *opt = &ddl_option_defs[j];
+
+			if (pg_strcasecmp(name, opt->name) == 0)
+			{
+				/* Error if this option was already specified */
+				if (seen_flags & opt->flag)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("option \"%s\" is specified more than once", name)));
+
+				seen_flags |= opt->flag;
+
+				/*
+				 * Set the flag if the value matches the set_on_true
+				 * condition: if set_on_true is true, set flag when bval is
+				 * true; if set_on_true is false, set flag when bval is false.
+				 */
+				if (bval == opt->set_on_true)
+					flags |= opt->flag;
+
+				found = true;
+				break;
+			}
+		}
+
+		if (!found)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unrecognized option: \"%s\"", name)));
+
+		pfree(name);
+	}
+
+	return flags;
+}
+
+/*
+ * pg_get_database_ddl
+ *
+ * Generate a CREATE DATABASE statement for the specified database name or oid.
+ *
+ * db_oid - OID of the database for which to generate the DDL.
+ * options - Variadic name/value pairs to modify the output.
+ */
+Datum
+pg_get_database_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			db_oid;
+	uint32		ddl_flags;
+	char	   *res;
+
+	if (PG_ARGISNULL(0))
+		PG_RETURN_NULL();
+
+	db_oid = PG_GETARG_OID(0);
+
+	/* Parse variadic options starting from argument 1 */
+	ddl_flags = parse_ddl_options(fcinfo, 1);
+
+	res = pg_get_database_ddl_worker(db_oid, ddl_flags);
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_database_ddl_worker(Oid db_oid, uint32 ddl_flags)
+{
+	const char *encoding = NULL;
+	bool		attr_isnull;
+	Datum		dbvalue;
+	HeapTuple	tuple_database;
+	Form_pg_database dbform;
+	StringInfoData buf;
+	AclResult	aclresult;
+
+	/* Variables for ddl_options parsing */
+	int			pretty_flags = 0;
+	bool		is_with_defaults = false;
+
+	/* Set the appropriate flags */
+	if (ddl_flags & PG_DDL_PRETTY_INDENT)
+		pretty_flags = GET_DDL_PRETTY_FLAGS(1);
+
+	is_with_defaults = (ddl_flags & PG_DDL_WITH_DEFAULTS) ? true : false;
+
+	/*
+	 * User must have connect privilege for target database.
+	 */
+	aclresult = object_aclcheck(DatabaseRelationId, db_oid, GetUserId(),
+								ACL_CONNECT);
+	if (aclresult != ACLCHECK_OK &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclcheck_error(aclresult, OBJECT_DATABASE,
+					   get_database_name(db_oid));
+	}
+
+	/* Look up the database in pg_database */
+	tuple_database = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(db_oid));
+	if (!HeapTupleIsValid(tuple_database))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("database with oid %u does not exist", db_oid));
+
+	dbform = (Form_pg_database) GETSTRUCT(tuple_database);
+
+	initStringInfo(&buf);
+
+	/* Build the CREATE DATABASE statement */
+	appendStringInfo(&buf, "CREATE DATABASE %s",
+					 quote_identifier(dbform->datname.data));
+	get_formatted_string(&buf, pretty_flags, 4, "WITH");
+
+	/* Set the OWNER in the DDL if owner is not omitted */
+	if (OidIsValid(dbform->datdba) && !(ddl_flags & PG_DDL_NO_OWNER))
+	{
+		char	   *dbowner = GetUserNameFromId(dbform->datdba, false);
+
+		if (dbowner != NULL)
+		{
+			get_formatted_string(&buf, pretty_flags, 8, "OWNER = %s",
+								 quote_identifier(dbowner));
+		}
+	}
+
+	/* Set the ENCODING in the DDL */
+	encoding = pg_encoding_to_char(dbform->encoding);
+
+	if (is_with_defaults ||
+		pg_strcasecmp(encoding, DDL_DEFAULTS.DATABASE.ENCODING) != 0)
+	{
+		get_formatted_string(&buf, pretty_flags, 8, "ENCODING = %s",
+							 quote_literal_cstr(encoding));
+	}
+
+	/* Fetch the value of LC_COLLATE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datcollate, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LC_COLLATE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	/* Fetch the value of LC_CTYPE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datctype, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LC_CTYPE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	/* Fetch the value of LOCALE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datlocale, &attr_isnull);
+	if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, pretty_flags, 8, "BUILTIN_LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	else if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "ICU_LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	else if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Fetch the value of ICU_RULES */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_daticurules, &attr_isnull);
+	if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "ICU_RULES = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Fetch the value of COLLATION_VERSION */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datcollversion, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "COLLATION_VERSION = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Set the appropriate LOCALE_PROVIDER */
+	if (dbform->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = builtin");
+	else if (dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = icu");
+	else if (is_with_defaults)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = libc");
+
+	/* Set the TABLESPACE in the DDL if tablespace is not omitted */
+	if (OidIsValid(dbform->dattablespace) && !(ddl_flags & PG_DDL_NO_TABLESPACE))
+	{
+		/* Get the tablespace name respective to the given tablespace oid */
+		char	   *dbTablespace = get_tablespace_name(dbform->dattablespace);
+
+		/* If it's with defaults, we skip default tablespace check */
+		if (is_with_defaults ||
+			(pg_strcasecmp(dbTablespace, DDL_DEFAULTS.DATABASE.TABLESPACE) != 0))
+			get_formatted_string(&buf, pretty_flags, 8, "TABLESPACE = %s",
+								 quote_identifier(dbTablespace));
+	}
+
+	if (is_with_defaults ||
+		(dbform->datallowconn != DDL_DEFAULTS.DATABASE.ALLOW_CONN))
+	{
+		get_formatted_string(&buf, pretty_flags, 8, "ALLOW_CONNECTIONS = %s",
+							 dbform->datallowconn ? "true" : "false");
+	}
+
+	if (is_with_defaults ||
+		(dbform->datconnlimit != DDL_DEFAULTS.DATABASE.CONN_LIMIT))
+	{
+		get_formatted_string(&buf, pretty_flags, 8, "CONNECTION LIMIT = %d",
+							 dbform->datconnlimit);
+	}
+
+	if (is_with_defaults || dbform->datistemplate)
+		get_formatted_string(&buf, pretty_flags, 8, "IS_TEMPLATE = %s",
+							 dbform->datistemplate ? "true" : "false");
+
+	appendStringInfoChar(&buf, ';');
+
+	ReleaseSysCache(tuple_database);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 361e2cfffeb..1e2f7d3ac35 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4034,6 +4034,13 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '9492', descr => 'get CREATE statement for database name and oid',
+  proname => 'pg_get_database_ddl', provariadic => 'any', proisstrict => 'f',
+  provolatile => 's', prorettype => 'text',
+  proargtypes => 'regdatabase any',
+  proargmodes => '{i,v}',
+  proallargtypes => '{regdatabase,any}',
+  prosrc => 'pg_get_database_ddl' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/include/utils/ddl_defaults.h b/src/include/utils/ddl_defaults.h
new file mode 100644
index 00000000000..9f21c42c05a
--- /dev/null
+++ b/src/include/utils/ddl_defaults.h
@@ -0,0 +1,37 @@
+/*-------------------------------------------------------------------------
+ *
+ * ddl_defaults.h
+ *	  Declarations for DDL defaults.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/ddl_defaults.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DDL_DEFAULTS_H
+#define DDL_DEFAULTS_H
+
+static const struct
+{
+	struct
+	{
+		const char *ENCODING;
+		const char *TABLESPACE;
+		int			CONN_LIMIT;
+		bool		ALLOW_CONN;
+	}			DATABASE;
+
+	/* Add more object types as needed */
+}			DDL_DEFAULTS = {
+
+	.DATABASE = {
+		.ENCODING = "UTF8",
+		.TABLESPACE = "pg_default",
+		.CONN_LIMIT = -1,
+		.ALLOW_CONN = true,
+	}
+};
+
+#endif							/* DDL_DEFAULTS_H */
diff --git a/src/test/regress/expected/database.out b/src/test/regress/expected/database.out
index 6b879b0f62a..5d2834771ac 100644
--- a/src/test/regress/expected/database.out
+++ b/src/test/regress/expected/database.out
@@ -1,3 +1,57 @@
+--
+-- Reconstruct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+    -- Remove %LOCALE_PROVIDER% placeholders
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)',
+        '',
+        'gi'
+    );
+
+	-- Remove LC_COLLATE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -16,6 +70,132 @@ CREATE ROLE regress_datdba_before;
 CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database');
+ERROR:  database "regression_database" does not exist
+LINE 1: SELECT pg_get_database_ddl('regression_database');
+                                   ^
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+ pg_get_database_ddl 
+---------------------
+ 
+(1 row)
+
+-- Without options
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+                                        ddl_filter                                         
+-------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after CONNECTION LIMIT = 123;
+(1 row)
+
+-- With No Owner
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false));
+                          ddl_filter                          
+--------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH CONNECTION LIMIT = 123;
+(1 row)
+
+-- With No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true, 'tablespace', false));
+                                                                        ddl_filter                                                                        
+----------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true));
+                                                                                    ddl_filter                                                                                    
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after ENCODING = 'UTF8' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'tablespace', false, 'defaults', true));
+                                                         ddl_filter                                                          
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        OWNER = regress_datdba_after
+        CONNECTION LIMIT = 123;
+(1 row)
+-- With No Owner and No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        CONNECTION LIMIT = 123;
+(1 row)
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'defaults', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        OWNER = regress_datdba_after
+        ENCODING = 'UTF8'
+        TABLESPACE = pg_default
+        ALLOW_CONNECTIONS = true
+        CONNECTION LIMIT = 123
+        IS_TEMPLATE = false;
+(1 row)
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false, 'defaults', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        ENCODING = 'UTF8'
+        ALLOW_CONNECTIONS = true
+        CONNECTION LIMIT = 123
+        IS_TEMPLATE = false;
+(1 row)
+-- Test with text values: 'yes', 'no', '1', '0', 'on', 'off'
+\pset format aligned
+-- Using 'yes' and 'no'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes'));
+                                                                     ddl_filter                                                                      
+-----------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ENCODING = 'UTF8' TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Using '1' and '0'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1'));
+                                                         ddl_filter                                                          
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Using 'on' and 'off'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on'));
+           ddl_filter            
+---------------------------------
+ CREATE DATABASE regression_utf8+
+     WITH                       +
+         CONNECTION LIMIT = 123;
+(1 row)
+
+-- Mixed boolean and text values
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no'));
+                                                         ddl_filter                                                          
+-----------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ENCODING = 'UTF8' ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Test duplicate option (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'owner', true);
+ERROR:  option "owner" is specified more than once
+-- Test invalid text value (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', 'invalid');
+ERROR:  argument 2: invalid value "invalid" for key "owner"
+HINT:  Valid values are: true, false, yes, no, 1, 0, on, off.
 DROP DATABASE regression_utf8;
+DROP FUNCTION ddl_filter(text);
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
diff --git a/src/test/regress/sql/database.sql b/src/test/regress/sql/database.sql
index 4ef36127291..b67b56006d8 100644
--- a/src/test/regress/sql/database.sql
+++ b/src/test/regress/sql/database.sql
@@ -1,3 +1,59 @@
+--
+-- Reconstruct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+    -- Remove %LOCALE_PROVIDER% placeholders
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)',
+        '',
+        'gi'
+    );
+
+	-- Remove LC_COLLATE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+		'',
+		'gi'
+	);
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
+
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -19,6 +75,62 @@ CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
 
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database');
+
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+
+-- Without options
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+
+-- With No Owner
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false));
+
+-- With No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true, 'tablespace', false));
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true));
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'tablespace', false, 'defaults', true));
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true));
+
+-- With No Owner and No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false));
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'defaults', true));
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false, 'defaults', true));
+
+-- Test with text values: 'yes', 'no', '1', '0', 'on', 'off'
+\pset format aligned
+-- Using 'yes' and 'no'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes'));
+
+-- Using '1' and '0'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1'));
+
+-- Using 'on' and 'off'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on'));
+
+-- Mixed boolean and text values
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no'));
+
+-- Test duplicate option (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'owner', true);
+
+-- Test invalid text value (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', 'invalid');
+
 DROP DATABASE regression_utf8;
+DROP FUNCTION ddl_filter(text);
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
-- 
2.51.0



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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-06 14:43  Japin Li <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Japin Li @ 2026-03-06 14:43 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Rafia Sabih <[email protected]>; Álvaro Herrera <[email protected]>; Euler Taveira <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers


Hi, Akshay

On Fri, 06 Mar 2026 at 17:21, Akshay Joshi <[email protected]> wrote:
> I have updated the documentation.
> Attached is the v12 patch, now ready for further review.
>

Thanks for updating the patch.

I might not have expressed myself clearly in my last email — apologies.
Here's a diff that should make it clearer.

diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index bbf82c1d6c0..1ed56ee71ab 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -13859,7 +13859,6 @@ parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
 	bool	   *nulls;
 	Oid		   *types;
 	int			nargs;
-	bool		found = false;
 
 	/* Extract variadic arguments */
 	nargs = extract_variadic_args(fcinfo, variadic_start, true,
@@ -13887,8 +13886,7 @@ parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
 	{
 		char	   *name;
 		bool		bval;
-
-		found = false;
+		bool		found = false;
 
 		/* Key must not be null */
 		if (nulls[i])
@@ -13925,8 +13923,8 @@ parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
 			if (!parse_bool(valstr, &bval))
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-						 errmsg("argument %d: invalid value \"%s\" for key \"%s\"",
-								i + 2, valstr, name),
+						 errmsg("value for option \"%s\" at position %d has invalid value \"%s\"",
+								name, i + 2, valstr),
 						 errhint("Valid values are: true, false, yes, no, 1, 0, on, off.")));
 			pfree(valstr);
 		}
@@ -13934,8 +13932,8 @@ parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
 		{
 			ereport(ERROR,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("argument %d: value for key \"%s\" must be boolean or text type",
-							i + 2, name)));
+					 errmsg("value for option \"%s\" at position %d has type %s, expected type boolean or text",
+							name, i + 2, format_type_be(types[i + 1]))));
 		}
 
 		/*
@@ -13983,7 +13981,7 @@ parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
 /*
  * pg_get_database_ddl
  *
- * Generate a CREATE DATABASE statement for the specified database name or oid.
+ * Generate a CREATE DATABASE statement for the specified database oid.
  *
  * db_oid - OID of the database for which to generate the DDL.
  * options - Variadic name/value pairs to modify the output.

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





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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-09 12:08  Akshay Joshi <[email protected]>
  parent: Japin Li <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Akshay Joshi @ 2026-03-09 12:08 UTC (permalink / raw)
  To: Japin Li <[email protected]>; +Cc: Rafia Sabih <[email protected]>; Álvaro Herrera <[email protected]>; Euler Taveira <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

I have resolved the review comments. Removed the hardcoded 'utf-8'
encoding, the logic now retrieves the encoding dynamically. This ensures
accuracy because the default encoding for CREATE DATABASE is derived from
the template database (template1) rather than being a static value.

Attached is the *v13 patch*, now ready for further review.

On Fri, Mar 6, 2026 at 8:13 PM Japin Li <[email protected]> wrote:

>
> Hi, Akshay
>
> On Fri, 06 Mar 2026 at 17:21, Akshay Joshi <[email protected]>
> wrote:
> > I have updated the documentation.
> > Attached is the v12 patch, now ready for further review.
> >
>
> Thanks for updating the patch.
>
> I might not have expressed myself clearly in my last email — apologies.
> Here's a diff that should make it clearer.
>
> diff --git a/src/backend/utils/adt/ruleutils.c
> b/src/backend/utils/adt/ruleutils.c
> index bbf82c1d6c0..1ed56ee71ab 100644
> --- a/src/backend/utils/adt/ruleutils.c
> +++ b/src/backend/utils/adt/ruleutils.c
> @@ -13859,7 +13859,6 @@ parse_ddl_options(FunctionCallInfo fcinfo, int
> variadic_start)
>         bool       *nulls;
>         Oid                *types;
>         int                     nargs;
> -       bool            found = false;
>
>         /* Extract variadic arguments */
>         nargs = extract_variadic_args(fcinfo, variadic_start, true,
> @@ -13887,8 +13886,7 @@ parse_ddl_options(FunctionCallInfo fcinfo, int
> variadic_start)
>         {
>                 char       *name;
>                 bool            bval;
> -
> -               found = false;
> +               bool            found = false;
>
>                 /* Key must not be null */
>                 if (nulls[i])
> @@ -13925,8 +13923,8 @@ parse_ddl_options(FunctionCallInfo fcinfo, int
> variadic_start)
>                         if (!parse_bool(valstr, &bval))
>                                 ereport(ERROR,
>
> (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> -                                                errmsg("argument %d:
> invalid value \"%s\" for key \"%s\"",
> -                                                               i + 2,
> valstr, name),
> +                                                errmsg("value for option
> \"%s\" at position %d has invalid value \"%s\"",
> +                                                               name, i +
> 2, valstr),
>                                                  errhint("Valid values
> are: true, false, yes, no, 1, 0, on, off.")));
>                         pfree(valstr);
>                 }
> @@ -13934,8 +13932,8 @@ parse_ddl_options(FunctionCallInfo fcinfo, int
> variadic_start)
>                 {
>                         ereport(ERROR,
>
> (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> -                                        errmsg("argument %d: value for
> key \"%s\" must be boolean or text type",
> -                                                       i + 2, name)));
> +                                        errmsg("value for option \"%s\"
> at position %d has type %s, expected type boolean or text",
> +                                                       name, i + 2,
> format_type_be(types[i + 1]))));
>                 }
>
>                 /*
> @@ -13983,7 +13981,7 @@ parse_ddl_options(FunctionCallInfo fcinfo, int
> variadic_start)
>  /*
>   * pg_get_database_ddl
>   *
> - * Generate a CREATE DATABASE statement for the specified database name
> or oid.
> + * Generate a CREATE DATABASE statement for the specified database oid.
>   *
>   * db_oid - OID of the database for which to generate the DDL.
>   * options - Variadic name/value pairs to modify the output.
>
> --
> Regards,
> Japin Li
> ChengDu WenWu Information Technology Co., Ltd.
>


Attachments:

  [application/octet-stream] v13-0001-Add-pg_get_database_ddl-function-to-reconstruct-ddl.patch (34.3K, 3-v13-0001-Add-pg_get_database_ddl-function-to-reconstruct-ddl.patch)
  download | inline diff:
From f0e001b44e85944cf479987f5545fa88a0b56677 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 6 Mar 2026 16:46:02 +0530
Subject: [PATCH v13] Add pg_get_database_ddl() function to reconstruct CREATE
 DATABASE statements.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This adds a new system function, pg_get_database_ddl(database_name/database_oid, ddl_options),
which reconstructs the CREATE DATABASE statement for a given database name or OID.

Supported ddl_options are 'pretty', 'owner', 'tablespace' and 'defaults' and respective
values could be 'yes'/'on'/true/'1' or 'no'/'off'/false/'0'.

Usage:
 SELECT pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes');
 SELECT pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1');
 SELECT pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on');
 SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no');

Reference: PG-150
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
Reviewed-by: Euler Taveira <[email protected]>
Reviewed-by: Quan Zongliang <[email protected]>
Reviewed-by: Japin Li <[email protected]>
Reviewed-by: Chao Li <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  91 +++++
 src/backend/catalog/system_functions.sql |   6 +
 src/backend/utils/adt/ruleutils.c        | 413 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 +
 src/include/utils/ddl_defaults.h         |  35 ++
 src/test/regress/expected/database.out   | 186 ++++++++++
 src/test/regress/sql/database.sql        | 120 +++++++
 7 files changed, 858 insertions(+)
 create mode 100644 src/include/utils/ddl_defaults.h

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 294f45e82a3..6915408ae30 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3845,4 +3845,95 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions, one for each object type.
+   </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_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>database_id</parameter> <type>regdatabase</type>
+        <optional>, <literal>VARIADIC</literal> <parameter>options</parameter>
+        <type>"any"</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE DATABASE</command> statement for the
+        specified database (identified by name or OID) from the system
+        catalogs. The optional variadic arguments are name/value pairs that
+        control the output
+        formatting and content (e.g., <literal>'pretty', true, 'owner', false</literal>).
+        Supported options are explained below.
+        </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+    The <parameter>options</parameter> for <function>pg_get_database_ddl</function>
+    provide fine-grained control over the generated SQL. Options are passed as
+    alternating key/value pairs where the key is a text string and the
+    value is either a boolean or a text string representing a boolean
+    (<literal>true</literal>, <literal>false</literal>, <literal>yes</literal>,
+    <literal>no</literal>, <literal>1</literal>, <literal>0</literal>,
+    <literal>on</literal>, <literal>off</literal>):
+    <itemizedlist>
+    <listitem>
+      <para>
+      <literal>'pretty', true</literal> (or <literal>'pretty', 'yes'</literal>):
+      Formats the output with newlines and indentation for better readability.
+      This option defaults to false.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'owner', false</literal> (or <literal>'owner', 'no'</literal>):
+      Omits the <literal>OWNER</literal> clause from the reconstructed statement.
+      This option defaults to true.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'tablespace', false</literal> (or <literal>'tablespace', '0'</literal>):
+      Omits the <literal>TABLESPACE</literal> clause from the reconstructed statement.
+      This option defaults to true.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'defaults', true</literal> (or <literal>'defaults', '1'</literal>):
+      Includes clauses for parameters that are currently at their default values
+      (e.g., <literal>CONNECTION LIMIT -1</literal>), which are normally omitted for brevity.
+      This option defaults to false.
+      </para>
+    </listitem>
+    </itemizedlist>
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 1c5b6d6df05..fa48e2f0775 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -372,3 +372,9 @@ CREATE OR REPLACE FUNCTION ts_debug(document text,
 BEGIN ATOMIC
     SELECT * FROM ts_debug(get_current_ts_config(), $1);
 END;
+
+CREATE OR REPLACE FUNCTION
+  pg_get_database_ddl(database_id regdatabase, VARIADIC options "any" DEFAULT NULL)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl';
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index f16f1535785..e762a628f8c 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -28,6 +28,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_depend.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_opclass.h"
@@ -57,8 +58,10 @@
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
 #include "rewrite/rewriteSupport.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/ddl_defaults.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
 #include "utils/hsearch.h"
@@ -89,11 +92,45 @@
 #define PRETTYFLAG_INDENT		0x0002
 #define PRETTYFLAG_SCHEMA		0x0004
 
+/* DDL Options flags */
+#define PG_DDL_PRETTY_INDENT	0x00000001
+#define PG_DDL_WITH_DEFAULTS	0x00000002
+#define PG_DDL_NO_OWNER			0x00000004
+#define PG_DDL_NO_TABLESPACE	0x00000008
+
+/*
+ * Structure to define DDL options for parse_ddl_options().
+ * This allows easy addition of new options in the future.
+ */
+typedef struct DDLOptionDef
+{
+	const char *name;			/* Option name (case-insensitive) */
+	uint32		flag;			/* Flag to set */
+	bool		set_on_true;	/* If true, set flag when value is true; if
+								 * false, set flag when value is false */
+}			DDLOptionDef;
+
+/*
+ * Array of supported DDL options.
+ * To add a new option, simply add an entry to this array.
+ */
+static const DDLOptionDef ddl_option_defs[] = {
+	{"pretty", PG_DDL_PRETTY_INDENT, true},
+	{"defaults", PG_DDL_WITH_DEFAULTS, true},
+	{"owner", PG_DDL_NO_OWNER, false},
+	{"tablespace", PG_DDL_NO_TABLESPACE, false},
+};
+
+
 /* Standard conversion of a "bool pretty" option to detailed flags */
 #define GET_PRETTY_FLAGS(pretty) \
 	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
 	 : PRETTYFLAG_INDENT)
 
+#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 +584,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 nSpaces,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char *pg_get_database_ddl_worker(Oid db_oid, uint32 ddl_flags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13760,3 +13802,374 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - Based on prettyFlags the output includes spaces and
+ *               newlines (\n).
+ * nSpaces - indent with specified number of space characters.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, int prettyFlags, int nSpaces, const char *fmt,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with spaces */
+		appendStringInfoSpaces(buf, nSpaces);
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	for (;;)
+	{
+		int			needed;
+
+		va_start(args, fmt);
+		needed = appendStringInfoVA(buf, fmt, args);
+		va_end(args);
+		if (needed == 0)
+			break;
+		enlargeStringInfo(buf, needed);
+	}
+}
+
+/*
+ * parse_ddl_options - Generic helper to parse variadic name/value options
+ * fcinfo: The FunctionCallInfo from the calling function
+ * variadic_start: The argument position where variadic arguments start
+ *
+ * Returns: Bitmask of flags based on the parsed options.
+ *
+ * Options are passed as name/value pairs.
+ * For example: pg_get_database_ddl('mydb', 'owner', false, 'pretty', true)
+ */
+static uint32
+parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
+{
+	uint32		flags = 0;
+	uint32		seen_flags = 0;
+	Datum	   *args;
+	bool	   *nulls;
+	Oid		   *types;
+	int			nargs;
+
+	/* Extract variadic arguments */
+	nargs = extract_variadic_args(fcinfo, variadic_start, true,
+								  &args, &types, &nulls);
+
+	/* If no options provided (VARIADIC NULL), return the empty bitmask */
+	if (nargs <= 0)
+		return flags;
+
+	/*
+	 * Handle the case where DEFAULT NULL was used and no explicit variadic
+	 * arguments were provided. In this case, we get a single NULL argument.
+	 */
+	if (nargs == 1 && nulls[0])
+		return flags;
+
+	/* Arguments must come in name/value pairs */
+	if (nargs % 2 != 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("variadic arguments must be name/value pairs"),
+				 errhint("Provide an even number of variadic arguments that can be divided into pairs.")));
+
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *name;
+		bool		bval;
+		bool		found = false;
+
+		/* Key must not be null */
+		if (nulls[i])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("name at variadic position %d is null", i + 1)));
+
+		/* Key must be text type */
+		if (types[i] != TEXTOID)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("name at variadic position %d has type %s, expected type %s",
+							i + 1, format_type_be(types[i]),
+							format_type_be(TEXTOID))));
+
+		name = TextDatumGetCString(args[i]);
+
+		/* Value must not be null */
+		if (nulls[i + 1])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("value for option \"%s\" must not be null",
+							name)));
+
+		/* Value must be boolean or text type */
+		if (types[i + 1] == BOOLOID)
+		{
+			bval = DatumGetBool(args[i + 1]);
+		}
+		else if (types[i + 1] == TEXTOID)
+		{
+			char	   *valstr = TextDatumGetCString(args[i + 1]);
+
+			if (!parse_bool(valstr, &bval))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("value for option \"%s\" at position %d has invalid value \"%s\"",
+								name, i + 2, valstr),
+						 errhint("Valid values are: true, false, yes, no, 1, 0, on, off.")));
+			pfree(valstr);
+		}
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("value for option \"%s\" at position %d has type %s, expected type boolean or text",
+							name, i + 2, format_type_be(types[i + 1]))));
+		}
+
+		/*
+		 * Look up the option in the ddl_option_defs array and set the
+		 * appropriate flag based on the value.
+		 */
+		for (int j = 0; j < lengthof(ddl_option_defs); j++)
+		{
+			const		DDLOptionDef *opt = &ddl_option_defs[j];
+
+			if (pg_strcasecmp(name, opt->name) == 0)
+			{
+				/* Error if this option was already specified */
+				if (seen_flags & opt->flag)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("option \"%s\" is specified more than once", name)));
+
+				seen_flags |= opt->flag;
+
+				/*
+				 * Set the flag if the value matches the set_on_true
+				 * condition: if set_on_true is true, set flag when bval is
+				 * true; if set_on_true is false, set flag when bval is false.
+				 */
+				if (bval == opt->set_on_true)
+					flags |= opt->flag;
+
+				found = true;
+				break;
+			}
+		}
+
+		if (!found)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unrecognized option: \"%s\"", name)));
+
+		pfree(name);
+	}
+
+	return flags;
+}
+
+/*
+ * pg_get_database_ddl
+ *
+ * Generate a CREATE DATABASE statement for the specified database oid.
+ *
+ * db_oid - OID of the database for which to generate the DDL.
+ * options - Variadic name/value pairs to modify the output.
+ */
+Datum
+pg_get_database_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			db_oid;
+	uint32		ddl_flags;
+	char	   *res;
+
+	if (PG_ARGISNULL(0))
+		PG_RETURN_NULL();
+
+	db_oid = PG_GETARG_OID(0);
+
+	/* Parse variadic options starting from argument 1 */
+	ddl_flags = parse_ddl_options(fcinfo, 1);
+
+	res = pg_get_database_ddl_worker(db_oid, ddl_flags);
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_database_ddl_worker(Oid db_oid, uint32 ddl_flags)
+{
+	const char *encoding;
+	bool		attr_isnull;
+	Datum		dbvalue;
+	HeapTuple	tuple_database;
+	Form_pg_database dbform;
+	StringInfoData buf;
+	AclResult	aclresult;
+	HeapTuple	tmpl_tuple;
+	int			tmpl_encoding = -1;
+
+	/* Variables for ddl_options parsing */
+	int			pretty_flags = 0;
+	bool		is_with_defaults = false;
+
+	/* Set the appropriate flags */
+	if (ddl_flags & PG_DDL_PRETTY_INDENT)
+		pretty_flags = GET_DDL_PRETTY_FLAGS(1);
+
+	is_with_defaults = (ddl_flags & PG_DDL_WITH_DEFAULTS) != 0;
+
+	/*
+	 * User must have connect privilege for target database.
+	 */
+	aclresult = object_aclcheck(DatabaseRelationId, db_oid, GetUserId(),
+								ACL_CONNECT);
+	if (aclresult != ACLCHECK_OK &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclcheck_error(aclresult, OBJECT_DATABASE,
+					   get_database_name(db_oid));
+	}
+
+	/* Look up the database in pg_database */
+	tuple_database = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(db_oid));
+	if (!HeapTupleIsValid(tuple_database))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("database with oid %u does not exist", db_oid));
+
+	dbform = (Form_pg_database) GETSTRUCT(tuple_database);
+
+	initStringInfo(&buf);
+
+	/* Build the CREATE DATABASE statement */
+	appendStringInfo(&buf, "CREATE DATABASE %s",
+					 quote_identifier(dbform->datname.data));
+	get_formatted_string(&buf, pretty_flags, 4, "WITH");
+
+	/* Set the OWNER in the DDL if owner is not omitted */
+	if (OidIsValid(dbform->datdba) && !(ddl_flags & PG_DDL_NO_OWNER))
+	{
+		char	   *dbowner = GetUserNameFromId(dbform->datdba, false);
+
+		get_formatted_string(&buf, pretty_flags, 8, "OWNER = %s",
+							 quote_identifier(dbowner));
+	}
+
+	/*
+	 * Emit ENCODING if it differs from template1's encoding, or if defaults
+	 * are requested.  The default encoding for CREATE DATABASE comes from the
+	 * template database (template1), not a fixed value.
+	 */
+	encoding = pg_encoding_to_char(dbform->encoding);
+
+
+	tmpl_tuple = SearchSysCache1(DATABASEOID,
+								 ObjectIdGetDatum(Template1DbOid));
+	if (HeapTupleIsValid(tmpl_tuple))
+	{
+		Form_pg_database tmplform = (Form_pg_database) GETSTRUCT(tmpl_tuple);
+
+		tmpl_encoding = tmplform->encoding;
+		ReleaseSysCache(tmpl_tuple);
+	}
+
+	if (is_with_defaults || dbform->encoding != tmpl_encoding)
+		get_formatted_string(&buf, pretty_flags, 8, "ENCODING = %s",
+							 quote_literal_cstr(encoding));
+
+
+	/* Fetch the value of LC_COLLATE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datcollate, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LC_COLLATE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	/* Fetch the value of LC_CTYPE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datctype, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LC_CTYPE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	/* Fetch the value of LOCALE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datlocale, &attr_isnull);
+	if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, pretty_flags, 8, "BUILTIN_LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	else if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "ICU_LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	else if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Fetch the value of ICU_RULES */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_daticurules, &attr_isnull);
+	if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "ICU_RULES = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Fetch the value of COLLATION_VERSION */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datcollversion, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "COLLATION_VERSION = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Set the appropriate LOCALE_PROVIDER */
+	if (dbform->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = builtin");
+	else if (dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = icu");
+	else if (is_with_defaults)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = libc");
+
+	/* Set the TABLESPACE in the DDL if tablespace is not omitted */
+	if (OidIsValid(dbform->dattablespace) && !(ddl_flags & PG_DDL_NO_TABLESPACE))
+	{
+		/* Get the tablespace name respective to the given tablespace oid */
+		char	   *dbTablespace = get_tablespace_name(dbform->dattablespace);
+
+		/* If it's with defaults, we skip default tablespace check */
+		if (is_with_defaults ||
+			(pg_strcasecmp(dbTablespace, DDL_DEFAULTS.DATABASE.TABLESPACE) != 0))
+			get_formatted_string(&buf, pretty_flags, 8, "TABLESPACE = %s",
+								 quote_identifier(dbTablespace));
+	}
+
+	if (is_with_defaults ||
+		(dbform->datallowconn != DDL_DEFAULTS.DATABASE.ALLOW_CONN))
+	{
+		get_formatted_string(&buf, pretty_flags, 8, "ALLOW_CONNECTIONS = %s",
+							 dbform->datallowconn ? "true" : "false");
+	}
+
+	if (is_with_defaults ||
+		(dbform->datconnlimit != DDL_DEFAULTS.DATABASE.CONN_LIMIT))
+	{
+		get_formatted_string(&buf, pretty_flags, 8, "CONNECTION LIMIT = %d",
+							 dbform->datconnlimit);
+	}
+
+	if (is_with_defaults || dbform->datistemplate)
+		get_formatted_string(&buf, pretty_flags, 8, "IS_TEMPLATE = %s",
+							 dbform->datistemplate ? "true" : "false");
+
+	appendStringInfoChar(&buf, ';');
+
+	ReleaseSysCache(tuple_database);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 361e2cfffeb..1e2f7d3ac35 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4034,6 +4034,13 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '9492', descr => 'get CREATE statement for database name and oid',
+  proname => 'pg_get_database_ddl', provariadic => 'any', proisstrict => 'f',
+  provolatile => 's', prorettype => 'text',
+  proargtypes => 'regdatabase any',
+  proargmodes => '{i,v}',
+  proallargtypes => '{regdatabase,any}',
+  prosrc => 'pg_get_database_ddl' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/include/utils/ddl_defaults.h b/src/include/utils/ddl_defaults.h
new file mode 100644
index 00000000000..eb9c7750651
--- /dev/null
+++ b/src/include/utils/ddl_defaults.h
@@ -0,0 +1,35 @@
+/*-------------------------------------------------------------------------
+ *
+ * ddl_defaults.h
+ *	  Declarations for DDL defaults.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/ddl_defaults.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DDL_DEFAULTS_H
+#define DDL_DEFAULTS_H
+
+static const struct
+{
+	struct
+	{
+		const char *TABLESPACE;
+		int			CONN_LIMIT;
+		bool		ALLOW_CONN;
+	}			DATABASE;
+
+	/* Add more object types as needed */
+}			DDL_DEFAULTS = {
+
+	.DATABASE = {
+		.TABLESPACE = "pg_default",
+		.CONN_LIMIT = -1,
+		.ALLOW_CONN = true,
+	}
+};
+
+#endif							/* DDL_DEFAULTS_H */
diff --git a/src/test/regress/expected/database.out b/src/test/regress/expected/database.out
index 6b879b0f62a..76eca22cf3a 100644
--- a/src/test/regress/expected/database.out
+++ b/src/test/regress/expected/database.out
@@ -1,3 +1,65 @@
+--
+-- Reconstruct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+    -- Remove %LOCALE_PROVIDER% placeholders
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_COLLATE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove ENCODING assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*ENCODING\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -16,6 +78,130 @@ CREATE ROLE regress_datdba_before;
 CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database');
+ERROR:  database "regression_database" does not exist
+LINE 1: SELECT pg_get_database_ddl('regression_database');
+                                   ^
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+ pg_get_database_ddl 
+---------------------
+ 
+(1 row)
+
+-- Without options
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+                                        ddl_filter                                         
+-------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after CONNECTION LIMIT = 123;
+(1 row)
+
+-- With No Owner
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false));
+                          ddl_filter                          
+--------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH CONNECTION LIMIT = 123;
+(1 row)
+
+-- With No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true, 'tablespace', false));
+                                                               ddl_filter                                                               
+----------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true));
+                                                                           ddl_filter                                                                           
+----------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'tablespace', false, 'defaults', true));
+                                                ddl_filter                                                 
+-----------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        OWNER = regress_datdba_after
+        CONNECTION LIMIT = 123;
+(1 row)
+-- With No Owner and No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        CONNECTION LIMIT = 123;
+(1 row)
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'defaults', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        OWNER = regress_datdba_after
+        TABLESPACE = pg_default
+        ALLOW_CONNECTIONS = true
+        CONNECTION LIMIT = 123
+        IS_TEMPLATE = false;
+(1 row)
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false, 'defaults', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        ALLOW_CONNECTIONS = true
+        CONNECTION LIMIT = 123
+        IS_TEMPLATE = false;
+(1 row)
+-- Test with text values: 'yes', 'no', '1', '0', 'on', 'off'
+\pset format aligned
+-- Using 'yes' and 'no'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes'));
+                                                            ddl_filter                                                             
+-----------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Using '1' and '0'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1'));
+                                                ddl_filter                                                 
+-----------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Using 'on' and 'off'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on'));
+           ddl_filter            
+---------------------------------
+ CREATE DATABASE regression_utf8+
+     WITH                       +
+         CONNECTION LIMIT = 123;
+(1 row)
+
+-- Mixed boolean and text values
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no'));
+                                                ddl_filter                                                 
+-----------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Test duplicate option (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'owner', true);
+ERROR:  option "owner" is specified more than once
+-- Test invalid text value (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', 'invalid');
+ERROR:  value for option "owner" at position 2 has invalid value "invalid"
+HINT:  Valid values are: true, false, yes, no, 1, 0, on, off.
 DROP DATABASE regression_utf8;
+DROP FUNCTION ddl_filter(text);
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
diff --git a/src/test/regress/sql/database.sql b/src/test/regress/sql/database.sql
index 4ef36127291..9ef926fac9c 100644
--- a/src/test/regress/sql/database.sql
+++ b/src/test/regress/sql/database.sql
@@ -1,3 +1,67 @@
+--
+-- Reconstruct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+    -- Remove %LOCALE_PROVIDER% placeholders
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_COLLATE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove ENCODING assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*ENCODING\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
+
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -19,6 +83,62 @@ CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
 
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database');
+
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+
+-- Without options
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+
+-- With No Owner
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false));
+
+-- With No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true, 'tablespace', false));
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true));
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'tablespace', false, 'defaults', true));
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true));
+
+-- With No Owner and No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false));
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'defaults', true));
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false, 'defaults', true));
+
+-- Test with text values: 'yes', 'no', '1', '0', 'on', 'off'
+\pset format aligned
+-- Using 'yes' and 'no'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes'));
+
+-- Using '1' and '0'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1'));
+
+-- Using 'on' and 'off'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on'));
+
+-- Mixed boolean and text values
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no'));
+
+-- Test duplicate option (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'owner', true);
+
+-- Test invalid text value (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', 'invalid');
+
 DROP DATABASE regression_utf8;
+DROP FUNCTION ddl_filter(text);
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
-- 
2.51.0



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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-10 10:16  Kirill Reshke <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Kirill Reshke @ 2026-03-10 10:16 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Japin Li <[email protected]>; Rafia Sabih <[email protected]>; Álvaro Herrera <[email protected]>; Euler Taveira <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

On Mon, 9 Mar 2026 at 17:08, Akshay Joshi <[email protected]> wrote:
>
> I have resolved the review comments. Removed the hardcoded 'utf-8' encoding, the logic now retrieves the encoding dynamically. This ensures accuracy because the default encoding for CREATE DATABASE is derived from the template database (template1) rather than being a static value.
>
> Attached is the v13 patch, now ready for further review.
>
> On Fri, Mar 6, 2026 at 8:13 PM Japin Li <[email protected]> wrote:
>>
>>
>> Hi, Akshay
>>
>> On Fri, 06 Mar 2026 at 17:21, Akshay Joshi <[email protected]> wrote:
>> > I have updated the documentation.
>> > Attached is the v12 patch, now ready for further review.
>> >
>>
>> Thanks for updating the patch.
>>
>> I might not have expressed myself clearly in my last email — apologies.
>> Here's a diff that should make it clearer.
>>
>> diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
>> index bbf82c1d6c0..1ed56ee71ab 100644
>> --- a/src/backend/utils/adt/ruleutils.c
>> +++ b/src/backend/utils/adt/ruleutils.c
>> @@ -13859,7 +13859,6 @@ parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
>>         bool       *nulls;
>>         Oid                *types;
>>         int                     nargs;
>> -       bool            found = false;
>>
>>         /* Extract variadic arguments */
>>         nargs = extract_variadic_args(fcinfo, variadic_start, true,
>> @@ -13887,8 +13886,7 @@ parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
>>         {
>>                 char       *name;
>>                 bool            bval;
>> -
>> -               found = false;
>> +               bool            found = false;
>>
>>                 /* Key must not be null */
>>                 if (nulls[i])
>> @@ -13925,8 +13923,8 @@ parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
>>                         if (!parse_bool(valstr, &bval))
>>                                 ereport(ERROR,
>>                                                 (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
>> -                                                errmsg("argument %d: invalid value \"%s\" for key \"%s\"",
>> -                                                               i + 2, valstr, name),
>> +                                                errmsg("value for option \"%s\" at position %d has invalid value \"%s\"",
>> +                                                               name, i + 2, valstr),
>>                                                  errhint("Valid values are: true, false, yes, no, 1, 0, on, off.")));
>>                         pfree(valstr);
>>                 }
>> @@ -13934,8 +13932,8 @@ parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
>>                 {
>>                         ereport(ERROR,
>>                                         (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
>> -                                        errmsg("argument %d: value for key \"%s\" must be boolean or text type",
>> -                                                       i + 2, name)));
>> +                                        errmsg("value for option \"%s\" at position %d has type %s, expected type boolean or text",
>> +                                                       name, i + 2, format_type_be(types[i + 1]))));
>>                 }
>>
>>                 /*
>> @@ -13983,7 +13981,7 @@ parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
>>  /*
>>   * pg_get_database_ddl
>>   *
>> - * Generate a CREATE DATABASE statement for the specified database name or oid.
>> + * Generate a CREATE DATABASE statement for the specified database oid.
>>   *
>>   * db_oid - OID of the database for which to generate the DDL.
>>   * options - Variadic name/value pairs to modify the output.
>>
>> --
>> Regards,
>> Japin Li
>> ChengDu WenWu Information Technology Co., Ltd.


Hi!
I noticed this in v13: DDLOptionDef is missing from typedefs.list.
This results in pgident to incorrectly format sources.



-- 
Best regards,
Kirill Reshke





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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-10 13:44  Akshay Joshi <[email protected]>
  parent: Kirill Reshke <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Akshay Joshi @ 2026-03-10 13:44 UTC (permalink / raw)
  To: Kirill Reshke <[email protected]>; +Cc: Japin Li <[email protected]>; Rafia Sabih <[email protected]>; Álvaro Herrera <[email protected]>; Euler Taveira <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

Thanks for the review. I wasn't aware of this change, as I'm still new to
PostgreSQL development.
Attached is the *v14 patch*, now ready for further review.

On Tue, Mar 10, 2026 at 3:46 PM Kirill Reshke <[email protected]>
wrote:

> On Mon, 9 Mar 2026 at 17:08, Akshay Joshi <[email protected]>
> wrote:
> >
> > I have resolved the review comments. Removed the hardcoded 'utf-8'
> encoding, the logic now retrieves the encoding dynamically. This ensures
> accuracy because the default encoding for CREATE DATABASE is derived from
> the template database (template1) rather than being a static value.
> >
> > Attached is the v13 patch, now ready for further review.
> >
> > On Fri, Mar 6, 2026 at 8:13 PM Japin Li <[email protected]> wrote:
> >>
> >>
> >> Hi, Akshay
> >>
> >> On Fri, 06 Mar 2026 at 17:21, Akshay Joshi <
> [email protected]> wrote:
> >> > I have updated the documentation.
> >> > Attached is the v12 patch, now ready for further review.
> >> >
> >>
> >> Thanks for updating the patch.
> >>
> >> I might not have expressed myself clearly in my last email — apologies.
> >> Here's a diff that should make it clearer.
> >>
> >> diff --git a/src/backend/utils/adt/ruleutils.c
> b/src/backend/utils/adt/ruleutils.c
> >> index bbf82c1d6c0..1ed56ee71ab 100644
> >> --- a/src/backend/utils/adt/ruleutils.c
> >> +++ b/src/backend/utils/adt/ruleutils.c
> >> @@ -13859,7 +13859,6 @@ parse_ddl_options(FunctionCallInfo fcinfo, int
> variadic_start)
> >>         bool       *nulls;
> >>         Oid                *types;
> >>         int                     nargs;
> >> -       bool            found = false;
> >>
> >>         /* Extract variadic arguments */
> >>         nargs = extract_variadic_args(fcinfo, variadic_start, true,
> >> @@ -13887,8 +13886,7 @@ parse_ddl_options(FunctionCallInfo fcinfo, int
> variadic_start)
> >>         {
> >>                 char       *name;
> >>                 bool            bval;
> >> -
> >> -               found = false;
> >> +               bool            found = false;
> >>
> >>                 /* Key must not be null */
> >>                 if (nulls[i])
> >> @@ -13925,8 +13923,8 @@ parse_ddl_options(FunctionCallInfo fcinfo, int
> variadic_start)
> >>                         if (!parse_bool(valstr, &bval))
> >>                                 ereport(ERROR,
> >>
>  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> >> -                                                errmsg("argument %d:
> invalid value \"%s\" for key \"%s\"",
> >> -                                                               i + 2,
> valstr, name),
> >> +                                                errmsg("value for
> option \"%s\" at position %d has invalid value \"%s\"",
> >> +                                                               name, i
> + 2, valstr),
> >>                                                  errhint("Valid values
> are: true, false, yes, no, 1, 0, on, off.")));
> >>                         pfree(valstr);
> >>                 }
> >> @@ -13934,8 +13932,8 @@ parse_ddl_options(FunctionCallInfo fcinfo, int
> variadic_start)
> >>                 {
> >>                         ereport(ERROR,
> >>
>  (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> >> -                                        errmsg("argument %d: value for
> key \"%s\" must be boolean or text type",
> >> -                                                       i + 2, name)));
> >> +                                        errmsg("value for option
> \"%s\" at position %d has type %s, expected type boolean or text",
> >> +                                                       name, i + 2,
> format_type_be(types[i + 1]))));
> >>                 }
> >>
> >>                 /*
> >> @@ -13983,7 +13981,7 @@ parse_ddl_options(FunctionCallInfo fcinfo, int
> variadic_start)
> >>  /*
> >>   * pg_get_database_ddl
> >>   *
> >> - * Generate a CREATE DATABASE statement for the specified database
> name or oid.
> >> + * Generate a CREATE DATABASE statement for the specified database oid.
> >>   *
> >>   * db_oid - OID of the database for which to generate the DDL.
> >>   * options - Variadic name/value pairs to modify the output.
> >>
> >> --
> >> Regards,
> >> Japin Li
> >> ChengDu WenWu Information Technology Co., Ltd.
>
>
> Hi!
> I noticed this in v13: DDLOptionDef is missing from typedefs.list.
> This results in pgident to incorrectly format sources.
>
>
>
> --
> Best regards,
> Kirill Reshke
>
>
>


Attachments:

  [application/octet-stream] v14-0001-Add-pg_get_database_ddl-function-to-reconstruct-ddl.patch (34.7K, 3-v14-0001-Add-pg_get_database_ddl-function-to-reconstruct-ddl.patch)
  download | inline diff:
From a8e1ac58c7c6dff0fae68d769ef722ef2203d490 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 6 Mar 2026 16:46:02 +0530
Subject: [PATCH v14] Add pg_get_database_ddl() function to reconstruct CREATE
 DATABASE statements.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This adds a new system function, pg_get_database_ddl(database_name/database_oid, ddl_options),
which reconstructs the CREATE DATABASE statement for a given database name or OID.

Supported ddl_options are 'pretty', 'owner', 'tablespace' and 'defaults' and respective
values could be 'yes'/'on'/true/'1' or 'no'/'off'/false/'0'.

Usage:
 SELECT pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes');
 SELECT pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1');
 SELECT pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on');
 SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no');

Reference: PG-150
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
Reviewed-by: Euler Taveira <[email protected]>
Reviewed-by: Quan Zongliang <[email protected]>
Reviewed-by: Japin Li <[email protected]>
Reviewed-by: Chao Li <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  91 +++++
 src/backend/catalog/system_functions.sql |   6 +
 src/backend/utils/adt/ruleutils.c        | 413 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 +
 src/include/utils/ddl_defaults.h         |  35 ++
 src/test/regress/expected/database.out   | 186 ++++++++++
 src/test/regress/sql/database.sql        | 120 +++++++
 src/tools/pgindent/typedefs.list         |   2 +
 8 files changed, 860 insertions(+)
 create mode 100644 src/include/utils/ddl_defaults.h

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 294f45e82a3..6915408ae30 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3845,4 +3845,95 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions, one for each object type.
+   </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_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>database_id</parameter> <type>regdatabase</type>
+        <optional>, <literal>VARIADIC</literal> <parameter>options</parameter>
+        <type>"any"</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE DATABASE</command> statement for the
+        specified database (identified by name or OID) from the system
+        catalogs. The optional variadic arguments are name/value pairs that
+        control the output
+        formatting and content (e.g., <literal>'pretty', true, 'owner', false</literal>).
+        Supported options are explained below.
+        </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+    The <parameter>options</parameter> for <function>pg_get_database_ddl</function>
+    provide fine-grained control over the generated SQL. Options are passed as
+    alternating key/value pairs where the key is a text string and the
+    value is either a boolean or a text string representing a boolean
+    (<literal>true</literal>, <literal>false</literal>, <literal>yes</literal>,
+    <literal>no</literal>, <literal>1</literal>, <literal>0</literal>,
+    <literal>on</literal>, <literal>off</literal>):
+    <itemizedlist>
+    <listitem>
+      <para>
+      <literal>'pretty', true</literal> (or <literal>'pretty', 'yes'</literal>):
+      Formats the output with newlines and indentation for better readability.
+      This option defaults to false.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'owner', false</literal> (or <literal>'owner', 'no'</literal>):
+      Omits the <literal>OWNER</literal> clause from the reconstructed statement.
+      This option defaults to true.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'tablespace', false</literal> (or <literal>'tablespace', '0'</literal>):
+      Omits the <literal>TABLESPACE</literal> clause from the reconstructed statement.
+      This option defaults to true.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'defaults', true</literal> (or <literal>'defaults', '1'</literal>):
+      Includes clauses for parameters that are currently at their default values
+      (e.g., <literal>CONNECTION LIMIT -1</literal>), which are normally omitted for brevity.
+      This option defaults to false.
+      </para>
+    </listitem>
+    </itemizedlist>
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 1c5b6d6df05..fa48e2f0775 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -372,3 +372,9 @@ CREATE OR REPLACE FUNCTION ts_debug(document text,
 BEGIN ATOMIC
     SELECT * FROM ts_debug(get_current_ts_config(), $1);
 END;
+
+CREATE OR REPLACE FUNCTION
+  pg_get_database_ddl(database_id regdatabase, VARIADIC options "any" DEFAULT NULL)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl';
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index f16f1535785..998b31a5733 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -28,6 +28,7 @@
 #include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_constraint.h"
+#include "catalog/pg_database.h"
 #include "catalog/pg_depend.h"
 #include "catalog/pg_language.h"
 #include "catalog/pg_opclass.h"
@@ -57,8 +58,10 @@
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
 #include "rewrite/rewriteSupport.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/ddl_defaults.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
 #include "utils/hsearch.h"
@@ -89,11 +92,45 @@
 #define PRETTYFLAG_INDENT		0x0002
 #define PRETTYFLAG_SCHEMA		0x0004
 
+/* DDL Options flags */
+#define PG_DDL_PRETTY_INDENT	0x00000001
+#define PG_DDL_WITH_DEFAULTS	0x00000002
+#define PG_DDL_NO_OWNER			0x00000004
+#define PG_DDL_NO_TABLESPACE	0x00000008
+
+/*
+ * Structure to define DDL options for parse_ddl_options().
+ * This allows easy addition of new options in the future.
+ */
+typedef struct DDLOptionDef
+{
+	const char *name;			/* Option name (case-insensitive) */
+	uint32		flag;			/* Flag to set */
+	bool		set_on_true;	/* If true, set flag when value is true; if
+								 * false, set flag when value is false */
+} DDLOptionDef;
+
+/*
+ * Array of supported DDL options.
+ * To add a new option, simply add an entry to this array.
+ */
+static const DDLOptionDef ddl_option_defs[] = {
+	{"pretty", PG_DDL_PRETTY_INDENT, true},
+	{"defaults", PG_DDL_WITH_DEFAULTS, true},
+	{"owner", PG_DDL_NO_OWNER, false},
+	{"tablespace", PG_DDL_NO_TABLESPACE, false},
+};
+
+
 /* Standard conversion of a "bool pretty" option to detailed flags */
 #define GET_PRETTY_FLAGS(pretty) \
 	((pretty) ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) \
 	 : PRETTYFLAG_INDENT)
 
+#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 +584,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 nSpaces,
+								 const char *fmt,...) pg_attribute_printf(4, 5);
+static char *pg_get_database_ddl_worker(Oid db_oid, uint32 ddl_flags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13760,3 +13802,374 @@ get_range_partbound_string(List *bound_datums)
 
 	return buf.data;
 }
+
+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.
+ *
+ * prettyFlags - Based on prettyFlags the output includes spaces and
+ *               newlines (\n).
+ * nSpaces - indent with specified number of space characters.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, int prettyFlags, int nSpaces, const char *fmt,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with spaces */
+		appendStringInfoSpaces(buf, nSpaces);
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	for (;;)
+	{
+		int			needed;
+
+		va_start(args, fmt);
+		needed = appendStringInfoVA(buf, fmt, args);
+		va_end(args);
+		if (needed == 0)
+			break;
+		enlargeStringInfo(buf, needed);
+	}
+}
+
+/*
+ * parse_ddl_options - Generic helper to parse variadic name/value options
+ * fcinfo: The FunctionCallInfo from the calling function
+ * variadic_start: The argument position where variadic arguments start
+ *
+ * Returns: Bitmask of flags based on the parsed options.
+ *
+ * Options are passed as name/value pairs.
+ * For example: pg_get_database_ddl('mydb', 'owner', false, 'pretty', true)
+ */
+static uint32
+parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
+{
+	uint32		flags = 0;
+	uint32		seen_flags = 0;
+	Datum	   *args;
+	bool	   *nulls;
+	Oid		   *types;
+	int			nargs;
+
+	/* Extract variadic arguments */
+	nargs = extract_variadic_args(fcinfo, variadic_start, true,
+								  &args, &types, &nulls);
+
+	/* If no options provided (VARIADIC NULL), return the empty bitmask */
+	if (nargs <= 0)
+		return flags;
+
+	/*
+	 * Handle the case where DEFAULT NULL was used and no explicit variadic
+	 * arguments were provided. In this case, we get a single NULL argument.
+	 */
+	if (nargs == 1 && nulls[0])
+		return flags;
+
+	/* Arguments must come in name/value pairs */
+	if (nargs % 2 != 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("variadic arguments must be name/value pairs"),
+				 errhint("Provide an even number of variadic arguments that can be divided into pairs.")));
+
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *name;
+		bool		bval;
+		bool		found = false;
+
+		/* Key must not be null */
+		if (nulls[i])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("name at variadic position %d is null", i + 1)));
+
+		/* Key must be text type */
+		if (types[i] != TEXTOID)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("name at variadic position %d has type %s, expected type %s",
+							i + 1, format_type_be(types[i]),
+							format_type_be(TEXTOID))));
+
+		name = TextDatumGetCString(args[i]);
+
+		/* Value must not be null */
+		if (nulls[i + 1])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("value for option \"%s\" must not be null",
+							name)));
+
+		/* Value must be boolean or text type */
+		if (types[i + 1] == BOOLOID)
+		{
+			bval = DatumGetBool(args[i + 1]);
+		}
+		else if (types[i + 1] == TEXTOID)
+		{
+			char	   *valstr = TextDatumGetCString(args[i + 1]);
+
+			if (!parse_bool(valstr, &bval))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("value for option \"%s\" at position %d has invalid value \"%s\"",
+								name, i + 2, valstr),
+						 errhint("Valid values are: true, false, yes, no, 1, 0, on, off.")));
+			pfree(valstr);
+		}
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("value for option \"%s\" at position %d has type %s, expected type boolean or text",
+							name, i + 2, format_type_be(types[i + 1]))));
+		}
+
+		/*
+		 * Look up the option in the ddl_option_defs array and set the
+		 * appropriate flag based on the value.
+		 */
+		for (int j = 0; j < lengthof(ddl_option_defs); j++)
+		{
+			const DDLOptionDef *opt = &ddl_option_defs[j];
+
+			if (pg_strcasecmp(name, opt->name) == 0)
+			{
+				/* Error if this option was already specified */
+				if (seen_flags & opt->flag)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("option \"%s\" is specified more than once", name)));
+
+				seen_flags |= opt->flag;
+
+				/*
+				 * Set the flag if the value matches the set_on_true
+				 * condition: if set_on_true is true, set flag when bval is
+				 * true; if set_on_true is false, set flag when bval is false.
+				 */
+				if (bval == opt->set_on_true)
+					flags |= opt->flag;
+
+				found = true;
+				break;
+			}
+		}
+
+		if (!found)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unrecognized option: \"%s\"", name)));
+
+		pfree(name);
+	}
+
+	return flags;
+}
+
+/*
+ * pg_get_database_ddl
+ *
+ * Generate a CREATE DATABASE statement for the specified database oid.
+ *
+ * db_oid - OID of the database for which to generate the DDL.
+ * options - Variadic name/value pairs to modify the output.
+ */
+Datum
+pg_get_database_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			db_oid;
+	uint32		ddl_flags;
+	char	   *res;
+
+	if (PG_ARGISNULL(0))
+		PG_RETURN_NULL();
+
+	db_oid = PG_GETARG_OID(0);
+
+	/* Parse variadic options starting from argument 1 */
+	ddl_flags = parse_ddl_options(fcinfo, 1);
+
+	res = pg_get_database_ddl_worker(db_oid, ddl_flags);
+
+	PG_RETURN_TEXT_P(string_to_text(res));
+}
+
+static char *
+pg_get_database_ddl_worker(Oid db_oid, uint32 ddl_flags)
+{
+	const char *encoding;
+	bool		attr_isnull;
+	Datum		dbvalue;
+	HeapTuple	tuple_database;
+	Form_pg_database dbform;
+	StringInfoData buf;
+	AclResult	aclresult;
+	HeapTuple	tmpl_tuple;
+	int			tmpl_encoding = -1;
+
+	/* Variables for ddl_options parsing */
+	int			pretty_flags = 0;
+	bool		is_with_defaults = false;
+
+	/* Set the appropriate flags */
+	if (ddl_flags & PG_DDL_PRETTY_INDENT)
+		pretty_flags = GET_DDL_PRETTY_FLAGS(1);
+
+	is_with_defaults = (ddl_flags & PG_DDL_WITH_DEFAULTS) != 0;
+
+	/*
+	 * User must have connect privilege for target database.
+	 */
+	aclresult = object_aclcheck(DatabaseRelationId, db_oid, GetUserId(),
+								ACL_CONNECT);
+	if (aclresult != ACLCHECK_OK &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclcheck_error(aclresult, OBJECT_DATABASE,
+					   get_database_name(db_oid));
+	}
+
+	/* Look up the database in pg_database */
+	tuple_database = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(db_oid));
+	if (!HeapTupleIsValid(tuple_database))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("database with oid %u does not exist", db_oid));
+
+	dbform = (Form_pg_database) GETSTRUCT(tuple_database);
+
+	initStringInfo(&buf);
+
+	/* Build the CREATE DATABASE statement */
+	appendStringInfo(&buf, "CREATE DATABASE %s",
+					 quote_identifier(dbform->datname.data));
+	get_formatted_string(&buf, pretty_flags, 4, "WITH");
+
+	/* Set the OWNER in the DDL if owner is not omitted */
+	if (OidIsValid(dbform->datdba) && !(ddl_flags & PG_DDL_NO_OWNER))
+	{
+		char	   *dbowner = GetUserNameFromId(dbform->datdba, false);
+
+		get_formatted_string(&buf, pretty_flags, 8, "OWNER = %s",
+							 quote_identifier(dbowner));
+	}
+
+	/*
+	 * Emit ENCODING if it differs from template1's encoding, or if defaults
+	 * are requested.  The default encoding for CREATE DATABASE comes from the
+	 * template database (template1), not a fixed value.
+	 */
+	encoding = pg_encoding_to_char(dbform->encoding);
+
+
+	tmpl_tuple = SearchSysCache1(DATABASEOID,
+								 ObjectIdGetDatum(Template1DbOid));
+	if (HeapTupleIsValid(tmpl_tuple))
+	{
+		Form_pg_database tmplform = (Form_pg_database) GETSTRUCT(tmpl_tuple);
+
+		tmpl_encoding = tmplform->encoding;
+		ReleaseSysCache(tmpl_tuple);
+	}
+
+	if (is_with_defaults || dbform->encoding != tmpl_encoding)
+		get_formatted_string(&buf, pretty_flags, 8, "ENCODING = %s",
+							 quote_literal_cstr(encoding));
+
+
+	/* Fetch the value of LC_COLLATE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datcollate, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LC_COLLATE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	/* Fetch the value of LC_CTYPE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datctype, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LC_CTYPE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	/* Fetch the value of LOCALE */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datlocale, &attr_isnull);
+	if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, pretty_flags, 8, "BUILTIN_LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	else if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "ICU_LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	else if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Fetch the value of ICU_RULES */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_daticurules, &attr_isnull);
+	if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "ICU_RULES = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Fetch the value of COLLATION_VERSION */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datcollversion, &attr_isnull);
+	if (!attr_isnull)
+		get_formatted_string(&buf, pretty_flags, 8, "COLLATION_VERSION = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/* Set the appropriate LOCALE_PROVIDER */
+	if (dbform->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = builtin");
+	else if (dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = icu");
+	else if (is_with_defaults)
+		get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = libc");
+
+	/* Set the TABLESPACE in the DDL if tablespace is not omitted */
+	if (OidIsValid(dbform->dattablespace) && !(ddl_flags & PG_DDL_NO_TABLESPACE))
+	{
+		/* Get the tablespace name respective to the given tablespace oid */
+		char	   *dbTablespace = get_tablespace_name(dbform->dattablespace);
+
+		/* If it's with defaults, we skip default tablespace check */
+		if (is_with_defaults ||
+			(pg_strcasecmp(dbTablespace, DDL_DEFAULTS.DATABASE.TABLESPACE) != 0))
+			get_formatted_string(&buf, pretty_flags, 8, "TABLESPACE = %s",
+								 quote_identifier(dbTablespace));
+	}
+
+	if (is_with_defaults ||
+		(dbform->datallowconn != DDL_DEFAULTS.DATABASE.ALLOW_CONN))
+	{
+		get_formatted_string(&buf, pretty_flags, 8, "ALLOW_CONNECTIONS = %s",
+							 dbform->datallowconn ? "true" : "false");
+	}
+
+	if (is_with_defaults ||
+		(dbform->datconnlimit != DDL_DEFAULTS.DATABASE.CONN_LIMIT))
+	{
+		get_formatted_string(&buf, pretty_flags, 8, "CONNECTION LIMIT = %d",
+							 dbform->datconnlimit);
+	}
+
+	if (is_with_defaults || dbform->datistemplate)
+		get_formatted_string(&buf, pretty_flags, 8, "IS_TEMPLATE = %s",
+							 dbform->datistemplate ? "true" : "false");
+
+	appendStringInfoChar(&buf, ';');
+
+	ReleaseSysCache(tuple_database);
+
+	return buf.data;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 361e2cfffeb..1e2f7d3ac35 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4034,6 +4034,13 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '9492', descr => 'get CREATE statement for database name and oid',
+  proname => 'pg_get_database_ddl', provariadic => 'any', proisstrict => 'f',
+  provolatile => 's', prorettype => 'text',
+  proargtypes => 'regdatabase any',
+  proargmodes => '{i,v}',
+  proallargtypes => '{regdatabase,any}',
+  prosrc => 'pg_get_database_ddl' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/include/utils/ddl_defaults.h b/src/include/utils/ddl_defaults.h
new file mode 100644
index 00000000000..8cc3733c11f
--- /dev/null
+++ b/src/include/utils/ddl_defaults.h
@@ -0,0 +1,35 @@
+/*-------------------------------------------------------------------------
+ *
+ * ddl_defaults.h
+ *	  Declarations for DDL defaults.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/ddl_defaults.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DDL_DEFAULTS_H
+#define DDL_DEFAULTS_H
+
+static const struct
+{
+	struct
+	{
+		const char *TABLESPACE;
+		int			CONN_LIMIT;
+		bool		ALLOW_CONN;
+	}			DATABASE;
+
+	/* Add more object types as needed */
+} DDL_DEFAULTS = {
+
+	.DATABASE = {
+		.TABLESPACE = "pg_default",
+		.CONN_LIMIT = -1,
+		.ALLOW_CONN = true,
+	}
+};
+
+#endif							/* DDL_DEFAULTS_H */
diff --git a/src/test/regress/expected/database.out b/src/test/regress/expected/database.out
index 6b879b0f62a..76eca22cf3a 100644
--- a/src/test/regress/expected/database.out
+++ b/src/test/regress/expected/database.out
@@ -1,3 +1,65 @@
+--
+-- Reconstruct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+    -- Remove %LOCALE_PROVIDER% placeholders
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_COLLATE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove ENCODING assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*ENCODING\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -16,6 +78,130 @@ CREATE ROLE regress_datdba_before;
 CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database');
+ERROR:  database "regression_database" does not exist
+LINE 1: SELECT pg_get_database_ddl('regression_database');
+                                   ^
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+ pg_get_database_ddl 
+---------------------
+ 
+(1 row)
+
+-- Without options
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+                                        ddl_filter                                         
+-------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after CONNECTION LIMIT = 123;
+(1 row)
+
+-- With No Owner
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false));
+                          ddl_filter                          
+--------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH CONNECTION LIMIT = 123;
+(1 row)
+
+-- With No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true, 'tablespace', false));
+                                                               ddl_filter                                                               
+----------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true));
+                                                                           ddl_filter                                                                           
+----------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'tablespace', false, 'defaults', true));
+                                                ddl_filter                                                 
+-----------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        OWNER = regress_datdba_after
+        CONNECTION LIMIT = 123;
+(1 row)
+-- With No Owner and No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        CONNECTION LIMIT = 123;
+(1 row)
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'defaults', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        OWNER = regress_datdba_after
+        TABLESPACE = pg_default
+        ALLOW_CONNECTIONS = true
+        CONNECTION LIMIT = 123
+        IS_TEMPLATE = false;
+(1 row)
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false, 'defaults', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        ALLOW_CONNECTIONS = true
+        CONNECTION LIMIT = 123
+        IS_TEMPLATE = false;
+(1 row)
+-- Test with text values: 'yes', 'no', '1', '0', 'on', 'off'
+\pset format aligned
+-- Using 'yes' and 'no'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes'));
+                                                            ddl_filter                                                             
+-----------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Using '1' and '0'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1'));
+                                                ddl_filter                                                 
+-----------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Using 'on' and 'off'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on'));
+           ddl_filter            
+---------------------------------
+ CREATE DATABASE regression_utf8+
+     WITH                       +
+         CONNECTION LIMIT = 123;
+(1 row)
+
+-- Mixed boolean and text values
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no'));
+                                                ddl_filter                                                 
+-----------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Test duplicate option (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'owner', true);
+ERROR:  option "owner" is specified more than once
+-- Test invalid text value (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', 'invalid');
+ERROR:  value for option "owner" at position 2 has invalid value "invalid"
+HINT:  Valid values are: true, false, yes, no, 1, 0, on, off.
 DROP DATABASE regression_utf8;
+DROP FUNCTION ddl_filter(text);
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
diff --git a/src/test/regress/sql/database.sql b/src/test/regress/sql/database.sql
index 4ef36127291..9ef926fac9c 100644
--- a/src/test/regress/sql/database.sql
+++ b/src/test/regress/sql/database.sql
@@ -1,3 +1,67 @@
+--
+-- Reconstruct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+    -- Remove %LOCALE_PROVIDER% placeholders
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_COLLATE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove ENCODING assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*ENCODING\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
+
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -19,6 +83,62 @@ CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
 
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database');
+
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+
+-- Without options
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+
+-- With No Owner
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false));
+
+-- With No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true, 'tablespace', false));
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true));
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'tablespace', false, 'defaults', true));
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true));
+
+-- With No Owner and No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false));
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'defaults', true));
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false, 'defaults', true));
+
+-- Test with text values: 'yes', 'no', '1', '0', 'on', 'off'
+\pset format aligned
+-- Using 'yes' and 'no'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes'));
+
+-- Using '1' and '0'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1'));
+
+-- Using 'on' and 'off'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on'));
+
+-- Mixed boolean and text values
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no'));
+
+-- Test duplicate option (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'owner', true);
+
+-- Test invalid text value (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', 'invalid');
+
 DROP DATABASE regression_utf8;
+DROP FUNCTION ddl_filter(text);
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3250564d4ff..94980c3111a 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -602,6 +602,8 @@ CycleCtr
 DBState
 DbOidName
 DCHCacheEntry
+DDL_DEFAULTS
+DDLOptionDef
 DEADLOCK_INFO
 DECountItem
 DH
-- 
2.51.0



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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-10 22:05  Zsolt Parragi <[email protected]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 38+ messages in thread

From: Zsolt Parragi @ 2026-03-10 22:05 UTC (permalink / raw)
  To: Akshay Joshi <[email protected]>; +Cc: Kirill Reshke <[email protected]>; Japin Li <[email protected]>; Rafia Sabih <[email protected]>; Álvaro Herrera <[email protected]>; Euler Taveira <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

Hello

Is ruleutils.c the best place for this function?

It's already huge, and it has a different scope: "Functions to convert
stored expressions/querytrees back to source text"

+ /* Fetch the value of COLLATION_VERSION */
+ dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+   Anum_pg_database_datcollversion, &attr_isnull);
+ if (!attr_isnull)
+ get_formatted_string(&buf, pretty_flags, 8, "COLLATION_VERSION = %s",
+ quote_literal_cstr(TextDatumGetCString(dbvalue)));

pg_dumpall only shows this for binary upgrade, otherwise skips it. Is
it okay for this command to print it by default, shouldn't it depend
on is_with_defaults or something similar?

+#ifndef DDL_DEFAULTS_H
+#define DDL_DEFAULTS_H
+
+static const struct
+{
+ struct
+ {
....

This file seems strange. A static const struct in a header with
uppercase names doesn't seem to follow postgres conventions?
DATCONNLIMIT_UNLIMITED alredy exists as a definition, and probably
should be used instead or referenced, or the existing uses should
refer to the new way of defining it.

+ /* Fetch the value of LC_COLLATE */
+ dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+   Anum_pg_database_datcollate, &attr_isnull);
+ if (!attr_isnull)
+ get_formatted_string(&buf, pretty_flags, 8, "LC_COLLATE = %s",
+ quote_literal_cstr(TextDatumGetCString(dbvalue)));
+ /* Fetch the value of LC_CTYPE */
+ dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+   Anum_pg_database_datctype, &attr_isnull);

Can these be ever nulls?

Also, pg_dump only emits LOCALE if ctype==collate, shouldn't this
follow the same pattern?

+ else if (is_with_defaults)
+ get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = libc");

Doesn't pg_dump always emit this? Shouldn't this function follow the
same convention? Emitting it seems to be a safer default, in case
postgres ever changes this.

+ /* Build the CREATE DATABASE statement */
+ appendStringInfo(&buf, "CREATE DATABASE %s",
+ quote_identifier(dbform->datname.data));
+ get_formatted_string(&buf, pretty_flags, 4, "WITH");

Shouldn't we only emit "WITH" if it is actually followed by something,
not unconditionally?

+/*
+ * get_formatted_string
+ *
+ * Return a formatted version of the string.

But it's a void function.


+ else if (!attr_isnull)
+ get_formatted_string(&buf, pretty_flags, 8, "LOCALE = %s",
+ quote_literal_cstr(TextDatumGetCString(dbvalue)));
+

Can this ever happen, shouldn't it be an assertion instead?





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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-03-11 18:56  Akshay Joshi <[email protected]>
  parent: Zsolt Parragi <[email protected]>
  0 siblings, 0 replies; 38+ messages in thread

From: Akshay Joshi @ 2026-03-11 18:56 UTC (permalink / raw)
  To: Zsolt Parragi <[email protected]>; +Cc: Kirill Reshke <[email protected]>; Japin Li <[email protected]>; Rafia Sabih <[email protected]>; Álvaro Herrera <[email protected]>; Euler Taveira <[email protected]>; Amul Sul <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

On Wed, Mar 11, 2026 at 3:35 AM Zsolt Parragi <[email protected]>
wrote:

> Hello
>
> Is ruleutils.c the best place for this function?
>
> It's already huge, and it has a different scope: "Functions to convert
> stored expressions/querytrees back to source text"
>

    Created the ddlutils.c file.

>
> + /* Fetch the value of COLLATION_VERSION */
> + dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
> +   Anum_pg_database_datcollversion, &attr_isnull);
> + if (!attr_isnull)
> + get_formatted_string(&buf, pretty_flags, 8, "COLLATION_VERSION = %s",
> + quote_literal_cstr(TextDatumGetCString(dbvalue)));
>
> pg_dumpall only shows this for binary upgrade, otherwise skips it. Is
> it okay for this command to print it by default, shouldn't it depend
> on is_with_defaults or something similar?
>

    Shows only when `is_with_defaults` is true.

>
> +#ifndef DDL_DEFAULTS_H
> +#define DDL_DEFAULTS_H
> +
> +static const struct
> +{
> + struct
> + {
> ....
>
> This file seems strange. A static const struct in a header with
> uppercase names doesn't seem to follow postgres conventions?
> DATCONNLIMIT_UNLIMITED alredy exists as a definition, and probably
> should be used instead or referenced, or the existing uses should
> refer to the new way of defining it.
>

    Removed the header file and implemented an alternative logic. Note that
a similar file may be necessary in the future to handle default values for
other pg_get_<object>_ddl.

>
> + /* Fetch the value of LC_COLLATE */
> + dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
> +   Anum_pg_database_datcollate, &attr_isnull);
> + if (!attr_isnull)
> + get_formatted_string(&buf, pretty_flags, 8, "LC_COLLATE = %s",
> + quote_literal_cstr(TextDatumGetCString(dbvalue)));
> + /* Fetch the value of LC_CTYPE */
> + dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
> +   Anum_pg_database_datctype, &attr_isnull);
>
> Can these be ever nulls?
>
> Also, pg_dump only emits LOCALE if ctype==collate, shouldn't this
> follow the same pattern?
>
> + else if (is_with_defaults)
> + get_formatted_string(&buf, pretty_flags, 8, "LOCALE_PROVIDER = libc");
>
> Doesn't pg_dump always emit this? Shouldn't this function follow the
> same convention? Emitting it seems to be a safer default, in case
> postgres ever changes this.
>
> + /* Build the CREATE DATABASE statement */
> + appendStringInfo(&buf, "CREATE DATABASE %s",
> + quote_identifier(dbform->datname.data));
> + get_formatted_string(&buf, pretty_flags, 4, "WITH");
>
> Shouldn't we only emit "WITH" if it is actually followed by something,
> not unconditionally?
>
> +/*
> + * get_formatted_string
> + *
> + * Return a formatted version of the string.
>
> But it's a void function.
>
>
> + else if (!attr_isnull)
> + get_formatted_string(&buf, pretty_flags, 8, "LOCALE = %s",
> + quote_literal_cstr(TextDatumGetCString(dbvalue)));
> +
>
> Can this ever happen, shouldn't it be an assertion instead?
>

 Fixed all the preceding review comments.
 Attached is the *v15 patch*, now ready for further review.


Attachments:

  [application/x-patch] v15-0001-Add-pg_get_database_ddl-function-to-reconstruct-ddl.patch (35.9K, 3-v15-0001-Add-pg_get_database_ddl-function-to-reconstruct-ddl.patch)
  download | inline diff:
From 757098bb278cc6ee1de9b3d1022829f4b74ffbc3 Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Fri, 6 Mar 2026 16:46:02 +0530
Subject: [PATCH v15] Add pg_get_database_ddl() function to reconstruct CREATE
 DATABASE statements.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This adds a new system function, pg_get_database_ddl(database_name/database_oid, ddl_options),
which reconstructs the CREATE DATABASE statement for a given database name or OID.

Supported ddl_options are 'pretty', 'owner', 'tablespace' and 'defaults' and respective
values could be 'yes'/'on'/true/'1' or 'no'/'off'/false/'0'.

Usage:
 SELECT pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes');
 SELECT pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1');
 SELECT pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on');
 SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no');

Reference: PG-150
Author: Akshay Joshi <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
Reviewed-by: Euler Taveira <[email protected]>
Reviewed-by: Quan Zongliang <[email protected]>
Reviewed-by: Japin Li <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
Reviewed-by: Rafia Sabih <[email protected]>
---
 doc/src/sgml/func/func-info.sgml         |  91 +++++
 src/backend/catalog/system_functions.sql |   6 +
 src/backend/utils/adt/Makefile           |   1 +
 src/backend/utils/adt/ddlutils.c         | 486 +++++++++++++++++++++++
 src/backend/utils/adt/meson.build        |   1 +
 src/backend/utils/adt/ruleutils.c        |   2 +-
 src/include/catalog/pg_proc.dat          |   7 +
 src/test/regress/expected/database.out   | 186 +++++++++
 src/test/regress/sql/database.sql        | 120 ++++++
 src/tools/pgindent/typedefs.list         |   1 +
 10 files changed, 900 insertions(+), 1 deletion(-)
 create mode 100644 src/backend/utils/adt/ddlutils.c

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 294f45e82a3..6915408ae30 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -3845,4 +3845,95 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
 
   </sect2>
 
+  <sect2 id="functions-get-object-ddl">
+   <title>Get Object DDL Functions</title>
+
+   <para>
+    The functions described in <xref linkend="functions-get-object-ddl-table"/>
+    return the Data Definition Language (DDL) statement for any given database object.
+    This feature is implemented as a set of distinct functions, one for each object type.
+   </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_database_ddl</primary>
+        </indexterm>
+        <function>pg_get_database_ddl</function>
+        ( <parameter>database_id</parameter> <type>regdatabase</type>
+        <optional>, <literal>VARIADIC</literal> <parameter>options</parameter>
+        <type>"any"</type> </optional> )
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Reconstructs the <command>CREATE DATABASE</command> statement for the
+        specified database (identified by name or OID) from the system
+        catalogs. The optional variadic arguments are name/value pairs that
+        control the output
+        formatting and content (e.g., <literal>'pretty', true, 'owner', false</literal>).
+        Supported options are explained below.
+        </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  <para>
+    The <parameter>options</parameter> for <function>pg_get_database_ddl</function>
+    provide fine-grained control over the generated SQL. Options are passed as
+    alternating key/value pairs where the key is a text string and the
+    value is either a boolean or a text string representing a boolean
+    (<literal>true</literal>, <literal>false</literal>, <literal>yes</literal>,
+    <literal>no</literal>, <literal>1</literal>, <literal>0</literal>,
+    <literal>on</literal>, <literal>off</literal>):
+    <itemizedlist>
+    <listitem>
+      <para>
+      <literal>'pretty', true</literal> (or <literal>'pretty', 'yes'</literal>):
+      Formats the output with newlines and indentation for better readability.
+      This option defaults to false.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'owner', false</literal> (or <literal>'owner', 'no'</literal>):
+      Omits the <literal>OWNER</literal> clause from the reconstructed statement.
+      This option defaults to true.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'tablespace', false</literal> (or <literal>'tablespace', '0'</literal>):
+      Omits the <literal>TABLESPACE</literal> clause from the reconstructed statement.
+      This option defaults to true.
+      </para>
+    </listitem>
+    <listitem>
+      <para>
+      <literal>'defaults', true</literal> (or <literal>'defaults', '1'</literal>):
+      Includes clauses for parameters that are currently at their default values
+      (e.g., <literal>CONNECTION LIMIT -1</literal>), which are normally omitted for brevity.
+      This option defaults to false.
+      </para>
+    </listitem>
+    </itemizedlist>
+  </para>
+
+  </sect2>
+
   </sect1>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 1c5b6d6df05..fa48e2f0775 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -372,3 +372,9 @@ CREATE OR REPLACE FUNCTION ts_debug(document text,
 BEGIN ATOMIC
     SELECT * FROM ts_debug(get_current_ts_config(), $1);
 END;
+
+CREATE OR REPLACE FUNCTION
+  pg_get_database_ddl(database_id regdatabase, VARIADIC options "any" DEFAULT NULL)
+RETURNS text
+LANGUAGE internal
+AS 'pg_get_database_ddl';
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index a8fd680589f..4fdd541f7bf 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -102,6 +102,7 @@ OBJS = \
 	regproc.o \
 	ri_triggers.o \
 	rowtypes.o \
+	ddlutils.o \
 	ruleutils.o \
 	selfuncs.o \
 	skipsupport.o \
diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c
new file mode 100644
index 00000000000..6e88b8a1a92
--- /dev/null
+++ b/src/backend/utils/adt/ddlutils.c
@@ -0,0 +1,486 @@
+/*-------------------------------------------------------------------------
+ *
+ * ddlutils.c
+ *	  Functions to reconstruct DDL statements from catalog data.
+ *
+ * Unlike ruleutils.c (which deparses expressions and query trees),
+ * these functions generate DDL by reading catalog attributes directly.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/ddlutils.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <stdarg.h>
+
+#include "access/htup_details.h"
+#include "catalog/pg_authid.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_tablespace.h"
+#include "commands/tablespace.h"
+#include "funcapi.h"
+#include "mb/pg_wchar.h"
+#include "miscadmin.h"
+#include "utils/acl.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+/* Pretty flags (subset needed for DDL formatting) */
+#define PRETTYFLAG_INDENT		0x0002
+
+/* DDL Options flags */
+#define PG_DDL_PRETTY_INDENT	0x00000001
+#define PG_DDL_WITH_DEFAULTS	0x00000002
+#define PG_DDL_NO_OWNER			0x00000004
+#define PG_DDL_NO_TABLESPACE	0x00000008
+
+/*
+ * Structure to define DDL options for parse_ddl_options().
+ * This allows easy addition of new options in the future.
+ */
+typedef struct DDLOptionDef
+{
+	const char *name;			/* Option name (case-insensitive) */
+	uint32		flag;			/* Flag to set */
+	bool		set_on_true;	/* If true, set flag when value is true; if
+								 * false, set flag when value is false */
+} DDLOptionDef;
+
+/*
+ * Array of supported DDL options.
+ * To add a new option, simply add an entry to this array.
+ */
+static const DDLOptionDef ddl_option_defs[] = {
+	{"pretty", PG_DDL_PRETTY_INDENT, true},
+	{"defaults", PG_DDL_WITH_DEFAULTS, true},
+	{"owner", PG_DDL_NO_OWNER, false},
+	{"tablespace", PG_DDL_NO_TABLESPACE, false},
+};
+
+#define GET_DDL_PRETTY_FLAGS(pretty) \
+	((pretty) ? (PRETTYFLAG_INDENT) \
+	 : 0)
+
+/* Local function declarations */
+static char *pg_get_database_ddl_worker(Oid db_oid, uint32 ddl_flags);
+static void get_formatted_string(StringInfo buf, int prettyFlags, int nSpaces, const char *fmt,...) pg_attribute_printf(4, 5);
+static uint32 parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start);
+
+/*
+ * get_formatted_string
+ *
+ * Helper function to append formatted strings to a StringInfo buffer, with
+ * optional pretty-printing based on flags.
+ *
+ * prettyFlags - Based on prettyFlags the output includes spaces and
+ *               newlines (\n).
+ * nSpaces - indent with specified number of space characters.
+ * fmt - printf-style format string used by appendStringInfoVA.
+ */
+static void
+get_formatted_string(StringInfo buf, int prettyFlags, int nSpaces, const char *fmt,...)
+{
+	va_list		args;
+
+	if (prettyFlags & PRETTYFLAG_INDENT)
+	{
+		appendStringInfoChar(buf, '\n');
+		/* Indent with spaces */
+		appendStringInfoSpaces(buf, nSpaces);
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	for (;;)
+	{
+		int			needed;
+
+		va_start(args, fmt);
+		needed = appendStringInfoVA(buf, fmt, args);
+		va_end(args);
+		if (needed == 0)
+			break;
+		enlargeStringInfo(buf, needed);
+	}
+}
+
+/*
+ * parse_ddl_options - Generic helper to parse variadic name/value options
+ * fcinfo: The FunctionCallInfo from the calling function
+ * variadic_start: The argument position where variadic arguments start
+ *
+ * Returns: Bitmask of flags based on the parsed options.
+ *
+ * Options are passed as name/value pairs.
+ * For example: pg_get_database_ddl('mydb', 'owner', false, 'pretty', true)
+ */
+static uint32
+parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
+{
+	uint32		flags = 0;
+	uint32		seen_flags = 0;
+	Datum	   *args;
+	bool	   *nulls;
+	Oid		   *types;
+	int			nargs;
+
+	/* Extract variadic arguments */
+	nargs = extract_variadic_args(fcinfo, variadic_start, true,
+								  &args, &types, &nulls);
+
+	/* If no options provided (VARIADIC NULL), return the empty bitmask */
+	if (nargs <= 0)
+		return flags;
+
+	/*
+	 * Handle the case where DEFAULT NULL was used and no explicit variadic
+	 * arguments were provided. In this case, we get a single NULL argument.
+	 */
+	if (nargs == 1 && nulls[0])
+		return flags;
+
+	/* Arguments must come in name/value pairs */
+	if (nargs % 2 != 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("variadic arguments must be name/value pairs"),
+				 errhint("Provide an even number of variadic arguments that can be divided into pairs.")));
+
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *name;
+		bool		bval;
+		bool		found = false;
+
+		/* Key must not be null */
+		if (nulls[i])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("name at variadic position %d is null", i + 1)));
+
+		/* Key must be text type */
+		if (types[i] != TEXTOID)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("name at variadic position %d has type %s, expected type %s",
+							i + 1, format_type_be(types[i]),
+							format_type_be(TEXTOID))));
+
+		name = TextDatumGetCString(args[i]);
+
+		/* Value must not be null */
+		if (nulls[i + 1])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("value for option \"%s\" must not be null",
+							name)));
+
+		/* Value must be boolean or text type */
+		if (types[i + 1] == BOOLOID)
+		{
+			bval = DatumGetBool(args[i + 1]);
+		}
+		else if (types[i + 1] == TEXTOID)
+		{
+			char	   *valstr = TextDatumGetCString(args[i + 1]);
+
+			if (!parse_bool(valstr, &bval))
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("value for option \"%s\" at position %d has invalid value \"%s\"",
+								name, i + 2, valstr),
+						 errhint("Valid values are: true, false, yes, no, 1, 0, on, off.")));
+			pfree(valstr);
+		}
+		else
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("value for option \"%s\" at position %d has type %s, expected type boolean or text",
+							name, i + 2, format_type_be(types[i + 1]))));
+		}
+
+		/*
+		 * Look up the option in the ddl_option_defs array and set the
+		 * appropriate flag based on the value.
+		 */
+		for (int j = 0; j < lengthof(ddl_option_defs); j++)
+		{
+			const DDLOptionDef *opt = &ddl_option_defs[j];
+
+			if (pg_strcasecmp(name, opt->name) == 0)
+			{
+				/* Error if this option was already specified */
+				if (seen_flags & opt->flag)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("option \"%s\" is specified more than once", name)));
+
+				seen_flags |= opt->flag;
+
+				/*
+				 * Set the flag if the value matches the set_on_true
+				 * condition: if set_on_true is true, set flag when bval is
+				 * true; if set_on_true is false, set flag when bval is false.
+				 */
+				if (bval == opt->set_on_true)
+					flags |= opt->flag;
+
+				found = true;
+				break;
+			}
+		}
+
+		if (!found)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unrecognized option: \"%s\"", name)));
+
+		pfree(name);
+	}
+
+	return flags;
+}
+
+/*
+ * pg_get_database_ddl
+ *
+ * Generate a CREATE DATABASE statement for the specified database oid.
+ *
+ * db_oid - OID of the database for which to generate the DDL.
+ * options - Variadic name/value pairs to modify the output.
+ */
+Datum
+pg_get_database_ddl(PG_FUNCTION_ARGS)
+{
+	Oid			db_oid;
+	uint32		ddl_flags;
+	char	   *res;
+
+	if (PG_ARGISNULL(0))
+		PG_RETURN_NULL();
+
+	db_oid = PG_GETARG_OID(0);
+
+	/* Parse variadic options starting from argument 1 */
+	ddl_flags = parse_ddl_options(fcinfo, 1);
+
+	res = pg_get_database_ddl_worker(db_oid, ddl_flags);
+
+	PG_RETURN_TEXT_P(cstring_to_text(res));
+}
+
+static char *
+pg_get_database_ddl_worker(Oid db_oid, uint32 ddl_flags)
+{
+	const char *encoding;
+	bool		attr_isnull;
+	Datum		dbvalue;
+	HeapTuple	tuple_database;
+	Form_pg_database dbform;
+	StringInfoData buf;
+	StringInfoData optbuf;
+	AclResult	aclresult;
+	HeapTuple	tmpl_tuple;
+	int			tmpl_encoding = -1;
+	char	   *collate;
+	char	   *ctype;
+
+	/* Variables for ddl_options parsing */
+	int			pretty_flags = 0;
+	bool		is_with_defaults = false;
+
+	/* Set the appropriate flags */
+	if (ddl_flags & PG_DDL_PRETTY_INDENT)
+		pretty_flags = GET_DDL_PRETTY_FLAGS(1);
+
+	is_with_defaults = (ddl_flags & PG_DDL_WITH_DEFAULTS) != 0;
+
+	/*
+	 * User must have connect privilege for target database.
+	 */
+	aclresult = object_aclcheck(DatabaseRelationId, db_oid, GetUserId(),
+								ACL_CONNECT);
+	if (aclresult != ACLCHECK_OK &&
+		!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+	{
+		aclcheck_error(aclresult, OBJECT_DATABASE,
+					   get_database_name(db_oid));
+	}
+
+	/* Look up the database in pg_database */
+	tuple_database = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(db_oid));
+	if (!HeapTupleIsValid(tuple_database))
+		ereport(ERROR,
+				errcode(ERRCODE_UNDEFINED_OBJECT),
+				errmsg("database with oid %u does not exist", db_oid));
+
+	dbform = (Form_pg_database) GETSTRUCT(tuple_database);
+
+	initStringInfo(&buf);
+	initStringInfo(&optbuf);
+
+	/*
+	 * Build the options into a separate buffer first, so we can emit WITH
+	 * only when there are options to show.
+	 */
+
+	/* Set the OWNER in the DDL if owner is not omitted */
+	if (OidIsValid(dbform->datdba) && !(ddl_flags & PG_DDL_NO_OWNER))
+	{
+		char	   *dbowner = GetUserNameFromId(dbform->datdba, false);
+
+		get_formatted_string(&optbuf, pretty_flags, 8, "OWNER = %s",
+							 quote_identifier(dbowner));
+	}
+
+	/*
+	 * Emit ENCODING if it differs from template1's encoding, or if defaults
+	 * are requested.  The default encoding for CREATE DATABASE comes from the
+	 * template database (template1), not a fixed value.
+	 */
+	encoding = pg_encoding_to_char(dbform->encoding);
+
+	tmpl_tuple = SearchSysCache1(DATABASEOID,
+								 ObjectIdGetDatum(Template1DbOid));
+	if (HeapTupleIsValid(tmpl_tuple))
+	{
+		Form_pg_database tmplform = (Form_pg_database) GETSTRUCT(tmpl_tuple);
+
+		tmpl_encoding = tmplform->encoding;
+		ReleaseSysCache(tmpl_tuple);
+	}
+
+	if (is_with_defaults || dbform->encoding != tmpl_encoding)
+		get_formatted_string(&optbuf, pretty_flags, 8, "ENCODING = %s",
+							 quote_literal_cstr(encoding));
+
+	/*
+	 * LC_COLLATE and LC_CTYPE are BKI_FORCE_NOT_NULL, always present. Emit
+	 * LOCALE when they match (like pg_dump), otherwise emit separately.
+	 */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+								Anum_pg_database_datcollate, &attr_isnull);
+	Assert(!attr_isnull);
+	collate = TextDatumGetCString(dbvalue);
+
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+								Anum_pg_database_datctype, &attr_isnull);
+	Assert(!attr_isnull);
+	ctype = TextDatumGetCString(dbvalue);
+
+	if (strcmp(collate, ctype) == 0)
+	{
+		get_formatted_string(&optbuf, pretty_flags, 8, "LOCALE = %s",
+								quote_literal_cstr(collate));
+	}
+	else
+	{
+		get_formatted_string(&optbuf, pretty_flags, 8, "LC_COLLATE = %s",
+								quote_literal_cstr(collate));
+		get_formatted_string(&optbuf, pretty_flags, 8, "LC_CTYPE = %s",
+								quote_literal_cstr(ctype));
+	}
+
+
+	/*
+	 * Fetch datlocale: emit as BUILTIN_LOCALE or ICU_LOCALE depending on the
+	 * provider.  For libc, datlocale should always be NULL.
+	 */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_datlocale, &attr_isnull);
+	if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&optbuf, pretty_flags, 8, "BUILTIN_LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	else if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&optbuf, pretty_flags, 8, "ICU_LOCALE = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	else
+		Assert(attr_isnull);
+
+	/* Fetch the value of ICU_RULES */
+	dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+							  Anum_pg_database_daticurules, &attr_isnull);
+	if (!attr_isnull && dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&optbuf, pretty_flags, 8, "ICU_RULES = %s",
+							 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+
+	/*
+	 * Emit COLLATION_VERSION only when defaults are requested.  Normally this
+	 * is an internal implementation detail that should be determined freshly
+	 * by the target cluster (similar to how pg_dump only emits it during
+	 * binary upgrades).
+	 */
+	if (is_with_defaults)
+	{
+		dbvalue = SysCacheGetAttr(DATABASEOID, tuple_database,
+								  Anum_pg_database_datcollversion, &attr_isnull);
+		if (!attr_isnull)
+			get_formatted_string(&optbuf, pretty_flags, 8, "COLLATION_VERSION = %s",
+								 quote_literal_cstr(TextDatumGetCString(dbvalue)));
+	}
+
+	/* Set the appropriate LOCALE_PROVIDER */
+	if (dbform->datlocprovider == COLLPROVIDER_BUILTIN)
+		get_formatted_string(&optbuf, pretty_flags, 8, "LOCALE_PROVIDER = builtin");
+	else if (dbform->datlocprovider == COLLPROVIDER_ICU)
+		get_formatted_string(&optbuf, pretty_flags, 8, "LOCALE_PROVIDER = icu");
+	else
+		get_formatted_string(&optbuf, pretty_flags, 8, "LOCALE_PROVIDER = libc");
+
+	/* Set the TABLESPACE in the DDL if tablespace is not omitted */
+	if (OidIsValid(dbform->dattablespace) && !(ddl_flags & PG_DDL_NO_TABLESPACE))
+	{
+		if (is_with_defaults ||
+			dbform->dattablespace != DEFAULTTABLESPACE_OID)
+		{
+			char	   *dbTablespace = get_tablespace_name(dbform->dattablespace);
+
+			get_formatted_string(&optbuf, pretty_flags, 8, "TABLESPACE = %s",
+								 quote_identifier(dbTablespace));
+		}
+	}
+
+	if (is_with_defaults || !dbform->datallowconn)
+	{
+		get_formatted_string(&optbuf, pretty_flags, 8, "ALLOW_CONNECTIONS = %s",
+							 dbform->datallowconn ? "true" : "false");
+	}
+
+	if (is_with_defaults ||
+		dbform->datconnlimit != DATCONNLIMIT_UNLIMITED)
+	{
+		get_formatted_string(&optbuf, pretty_flags, 8, "CONNECTION LIMIT = %d",
+							 dbform->datconnlimit);
+	}
+
+	if (is_with_defaults || dbform->datistemplate)
+		get_formatted_string(&optbuf, pretty_flags, 8, "IS_TEMPLATE = %s",
+							 dbform->datistemplate ? "true" : "false");
+
+	/* Build the CREATE DATABASE statement */
+	appendStringInfo(&buf, "CREATE DATABASE %s",
+					 quote_identifier(dbform->datname.data));
+
+	/* Only emit WITH if there are options */
+	if (optbuf.len > 0)
+	{
+		get_formatted_string(&buf, pretty_flags, 4, "WITH");
+		appendStringInfoString(&buf, optbuf.data);
+	}
+
+	pfree(optbuf.data);
+	appendStringInfoChar(&buf, ';');
+
+	ReleaseSysCache(tuple_database);
+
+	return buf.data;
+}
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index fb8294d7e4a..f9893b5dfb0 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -98,6 +98,7 @@ backend_sources += files(
   'regproc.c',
   'ri_triggers.c',
   'rowtypes.c',
+  'ddlutils.c',
   'ruleutils.c',
   'selfuncs.c',
   'skipsupport.c',
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index f16f1535785..997c671aef0 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -57,6 +57,7 @@
 #include "rewrite/rewriteHandler.h"
 #include "rewrite/rewriteManip.h"
 #include "rewrite/rewriteSupport.h"
+#include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
@@ -547,7 +548,6 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
 										  deparse_context *context,
 										  bool showimplicit,
 										  bool needcomma);
-
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 361e2cfffeb..1e2f7d3ac35 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4034,6 +4034,13 @@
   proname => 'pg_get_function_sqlbody', provolatile => 's',
   prorettype => 'text', proargtypes => 'oid',
   prosrc => 'pg_get_function_sqlbody' },
+{ oid => '9492', descr => 'get CREATE statement for database name and oid',
+  proname => 'pg_get_database_ddl', provariadic => 'any', proisstrict => 'f',
+  provolatile => 's', prorettype => 'text',
+  proargtypes => 'regdatabase any',
+  proargmodes => '{i,v}',
+  proallargtypes => '{regdatabase,any}',
+  prosrc => 'pg_get_database_ddl' },
 
 { oid => '1686', descr => 'list of SQL keywords',
   proname => 'pg_get_keywords', procost => '10', prorows => '500',
diff --git a/src/test/regress/expected/database.out b/src/test/regress/expected/database.out
index 6b879b0f62a..76eca22cf3a 100644
--- a/src/test/regress/expected/database.out
+++ b/src/test/regress/expected/database.out
@@ -1,3 +1,65 @@
+--
+-- Reconstruct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+    -- Remove %LOCALE_PROVIDER% placeholders
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_COLLATE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove ENCODING assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*ENCODING\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -16,6 +78,130 @@ CREATE ROLE regress_datdba_before;
 CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database');
+ERROR:  database "regression_database" does not exist
+LINE 1: SELECT pg_get_database_ddl('regression_database');
+                                   ^
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+ pg_get_database_ddl 
+---------------------
+ 
+(1 row)
+
+-- Without options
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+                                        ddl_filter                                         
+-------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after CONNECTION LIMIT = 123;
+(1 row)
+
+-- With No Owner
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false));
+                          ddl_filter                          
+--------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH CONNECTION LIMIT = 123;
+(1 row)
+
+-- With No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true, 'tablespace', false));
+                                                               ddl_filter                                                               
+----------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true));
+                                                                           ddl_filter                                                                           
+----------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH OWNER = regress_datdba_after TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'tablespace', false, 'defaults', true));
+                                                ddl_filter                                                 
+-----------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        OWNER = regress_datdba_after
+        CONNECTION LIMIT = 123;
+(1 row)
+-- With No Owner and No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        CONNECTION LIMIT = 123;
+(1 row)
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'defaults', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        OWNER = regress_datdba_after
+        TABLESPACE = pg_default
+        ALLOW_CONNECTIONS = true
+        CONNECTION LIMIT = 123
+        IS_TEMPLATE = false;
+(1 row)
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false, 'defaults', true));
+ddl_filter
+CREATE DATABASE regression_utf8
+    WITH
+        ALLOW_CONNECTIONS = true
+        CONNECTION LIMIT = 123
+        IS_TEMPLATE = false;
+(1 row)
+-- Test with text values: 'yes', 'no', '1', '0', 'on', 'off'
+\pset format aligned
+-- Using 'yes' and 'no'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes'));
+                                                            ddl_filter                                                             
+-----------------------------------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH TABLESPACE = pg_default ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Using '1' and '0'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1'));
+                                                ddl_filter                                                 
+-----------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Using 'on' and 'off'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on'));
+           ddl_filter            
+---------------------------------
+ CREATE DATABASE regression_utf8+
+     WITH                       +
+         CONNECTION LIMIT = 123;
+(1 row)
+
+-- Mixed boolean and text values
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no'));
+                                                ddl_filter                                                 
+-----------------------------------------------------------------------------------------------------------
+ CREATE DATABASE regression_utf8 WITH ALLOW_CONNECTIONS = true CONNECTION LIMIT = 123 IS_TEMPLATE = false;
+(1 row)
+
+-- Test duplicate option (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'owner', true);
+ERROR:  option "owner" is specified more than once
+-- Test invalid text value (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', 'invalid');
+ERROR:  value for option "owner" at position 2 has invalid value "invalid"
+HINT:  Valid values are: true, false, yes, no, 1, 0, on, off.
 DROP DATABASE regression_utf8;
+DROP FUNCTION ddl_filter(text);
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
diff --git a/src/test/regress/sql/database.sql b/src/test/regress/sql/database.sql
index 4ef36127291..9ef926fac9c 100644
--- a/src/test/regress/sql/database.sql
+++ b/src/test/regress/sql/database.sql
@@ -1,3 +1,67 @@
+--
+-- Reconstruct DDL
+--
+-- To produce stable regression test output, it's usually necessary to
+-- ignore collation and locale related details. This filter
+-- functions removes collation and locale related details.
+
+CREATE OR REPLACE FUNCTION ddl_filter(ddl_input TEXT)
+RETURNS TEXT AS $$
+DECLARE
+    cleaned_ddl TEXT;
+BEGIN
+    -- Remove %LOCALE_PROVIDER% placeholders
+    cleaned_ddl := regexp_replace(
+        ddl_input,
+        '\s*\mLOCALE_PROVIDER\M\s*=\s*([''"]?[^''"\s]+[''"]?)',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_COLLATE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_COLLATE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove LC_CTYPE assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*LC_CTYPE\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %LOCALE% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*LOCALE\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove %COLLATION% placeholders
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*\S*COLLATION\S*\s*=?\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    -- Remove ENCODING assignments
+    cleaned_ddl := regexp_replace(
+        cleaned_ddl,
+        '\s*ENCODING\s*=\s*([''"])[^''"]*\1',
+        '',
+        'gi'
+    );
+
+    RETURN cleaned_ddl;
+END;
+$$ LANGUAGE plpgsql;
+
 CREATE DATABASE regression_tbd
 	ENCODING utf8 LC_COLLATE "C" LC_CTYPE "C" TEMPLATE template0;
 ALTER DATABASE regression_tbd RENAME TO regression_utf8;
@@ -19,6 +83,62 @@ CREATE ROLE regress_datdba_after;
 ALTER DATABASE regression_utf8 OWNER TO regress_datdba_before;
 REASSIGN OWNED BY regress_datdba_before TO regress_datdba_after;
 
+-- Test pg_get_database_ddl
+-- Database doesn't exists
+SELECT pg_get_database_ddl('regression_database');
+
+-- Test NULL value
+SELECT pg_get_database_ddl(NULL);
+
+-- Without options
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8'));
+
+-- With No Owner
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false));
+
+-- With No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true, 'tablespace', false));
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults', true));
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'tablespace', false, 'defaults', true));
+
+-- With Pretty formatted
+\pset format unaligned
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true));
+
+-- With No Owner and No Tablespace
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false));
+
+-- With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'defaults', true));
+
+-- With No Owner, No Tablespace and With Defaults
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'pretty', true, 'owner', false, 'tablespace', false, 'defaults', true));
+
+-- Test with text values: 'yes', 'no', '1', '0', 'on', 'off'
+\pset format aligned
+-- Using 'yes' and 'no'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'no', 'defaults', 'yes'));
+
+-- Using '1' and '0'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', '0', 'tablespace', '0', 'defaults', '1'));
+
+-- Using 'on' and 'off'
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', 'off', 'pretty', 'on'));
+
+-- Mixed boolean and text values
+SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'owner', false, 'defaults', 'true', 'tablespace', 'no'));
+
+-- Test duplicate option (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', false, 'owner', true);
+
+-- Test invalid text value (should error)
+SELECT pg_get_database_ddl('regression_utf8', 'owner', 'invalid');
+
 DROP DATABASE regression_utf8;
+DROP FUNCTION ddl_filter(text);
 DROP ROLE regress_datdba_before;
 DROP ROLE regress_datdba_after;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3da19d41413..622d10541d0 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -602,6 +602,7 @@ CycleCtr
 DBState
 DbOidName
 DCHCacheEntry
+DDLOptionDef
 DEADLOCK_INFO
 DECountItem
 DH
-- 
2.51.0



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


end of thread, other threads:[~2026-03-11 18:56 UTC | newest]

Thread overview: 38+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2025-11-12 12:04 [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement Akshay Joshi <[email protected]>
2025-11-13 04:17 ` Quan Zongliang <[email protected]>
2025-11-13 04:48   ` Quan Zongliang <[email protected]>
2025-11-13 08:32     ` Akshay Joshi <[email protected]>
2025-11-14 05:49       ` Japin Li <[email protected]>
2025-11-17 14:34         ` Akshay Joshi <[email protected]>
2025-11-18 00:28           ` Chao Li <[email protected]>
2025-11-18 08:03             ` Akshay Joshi <[email protected]>
2025-11-19 10:17               ` Japin Li <[email protected]>
2025-11-19 11:06                 ` Akshay Joshi <[email protected]>
2025-11-20 01:39                   ` Japin Li <[email protected]>
2025-11-19 10:47               ` Álvaro Herrera <[email protected]>
2025-11-19 11:07                 ` Akshay Joshi <[email protected]>
2025-11-20 09:18                 ` Akshay Joshi <[email protected]>
2025-12-11 13:59                   ` Euler Taveira <[email protected]>
2025-12-12 10:52                     ` Akshay Joshi <[email protected]>
2025-12-12 15:19                       ` Euler Taveira <[email protected]>
2025-11-14 07:03       ` Chao Li <[email protected]>
2025-11-13 08:30   ` Akshay Joshi <[email protected]>
2025-11-13 09:36     ` Quan Zongliang <[email protected]>
2025-11-14 01:13 Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement Quan Zongliang <[email protected]>
2025-11-14 11:12 Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement Álvaro Herrera <[email protected]>
2025-11-17 14:39 ` Akshay Joshi <[email protected]>
2026-03-04 08:39 Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement Japin Li <[email protected]>
2026-03-04 09:30 ` Japin Li <[email protected]>
2026-03-04 12:59   ` Akshay Joshi <[email protected]>
2026-03-04 14:01     ` Japin Li <[email protected]>
2026-03-05 09:50       ` Akshay Joshi <[email protected]>
2026-03-05 14:45         ` Rafia Sabih <[email protected]>
2026-03-05 16:15           ` Akshay Joshi <[email protected]>
2026-03-06 07:37             ` Rafia Sabih <[email protected]>
2026-03-06 11:51               ` Akshay Joshi <[email protected]>
2026-03-06 14:43                 ` Japin Li <[email protected]>
2026-03-09 12:08                   ` Akshay Joshi <[email protected]>
2026-03-10 10:16                     ` Kirill Reshke <[email protected]>
2026-03-10 13:44                       ` Akshay Joshi <[email protected]>
2026-03-10 22:05                         ` Zsolt Parragi <[email protected]>
2026-03-11 18:56                           ` Akshay Joshi <[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