diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index c393832d94c..d266c2ea4bf 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -3797,4 +3797,55 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} + + Get Object DDL Functions + + + The functions shown in + print the DDL statements for various database objects. + (This is a decompiled reconstruction, not the original text + of the command.) + + + + Get Object DDL Functions + + + + + Function + + + Description + + + + + + + + + pg_get_subscription_ddl + + pg_get_subscription_ddl ( subscription text ) + text + + + Reconstructs the creating command for a subscription. + The result is a complete CREATE SUBSCRIPTION + statement. The connect option set to + false. + + + This function is restricted to users that have the + pg_read_all_data and/or + pg_create_subscription privilege. + + + + +
+ +
+ diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c index b885890de37..789d0dfb398 100644 --- a/src/backend/catalog/pg_subscription.c +++ b/src/backend/catalog/pg_subscription.c @@ -32,8 +32,6 @@ #include "utils/rel.h" #include "utils/syscache.h" -static List *textarray_to_stringlist(ArrayType *textarray); - /* * Add a comma-separated list of publication names to the 'dest' string. */ @@ -240,7 +238,7 @@ DisableSubscription(Oid subid) * * Note: the resulting list of strings is pallocated here. */ -static List * +List * textarray_to_stringlist(ArrayType *textarray) { Datum *elems; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 21663af6979..c1c3a682858 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -35,6 +35,7 @@ #include "catalog/pg_partitioned_table.h" #include "catalog/pg_proc.h" #include "catalog/pg_statistic_ext.h" +#include "catalog/pg_subscription.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" #include "commands/defrem.h" @@ -57,6 +58,7 @@ #include "rewrite/rewriteHandler.h" #include "rewrite/rewriteManip.h" #include "rewrite/rewriteSupport.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -13715,3 +13717,178 @@ get_range_partbound_string(List *bound_datums) return buf->data; } + +/* + * pg_get_subscription_ddl + * Get CREATE SUBSCRIPTION statement for the given subscription + */ +Datum +pg_get_subscription_ddl(PG_FUNCTION_ARGS) +{ + char *subname = text_to_cstring(PG_GETARG_TEXT_P(0)); + StringInfo pubnames; + StringInfoData buf; + HeapTuple tup; + char *conninfo; + List *publist; + Datum datum; + bool isnull; + + /* + * To prevent unprivileged users from initiating unauthorized network + * connections, dumping subscription creation is restricted. A user must + * be specifically authorized (via the appropriate role privilege) to + * create subscriptions and/or to read all data. + */ + if (!(has_privs_of_role(GetUserId(), ROLE_PG_CREATE_SUBSCRIPTION) || + has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_DATA))) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied to get the create subscription ddl"), + errdetail("Only roles with privileges of the \"%s\" and/or \"%s\" role may get ddl.", + "pg_create_subscription", "pg_read_all_data"))); + + /* Look up the subscription in pg_subscription */ + tup = SearchSysCache2(SUBSCRIPTIONNAME, ObjectIdGetDatum(MyDatabaseId), + CStringGetDatum(subname)); + if (!HeapTupleIsValid(tup)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("subscription \"%s\" does not exist", subname))); + + initStringInfo(&buf); + + /* Build the CREATE SUBSCRIPTION statement */ + appendStringInfo(&buf, "CREATE SUBSCRIPTION %s ", + quote_identifier(subname)); + + /* Get conninfo */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subconninfo); + conninfo = TextDatumGetCString(datum); + + /* Append connection info to the CREATE SUBSCRIPTION statement */ + appendStringInfo(&buf, "CONNECTION \'%s\'", conninfo); + + /* Build list of quoted publications and append them to query */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subpublications); + publist = textarray_to_stringlist(DatumGetArrayTypeP(datum)); + pubnames = makeStringInfo(); + GetPublicationsStr(publist, pubnames, false); + appendStringInfo(&buf, " PUBLICATION %s", pubnames->data); + + /* + * Add options using WITH clause. The 'connect' option value given at the + * time of subscription creation is not available in the catalog. When + * creating a subscription, the remote host is not reachable or in an + * unclear state, in that case, the subscription can be created using + * 'connect = false' option. This is what pg_dump uses. + * + * The status or value of the options 'create_slot' and 'copy_data' not + * available in the catalog table. We can use default values i.e. TRUE + * for both. This is what pg_dump uses. + */ + appendStringInfoString(&buf, " WITH (connect = false"); + + /* Get slotname */ + datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subslotname, + &isnull); + if (!isnull) + appendStringInfo(&buf, ", slot_name = \'%s\'", + NameStr(*DatumGetName(datum))); + else + { + appendStringInfoString(&buf, ", slot_name = none"); + /* Setting slot_name to none must set create_slot to false */ + appendStringInfoString(&buf, ", create_slot = false"); + } + + /* Get enabled option */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subenabled); + /* Setting 'slot_name' to none must set 'enabled' to false as well */ + if (!DatumGetBool(datum) || isnull) + appendStringInfoString(&buf, ", enabled = false"); + else + appendStringInfoString(&buf, ", enabled = true"); + + /* Get binary option */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subbinary); + appendStringInfo(&buf, ", binary = %s", + DatumGetBool(datum) ? "true" : "false"); + + /* Get streaming option */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_substream); + if (DatumGetChar(datum) == LOGICALREP_STREAM_OFF) + appendStringInfoString(&buf, ", streaming = off"); + else if (DatumGetChar(datum) == LOGICALREP_STREAM_ON) + appendStringInfoString(&buf, ", streaming = on"); + else + appendStringInfoString(&buf, ", streaming = parallel"); + + /* Get sync commit option */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subsynccommit); + appendStringInfo(&buf, ", synchronous_commit = %s", + TextDatumGetCString(datum)); + + /* Get two-phase commit option */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subtwophasestate); + if (DatumGetChar(datum) == LOGICALREP_TWOPHASE_STATE_DISABLED) + appendStringInfoString(&buf, ", two_phase = off"); + else + appendStringInfoString(&buf, ", two_phase = on"); + + /* Disable on error? */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subdisableonerr); + appendStringInfo(&buf, ", disable_on_error = %s", + DatumGetBool(datum) ? "on" : "off"); + + /* Password required? */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subpasswordrequired); + appendStringInfo(&buf, ", password_required = %s", + DatumGetBool(datum) ? "on" : "off"); + + /* Run as owner? */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subrunasowner); + appendStringInfo(&buf, ", run_as_owner = %s", + DatumGetBool(datum) ? "on" : "off"); + + /* Get origin */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_suborigin); + appendStringInfo(&buf, ", origin = %s", TextDatumGetCString(datum)); + + /* Failover? */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subfailover); + appendStringInfo(&buf, ", failover = %s", + DatumGetBool(datum) ? "on" : "off"); + + /* Retain dead tuples? */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subretaindeadtuples); + appendStringInfo(&buf, ", retain_dead_tuples = %s", + DatumGetBool(datum) ? "on" : "off"); + + /* Max retention duration */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_submaxretention); + appendStringInfo(&buf, ", max_retention_duration = %lu", + Int32GetDatum(datum)); + + /* Finally close parenthesis and add semicolon to the statement */ + appendStringInfoString(&buf, ");"); + + ReleaseSysCache(tup); + + PG_RETURN_TEXT_P(string_to_text(buf.data)); +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 01eba3b5a19..53fe7d6ff2d 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3993,6 +3993,9 @@ { oid => '1387', descr => 'constraint description', proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_constraintdef' }, +{ oid => '8001', descr => 'get CREATE statement for subscription', + proname => 'pg_get_subscription_ddl', prorettype => 'text', + proargtypes => 'text', prosrc => 'pg_get_subscription_ddl' }, { oid => '1716', descr => 'deparse an encoded expression', proname => 'pg_get_expr', provolatile => 's', prorettype => 'text', proargtypes => 'pg_node_tree oid', prosrc => 'pg_get_expr' }, diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h index 55cb9b1eefa..3082ab8dffc 100644 --- a/src/include/catalog/pg_subscription.h +++ b/src/include/catalog/pg_subscription.h @@ -22,6 +22,7 @@ #include "catalog/pg_subscription_d.h" /* IWYU pragma: export */ #include "lib/stringinfo.h" #include "nodes/pg_list.h" +#include "utils/array.h" /* ---------------- * pg_subscription definition. cpp turns this into @@ -207,5 +208,6 @@ extern int CountDBSubscriptions(Oid dbid); extern void GetPublicationsStr(List *publications, StringInfo dest, bool quote_literal); +extern List *textarray_to_stringlist(ArrayType *textarray); #endif /* PG_SUBSCRIPTION_H */ diff --git a/src/test/regress/expected/subscription_ddl.out b/src/test/regress/expected/subscription_ddl.out new file mode 100644 index 00000000000..1f4fa08c63c --- /dev/null +++ b/src/test/regress/expected/subscription_ddl.out @@ -0,0 +1,85 @@ +-- +-- Get CREATE SUBSCRIPTION statement +-- +CREATE ROLE createsub_role LOGIN; +CREATE ROLE readalldata_role LOGIN; +-- Create subscription with minimal options +CREATE SUBSCRIPTION testsub1 CONNECTION 'dbname=db_doesnotexist' + PUBLICATION testpub1 WITH (connect=false); +WARNING: subscription was created, but is not connected +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. +-- Check that the subscription ddl is correctly created +SELECT pg_get_subscription_ddl('testsub1'); + pg_get_subscription_ddl +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE SUBSCRIPTION testsub1 CONNECTION 'dbname=db_doesnotexist' PUBLICATION "testpub1" WITH (connect = false, slot_name = 'testsub1', enabled = false, binary = false, streaming = parallel, synchronous_commit = off, two_phase = off, disable_on_error = off, password_required = on, run_as_owner = off, origin = any, failover = off, retain_dead_tuples = off, max_retention_duration = 0); +(1 row) + +-- Create subscription with more options +CREATE SUBSCRIPTION "TestSubddL2" CONNECTION 'host=unknown user=dvd password=pass123' + PUBLICATION "testpub2", "TestPub3" WITH (connect=false, slot_name='slot1', + enabled=off); +WARNING: subscription was created, but is not connected +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. +SELECT pg_get_subscription_ddl('TestSubddL2'); + pg_get_subscription_ddl +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE SUBSCRIPTION "TestSubddL2" CONNECTION 'host=unknown user=dvd password=pass123' PUBLICATION "testpub2", "TestPub3" WITH (connect = false, slot_name = 'slot1', enabled = false, binary = false, streaming = parallel, synchronous_commit = off, two_phase = off, disable_on_error = off, password_required = on, run_as_owner = off, origin = any, failover = off, retain_dead_tuples = off, max_retention_duration = 0); +(1 row) + +-- Create subscription with all options +CREATE SUBSCRIPTION testsub3 CONNECTION 'host=unknown user=dvd password=pass12' + PUBLICATION testpub4 WITH (connect=false, slot_name=none, enabled=false, + create_slot=false, copy_data=false, binary=true, streaming=off, + synchronous_commit=local, two_phase=true, disable_on_error=true, + password_required=false, run_as_owner=true, origin=none, failover=true, + retain_dead_tuples=false, max_retention_duration=100); +NOTICE: max_retention_duration is ineffective when retain_dead_tuples is disabled +WARNING: subscription was created, but is not connected +HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription. +SELECT pg_get_subscription_ddl('testsub3'); + pg_get_subscription_ddl +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE SUBSCRIPTION testsub3 CONNECTION 'host=unknown user=dvd password=pass12' PUBLICATION "testpub4" WITH (connect = false, slot_name = none, create_slot = false, enabled = false, binary = true, streaming = off, synchronous_commit = local, two_phase = on, disable_on_error = on, password_required = off, run_as_owner = on, origin = none, failover = on, retain_dead_tuples = off, max_retention_duration = 100); +(1 row) + +-- Non-superusers and which don't have pg_create_subscription and/or +-- pg_read_all_data permission can't get ddl +SET SESSION AUTHORIZATION 'createsub_role'; +SELECT pg_get_subscription_ddl('TestSubddL2'); +ERROR: permission denied to get the create subscription ddl +DETAIL: Only roles with privileges of the "pg_create_subscription" and/or "pg_read_all_data" role may get ddl. +RESET SESSION AUTHORIZATION; +SET SESSION AUTHORIZATION 'readalldata_role'; +SELECT pg_get_subscription_ddl('TestSubddL2'); +ERROR: permission denied to get the create subscription ddl +DETAIL: Only roles with privileges of the "pg_create_subscription" and/or "pg_read_all_data" role may get ddl. +RESET SESSION AUTHORIZATION; +-- Administrators can change who can access this function +GRANT pg_create_subscription TO createsub_role; +GRANT pg_read_all_data TO readalldata_role; +SET SESSION AUTHORIZATION 'createsub_role'; +SELECT pg_get_subscription_ddl('TestSubddL2'); + pg_get_subscription_ddl +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE SUBSCRIPTION "TestSubddL2" CONNECTION 'host=unknown user=dvd password=pass123' PUBLICATION "testpub2", "TestPub3" WITH (connect = false, slot_name = 'slot1', enabled = false, binary = false, streaming = parallel, synchronous_commit = off, two_phase = off, disable_on_error = off, password_required = on, run_as_owner = off, origin = any, failover = off, retain_dead_tuples = off, max_retention_duration = 0); +(1 row) + +RESET SESSION AUTHORIZATION; +SET SESSION AUTHORIZATION 'readalldata_role'; +SELECT pg_get_subscription_ddl('TestSubddL2'); + pg_get_subscription_ddl +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + CREATE SUBSCRIPTION "TestSubddL2" CONNECTION 'host=unknown user=dvd password=pass123' PUBLICATION "testpub2", "TestPub3" WITH (connect = false, slot_name = 'slot1', enabled = false, binary = false, streaming = parallel, synchronous_commit = off, two_phase = off, disable_on_error = off, password_required = on, run_as_owner = off, origin = any, failover = off, retain_dead_tuples = off, max_retention_duration = 0); +(1 row) + +RESET SESSION AUTHORIZATION; +REVOKE pg_create_subscription FROM createsub_role; +REVOKE pg_read_all_data FROM readalldata_role; +ALTER SUBSCRIPTION testsub1 SET (slot_name=NONE); +DROP SUBSCRIPTION testsub1; +ALTER SUBSCRIPTION "TestSubddL2" SET (slot_name=NONE); +DROP SUBSCRIPTION "TestSubddL2"; +DROP SUBSCRIPTION testsub3; +DROP ROLE createsub_role; +DROP ROLE readalldata_role; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index fbffc67ae60..e6c2bf09c4e 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -28,7 +28,7 @@ test: strings md5 numerology point lseg line box path polygon circle date time t # geometry depends on point, lseg, line, box, path, polygon, circle # horology depends on date, time, timetz, timestamp, timestamptz, interval # ---------- -test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import +test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database stats_import subscription_ddl # ---------- # Load huge amounts of data diff --git a/src/test/regress/sql/subscription_ddl.sql b/src/test/regress/sql/subscription_ddl.sql new file mode 100644 index 00000000000..6e8eecf010e --- /dev/null +++ b/src/test/regress/sql/subscription_ddl.sql @@ -0,0 +1,55 @@ +-- +-- Get CREATE SUBSCRIPTION statement +-- + +CREATE ROLE createsub_role LOGIN; +CREATE ROLE readalldata_role LOGIN; + +-- Create subscription with minimal options +CREATE SUBSCRIPTION testsub1 CONNECTION 'dbname=db_doesnotexist' + PUBLICATION testpub1 WITH (connect=false); +-- Check that the subscription ddl is correctly created +SELECT pg_get_subscription_ddl('testsub1'); + +-- Create subscription with more options +CREATE SUBSCRIPTION "TestSubddL2" CONNECTION 'host=unknown user=dvd password=pass123' + PUBLICATION "testpub2", "TestPub3" WITH (connect=false, slot_name='slot1', + enabled=off); +SELECT pg_get_subscription_ddl('TestSubddL2'); + +-- Create subscription with all options +CREATE SUBSCRIPTION testsub3 CONNECTION 'host=unknown user=dvd password=pass12' + PUBLICATION testpub4 WITH (connect=false, slot_name=none, enabled=false, + create_slot=false, copy_data=false, binary=true, streaming=off, + synchronous_commit=local, two_phase=true, disable_on_error=true, + password_required=false, run_as_owner=true, origin=none, failover=true, + retain_dead_tuples=false, max_retention_duration=100); +SELECT pg_get_subscription_ddl('testsub3'); + +-- Non-superusers and which don't have pg_create_subscription and/or +-- pg_read_all_data permission can't get ddl +SET SESSION AUTHORIZATION 'createsub_role'; +SELECT pg_get_subscription_ddl('TestSubddL2'); +RESET SESSION AUTHORIZATION; +SET SESSION AUTHORIZATION 'readalldata_role'; +SELECT pg_get_subscription_ddl('TestSubddL2'); +RESET SESSION AUTHORIZATION; +-- Administrators can change who can access this function +GRANT pg_create_subscription TO createsub_role; +GRANT pg_read_all_data TO readalldata_role; +SET SESSION AUTHORIZATION 'createsub_role'; +SELECT pg_get_subscription_ddl('TestSubddL2'); +RESET SESSION AUTHORIZATION; +SET SESSION AUTHORIZATION 'readalldata_role'; +SELECT pg_get_subscription_ddl('TestSubddL2'); + +RESET SESSION AUTHORIZATION; +REVOKE pg_create_subscription FROM createsub_role; +REVOKE pg_read_all_data FROM readalldata_role; +ALTER SUBSCRIPTION testsub1 SET (slot_name=NONE); +DROP SUBSCRIPTION testsub1; +ALTER SUBSCRIPTION "TestSubddL2" SET (slot_name=NONE); +DROP SUBSCRIPTION "TestSubddL2"; +DROP SUBSCRIPTION testsub3; +DROP ROLE createsub_role; +DROP ROLE readalldata_role;