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

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

From: Álvaro Herrera @ 2026-02-25 11:53 UTC (permalink / raw)
  To: Amul Sul <[email protected]>; +Cc: Akshay Joshi <[email protected]>; Andrew Dunstan <[email protected]>; Euler Taveira <[email protected]>; Chao Li <[email protected]>; japin <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

On 2026-Feb-24, Amul Sul wrote:

> On Mon, Feb 23, 2026 at 7:27 PM Akshay Joshi
> <[email protected]> wrote:
> >
> > On Mon, Feb 23, 2026 at 6:50 PM Amul Sul <[email protected]> wrote:

> > >
> > > +       /* If it's with defaults, we skip default encoding check */
> > > +       if (is_with_defaults ||
> > > +           (pg_strcasecmp(pg_encoding_to_char(dbform->encoding),
> > > +                          DDL_DEFAULTS.DATABASE.ENCODING) != 0))
> > >
> > >
> > > Comment doesn't quite match the logic in the condition.
> >
> > Will update this in my next patch. When we decide which approach to follow:
> > 1) Double Dash:
> > v8-0001-Add-pg_get_database_ddl-function-to-reconstruct-double-dash.patch
> > 2) DefElem (Key-Value):
> > v8-0002-Add-pg_get_database_ddl-function-to-reconstruct-DefElem.patch
> 
> That’s a bit subjective, as different people will likely have
> different opinions. I prefer the first version without the -- prefix,
> since the counterpart would be the default behavior. For example, if
> "no-tablespace" is not specified, the tablespace would be included in
> the DDL dump by default.

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.

Regards

-- 
Álvaro Herrera               48°01'N 7°57'E  —  https://www.EnterpriseDB.com/






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

* Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement
@ 2026-02-25 21:19  Euler Taveira <[email protected]>
  parent: Álvaro Herrera <[email protected]>
  0 siblings, 1 reply; 18+ messages in thread

From: Euler Taveira @ 2026-02-25 21:19 UTC (permalink / raw)
  To: Álvaro Herrera <[email protected]>; Amul Sul <[email protected]>; +Cc: Akshay Joshi <[email protected]>; Andrew Dunstan <[email protected]>; Chao Li <[email protected]>; japin <[email protected]>; Quan Zongliang <[email protected]>; pgsql-hackers

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.


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






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

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

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

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.
>
>
> --
> Euler Taveira
> EDB   https://www.enterprisedb.com/
>


Attachments:

  [application/octet-stream] v9-0001-Add-pg_get_database_ddl-function-to-reconstruct-ddl.patch (32.8K, 3-v9-0001-Add-pg_get_database_ddl-function-to-reconstruct-ddl.patch)
  download | inline diff:
From 8c1e39cbae52a44ece8dd03b74663892222f743a Mon Sep 17 00:00:00 2001
From: Akshay Joshi <[email protected]>
Date: Wed, 24 Sep 2025 17:47:59 +0530
Subject: [PATCH v9] 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        | 397 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 +
 src/include/utils/ddl_defaults.h         |  39 +++
 src/test/regress/expected/database.out   | 170 ++++++++++
 src/test/regress/sql/database.sql        | 108 ++++++
 7 files changed, 813 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..988d9d2b36a 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) */
+	uint64		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, uint64 ddl_flags);
 
 #define only_marker(rte)  ((rte)->inh ? "" : "ONLY ")
 
@@ -13760,3 +13802,358 @@ 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 */
+		for (int i = 0; i < nSpaces; i++)
+		{
+			appendStringInfoChar(buf, ' ');
+		}
+	}
+	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 uint64
+parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start)
+{
+	uint64		flags = 0;
+	Datum	   *args;
+	bool	   *nulls;
+	Oid		   *types;
+	int			nargs;
+	int			i;
+
+	/* 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;
+
+	/* No arguments provided */
+	if (nargs == 0)
+		return flags;
+
+	/* 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()")));
+
+	for (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("argument %d: key must not be null", i + 1)));
+
+		/* 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)));
+
+		name = TextDatumGetCString(args[i]);
+
+		/* Value must not be null */
+		if (nulls[i + 1])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("argument %d: value must not be null for key \"%s\"",
+							i + 2, 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.
+		 */
+		{
+			bool		found = false;
+			int			j;
+
+			for (j = 0; j < lengthof(ddl_option_defs); j++)
+			{
+				const		DDLOptionDef *opt = &ddl_option_defs[j];
+
+				if (pg_strcasecmp(name, opt->name) == 0)
+				{
+					/*
+					 * 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);
+	uint64		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, uint64 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..5b4aa6c054d 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,122 @@ 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 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..888d1e5283f 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,58 @@ 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 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] 18+ 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]>
  parent: Akshay Joshi <[email protected]>
  0 siblings, 1 reply; 18+ 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] 18+ 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; 18+ 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] 18+ 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; 18+ 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] 18+ 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; 18+ 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] 18+ 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; 18+ 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] 18+ 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; 18+ 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] 18+ 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; 18+ 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] 18+ 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; 18+ 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] 18+ 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; 18+ 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] 18+ 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; 18+ 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] 18+ 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; 18+ 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] 18+ 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; 18+ 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] 18+ 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; 18+ 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] 18+ 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; 18+ 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] 18+ 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; 18+ 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] 18+ messages in thread


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

Thread overview: 18+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-02-25 11:53 Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement Álvaro Herrera <[email protected]>
2026-02-25 21:19 ` Euler Taveira <[email protected]>
2026-02-27 12:22   ` Akshay Joshi <[email protected]>
2026-03-04 08:39     ` 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