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:4881981f92Add infrastructure for pg_get_*_ddl functions76e514ebb4Add pg_get_role_ddl() functionb99fd9fd7fAdd pg_get_tablespace_ddl() functiona4f774cf1cAdd pg_get_database_ddl() function6c7bce28c8Fixups fora4f774cf1c1f108fc02eFix pfree crash in pg_get_role_ddl() and pg_get_database_ddl().cda0c4c5d6Reject invalid databases in pg_get_database_ddl()79fba6ebabdoc: Fix missing role attribute in pg_get_tablespace_ddl() description.c529ee38b9Convert ddlutils regression tests to TAP tests.df1bac400fFix timezone dependence in test_misc/012_ddlutils.pl5642a0367cAvoid SIGSEGV in pg_get_database_ddl() on NULL tablespace2af70e9374Fix incorrect declarations of variadic pg_get_*_ddl() functions.d6ed87d198Use 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:
@@ -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>
|
||||
|
||||
@@ -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">§</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">§</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">§</ulink>
|
||||
</para>
|
||||
</listitem>
|
||||
|
||||
<!--
|
||||
Author: Peter Eisentraut <peter@eisentraut.org>
|
||||
2025-08-21 [53eff471c] PL/Python: Add event trigger support
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ backend_sources += files(
|
||||
'datetime.c',
|
||||
'datum.c',
|
||||
'dbsize.c',
|
||||
'ddlutils.c',
|
||||
'domains.c',
|
||||
'encode.c',
|
||||
'enum.c',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -57,6 +57,6 @@
|
||||
*/
|
||||
|
||||
/* yyyymmddN */
|
||||
#define CATALOG_VERSION_NO 202609102
|
||||
#define CATALOG_VERSION_NO 202609131
|
||||
|
||||
#endif
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user