From 09508ffa3a627f3fe92efeee1ae4b932bbf34cfe Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <andrew@dunslane.net>
Date: Thu, 19 Mar 2026 09:50:41 -0400
Subject: [PATCH v4 1/4] Add infrastructure for pg_get_*_ddl functions

Add parse_ddl_options(), append_ddl_option(), and append_guc_value()
helper functions in a new ddlutils.c file that provide common option
parsing and output formatting for the pg_get_*_ddl family of functions
which will follow in later patches.  These accept VARIADIC text
arguments as alternating name/value pairs.

Callers declare an array of DdlOption descriptors specifying the
accepted option names and their types (boolean, text, or integer).
parse_ddl_options() matches each supplied pair against the array,
validates the value, and fills in the result fields.  This
descriptor-based scheme is based on an idea from Euler Taveira.

This is placed in a new ddlutils.c file which will contain the
pg_get_*_ddl functions.

Author: Akshay Joshi <akshay.joshi@enterprisedb.com>
Co-authored-by: Andrew Dunstan <andrew@dunslane.net>
Co-authored-by: Euler Taveira <euler@eulerto.com>
Discussion: https://postgr.es/m/CAKWEB6rmnmGKUA87Zmq-s=b3Scsnj02C0kObQjnbL2ajfPWGEw@mail.gmail.com
Discussion: https://postgr.es/m/4c5f895e-3281-48f8-b943-9228b7da6471@gmail.com
Discussion: https://postgr.es/m/CANxoLDc6FHBYJvcgOnZyS+jF0NUo3Lq_83-rttBuJgs9id_UDg@mail.gmail.com
Discussion: https://postgr.es/m/e247c261-e3fb-4810-81e0-a65893170e94@dunslane.net
---
 src/backend/utils/adt/Makefile    |   1 +
 src/backend/utils/adt/ddlutils.c  | 275 ++++++++++++++++++++++++++++++
 src/backend/utils/adt/meson.build |   1 +
 src/tools/pgindent/typedefs.list  |   2 +
 4 files changed, 279 insertions(+)
 create mode 100644 src/backend/utils/adt/ddlutils.c

diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index a8fd680589f..0c7621957c1 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -31,6 +31,7 @@ OBJS = \
 	datetime.o \
 	datum.o \
 	dbsize.o \
+	ddlutils.o \
 	domains.o \
 	encode.o \
 	enum.o \
diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c
new file mode 100644
index 00000000000..4bf7d9c38ae
--- /dev/null
+++ b/src/backend/utils/adt/ddlutils.c
@@ -0,0 +1,275 @@
+/*-------------------------------------------------------------------------
+ *
+ * ddlutils.c
+ *		Utility functions for generating DDL statements
+ *
+ * This file contains the pg_get_*_ddl family of functions that generate
+ * DDL statements to recreate database objects such as roles, tablespaces,
+ * and databases, along with common infrastructure for option parsing and
+ * pretty-printing.
+ *
+ * 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 "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/varlena.h"
+
+/* Option value types for DDL option parsing */
+typedef enum
+{
+	DDL_OPT_BOOL,
+	DDL_OPT_TEXT,
+	DDL_OPT_INT,
+} DdlOptType;
+
+/*
+ * A single DDL option descriptor: caller fills in name and type,
+ * parse_ddl_options fills in isset + the appropriate value field.
+ */
+typedef struct DdlOption
+{
+	const char *name;			/* option name (case-insensitive match) */
+	DdlOptType	type;			/* expected value type */
+	bool		isset;			/* true if caller supplied this option */
+	/* fields for specific option types */
+	union
+	{
+		bool		boolval;	/* filled in for DDL_OPT_BOOL */
+		char	   *textval;	/* filled in for DDL_OPT_TEXT (palloc'd) */
+		int			intval;		/* filled in for DDL_OPT_INT */
+	};
+} DdlOption;
+
+
+static void parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start,
+							  DdlOption *opts, int nopts);
+static void append_ddl_option(StringInfo buf, bool pretty, int indent,
+							  const char *fmt,...)
+			pg_attribute_printf(4, 5);
+static void append_guc_value(StringInfo buf, const char *name,
+							 const char *value);
+
+
+/*
+ * parse_ddl_options
+ * 		Parse variadic name/value option pairs
+ *
+ * Options are passed as alternating key/value text pairs.  The caller
+ * provides an array of DdlOption descriptors specifying the accepted
+ * option names and their types; this function matches each supplied
+ * pair against the array, validates the value, and fills in the
+ * result fields.
+ */
+static void
+parse_ddl_options(FunctionCallInfo fcinfo, int variadic_start,
+				  DdlOption *opts, int nopts)
+{
+	Datum	   *args;
+	bool	   *nulls;
+	Oid		   *types;
+	int			nargs;
+
+	/* Clear all output fields */
+	for (int i = 0; i < nopts; i++)
+	{
+		opts[i].isset = false;
+		switch (opts[i].type)
+		{
+			case DDL_OPT_BOOL:
+				opts[i].boolval = false;
+				break;
+			case DDL_OPT_TEXT:
+				opts[i].textval = NULL;
+				break;
+			case DDL_OPT_INT:
+				opts[i].intval = 0;
+				break;
+		}
+	}
+
+	nargs = extract_variadic_args(fcinfo, variadic_start, true,
+								  &args, &types, &nulls);
+
+	if (nargs <= 0)
+		return;
+
+	/* Handle DEFAULT NULL case */
+	if (nargs == 1 && nulls[0])
+		return;
+
+	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 each option name/value pair, find corresponding positional option
+	 * for the option name, and assign the option value.
+	 */
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *name;
+		char	   *valstr;
+		DdlOption  *opt = NULL;
+
+		if (nulls[i])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("option name at variadic position %d is null", i + 1)));
+
+		name = TextDatumGetCString(args[i]);
+
+		if (nulls[i + 1])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("value for option \"%s\" must not be null", name)));
+
+		/* Find matching option descriptor */
+		for (int j = 0; j < nopts; j++)
+		{
+			if (pg_strcasecmp(name, opts[j].name) == 0)
+			{
+				opt = &opts[j];
+				break;
+			}
+		}
+
+		if (opt == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unrecognized option: \"%s\"", name)));
+
+		if (opt->isset)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("option \"%s\" is specified more than once",
+							name)));
+
+		valstr = TextDatumGetCString(args[i + 1]);
+
+		switch (opt->type)
+		{
+			case DDL_OPT_BOOL:
+				if (!parse_bool(valstr, &opt->boolval))
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("invalid value for boolean option \"%s\": %s",
+									name, valstr)));
+				break;
+
+			case DDL_OPT_TEXT:
+				opt->textval = valstr;
+				valstr = NULL;	/* don't pfree below */
+				break;
+
+			case DDL_OPT_INT:
+				{
+					char	   *endp;
+					long		val;
+
+					errno = 0;
+					val = strtol(valstr, &endp, 10);
+					if (*endp != '\0' || errno == ERANGE ||
+						val < PG_INT32_MIN || val > PG_INT32_MAX)
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								 errmsg("invalid value for integer option \"%s\": %s",
+										name, valstr)));
+					opt->intval = (int) val;
+				}
+				break;
+		}
+
+		opt->isset = true;
+
+		if (valstr)
+			pfree(valstr);
+		pfree(name);
+	}
+}
+
+/*
+ * Helper to append a formatted string with optional pretty-printing.
+ */
+static void
+append_ddl_option(StringInfo buf, bool pretty, int indent,
+				  const char *fmt,...)
+{
+	if (pretty)
+	{
+		appendStringInfoChar(buf, '\n');
+		appendStringInfoSpaces(buf, indent);
+	}
+	else
+		appendStringInfoChar(buf, ' ');
+
+	for (;;)
+	{
+		va_list		args;
+		int			needed;
+
+		va_start(args, fmt);
+		needed = appendStringInfoVA(buf, fmt, args);
+		va_end(args);
+		if (needed == 0)
+			break;
+		enlargeStringInfo(buf, needed);
+	}
+}
+
+/*
+ * append_guc_value
+ *		Append a GUC setting value to buf, handling GUC_LIST_QUOTE properly.
+ *
+ * Variables marked GUC_LIST_QUOTE were already fully quoted before they
+ * were stored in the setconfig array.  We break the list value apart
+ * and re-quote the elements as string literals.  For all other variables
+ * we simply quote the value as a single string literal.
+ *
+ * The caller has already appended "SET <name> TO " to buf.
+ */
+static void
+append_guc_value(StringInfo buf, const char *name, const char *value)
+{
+	char	   *rawval;
+
+	rawval = pstrdup(value);
+
+	if (GetConfigOptionFlags(name, true) & GUC_LIST_QUOTE)
+	{
+		List	   *namelist;
+		bool		first = true;
+
+		/* Parse string into list of identifiers */
+		if (!SplitGUCList(rawval, ',', &namelist))
+		{
+			/* this shouldn't fail really */
+			elog(ERROR, "invalid list syntax in setconfig item");
+		}
+		/* Special case: represent an empty list as NULL */
+		if (namelist == NIL)
+			appendStringInfoString(buf, "NULL");
+		foreach_ptr(char, curname, namelist)
+		{
+			if (first)
+				first = false;
+			else
+				appendStringInfoString(buf, ", ");
+			appendStringInfoString(buf, quote_literal_cstr(curname));
+		}
+		list_free(namelist);
+	}
+	else
+		appendStringInfoString(buf, quote_literal_cstr(rawval));
+
+	pfree(rawval);
+}
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index fb8294d7e4a..d793f8145f6 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -30,6 +30,7 @@ backend_sources += files(
   'datetime.c',
   'datum.c',
   'dbsize.c',
+  'ddlutils.c',
   'domains.c',
   'encode.c',
   'enum.c',
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 5bc517602b1..c1a5948b191 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -626,6 +626,8 @@ DSMREntryType
 DSMRegistryCtxStruct
 DSMRegistryEntry
 DWORD
+DdlOptType
+DdlOption
 DataDirSyncMethod
 DataDumperPtr
 DataPageDeleteStack
-- 
2.43.0

