Subject: Re: [PATCH] Add pg_get_database_ddl() function to reconstruct CREATE DATABASE statement

From: Álvaro Herrera <[email-masked]>
Date: Wed, 25 Feb 2026 12:53:57 +0100

On 2026-Feb-24, Amul Sul wrote:

> On Mon, Feb 23, 2026 at 7:27 PM Akshay Joshi
> <[email-masked]> wrote:
> >
> > On Mon, Feb 23, 2026 at 6:50 PM Amul Sul <[email-masked]> 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/

---

From: "Euler Taveira" <[email-masked]>
Date: Wed, 25 Feb 2026 18:19:27 -0300

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

---

From: Akshay Joshi <[email-masked]>
Date: Fri, 27 Feb 2026 17:52:36 +0530

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.

---

From: Japin Li <[email-masked]>
Date: Wed, 04 Mar 2026 16:39:06 +0800

On Fri, 27 Feb 2026 at 17:52, Akshay Joshi <[email-masked]> 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:
> Usage Examples:
>   SELECT pg_get_database_ddl('mydb', 'owner', 'no', 'tablespace', 'no', 'pretty', 'on');
>   -- Using boolean values
> 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.
>

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.

---

From: Japin Li <[email-masked]>
Date: Wed, 04 Mar 2026 17:30:34 +0800

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

---

From: Akshay Joshi <[email-masked]>
Date: Wed, 4 Mar 2026 18:29:53 +0530

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-masked]> wrote:

> On 2026-Mar-04, Japin Li wrote:
>
> I think this should throw an error that 'pretty' was contradictorily
> specified twice.  (Maybe also an error if an option appears twice,
> period.)
>
> --

---

From: Japin Li <[email-masked]>
Date: Wed, 04 Mar 2026 22:01:19 +0800

On Wed, 04 Mar 2026 at 18:29, Akshay Joshi <[email-masked]> 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?

---

From: Akshay Joshi <[email-masked]>
Date: Thu, 5 Mar 2026 15:20:23 +0530

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-masked]> 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.
> >
> quote_identifier(dbowner));
> +       }
>
>  (pg_strcasecmp(pg_encoding_to_char(dbform->encoding),
> +
> DDL_DEFAULTS.DATABASE.ENCODING) != 0))
> +               {
> quote_literal_cstr(
>                              pg_encoding_to_char(dbform->encoding)));
> +               }
>

---

From: Rafia Sabih <[email-masked]>
Date: Thu, 5 Mar 2026 06:45:10 -0800

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?

-- 
Regards,
Rafia Sabih
CYBERTEC PostgreSQL International GmbH

---

From: Akshay Joshi <[email-masked]>
Date: Thu, 5 Mar 2026 21:45:48 +0530

> patch, it is not clear to me what is the default value for each of the

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

    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.

> +SELECT ddl_filter(pg_get_database_ddl('regression_utf8', 'defaults',
> true, 'tablespace', false));
> +
>  ddl_filter
>
>

    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.

---

From: Rafia Sabih <[email-masked]>
Date: Thu, 5 Mar 2026 23:37:01 -0800

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

>     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.
>
>>
>> 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
This information should also be included in the documentation.

---

From: Akshay Joshi <[email-masked]>
Date: Fri, 6 Mar 2026 17:21:57 +0530

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-masked]>
wrote:

>>> 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
>>>
>>
>>> owner option is omitted but the output contains the owner information.
>>> So does it mean that the default value for each option is true?
>>> I find this rather confusing, it is worth documenting what is the
>>> default value for each of these options.
>>>
>>
>> Right, I think this needs to be added in the documentation as part of
> this patch. Basically, here
> <para>

---

From: Japin Li <[email-masked]>
Date: Fri, 06 Mar 2026 22:43:10 +0800

Hi, Akshay

On Fri, 06 Mar 2026 at 17:21, Akshay Joshi <[email-masked]> wrote:
> 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.

---

From: Akshay Joshi <[email-masked]>
Date: Mon, 9 Mar 2026 17:38:01 +0530

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-masked]> wrote:

> -                                                               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);
>                 }
> -                                        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,

---

From: Kirill Reshke <[email-masked]>
Date: Tue, 10 Mar 2026 15:16:25 +0500

On Mon, 9 Mar 2026 at 17:08, Akshay Joshi <[email-masked]> 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.
>

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

-- 
Best regards,
Kirill Reshke

---

From: Akshay Joshi <[email-masked]>
Date: Tue, 10 Mar 2026 19:14:20 +0530

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-masked]>
wrote:

> >> +                                                errmsg("value for
> option \"%s\" at position %d has invalid value \"%s\"",
> >> +                                                               name, i
> + 2, valstr),
> >>                                                  errhint("Valid values
> \"%s\" at position %d has type %s, expected type boolean or text",
> >> +                                                       name, i + 2,

---

From: Zsolt Parragi <[email-masked]>
Date: Tue, 10 Mar 2026 22:05:25 +0000

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?

---

From: Akshay Joshi <[email-masked]>
Date: Thu, 12 Mar 2026 00:26:17 +0530

> stored expressions/querytrees back to source text"

    Created the ddlutils.c file.

> on is_with_defaults or something similar?

    Shows only when `is_with_defaults` is true.

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

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