1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00

Revert pg_get_role_ddl(), pg_get_tablespace_ddl(), and pg_get_database_ddl().

List of commits reverted:

  4881981f92  Add infrastructure for pg_get_*_ddl functions
  76e514ebb4  Add pg_get_role_ddl() function
  b99fd9fd7f  Add pg_get_tablespace_ddl() function
  a4f774cf1c  Add pg_get_database_ddl() function
  6c7bce28c8  Fixups for a4f774cf1c
  1f108fc02e  Fix pfree crash in pg_get_role_ddl() and pg_get_database_ddl().
  cda0c4c5d6  Reject invalid databases in pg_get_database_ddl()
  79fba6ebab  doc: Fix missing role attribute in pg_get_tablespace_ddl() description.
  c529ee38b9  Convert ddlutils regression tests to TAP tests.
  df1bac400f  Fix timezone dependence in test_misc/012_ddlutils.pl
  5642a0367c  Avoid SIGSEGV in pg_get_database_ddl() on NULL tablespace
  2af70e9374  Fix incorrect declarations of variadic pg_get_*_ddl() functions.
  d6ed87d198  Use named boolean parameters for pg_get_*_ddl option arguments

plus the corresponding release notes entries.

Discussion: https://postgr.es/m/20260827015242.54.noahmisch@microsoft.com
This commit is contained in:
Andrew Dunstan
2026-09-12 11:54:41 -04:00
parent e14a2354d9
commit db169985c1
10 changed files with 3 additions and 1500 deletions

View File

@@ -3843,122 +3843,4 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres}
</sect2>
<sect2 id="functions-get-object-ddl">
<title>Get Object DDL Functions</title>
<para>
The functions shown in <xref linkend="functions-get-object-ddl-table"/>
reconstruct DDL statements for various global database objects.
Each function returns a set of text rows, one SQL statement per row.
(This is a decompiled reconstruction, not the original text of the
command.)
</para>
<table id="functions-get-object-ddl-table">
<title>Get Object DDL Functions</title>
<tgroup cols="1">
<thead>
<row>
<entry role="func_table_entry"><para role="func_signature">
Function
</para>
<para>
Description
</para></entry>
</row>
</thead>
<tbody>
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
<primary>pg_get_database_ddl</primary>
</indexterm>
<function>pg_get_database_ddl</function>
( <parameter>database</parameter> <type>regdatabase</type>
<optional>, <parameter>pretty</parameter> <type>boolean</type>
<literal>DEFAULT</literal> false</optional>
<optional>, <parameter>owner</parameter> <type>boolean</type>
<literal>DEFAULT</literal> true</optional>
<optional>, <parameter>tablespace</parameter> <type>boolean</type>
<literal>DEFAULT</literal> true</optional> )
<returnvalue>setof text</returnvalue>
</para>
<para>
Reconstructs the <link linkend="sql-createdatabase"><command>CREATE
DATABASE</command></link> statement for the specified database,
followed by <link linkend="sql-alterdatabase"><command>ALTER DATABASE
</command></link> statements for connection limit, template status,
and configuration settings. Each statement is returned as a separate
row. When <parameter>pretty</parameter> is true, the output is
pretty-printed. When <parameter>owner</parameter> is false, the
<literal>OWNER</literal> clause is omitted. When
<parameter>tablespace</parameter> is false, the
<literal>TABLESPACE</literal> clause is omitted.
</para></entry>
</row>
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
<primary>pg_get_role_ddl</primary>
</indexterm>
<function>pg_get_role_ddl</function>
( <parameter>role</parameter> <type>regrole</type>
<optional>, <parameter>pretty</parameter> <type>boolean</type>
<literal>DEFAULT</literal> false</optional>
<optional>, <parameter>memberships</parameter> <type>boolean</type>
<literal>DEFAULT</literal> true</optional> )
<returnvalue>setof text</returnvalue>
</para>
<para>
Reconstructs the <link linkend="sql-createrole"><command>CREATE ROLE
</command></link> statement and any <link linkend="sql-alterrole">
<command>ALTER ROLE ... SET</command></link> statements for the given
role. Each statement is returned as a separate row.
Password information is never included in the output.
When <parameter>pretty</parameter> is true, the output is
pretty-printed. When <parameter>memberships</parameter> is false,
<link linkend="sql-grant"><command>GRANT</command></link> statements
for role memberships are omitted.
</para></entry>
</row>
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
<primary>pg_get_tablespace_ddl</primary>
</indexterm>
<function>pg_get_tablespace_ddl</function>
( <parameter>tablespace</parameter> <type>oid</type>
<optional>, <parameter>pretty</parameter> <type>boolean</type>
<literal>DEFAULT</literal> false</optional>
<optional>, <parameter>owner</parameter> <type>boolean</type>
<literal>DEFAULT</literal> true</optional> )
<returnvalue>setof text</returnvalue>
</para>
<para role="func_signature">
<function>pg_get_tablespace_ddl</function>
( <parameter>tablespace</parameter> <type>name</type>
<optional>, <parameter>pretty</parameter> <type>boolean</type>
<literal>DEFAULT</literal> false</optional>
<optional>, <parameter>owner</parameter> <type>boolean</type>
<literal>DEFAULT</literal> true</optional> )
<returnvalue>setof text</returnvalue>
</para>
<para>
Reconstructs the <link linkend="sql-createtablespace"><command>CREATE
TABLESPACE</command></link> statement for the specified tablespace (by
OID or name). If the tablespace has options set, an
<link linkend="sql-altertablespace"><command>ALTER TABLESPACE ... SET
</command></link> statement is also returned. Each statement is
returned as a separate row. When <parameter>pretty</parameter> is
true, the output is pretty-printed. When <parameter>owner</parameter>
is false, the <literal>OWNER</literal> clause is omitted.
</para></entry>
</row>
</tbody>
</tgroup>
</table>
</sect2>
</sect1>

View File

@@ -2278,42 +2278,6 @@ The Dutch stemmer has also been updated. The old Dutch stemmer is available via
</para>
</listitem>
<!--
Author: Andrew Dunstan <andrew@dunslane.net>
2026-04-05 [76e514ebb] Add pg_get_role_ddl() function
-->
<listitem>
<para>
Add function <link linkend="functions-get-object-ddl-table"><function>pg_get_role_ddl()</function></link> to output role creation commands (Mario Gonzalez, Bryan Green, Andrew Dunstan, Euler Taveira)
<ulink url="&commit_baseurl;76e514ebb">&sect;</ulink>
</para>
</listitem>
<!--
Author: Andrew Dunstan <andrew@dunslane.net>
2026-04-05 [b99fd9fd7] Add pg_get_tablespace_ddl() function
-->
<listitem>
<para>
Add function <link linkend="functions-get-object-ddl-table"><function>pg_get_tablespace_ddl()</function></link> to output tablespace creation commands (Nishant Sharma, Manni Wood, Andrew Dunstan, Euler Taveira)
<ulink url="&commit_baseurl;b99fd9fd7">&sect;</ulink>
</para>
</listitem>
<!--
Author: Andrew Dunstan <andrew@dunslane.net>
2026-04-05 [a4f774cf1] Add pg_get_database_ddl() function
-->
<listitem>
<para>
Add function <link linkend="functions-get-object-ddl-table"><function>pg_get_database_ddl()</function></link> to output database creation commands (Akshay Joshi, Andrew Dunstan, Euler Taveira)
<ulink url="&commit_baseurl;a4f774cf1">&sect;</ulink>
</para>
</listitem>
<!--
Author: Peter Eisentraut <peter@eisentraut.org>
2025-08-21 [53eff471c] PL/Python: Add event trigger support

View File

@@ -1,977 +0,0 @@
/*-------------------------------------------------------------------------
*
* 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 "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/pg_auth_members.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_database.h"
#include "catalog/pg_db_role_setting.h"
#include "catalog/pg_tablespace.h"
#include "commands/tablespace.h"
#include "common/relpath.h"
#include "funcapi.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/datetime.h"
#include "utils/fmgroids.h"
#include "utils/guc.h"
#include "utils/lsyscache.h"
#include "utils/pg_locale.h"
#include "utils/rel.h"
#include "utils/ruleutils.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
#include "utils/varlena.h"
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);
static List *pg_get_role_ddl_internal(Oid roleid, bool pretty,
bool memberships);
static List *pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner);
static Datum pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid);
static List *pg_get_database_ddl_internal(Oid dbid, bool pretty,
bool no_owner, bool no_tablespace);
/*
* 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);
}
/*
* pg_get_role_ddl_internal
* Generate DDL statements to recreate a role
*
* Returns a List of palloc'd strings, each being a complete SQL statement.
* The first list element is always the CREATE ROLE statement; subsequent
* elements are ALTER ROLE SET statements for any role-specific or
* role-in-database configuration settings. If memberships is true,
* GRANT statements for role memberships are appended.
*/
static List *
pg_get_role_ddl_internal(Oid roleid, bool pretty, bool memberships)
{
HeapTuple tuple;
Form_pg_authid roleform;
StringInfoData buf;
char *rolname;
Datum rolevaliduntil;
bool isnull;
Relation rel;
ScanKeyData scankey;
SysScanDesc scan;
List *statements = NIL;
tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("role with OID %u does not exist", roleid)));
roleform = (Form_pg_authid) GETSTRUCT(tuple);
rolname = pstrdup(NameStr(roleform->rolname));
/* User must have SELECT privilege on pg_authid. */
if (pg_class_aclcheck(AuthIdRelationId, GetUserId(), ACL_SELECT) != ACLCHECK_OK)
{
ReleaseSysCache(tuple);
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied for role %s", rolname)));
}
/*
* We don't support generating DDL for system roles. The primary reason
* for this is that users shouldn't be recreating them.
*/
if (IsReservedName(rolname))
ereport(ERROR,
(errcode(ERRCODE_RESERVED_NAME),
errmsg("role name \"%s\" is reserved", rolname),
errdetail("Role names starting with \"pg_\" are reserved for system roles.")));
initStringInfo(&buf);
appendStringInfo(&buf, "CREATE ROLE %s", quote_identifier(rolname));
/*
* Append role attributes. The order here follows the same sequence as
* you'd typically write them in a CREATE ROLE command, though any order
* is actually acceptable to the parser.
*/
append_ddl_option(&buf, pretty, 4, "%s",
roleform->rolsuper ? "SUPERUSER" : "NOSUPERUSER");
append_ddl_option(&buf, pretty, 4, "%s",
roleform->rolinherit ? "INHERIT" : "NOINHERIT");
append_ddl_option(&buf, pretty, 4, "%s",
roleform->rolcreaterole ? "CREATEROLE" : "NOCREATEROLE");
append_ddl_option(&buf, pretty, 4, "%s",
roleform->rolcreatedb ? "CREATEDB" : "NOCREATEDB");
append_ddl_option(&buf, pretty, 4, "%s",
roleform->rolcanlogin ? "LOGIN" : "NOLOGIN");
append_ddl_option(&buf, pretty, 4, "%s",
roleform->rolreplication ? "REPLICATION" : "NOREPLICATION");
append_ddl_option(&buf, pretty, 4, "%s",
roleform->rolbypassrls ? "BYPASSRLS" : "NOBYPASSRLS");
/*
* CONNECTION LIMIT is only interesting if it's not -1 (the default,
* meaning no limit).
*/
if (roleform->rolconnlimit >= 0)
append_ddl_option(&buf, pretty, 4, "CONNECTION LIMIT %d",
roleform->rolconnlimit);
rolevaliduntil = SysCacheGetAttr(AUTHOID, tuple,
Anum_pg_authid_rolvaliduntil,
&isnull);
if (!isnull)
{
TimestampTz ts;
int tz;
struct pg_tm tm;
fsec_t fsec;
const char *tzn;
char ts_str[MAXDATELEN + 1];
ts = DatumGetTimestampTz(rolevaliduntil);
if (TIMESTAMP_NOT_FINITE(ts))
EncodeSpecialTimestamp(ts, ts_str);
else if (timestamp2tm(ts, &tz, &tm, &fsec, &tzn, NULL) == 0)
EncodeDateTime(&tm, fsec, true, tz, tzn, USE_ISO_DATES, ts_str);
else
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("timestamp out of range")));
append_ddl_option(&buf, pretty, 4, "VALID UNTIL %s",
quote_literal_cstr(ts_str));
}
ReleaseSysCache(tuple);
/*
* We intentionally omit PASSWORD. There's no way to retrieve the
* original password text from the stored hash, and even if we could,
* exposing passwords through a SQL function would be a security issue.
* Users must set passwords separately after recreating roles.
*/
appendStringInfoChar(&buf, ';');
statements = lappend(statements, pstrdup(buf.data));
/*
* Now scan pg_db_role_setting for ALTER ROLE SET configurations.
*
* These can be role-wide (setdatabase = 0) or specific to a particular
* database (setdatabase = a valid DB OID). It generates one ALTER
* statement per setting.
*/
rel = table_open(DbRoleSettingRelationId, AccessShareLock);
ScanKeyInit(&scankey,
Anum_pg_db_role_setting_setrole,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(roleid));
scan = systable_beginscan(rel, DbRoleSettingDatidRolidIndexId, true,
NULL, 1, &scankey);
while (HeapTupleIsValid(tuple = systable_getnext(scan)))
{
Form_pg_db_role_setting setting = (Form_pg_db_role_setting) GETSTRUCT(tuple);
Oid datid = setting->setdatabase;
Datum datum;
ArrayType *role_settings;
Datum *settings;
bool *nulls;
int nsettings;
char *datname = NULL;
/*
* If setdatabase is valid, this is a role-in-database setting;
* otherwise it's a role-wide setting. Look up the database name once
* for all settings in this row.
*/
if (OidIsValid(datid))
{
datname = get_database_name(datid);
/* Database has been dropped; skip all settings in this row. */
if (datname == NULL)
continue;
}
/*
* The setconfig column is a text array in "name=value" format. It
* should never be null for a valid row, but be defensive.
*/
datum = heap_getattr(tuple, Anum_pg_db_role_setting_setconfig,
RelationGetDescr(rel), &isnull);
if (isnull)
continue;
role_settings = DatumGetArrayTypePCopy(datum);
deconstruct_array_builtin(role_settings, TEXTOID, &settings, &nulls, &nsettings);
for (int i = 0; i < nsettings; i++)
{
char *s,
*p;
if (nulls[i])
continue;
s = TextDatumGetCString(settings[i]);
p = strchr(s, '=');
if (p == NULL)
{
pfree(s);
continue;
}
*p++ = '\0';
/* Build a fresh ALTER ROLE statement for this setting */
resetStringInfo(&buf);
appendStringInfo(&buf, "ALTER ROLE %s", quote_identifier(rolname));
if (datname != NULL)
appendStringInfo(&buf, " IN DATABASE %s",
quote_identifier(datname));
appendStringInfo(&buf, " SET %s TO ",
quote_identifier(s));
append_guc_value(&buf, s, p);
appendStringInfoChar(&buf, ';');
statements = lappend(statements, pstrdup(buf.data));
pfree(s);
}
pfree(settings);
pfree(nulls);
pfree(role_settings);
if (datname != NULL)
pfree(datname);
}
systable_endscan(scan);
table_close(rel, AccessShareLock);
/*
* Scan pg_auth_members for role memberships. We look for rows where
* member = roleid, meaning this role has been granted membership in other
* roles.
*/
if (memberships)
{
rel = table_open(AuthMemRelationId, AccessShareLock);
ScanKeyInit(&scankey,
Anum_pg_auth_members_member,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(roleid));
scan = systable_beginscan(rel, AuthMemMemRoleIndexId, true,
NULL, 1, &scankey);
while (HeapTupleIsValid(tuple = systable_getnext(scan)))
{
Form_pg_auth_members memform = (Form_pg_auth_members) GETSTRUCT(tuple);
char *granted_role;
char *grantor;
granted_role = GetUserNameFromId(memform->roleid, false);
grantor = GetUserNameFromId(memform->grantor, false);
resetStringInfo(&buf);
appendStringInfo(&buf, "GRANT %s TO %s",
quote_identifier(granted_role),
quote_identifier(rolname));
appendStringInfo(&buf, " WITH ADMIN %s, INHERIT %s, SET %s",
memform->admin_option ? "TRUE" : "FALSE",
memform->inherit_option ? "TRUE" : "FALSE",
memform->set_option ? "TRUE" : "FALSE");
appendStringInfo(&buf, " GRANTED BY %s;",
quote_identifier(grantor));
statements = lappend(statements, pstrdup(buf.data));
pfree(granted_role);
pfree(grantor);
}
systable_endscan(scan);
table_close(rel, AccessShareLock);
}
pfree(buf.data);
pfree(rolname);
return statements;
}
/*
* pg_get_role_ddl
* Return DDL to recreate a role as a set of text rows.
*
* Each row is a complete SQL statement. The first row is always the
* CREATE ROLE statement; subsequent rows are ALTER ROLE SET statements
* and optionally GRANT statements for role memberships.
*/
Datum
pg_get_role_ddl(PG_FUNCTION_ARGS)
{
FuncCallContext *funcctx;
List *statements;
if (SRF_IS_FIRSTCALL())
{
MemoryContext oldcontext;
Oid roleid;
bool pretty;
bool memberships;
funcctx = SRF_FIRSTCALL_INIT();
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
roleid = PG_GETARG_OID(0);
pretty = PG_GETARG_BOOL(1);
memberships = PG_GETARG_BOOL(2);
statements = pg_get_role_ddl_internal(roleid, pretty, memberships);
funcctx->user_fctx = statements;
funcctx->max_calls = list_length(statements);
MemoryContextSwitchTo(oldcontext);
}
funcctx = SRF_PERCALL_SETUP();
statements = (List *) funcctx->user_fctx;
if (funcctx->call_cntr < funcctx->max_calls)
{
char *stmt;
stmt = list_nth(statements, funcctx->call_cntr);
SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
}
else
{
list_free_deep(statements);
SRF_RETURN_DONE(funcctx);
}
}
/*
* pg_get_tablespace_ddl_internal
* Generate DDL statements to recreate a tablespace.
*
* Returns a List of palloc'd strings. The first element is the
* CREATE TABLESPACE statement; if the tablespace has reloptions,
* a second element with ALTER TABLESPACE SET (...) is appended.
*/
static List *
pg_get_tablespace_ddl_internal(Oid tsid, bool pretty, bool no_owner)
{
HeapTuple tuple;
Form_pg_tablespace tspForm;
StringInfoData buf;
char *spcname;
char *spcowner;
char *path;
bool isNull;
Datum datum;
List *statements = NIL;
tuple = SearchSysCache1(TABLESPACEOID, ObjectIdGetDatum(tsid));
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("tablespace with OID %u does not exist",
tsid)));
tspForm = (Form_pg_tablespace) GETSTRUCT(tuple);
spcname = pstrdup(NameStr(tspForm->spcname));
/* User must have SELECT privilege on pg_tablespace. */
if (pg_class_aclcheck(TableSpaceRelationId, GetUserId(), ACL_SELECT) != ACLCHECK_OK)
{
ReleaseSysCache(tuple);
aclcheck_error(ACLCHECK_NO_PRIV, OBJECT_TABLESPACE, spcname);
}
/*
* We don't support generating DDL for system tablespaces. The primary
* reason for this is that users shouldn't be recreating them.
*/
if (IsReservedName(spcname))
ereport(ERROR,
(errcode(ERRCODE_RESERVED_NAME),
errmsg("tablespace name \"%s\" is reserved", spcname),
errdetail("Tablespace names starting with \"pg_\" are reserved for system tablespaces.")));
initStringInfo(&buf);
/* Start building the CREATE TABLESPACE statement */
appendStringInfo(&buf, "CREATE TABLESPACE %s", quote_identifier(spcname));
/* Add OWNER clause */
if (!no_owner)
{
spcowner = GetUserNameFromId(tspForm->spcowner, false);
append_ddl_option(&buf, pretty, 4, "OWNER %s",
quote_identifier(spcowner));
pfree(spcowner);
}
/* Find tablespace directory path */
path = get_tablespace_location(tsid);
/* Add directory LOCATION (path), if it exists */
if (path[0] != '\0')
{
/*
* Special case: if the tablespace was created with GUC
* "allow_in_place_tablespaces = true" and "LOCATION ''", path will
* begin with "pg_tblspc/". In that case, show "LOCATION ''" as the
* user originally specified.
*/
if (strncmp(PG_TBLSPC_DIR_SLASH, path, strlen(PG_TBLSPC_DIR_SLASH)) == 0)
append_ddl_option(&buf, pretty, 4, "LOCATION ''");
else
append_ddl_option(&buf, pretty, 4, "LOCATION %s",
quote_literal_cstr(path));
}
pfree(path);
appendStringInfoChar(&buf, ';');
statements = lappend(statements, pstrdup(buf.data));
/* Check for tablespace options */
datum = SysCacheGetAttr(TABLESPACEOID, tuple,
Anum_pg_tablespace_spcoptions, &isNull);
if (!isNull)
{
resetStringInfo(&buf);
appendStringInfo(&buf, "ALTER TABLESPACE %s SET (",
quote_identifier(spcname));
get_reloptions(&buf, datum);
appendStringInfoString(&buf, ");");
statements = lappend(statements, pstrdup(buf.data));
}
ReleaseSysCache(tuple);
pfree(spcname);
pfree(buf.data);
return statements;
}
/*
* pg_get_tablespace_ddl_srf - common SRF logic for tablespace DDL
*/
static Datum
pg_get_tablespace_ddl_srf(FunctionCallInfo fcinfo, Oid tsid)
{
FuncCallContext *funcctx;
List *statements;
if (SRF_IS_FIRSTCALL())
{
MemoryContext oldcontext;
bool pretty;
bool no_owner;
funcctx = SRF_FIRSTCALL_INIT();
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
pretty = PG_GETARG_BOOL(1);
no_owner = !PG_GETARG_BOOL(2);
statements = pg_get_tablespace_ddl_internal(tsid, pretty, no_owner);
funcctx->user_fctx = statements;
funcctx->max_calls = list_length(statements);
MemoryContextSwitchTo(oldcontext);
}
funcctx = SRF_PERCALL_SETUP();
statements = (List *) funcctx->user_fctx;
if (funcctx->call_cntr < funcctx->max_calls)
{
char *stmt;
stmt = (char *) list_nth(statements, funcctx->call_cntr);
SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
}
else
{
list_free_deep(statements);
SRF_RETURN_DONE(funcctx);
}
}
/*
* pg_get_tablespace_ddl_oid
* Return DDL to recreate a tablespace, taking OID.
*/
Datum
pg_get_tablespace_ddl_oid(PG_FUNCTION_ARGS)
{
Oid tsid = PG_GETARG_OID(0);
return pg_get_tablespace_ddl_srf(fcinfo, tsid);
}
/*
* pg_get_tablespace_ddl_name
* Return DDL to recreate a tablespace, taking name.
*/
Datum
pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS)
{
Name tspname = PG_GETARG_NAME(0);
Oid tsid = get_tablespace_oid(NameStr(*tspname), false);
return pg_get_tablespace_ddl_srf(fcinfo, tsid);
}
/*
* pg_get_database_ddl_internal
* Generate DDL statements to recreate a database.
*
* Returns a List of palloc'd strings. The first element is the
* CREATE DATABASE statement; subsequent elements are ALTER DATABASE
* statements for properties and configuration settings.
*/
static List *
pg_get_database_ddl_internal(Oid dbid, bool pretty,
bool no_owner, bool no_tablespace)
{
HeapTuple tuple;
Form_pg_database dbform;
StringInfoData buf;
bool isnull;
Datum datum;
const char *encoding;
char *dbname;
char *collate;
char *ctype;
Relation rel;
ScanKeyData scankey[2];
SysScanDesc scan;
List *statements = NIL;
AclResult aclresult;
tuple = SearchSysCache1(DATABASEOID, ObjectIdGetDatum(dbid));
if (!HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("database with OID %u does not exist", dbid)));
/* User must have connect privilege for target database. */
aclresult = object_aclcheck(DatabaseRelationId, dbid, GetUserId(), ACL_CONNECT);
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, OBJECT_DATABASE,
get_database_name(dbid));
dbform = (Form_pg_database) GETSTRUCT(tuple);
dbname = pstrdup(NameStr(dbform->datname));
/*
* Reject invalid databases. Deparsing a pg_database row in invalid state
* can produce SQL that is not executable, such as CONNECTION LIMIT = -2.
*/
if (database_is_invalid_form(dbform))
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot generate DDL for invalid database \"%s\"",
dbname)));
/*
* We don't support generating DDL for system databases. The primary
* reason for this is that users shouldn't be recreating them.
*/
if (strcmp(dbname, "template0") == 0 || strcmp(dbname, "template1") == 0)
ereport(ERROR,
(errcode(ERRCODE_RESERVED_NAME),
errmsg("database \"%s\" is a system database", dbname),
errdetail("DDL generation is not supported for template0 and template1.")));
initStringInfo(&buf);
/* --- Build CREATE DATABASE statement --- */
appendStringInfo(&buf, "CREATE DATABASE %s", quote_identifier(dbname));
/*
* Always use template0: the target database already contains the catalog
* data from whatever template was used originally, so we must start from
* the pristine template to avoid duplication.
*/
append_ddl_option(&buf, pretty, 4, "WITH TEMPLATE = template0");
/* ENCODING */
encoding = pg_encoding_to_char(dbform->encoding);
if (strlen(encoding) > 0)
append_ddl_option(&buf, pretty, 4, "ENCODING = %s",
quote_literal_cstr(encoding));
/* LOCALE_PROVIDER */
if (dbform->datlocprovider == COLLPROVIDER_BUILTIN ||
dbform->datlocprovider == COLLPROVIDER_ICU ||
dbform->datlocprovider == COLLPROVIDER_LIBC)
append_ddl_option(&buf, pretty, 4, "LOCALE_PROVIDER = %s",
collprovider_name(dbform->datlocprovider));
else
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("unrecognized locale provider: %c",
dbform->datlocprovider)));
/* LOCALE, LC_COLLATE, LC_CTYPE */
datum = SysCacheGetAttr(DATABASEOID, tuple,
Anum_pg_database_datcollate, &isnull);
collate = isnull ? NULL : TextDatumGetCString(datum);
datum = SysCacheGetAttr(DATABASEOID, tuple,
Anum_pg_database_datctype, &isnull);
ctype = isnull ? NULL : TextDatumGetCString(datum);
if (collate != NULL && ctype != NULL && strcmp(collate, ctype) == 0)
{
append_ddl_option(&buf, pretty, 4, "LOCALE = %s",
quote_literal_cstr(collate));
}
else
{
if (collate != NULL)
append_ddl_option(&buf, pretty, 4, "LC_COLLATE = %s",
quote_literal_cstr(collate));
if (ctype != NULL)
append_ddl_option(&buf, pretty, 4, "LC_CTYPE = %s",
quote_literal_cstr(ctype));
}
/* LOCALE (provider-specific) */
datum = SysCacheGetAttr(DATABASEOID, tuple,
Anum_pg_database_datlocale, &isnull);
if (!isnull)
{
const char *locale = TextDatumGetCString(datum);
if (dbform->datlocprovider == COLLPROVIDER_BUILTIN)
append_ddl_option(&buf, pretty, 4, "BUILTIN_LOCALE = %s",
quote_literal_cstr(locale));
else if (dbform->datlocprovider == COLLPROVIDER_ICU)
append_ddl_option(&buf, pretty, 4, "ICU_LOCALE = %s",
quote_literal_cstr(locale));
}
/* ICU_RULES */
datum = SysCacheGetAttr(DATABASEOID, tuple,
Anum_pg_database_daticurules, &isnull);
if (!isnull && dbform->datlocprovider == COLLPROVIDER_ICU)
append_ddl_option(&buf, pretty, 4, "ICU_RULES = %s",
quote_literal_cstr(TextDatumGetCString(datum)));
/* TABLESPACE */
if (!no_tablespace && OidIsValid(dbform->dattablespace))
{
char *spcname = get_tablespace_name(dbform->dattablespace);
if (spcname == NULL)
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("tablespace with OID %u does not exist",
dbform->dattablespace),
errdetail("It may have been concurrently dropped.")));
if (pg_strcasecmp(spcname, "pg_default") != 0)
append_ddl_option(&buf, pretty, 4, "TABLESPACE = %s",
quote_identifier(spcname));
}
appendStringInfoChar(&buf, ';');
statements = lappend(statements, pstrdup(buf.data));
/* OWNER */
if (!no_owner && OidIsValid(dbform->datdba))
{
char *owner = GetUserNameFromId(dbform->datdba, false);
resetStringInfo(&buf);
appendStringInfo(&buf, "ALTER DATABASE %s OWNER TO %s;",
quote_identifier(dbname), quote_identifier(owner));
pfree(owner);
statements = lappend(statements, pstrdup(buf.data));
}
/* CONNECTION LIMIT */
if (dbform->datconnlimit != -1)
{
resetStringInfo(&buf);
appendStringInfo(&buf, "ALTER DATABASE %s CONNECTION LIMIT = %d;",
quote_identifier(dbname), dbform->datconnlimit);
statements = lappend(statements, pstrdup(buf.data));
}
/* IS_TEMPLATE */
if (dbform->datistemplate)
{
resetStringInfo(&buf);
appendStringInfo(&buf, "ALTER DATABASE %s IS_TEMPLATE = true;",
quote_identifier(dbname));
statements = lappend(statements, pstrdup(buf.data));
}
/* ALLOW_CONNECTIONS */
if (!dbform->datallowconn)
{
resetStringInfo(&buf);
appendStringInfo(&buf, "ALTER DATABASE %s ALLOW_CONNECTIONS = false;",
quote_identifier(dbname));
statements = lappend(statements, pstrdup(buf.data));
}
ReleaseSysCache(tuple);
/*
* Now scan pg_db_role_setting for ALTER DATABASE SET configurations.
*
* It is only database-wide (setrole = 0). It generates one ALTER
* statement per setting.
*/
rel = table_open(DbRoleSettingRelationId, AccessShareLock);
ScanKeyInit(&scankey[0],
Anum_pg_db_role_setting_setdatabase,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(dbid));
ScanKeyInit(&scankey[1],
Anum_pg_db_role_setting_setrole,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(InvalidOid));
scan = systable_beginscan(rel, DbRoleSettingDatidRolidIndexId, true,
NULL, 2, scankey);
while (HeapTupleIsValid(tuple = systable_getnext(scan)))
{
ArrayType *dbconfig;
Datum *settings;
bool *nulls;
int nsettings;
/*
* The setconfig column is a text array in "name=value" format. It
* should never be null for a valid row, but be defensive.
*/
datum = heap_getattr(tuple, Anum_pg_db_role_setting_setconfig,
RelationGetDescr(rel), &isnull);
if (isnull)
continue;
dbconfig = DatumGetArrayTypePCopy(datum);
deconstruct_array_builtin(dbconfig, TEXTOID, &settings, &nulls, &nsettings);
for (int i = 0; i < nsettings; i++)
{
char *s,
*p;
if (nulls[i])
continue;
s = TextDatumGetCString(settings[i]);
p = strchr(s, '=');
if (p == NULL)
{
pfree(s);
continue;
}
*p++ = '\0';
resetStringInfo(&buf);
appendStringInfo(&buf, "ALTER DATABASE %s SET %s TO ",
quote_identifier(dbname),
quote_identifier(s));
append_guc_value(&buf, s, p);
appendStringInfoChar(&buf, ';');
statements = lappend(statements, pstrdup(buf.data));
pfree(s);
}
pfree(settings);
pfree(nulls);
pfree(dbconfig);
}
systable_endscan(scan);
table_close(rel, AccessShareLock);
pfree(buf.data);
pfree(dbname);
return statements;
}
/*
* pg_get_database_ddl
* Return DDL to recreate a database as a set of text rows.
*/
Datum
pg_get_database_ddl(PG_FUNCTION_ARGS)
{
FuncCallContext *funcctx;
List *statements;
if (SRF_IS_FIRSTCALL())
{
MemoryContext oldcontext;
Oid dbid;
bool pretty;
bool no_owner;
bool no_tablespace;
funcctx = SRF_FIRSTCALL_INIT();
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
dbid = PG_GETARG_OID(0);
pretty = PG_GETARG_BOOL(1);
no_owner = !PG_GETARG_BOOL(2);
no_tablespace = !PG_GETARG_BOOL(3);
statements = pg_get_database_ddl_internal(dbid, pretty, no_owner,
no_tablespace);
funcctx->user_fctx = statements;
funcctx->max_calls = list_length(statements);
MemoryContextSwitchTo(oldcontext);
}
funcctx = SRF_PERCALL_SETUP();
statements = (List *) funcctx->user_fctx;
if (funcctx->call_cntr < funcctx->max_calls)
{
char *stmt;
stmt = list_nth(statements, funcctx->call_cntr);
SRF_RETURN_NEXT(funcctx, CStringGetTextDatum(stmt));
}
else
{
list_free_deep(statements);
SRF_RETURN_DONE(funcctx);
}
}

View File

@@ -30,7 +30,6 @@ backend_sources += files(
'datetime.c',
'datum.c',
'dbsize.c',
'ddlutils.c',
'domains.c',
'encode.c',
'enum.c',

View File

@@ -542,7 +542,7 @@ static void add_cast_to(StringInfo buf, Oid typid);
static char *generate_qualified_type_name(Oid typid);
static text *string_to_text(char *str);
static char *flatten_reloptions(Oid relid);
void get_reloptions(StringInfo buf, Datum reloptions);
static void get_reloptions(StringInfo buf, Datum reloptions);
static void get_json_path_spec(Node *path_spec, deparse_context *context,
bool showimplicit);
static void get_json_table_columns(TableFunc *tf, JsonTablePathScan *scan,
@@ -13726,7 +13726,7 @@ string_to_text(char *str)
/*
* Generate a C string representing a relation options from text[] datum.
*/
void
static void
get_reloptions(StringInfo buf, Datum reloptions)
{
Datum *options;

View File

@@ -57,6 +57,6 @@
*/
/* yyyymmddN */
#define CATALOG_VERSION_NO 202609102
#define CATALOG_VERSION_NO 202609131
#endif

View File

@@ -8587,28 +8587,6 @@
{ oid => '2508', descr => 'constraint description with pretty-print option',
proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text',
proargtypes => 'oid bool', prosrc => 'pg_get_constraintdef_ext' },
{ oid => '6501', descr => 'get DDL to recreate a role',
proname => 'pg_get_role_ddl', prorows => '10', proretset => 't',
provolatile => 's', pronargdefaults => '2', prorettype => 'text',
proargtypes => 'regrole bool bool',
proargnames => '{role,pretty,memberships}', proargdefaults => '{false,true}',
prosrc => 'pg_get_role_ddl' },
{ oid => '6499', descr => 'get DDL to recreate a tablespace',
proname => 'pg_get_tablespace_ddl', prorows => '10', proretset => 't',
provolatile => 's', pronargdefaults => '2', prorettype => 'text',
proargtypes => 'oid bool bool', proargnames => '{tablespace,pretty,owner}',
proargdefaults => '{false,true}', prosrc => 'pg_get_tablespace_ddl_oid' },
{ oid => '6500', descr => 'get DDL to recreate a tablespace',
proname => 'pg_get_tablespace_ddl', prorows => '10', proretset => 't',
provolatile => 's', pronargdefaults => '2', prorettype => 'text',
proargtypes => 'name bool bool', proargnames => '{tablespace,pretty,owner}',
proargdefaults => '{false,true}', prosrc => 'pg_get_tablespace_ddl_name' },
{ oid => '6502', descr => 'get DDL to recreate a database',
proname => 'pg_get_database_ddl', prorows => '10', proretset => 't',
provolatile => 's', pronargdefaults => '3', prorettype => 'text',
proargtypes => 'regdatabase bool bool bool',
proargnames => '{database,pretty,owner,tablespace}',
proargdefaults => '{false,true,true}', prosrc => 'pg_get_database_ddl' },
{ oid => '2509',
descr => 'deparse an encoded expression with pretty-print option',
proname => 'pg_get_expr', provolatile => 's', prorettype => 'text',

View File

@@ -51,7 +51,6 @@ extern char *get_window_frame_options_for_explain(int frameOptions,
extern char *generate_collation_name(Oid collid);
extern char *generate_opclass_name(Oid opclass);
extern char *get_range_partbound_string(List *bound_datums);
extern void get_reloptions(StringInfo buf, Datum reloptions);
extern char *pg_get_statisticsobjdef_string(Oid statextid);

View File

@@ -20,7 +20,6 @@ tests += {
't/009_log_temp_files.pl',
't/010_index_concurrently_upsert.pl',
't/011_lock_stats.pl',
't/012_ddlutils.pl',
't/013_temp_obj_multisession.pl',
't/015_temp_schema_exit_deferrable.pl',
],

View File

@@ -1,341 +0,0 @@
# Copyright (c) 2026, PostgreSQL Global Development Group
# Tests for pg_get_database_ddl(), pg_get_tablespace_ddl(), and
# pg_get_role_ddl(). These are TAP tests rather than plain regression
# tests because they create databases and tablespaces, which are
# heavyweight operations that should run only once rather than being
# repeated with every invocation of the core regression suite.
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
my $node = PostgreSQL::Test::Cluster->new('main');
$node->init;
# Force UTC so that timestamptz values (e.g. VALID UNTIL) render the same
# way regardless of the host's local timezone.
$node->append_conf('postgresql.conf', "timezone = 'UTC'\n");
$node->start;
# Perl helper that strips locale/collation details from DDL output so
# that results are stable across platforms.
sub ddl_filter
{
my ($text) = @_;
$text =~ s/\s*\bLOCALE_PROVIDER\b\s*=\s*(?:'[^']*'|"[^"]*"|\S+)//gi;
$text =~ s/\s*LC_COLLATE\s*=\s*(['"])[^'"]*\1//gi;
$text =~ s/\s*LC_CTYPE\s*=\s*(['"])[^'"]*\1//gi;
$text =~ s/\s*\S*LOCALE\S*\s*=?\s*(['"])[^'"]*\1//gi;
$text =~ s/\s*\S*COLLATION\S*\s*=?\s*(['"])[^'"]*\1//gi;
return $text;
}
########################################################################
# pg_get_role_ddl tests
########################################################################
# Basic role
$node->safe_psql('postgres', 'CREATE ROLE regress_role_ddl_test1');
my $result = $node->safe_psql('postgres',
q{SELECT * FROM pg_get_role_ddl('regress_role_ddl_test1')});
like(
$result,
qr/CREATE ROLE regress_role_ddl_test1 .* NOLOGIN/,
'basic role DDL');
# Role with multiple privileges
$node->safe_psql(
'postgres', q{
CREATE ROLE regress_role_ddl_test2
LOGIN SUPERUSER CREATEDB CREATEROLE
CONNECTION LIMIT 5
VALID UNTIL '2030-12-31 23:59:59+00'});
$result = $node->safe_psql('postgres',
q{SELECT * FROM pg_get_role_ddl('regress_role_ddl_test2')});
like($result, qr/SUPERUSER/, 'role with SUPERUSER');
like($result, qr/CREATEDB/, 'role with CREATEDB');
like($result, qr/CONNECTION LIMIT 5/, 'role with CONNECTION LIMIT');
like($result, qr/VALID UNTIL '2030-12-31/, 'role with VALID UNTIL');
# Role with configuration parameters
$node->safe_psql(
'postgres', q{
ALTER ROLE regress_role_ddl_test1 SET work_mem TO '256MB';
ALTER ROLE regress_role_ddl_test1 SET search_path TO myschema, public});
$result = $node->safe_psql('postgres',
q{SELECT * FROM pg_get_role_ddl('regress_role_ddl_test1')});
like($result, qr/SET work_mem TO '256MB'/, 'role with work_mem setting');
like($result, qr/SET search_path TO/, 'role with search_path setting');
# Role with database-specific configuration (needs a real database)
$node->safe_psql(
'postgres', q{
CREATE DATABASE regression_ddlutils_test
TEMPLATE template0 ENCODING 'UTF8' LC_COLLATE 'C' LC_CTYPE 'C';
ALTER ROLE regress_role_ddl_test2
IN DATABASE regression_ddlutils_test SET work_mem TO '128MB'});
$result = $node->safe_psql('postgres',
q{SELECT * FROM pg_get_role_ddl('regress_role_ddl_test2')});
like(
$result,
qr/IN DATABASE regression_ddlutils_test SET work_mem TO '128MB'/,
'role with database-specific setting');
# Role with special characters (requires quoting)
$node->safe_psql('postgres', q{CREATE ROLE "regress_role-with-dash"});
$result = $node->safe_psql('postgres',
q{SELECT * FROM pg_get_role_ddl('regress_role-with-dash')});
like($result, qr/"regress_role-with-dash"/, 'role name requiring quoting');
# Pretty-printed output
$result = $node->safe_psql('postgres',
q{SELECT * FROM pg_get_role_ddl('regress_role_ddl_test2', pretty => true)}
);
like($result, qr/\n\s+SUPERUSER/, 'role pretty-print indents attributes');
# Role with memberships
$node->safe_psql(
'postgres', q{
CREATE ROLE regress_role_ddl_grantor CREATEROLE;
CREATE ROLE regress_role_ddl_group1;
CREATE ROLE regress_role_ddl_group2;
CREATE ROLE regress_role_ddl_member;
GRANT regress_role_ddl_group1 TO regress_role_ddl_grantor WITH ADMIN TRUE;
GRANT regress_role_ddl_group2 TO regress_role_ddl_grantor WITH ADMIN TRUE;
SET ROLE regress_role_ddl_grantor;
GRANT regress_role_ddl_group1 TO regress_role_ddl_member
WITH INHERIT TRUE, SET FALSE;
GRANT regress_role_ddl_group2 TO regress_role_ddl_member
WITH ADMIN TRUE;
RESET ROLE});
$result = $node->safe_psql('postgres',
q{SELECT * FROM pg_get_role_ddl('regress_role_ddl_member')});
like(
$result,
qr/GRANT regress_role_ddl_group1 TO regress_role_ddl_member/,
'role with memberships includes GRANT');
like($result, qr/SET FALSE/, 'membership includes SET FALSE');
like($result, qr/ADMIN TRUE/, 'membership includes ADMIN TRUE');
# Memberships suppressed
$result = $node->safe_psql('postgres',
q{SELECT * FROM pg_get_role_ddl('regress_role_ddl_member', memberships => false)}
);
unlike($result, qr/GRANT/, 'memberships suppressed');
# Non-existent role (should error)
my ($ret, $stdout, $stderr) =
$node->psql('postgres', q{SELECT * FROM pg_get_role_ddl(9999999::oid)});
isnt($ret, 0, 'non-existent role errors');
like($stderr, qr/does not exist/, 'non-existent role error message');
# NULL input (should return no rows)
$result =
$node->safe_psql('postgres', q{SELECT count(*) FROM pg_get_role_ddl(NULL)});
is($result, '0', 'NULL role returns no rows');
# Permission check: revoke SELECT on pg_authid
$node->safe_psql(
'postgres', q{
CREATE ROLE regress_role_ddl_noaccess;
REVOKE SELECT ON pg_authid FROM PUBLIC});
($ret, $stdout, $stderr) = $node->psql(
'postgres',
q{SET ROLE regress_role_ddl_noaccess;
SELECT * FROM pg_get_role_ddl('regress_role_ddl_test1')});
isnt($ret, 0, 'role DDL denied without pg_authid access');
$node->safe_psql(
'postgres', q{
GRANT SELECT ON pg_authid TO PUBLIC});
########################################################################
# pg_get_database_ddl tests
########################################################################
# Set up: the test database was already created above for role tests.
$node->safe_psql(
'postgres', q{
ALTER DATABASE regression_ddlutils_test OWNER TO regress_role_ddl_test2;
ALTER DATABASE regression_ddlutils_test CONNECTION LIMIT 123;
ALTER DATABASE regression_ddlutils_test SET random_page_cost = 2.0;
ALTER ROLE regress_role_ddl_test2
IN DATABASE regression_ddlutils_test SET random_page_cost = 1.1});
# Non-existent database
($ret, $stdout, $stderr) = $node->psql('postgres',
q{SELECT * FROM pg_get_database_ddl('regression_no_such_db')});
isnt($ret, 0, 'non-existent database errors');
# NULL input
$result = $node->safe_psql('postgres',
q{SELECT count(*) FROM pg_get_database_ddl(NULL)});
is($result, '0', 'NULL database returns no rows');
# Invalid option (bad boolean cast)
($ret, $stdout, $stderr) = $node->psql('postgres',
q{SELECT * FROM pg_get_database_ddl('regression_ddlutils_test', owner => 'invalid')}
);
isnt($ret, 0, 'invalid boolean option errors');
like(
$stderr,
qr/invalid input syntax for type boolean/,
'invalid option error message');
# Duplicate named argument
($ret, $stdout, $stderr) = $node->psql(
'postgres',
q{SELECT * FROM pg_get_database_ddl('regression_ddlutils_test',
owner => false, owner => true)});
isnt($ret, 0, 'duplicate option errors');
# Basic output (without locale details)
$result = ddl_filter(
$node->safe_psql(
'postgres',
q{SELECT pg_get_database_ddl
FROM pg_get_database_ddl('regression_ddlutils_test')}));
like(
$result,
qr/CREATE DATABASE regression_ddlutils_test/,
'database DDL includes CREATE');
like($result, qr/TEMPLATE = template0/, 'database DDL includes TEMPLATE');
like($result, qr/ENCODING = 'UTF8'/, 'database DDL includes ENCODING');
like(
$result,
qr/OWNER TO regress_role_ddl_test2/,
'database DDL includes OWNER');
like($result, qr/CONNECTION LIMIT = 123/, 'database DDL includes CONNLIMIT');
like(
$result,
qr/SET random_page_cost TO '2.0'/,
'database DDL includes GUC setting');
# Pretty-printed output
$result = ddl_filter(
$node->safe_psql(
'postgres',
q{SELECT pg_get_database_ddl
FROM pg_get_database_ddl('regression_ddlutils_test',
pretty => true, tablespace => false)}));
like($result, qr/\n\s+WITH TEMPLATE/, 'database DDL pretty-prints WITH');
# Owner suppressed
$result = ddl_filter(
$node->safe_psql(
'postgres',
q{SELECT pg_get_database_ddl
FROM pg_get_database_ddl('regression_ddlutils_test', owner => false)}));
unlike($result, qr/OWNER/, 'database DDL owner suppressed');
# Permission check
$node->safe_psql(
'postgres', q{
REVOKE CONNECT ON DATABASE regression_ddlutils_test FROM PUBLIC});
($ret, $stdout, $stderr) = $node->psql(
'postgres',
q{SET ROLE regress_role_ddl_noaccess;
SELECT * FROM pg_get_database_ddl('regression_ddlutils_test')});
isnt($ret, 0, 'database DDL denied without CONNECT');
$node->safe_psql(
'postgres', q{
GRANT CONNECT ON DATABASE regression_ddlutils_test TO PUBLIC});
########################################################################
# pg_get_tablespace_ddl tests
########################################################################
# Non-existent tablespace by name
($ret, $stdout, $stderr) = $node->psql('postgres',
q{SELECT * FROM pg_get_tablespace_ddl('regress_nonexistent_tblsp')});
isnt($ret, 0, 'non-existent tablespace errors');
# Non-existent tablespace by OID
($ret, $stdout, $stderr) =
$node->psql('postgres', q{SELECT * FROM pg_get_tablespace_ddl(0::oid)});
isnt($ret, 0, 'non-existent tablespace OID errors');
# NULL input (name and OID variants)
$result = $node->safe_psql('postgres',
q{SELECT count(*) FROM pg_get_tablespace_ddl(NULL::name)});
is($result, '0', 'NULL tablespace name returns no rows');
$result = $node->safe_psql('postgres',
q{SELECT count(*) FROM pg_get_tablespace_ddl(NULL::oid)});
is($result, '0', 'NULL tablespace OID returns no rows');
# Tablespace name requiring quoting
$node->safe_psql(
'postgres', q{
SET allow_in_place_tablespaces = true;
CREATE TABLESPACE "regress_ tblsp" OWNER regress_role_ddl_test1
LOCATION ''});
$result = $node->safe_psql('postgres',
q{SELECT * FROM pg_get_tablespace_ddl('regress_ tblsp')});
like($result, qr/"regress_ tblsp"/, 'tablespace name is quoted');
# Rename and add options; reuse this tablespace for the remaining tests
$node->safe_psql(
'postgres', q{
ALTER TABLESPACE "regress_ tblsp" RENAME TO regress_allopt_tblsp;
ALTER TABLESPACE regress_allopt_tblsp
SET (seq_page_cost = '1.5', random_page_cost = '1.1234567890',
effective_io_concurrency = '17', maintenance_io_concurrency = '18')});
# Tablespace with multiple options
$result = $node->safe_psql('postgres',
q{SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp')});
like(
$result,
qr/CREATE TABLESPACE regress_allopt_tblsp/,
'tablespace DDL includes CREATE');
like(
$result,
qr/OWNER regress_role_ddl_test1/,
'tablespace DDL includes OWNER');
like($result, qr/seq_page_cost='1.5'/, 'tablespace DDL includes options');
# Pretty-printed output
$result = $node->safe_psql(
'postgres',
q{SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp',
pretty => true)});
like($result, qr/\n\s+OWNER/, 'tablespace DDL pretty-prints OWNER');
# Owner suppressed
$result = $node->safe_psql(
'postgres',
q{SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp',
owner => false)});
unlike($result, qr/OWNER/, 'tablespace DDL owner suppressed');
# Lookup by OID
$result = $node->safe_psql(
'postgres', q{
SELECT pg_get_tablespace_ddl
FROM pg_get_tablespace_ddl(
(SELECT oid FROM pg_tablespace
WHERE spcname = 'regress_allopt_tblsp'))});
like(
$result,
qr/CREATE TABLESPACE regress_allopt_tblsp/,
'tablespace DDL by OID');
# Permission check
$node->safe_psql('postgres', q{REVOKE SELECT ON pg_tablespace FROM PUBLIC});
($ret, $stdout, $stderr) = $node->psql(
'postgres',
q{SET ROLE regress_role_ddl_noaccess;
SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp')});
isnt($ret, 0, 'tablespace DDL denied without pg_tablespace access');
$node->safe_psql(
'postgres', q{
GRANT SELECT ON pg_tablespace TO PUBLIC});
$node->stop;
done_testing();