diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index 7cb020b6fd3..e14c209bf14 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -3910,6 +3910,34 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} role memberships and their options. + + + + pg_get_tablespace_ddl + + pg_get_tablespace_ddl + ( tablespace oid + , VARIADIC options + text ) + setof text + + + pg_get_tablespace_ddl + ( tablespace name + , VARIADIC options + text ) + setof text + + + Reconstructs the CREATE TABLESPACE statement for + the specified tablespace (by OID or name). If the tablespace has + options set, an ALTER TABLESPACE ... SET statement + is also returned. Each statement is returned as a separate row. + The following options are supported: pretty (boolean) + for formatted output and owner (boolean) to include + OWNER. + + diff --git a/src/backend/utils/adt/ddlutils.c b/src/backend/utils/adt/ddlutils.c index bcea1ec5981..d953963a712 100644 --- a/src/backend/utils/adt/ddlutils.c +++ b/src/backend/utils/adt/ddlutils.c @@ -20,11 +20,13 @@ #include "access/genam.h" #include "access/htup_details.h" -#include "access/relation.h" #include "access/table.h" #include "catalog/pg_auth_members.h" #include "catalog/pg_authid.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 "miscadmin.h" #include "utils/acl.h" @@ -35,6 +37,7 @@ #include "utils/guc.h" #include "utils/lsyscache.h" #include "utils/rel.h" +#include "utils/ruleutils.h" #include "utils/syscache.h" #include "utils/timestamp.h" #include "utils/varlena.h" @@ -75,6 +78,8 @@ 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, bool isnull); /* @@ -634,3 +639,202 @@ pg_get_role_ddl(PG_FUNCTION_ARGS) 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, bool isnull) +{ + FuncCallContext *funcctx; + List *statements; + + if (SRF_IS_FIRSTCALL()) + { + MemoryContext oldcontext; + DdlOption opts[] = { + {"pretty", DDL_OPT_BOOL}, + {"owner", DDL_OPT_BOOL}, + }; + + funcctx = SRF_FIRSTCALL_INIT(); + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + + if (isnull) + { + MemoryContextSwitchTo(oldcontext); + SRF_RETURN_DONE(funcctx); + } + + parse_ddl_options(fcinfo, 1, opts, lengthof(opts)); + + statements = pg_get_tablespace_ddl_internal(tsid, + opts[0].isset && opts[0].boolval, + opts[1].isset && !opts[1].boolval); + 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 = InvalidOid; + bool isnull; + + isnull = PG_ARGISNULL(0); + if (!isnull) + tsid = PG_GETARG_OID(0); + + return pg_get_tablespace_ddl_srf(fcinfo, tsid, isnull); +} + +/* + * pg_get_tablespace_ddl_name + * Return DDL to recreate a tablespace, taking name. + */ +Datum +pg_get_tablespace_ddl_name(PG_FUNCTION_ARGS) +{ + Oid tsid = InvalidOid; + Name tspname; + bool isnull; + + isnull = PG_ARGISNULL(0); + + if (!isnull) + { + tspname = PG_GETARG_NAME(0); + tsid = get_tablespace_oid(NameStr(*tspname), false); + } + + return pg_get_tablespace_ddl_srf(fcinfo, tsid, isnull); +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index aec5556b008..35083fcc733 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -547,7 +547,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); -static void get_reloptions(StringInfo buf, Datum reloptions); +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, @@ -14240,7 +14240,7 @@ string_to_text(char *str) /* * Generate a C string representing a relation options from text[] datum. */ -static void +void get_reloptions(StringInfo buf, Datum reloptions) { Datum *options; diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 76f008a84e7..8eca4cf98a1 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8611,6 +8611,22 @@ proallargtypes => '{regrole,text}', pronargdefaults => '1', proargdefaults => '{NULL}', prosrc => 'pg_get_role_ddl' }, +{ oid => '8758', descr => 'get DDL to recreate a tablespace', + proname => 'pg_get_tablespace_ddl', provariadic => 'text', proisstrict => 'f', + provolatile => 's', proretset => 't', prorows => '10', prorettype => 'text', + proargtypes => 'oid text', + proargmodes => '{i,v}', + proallargtypes => '{oid,text}', + pronargdefaults => '1', proargdefaults => '{NULL}', + prosrc => 'pg_get_tablespace_ddl_oid' }, +{ oid => '8759', descr => 'get DDL to recreate a tablespace', + proname => 'pg_get_tablespace_ddl', provariadic => 'text', proisstrict => 'f', + provolatile => 's', proretset => 't', prorows => '10', prorettype => 'text', + proargtypes => 'name text', + proargmodes => '{i,v}', + proallargtypes => '{name,text}', + pronargdefaults => '1', proargdefaults => '{NULL}', + prosrc => 'pg_get_tablespace_ddl_name' }, { oid => '2509', descr => 'deparse an encoded expression with pretty-print option', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h index 059e6474f3b..25c05e2f649 100644 --- a/src/include/utils/ruleutils.h +++ b/src/include/utils/ruleutils.h @@ -51,6 +51,7 @@ 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); diff --git a/src/test/regress/expected/tablespace_ddl.out b/src/test/regress/expected/tablespace_ddl.out new file mode 100644 index 00000000000..e52043273a9 --- /dev/null +++ b/src/test/regress/expected/tablespace_ddl.out @@ -0,0 +1,84 @@ +-- +-- Tests for pg_get_tablespace_ddl() +-- +SET allow_in_place_tablespaces = true; +CREATE ROLE regress_tblspc_ddl_user; +-- error: non-existent tablespace by name +SELECT * FROM pg_get_tablespace_ddl('regress_nonexistent_tblsp'); +ERROR: tablespace "regress_nonexistent_tblsp" does not exist +-- error: non-existent tablespace by OID +SELECT * FROM pg_get_tablespace_ddl(0::oid); +ERROR: tablespace with OID 0 does not exist +-- NULL input returns no rows (name variant) +SELECT * FROM pg_get_tablespace_ddl(NULL::name); + pg_get_tablespace_ddl +----------------------- +(0 rows) + +-- NULL input returns no rows (OID variant) +SELECT * FROM pg_get_tablespace_ddl(NULL::oid); + pg_get_tablespace_ddl +----------------------- +(0 rows) + +-- tablespace name requiring quoting +CREATE TABLESPACE "regress_ tblsp" OWNER regress_tblspc_ddl_user LOCATION ''; +SELECT * FROM pg_get_tablespace_ddl('regress_ tblsp'); + pg_get_tablespace_ddl +------------------------------------------------------------------------------- + CREATE TABLESPACE "regress_ tblsp" OWNER regress_tblspc_ddl_user LOCATION ''; +(1 row) + +DROP TABLESPACE "regress_ tblsp"; +-- tablespace with multiple options +CREATE TABLESPACE regress_allopt_tblsp OWNER regress_tblspc_ddl_user LOCATION '' + WITH (seq_page_cost = '1.5', random_page_cost = '1.1234567890', + effective_io_concurrency = '17', maintenance_io_concurrency = '18'); +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp'); + pg_get_tablespace_ddl +------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLESPACE regress_allopt_tblsp OWNER regress_tblspc_ddl_user LOCATION ''; + ALTER TABLESPACE regress_allopt_tblsp SET (seq_page_cost='1.5', random_page_cost='1.1234567890', effective_io_concurrency='17', maintenance_io_concurrency='18'); +(2 rows) + +-- pretty-printed output +\pset format unaligned +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp', 'pretty', 'true'); +pg_get_tablespace_ddl +CREATE TABLESPACE regress_allopt_tblsp + OWNER regress_tblspc_ddl_user + LOCATION ''; +ALTER TABLESPACE regress_allopt_tblsp SET (seq_page_cost='1.5', random_page_cost='1.1234567890', effective_io_concurrency='17', maintenance_io_concurrency='18'); +(2 rows) +\pset format aligned +-- tablespace with owner suppressed +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp', 'owner', 'false'); + pg_get_tablespace_ddl +------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE TABLESPACE regress_allopt_tblsp LOCATION ''; + ALTER TABLESPACE regress_allopt_tblsp SET (seq_page_cost='1.5', random_page_cost='1.1234567890', effective_io_concurrency='17', maintenance_io_concurrency='18'); +(2 rows) + +DROP TABLESPACE regress_allopt_tblsp; +-- test by OID +CREATE TABLESPACE regress_oid_tblsp OWNER regress_tblspc_ddl_user LOCATION ''; +SELECT oid AS tsid FROM pg_tablespace WHERE spcname = 'regress_oid_tblsp' \gset +SELECT * FROM pg_get_tablespace_ddl(:tsid); + pg_get_tablespace_ddl +-------------------------------------------------------------------------------- + CREATE TABLESPACE regress_oid_tblsp OWNER regress_tblspc_ddl_user LOCATION ''; +(1 row) + +DROP TABLESPACE regress_oid_tblsp; +-- Permission check: revoke SELECT on pg_tablespace +CREATE TABLESPACE regress_acl_tblsp OWNER regress_tblspc_ddl_user LOCATION ''; +CREATE ROLE regress_tblspc_ddl_noaccess; +REVOKE SELECT ON pg_tablespace FROM PUBLIC; +SET ROLE regress_tblspc_ddl_noaccess; +SELECT * FROM pg_get_tablespace_ddl('regress_acl_tblsp'); -- should fail +ERROR: permission denied for tablespace regress_acl_tblsp +RESET ROLE; +GRANT SELECT ON pg_tablespace TO PUBLIC; +DROP TABLESPACE regress_acl_tblsp; +DROP ROLE regress_tblspc_ddl_noaccess; +DROP ROLE regress_tblspc_ddl_user; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 84efbf8c104..fabaebf2c78 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -130,7 +130,7 @@ test: partition_merge partition_split partition_join partition_prune reloptions # oidjoins is read-only, though, and should run late for best coverage test: oidjoins event_trigger -test: role_ddl +test: role_ddl tablespace_ddl # event_trigger_login cannot run concurrently with any other tests because # on-login event handling could catch connection of a concurrent test. diff --git a/src/test/regress/sql/tablespace_ddl.sql b/src/test/regress/sql/tablespace_ddl.sql new file mode 100644 index 00000000000..ee3cc6e2e1e --- /dev/null +++ b/src/test/regress/sql/tablespace_ddl.sql @@ -0,0 +1,58 @@ +-- +-- Tests for pg_get_tablespace_ddl() +-- + +SET allow_in_place_tablespaces = true; +CREATE ROLE regress_tblspc_ddl_user; + +-- error: non-existent tablespace by name +SELECT * FROM pg_get_tablespace_ddl('regress_nonexistent_tblsp'); + +-- error: non-existent tablespace by OID +SELECT * FROM pg_get_tablespace_ddl(0::oid); + +-- NULL input returns no rows (name variant) +SELECT * FROM pg_get_tablespace_ddl(NULL::name); + +-- NULL input returns no rows (OID variant) +SELECT * FROM pg_get_tablespace_ddl(NULL::oid); + +-- tablespace name requiring quoting +CREATE TABLESPACE "regress_ tblsp" OWNER regress_tblspc_ddl_user LOCATION ''; +SELECT * FROM pg_get_tablespace_ddl('regress_ tblsp'); +DROP TABLESPACE "regress_ tblsp"; + +-- tablespace with multiple options +CREATE TABLESPACE regress_allopt_tblsp OWNER regress_tblspc_ddl_user LOCATION '' + WITH (seq_page_cost = '1.5', random_page_cost = '1.1234567890', + effective_io_concurrency = '17', maintenance_io_concurrency = '18'); +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp'); + +-- pretty-printed output +\pset format unaligned +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp', 'pretty', 'true'); +\pset format aligned + +-- tablespace with owner suppressed +SELECT * FROM pg_get_tablespace_ddl('regress_allopt_tblsp', 'owner', 'false'); + +DROP TABLESPACE regress_allopt_tblsp; + +-- test by OID +CREATE TABLESPACE regress_oid_tblsp OWNER regress_tblspc_ddl_user LOCATION ''; +SELECT oid AS tsid FROM pg_tablespace WHERE spcname = 'regress_oid_tblsp' \gset +SELECT * FROM pg_get_tablespace_ddl(:tsid); +DROP TABLESPACE regress_oid_tblsp; + +-- Permission check: revoke SELECT on pg_tablespace +CREATE TABLESPACE regress_acl_tblsp OWNER regress_tblspc_ddl_user LOCATION ''; +CREATE ROLE regress_tblspc_ddl_noaccess; +REVOKE SELECT ON pg_tablespace FROM PUBLIC; +SET ROLE regress_tblspc_ddl_noaccess; +SELECT * FROM pg_get_tablespace_ddl('regress_acl_tblsp'); -- should fail +RESET ROLE; +GRANT SELECT ON pg_tablespace TO PUBLIC; +DROP TABLESPACE regress_acl_tblsp; +DROP ROLE regress_tblspc_ddl_noaccess; + +DROP ROLE regress_tblspc_ddl_user;