From f9df55d9c9dd556f96ea1fbeefa207450258e4d6 Mon Sep 17 00:00:00 2001 From: Vaibhav Dalvi Date: Mon, 29 Sep 2025 04:14:10 +0000 Subject: [PATCH 1/4] Add pg_get_subscription_ddl() function This new SQL-callable function returns the `CREATE SUBSCRIPTION` statement for a given subscription name. Like `pg_dump`, the returned DDL explicitly sets `connect = false`. This is because the original `CONNECT` option value is not cataloged, and using `connect = false` ensures the DDL can be successfully executed even if the remote publisher is unreachable. By default, the function is restricted to superusers. This is a security measure because subscription connection strings often contain sensitive information, such as passwords. EXECUTE permission can be granted to other users as needed. Author: Vaibhav Dalvi Discussion: https://www.postgresql.org/message-id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9@dunslane.net --- doc/src/sgml/func/func-info.sgml | 50 ++++++ src/backend/catalog/pg_subscription.c | 4 +- src/backend/catalog/system_functions.sql | 2 + src/backend/utils/adt/ruleutils.c | 157 ++++++++++++++++++ src/include/catalog/pg_proc.dat | 3 + src/include/catalog/pg_subscription.h | 2 + .../regress/expected/subscription_ddl.out | 64 +++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/subscription_ddl.sql | 43 +++++ 9 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/subscription_ddl.out create mode 100644 src/test/regress/sql/subscription_ddl.sql diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index c393832d94c..3eccbf8bdf0 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -3797,4 +3797,54 @@ 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. This statement omits default values. The + connect option set to false. + + + This function is restricted to superusers by default, but other users + can be granted EXECUTE to run the function. + + + + +
+ +
+ 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/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 2d946d6d9e9..d9a7e67103e 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -782,6 +782,8 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION pg_get_subscription_ddl(text) FROM public; + -- -- We also set up some things as accessible to standard roles. -- diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 21663af6979..bb7a2a6b831 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" @@ -13715,3 +13716,159 @@ 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) +{ + StringInfo pubnames = makeStringInfo(); + StringInfoData buf; + HeapTuple tup; + char *subname; + char *conninfo; + List *publist; + Datum datum; + bool isnull; + + if (PG_ARGISNULL(0)) + PG_RETURN_NULL(); + subname = text_to_cstring(PG_GETARG_TEXT_P(0)); + + /* 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)); + 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. + */ + appendStringInfo(&buf, " WITH (connect = false"); + + /* Get slotname */ + datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subslotname, + &isnull); + if (!isnull) + { + char *slotname = pstrdup(NameStr(*DatumGetName(datum))); + + appendStringInfo(&buf, ", slot_name = \'%s\'", slotname); + } + else + { + appendStringInfo(&buf, ", slot_name = none"); + /* Setting slot_name to none must set create_slot to false */ + appendStringInfo(&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) + appendStringInfo(&buf, ", enabled = false"); + + /* Get binary option */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subbinary); + if (DatumGetBool(datum)) + appendStringInfo(&buf, ", binary = true"); + + /* Get streaming option */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_substream); + if (DatumGetChar(datum) == LOGICALREP_STREAM_OFF) + appendStringInfo(&buf, ", streaming = off"); + else if (DatumGetChar(datum) == LOGICALREP_STREAM_ON) + appendStringInfo(&buf, ", streaming = on"); + + /* Get sync commit option */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subsynccommit); + if (strcmp(TextDatumGetCString(datum), "on") == 0) + appendStringInfo(&buf, ", synchronous_commit = on"); + else if (strcmp(TextDatumGetCString(datum), "local") == 0) + appendStringInfo(&buf, ", synchronous_commit = local"); + else if (strcmp(TextDatumGetCString(datum), "remote_write") == 0) + appendStringInfo(&buf, ", synchronous_commit = remote_write"); + else if (strcmp(TextDatumGetCString(datum), "remote_apply") == 0) + appendStringInfo(&buf, ", synchronous_commit = remote_apply"); + + /* Get two-phase commit option */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subtwophasestate); + if (DatumGetChar(datum) != LOGICALREP_TWOPHASE_STATE_DISABLED) + appendStringInfo(&buf, ", two_phase = on"); + + /* Disable on error? */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subdisableonerr); + if (DatumGetBool(datum)) + appendStringInfo(&buf, ", disable_on_error = on"); + + /* Password required? */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subpasswordrequired); + if (!DatumGetBool(datum)) + appendStringInfo(&buf, ", password_required = off"); + + /* Run as owner? */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subrunasowner); + if (DatumGetBool(datum)) + appendStringInfo(&buf, ", run_as_owner = on"); + + /* Get origin */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_suborigin); + if (pg_strcasecmp(TextDatumGetCString(datum), LOGICALREP_ORIGIN_ANY) != 0) + appendStringInfo(&buf, ", origin = none"); + + /* Failover? */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subfailover); + if (DatumGetBool(datum)) + appendStringInfo(&buf, ", failover = on"); + + /* Finally close parenthesis and add semicolon to the statement */ + appendStringInfo(&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..3adacc85975 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 => '703', 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..20089e2912c --- /dev/null +++ b/src/test/regress/expected/subscription_ddl.out @@ -0,0 +1,64 @@ +-- +-- Get CREATE SUBSCRIPTION statement +-- +CREATE ROLE sub_nonsup_user 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); +(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); +(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); +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); +(1 row) + +-- Non-superuser can't see subscription ddl +SET SESSION AUTHORIZATION 'sub_nonsup_user'; +SELECT pg_get_subscription_ddl('TestSubddL2'); +ERROR: permission denied for function pg_get_subscription_ddl +RESET SESSION AUTHORIZATION; +-- Administrators can change who can access this function +GRANT EXECUTE ON FUNCTION pg_get_subscription_ddl TO sub_nonsup_user; +SET SESSION AUTHORIZATION 'sub_nonsup_user'; +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); +(1 row) + +RESET SESSION AUTHORIZATION; +REVOKE EXECUTE ON FUNCTION pg_get_subscription_ddl FROM sub_nonsup_user; +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 sub_nonsup_user; 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..29500444580 --- /dev/null +++ b/src/test/regress/sql/subscription_ddl.sql @@ -0,0 +1,43 @@ +-- +-- Get CREATE SUBSCRIPTION statement +-- + +CREATE ROLE sub_nonsup_user 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); +SELECT pg_get_subscription_ddl('testsub3'); + +-- Non-superuser can't see subscription ddl +SET SESSION AUTHORIZATION 'sub_nonsup_user'; +SELECT pg_get_subscription_ddl('TestSubddL2'); +RESET SESSION AUTHORIZATION; +-- Administrators can change who can access this function +GRANT EXECUTE ON FUNCTION pg_get_subscription_ddl TO sub_nonsup_user; +SET SESSION AUTHORIZATION 'sub_nonsup_user'; +SELECT pg_get_subscription_ddl('TestSubddL2'); + +RESET SESSION AUTHORIZATION; +REVOKE EXECUTE ON FUNCTION pg_get_subscription_ddl FROM sub_nonsup_user; +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 sub_nonsup_user; From cd3d5c6ffd5522112101b40a06a61c2d3eacbb97 Mon Sep 17 00:00:00 2001 From: Vaibhav Dalvi Date: Fri, 3 Oct 2025 11:40:01 +0000 Subject: [PATCH 2/4] Fixed 1st round of review comments - use appendStringInfoString() - use OID > 8000 - Add missed two options --- src/backend/utils/adt/ruleutils.c | 50 ++++++++++++------- src/include/catalog/pg_proc.dat | 2 +- .../regress/expected/subscription_ddl.out | 13 +++-- src/test/regress/sql/subscription_ddl.sql | 3 +- 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index bb7a2a6b831..f20cd15d90e 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -13731,6 +13731,7 @@ pg_get_subscription_ddl(PG_FUNCTION_ARGS) char *conninfo; List *publist; Datum datum; + int maxret; bool isnull; if (PG_ARGISNULL(0)) @@ -13777,7 +13778,7 @@ pg_get_subscription_ddl(PG_FUNCTION_ARGS) * available in the catalog table. We can use default values i.e. TRUE * for both. This is what pg_dump uses. */ - appendStringInfo(&buf, " WITH (connect = false"); + appendStringInfoString(&buf, " WITH (connect = false"); /* Get slotname */ datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup, @@ -13791,9 +13792,9 @@ pg_get_subscription_ddl(PG_FUNCTION_ARGS) } else { - appendStringInfo(&buf, ", slot_name = none"); + appendStringInfoString(&buf, ", slot_name = none"); /* Setting slot_name to none must set create_slot to false */ - appendStringInfo(&buf, ", create_slot = false"); + appendStringInfoString(&buf, ", create_slot = false"); } /* Get enabled option */ @@ -13801,72 +13802,85 @@ pg_get_subscription_ddl(PG_FUNCTION_ARGS) Anum_pg_subscription_subenabled); /* Setting 'slot_name' to none must set 'enabled' to false as well */ if (!DatumGetBool(datum) || isnull) - appendStringInfo(&buf, ", enabled = false"); + appendStringInfoString(&buf, ", enabled = false"); /* Get binary option */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_subbinary); if (DatumGetBool(datum)) - appendStringInfo(&buf, ", binary = true"); + appendStringInfoString(&buf, ", binary = true"); /* Get streaming option */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_substream); if (DatumGetChar(datum) == LOGICALREP_STREAM_OFF) - appendStringInfo(&buf, ", streaming = off"); + appendStringInfoString(&buf, ", streaming = off"); else if (DatumGetChar(datum) == LOGICALREP_STREAM_ON) - appendStringInfo(&buf, ", streaming = on"); + appendStringInfoString(&buf, ", streaming = on"); /* Get sync commit option */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_subsynccommit); if (strcmp(TextDatumGetCString(datum), "on") == 0) - appendStringInfo(&buf, ", synchronous_commit = on"); + appendStringInfoString(&buf, ", synchronous_commit = on"); else if (strcmp(TextDatumGetCString(datum), "local") == 0) - appendStringInfo(&buf, ", synchronous_commit = local"); + appendStringInfoString(&buf, ", synchronous_commit = local"); else if (strcmp(TextDatumGetCString(datum), "remote_write") == 0) - appendStringInfo(&buf, ", synchronous_commit = remote_write"); + appendStringInfoString(&buf, ", synchronous_commit = remote_write"); else if (strcmp(TextDatumGetCString(datum), "remote_apply") == 0) - appendStringInfo(&buf, ", synchronous_commit = remote_apply"); + appendStringInfoString(&buf, ", synchronous_commit = remote_apply"); /* Get two-phase commit option */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_subtwophasestate); if (DatumGetChar(datum) != LOGICALREP_TWOPHASE_STATE_DISABLED) - appendStringInfo(&buf, ", two_phase = on"); + appendStringInfoString(&buf, ", two_phase = on"); /* Disable on error? */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_subdisableonerr); if (DatumGetBool(datum)) - appendStringInfo(&buf, ", disable_on_error = on"); + appendStringInfoString(&buf, ", disable_on_error = on"); /* Password required? */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_subpasswordrequired); if (!DatumGetBool(datum)) - appendStringInfo(&buf, ", password_required = off"); + appendStringInfoString(&buf, ", password_required = off"); /* Run as owner? */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_subrunasowner); if (DatumGetBool(datum)) - appendStringInfo(&buf, ", run_as_owner = on"); + appendStringInfoString(&buf, ", run_as_owner = on"); /* Get origin */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_suborigin); if (pg_strcasecmp(TextDatumGetCString(datum), LOGICALREP_ORIGIN_ANY) != 0) - appendStringInfo(&buf, ", origin = none"); + appendStringInfoString(&buf, ", origin = none"); /* Failover? */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_subfailover); if (DatumGetBool(datum)) - appendStringInfo(&buf, ", failover = on"); + appendStringInfoString(&buf, ", failover = on"); + + /* Retain dead tuples? */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subretaindeadtuples); + if (DatumGetBool(datum)) + appendStringInfoString(&buf, ", retain_dead_tuples = on"); + + /* Max retention duration */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_submaxretention); + maxret = Int32GetDatum(datum); + if (maxret) + appendStringInfo(&buf, ", max_retention_duration = %d", maxret); /* Finally close parenthesis and add semicolon to the statement */ - appendStringInfo(&buf, ");"); + appendStringInfoString(&buf, ");"); ReleaseSysCache(tup); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 3adacc85975..53fe7d6ff2d 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -3993,7 +3993,7 @@ { oid => '1387', descr => 'constraint description', proname => 'pg_get_constraintdef', provolatile => 's', prorettype => 'text', proargtypes => 'oid', prosrc => 'pg_get_constraintdef' }, -{ oid => '703', descr => 'get CREATE statement for subscription', +{ 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', diff --git a/src/test/regress/expected/subscription_ddl.out b/src/test/regress/expected/subscription_ddl.out index 20089e2912c..8a2f872cad9 100644 --- a/src/test/regress/expected/subscription_ddl.out +++ b/src/test/regress/expected/subscription_ddl.out @@ -31,13 +31,18 @@ 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); + password_required=false, run_as_owner=true, origin=none, failover=true, + retain_dead_tuples=true, max_retention_duration=100); +WARNING: commit timestamp and origin data required for detecting conflicts won't be retained +HINT: Consider setting "track_commit_timestamp" to true. +WARNING: deleted rows to detect conflicts would not be removed until the subscription is enabled +HINT: Consider setting retain_dead_tuples to 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. 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); + 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 = on, max_retention_duration = 100); (1 row) -- Non-superuser can't see subscription ddl diff --git a/src/test/regress/sql/subscription_ddl.sql b/src/test/regress/sql/subscription_ddl.sql index 29500444580..04a54364dff 100644 --- a/src/test/regress/sql/subscription_ddl.sql +++ b/src/test/regress/sql/subscription_ddl.sql @@ -21,7 +21,8 @@ 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); + password_required=false, run_as_owner=true, origin=none, failover=true, + retain_dead_tuples=true, max_retention_duration=100); SELECT pg_get_subscription_ddl('testsub3'); -- Non-superuser can't see subscription ddl From 74c56a657228d05b56f0e8ecd89fdc26a4ee6cec Mon Sep 17 00:00:00 2001 From: Vaibhav Dalvi Date: Wed, 8 Oct 2025 12:19:13 +0000 Subject: [PATCH 3/4] work on 2nd round of review comments - Allow users to access this function those have pg_create_subscription and pg_read_all_data permission - Don't omit default value because it may change in future Author: Vaibhav Dalvi Reviewed-by: Nishant Sharma, Reviewed-by: Ian Barwick Reviewed-by: Akshay Joshi Reviewed-by: Phil Alger Discussion: https://www.postgresql.org/message-id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9@dunslane.net --- doc/src/sgml/func/func-info.sgml | 9 ++- src/backend/catalog/system_functions.sql | 2 - src/backend/utils/adt/ruleutils.c | 80 ++++++++++--------- .../regress/expected/subscription_ddl.out | 53 ++++++++---- src/test/regress/sql/subscription_ddl.sql | 25 ++++-- 5 files changed, 102 insertions(+), 67 deletions(-) diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml index 3eccbf8bdf0..d266c2ea4bf 100644 --- a/doc/src/sgml/func/func-info.sgml +++ b/doc/src/sgml/func/func-info.sgml @@ -3833,12 +3833,13 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} Reconstructs the creating command for a subscription. The result is a complete CREATE SUBSCRIPTION - statement. This statement omits default values. The - connect option set to false. + statement. The connect option set to + false. - This function is restricted to superusers by default, but other users - can be granted EXECUTE to run the function. + 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/system_functions.sql b/src/backend/catalog/system_functions.sql index d9a7e67103e..2d946d6d9e9 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -782,8 +782,6 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; -REVOKE EXECUTE ON FUNCTION pg_get_subscription_ddl(text) FROM public; - -- -- We also set up some things as accessible to standard roles. -- diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index f20cd15d90e..c1c3a682858 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -58,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" @@ -13724,19 +13725,28 @@ get_range_partbound_string(List *bound_datums) Datum pg_get_subscription_ddl(PG_FUNCTION_ARGS) { - StringInfo pubnames = makeStringInfo(); + char *subname = text_to_cstring(PG_GETARG_TEXT_P(0)); + StringInfo pubnames; StringInfoData buf; HeapTuple tup; - char *subname; char *conninfo; List *publist; Datum datum; - int maxret; bool isnull; - if (PG_ARGISNULL(0)) - PG_RETURN_NULL(); - subname = text_to_cstring(PG_GETARG_TEXT_P(0)); + /* + * 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), @@ -13764,6 +13774,7 @@ pg_get_subscription_ddl(PG_FUNCTION_ARGS) 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); @@ -13785,11 +13796,8 @@ pg_get_subscription_ddl(PG_FUNCTION_ARGS) Anum_pg_subscription_subslotname, &isnull); if (!isnull) - { - char *slotname = pstrdup(NameStr(*DatumGetName(datum))); - - appendStringInfo(&buf, ", slot_name = \'%s\'", slotname); - } + appendStringInfo(&buf, ", slot_name = \'%s\'", + NameStr(*DatumGetName(datum))); else { appendStringInfoString(&buf, ", slot_name = none"); @@ -13803,12 +13811,14 @@ pg_get_subscription_ddl(PG_FUNCTION_ARGS) /* 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); - if (DatumGetBool(datum)) - appendStringInfoString(&buf, ", binary = true"); + appendStringInfo(&buf, ", binary = %s", + DatumGetBool(datum) ? "true" : "false"); /* Get streaming option */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, @@ -13817,67 +13827,63 @@ pg_get_subscription_ddl(PG_FUNCTION_ARGS) 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); - if (strcmp(TextDatumGetCString(datum), "on") == 0) - appendStringInfoString(&buf, ", synchronous_commit = on"); - else if (strcmp(TextDatumGetCString(datum), "local") == 0) - appendStringInfoString(&buf, ", synchronous_commit = local"); - else if (strcmp(TextDatumGetCString(datum), "remote_write") == 0) - appendStringInfoString(&buf, ", synchronous_commit = remote_write"); - else if (strcmp(TextDatumGetCString(datum), "remote_apply") == 0) - appendStringInfoString(&buf, ", synchronous_commit = remote_apply"); + 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) + 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); - if (DatumGetBool(datum)) - appendStringInfoString(&buf, ", disable_on_error = on"); + appendStringInfo(&buf, ", disable_on_error = %s", + DatumGetBool(datum) ? "on" : "off"); /* Password required? */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_subpasswordrequired); - if (!DatumGetBool(datum)) - appendStringInfoString(&buf, ", password_required = off"); + appendStringInfo(&buf, ", password_required = %s", + DatumGetBool(datum) ? "on" : "off"); /* Run as owner? */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_subrunasowner); - if (DatumGetBool(datum)) - appendStringInfoString(&buf, ", run_as_owner = on"); + appendStringInfo(&buf, ", run_as_owner = %s", + DatumGetBool(datum) ? "on" : "off"); /* Get origin */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_suborigin); - if (pg_strcasecmp(TextDatumGetCString(datum), LOGICALREP_ORIGIN_ANY) != 0) - appendStringInfoString(&buf, ", origin = none"); + appendStringInfo(&buf, ", origin = %s", TextDatumGetCString(datum)); /* Failover? */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_subfailover); - if (DatumGetBool(datum)) - appendStringInfoString(&buf, ", failover = on"); + appendStringInfo(&buf, ", failover = %s", + DatumGetBool(datum) ? "on" : "off"); /* Retain dead tuples? */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_subretaindeadtuples); - if (DatumGetBool(datum)) - appendStringInfoString(&buf, ", retain_dead_tuples = on"); + appendStringInfo(&buf, ", retain_dead_tuples = %s", + DatumGetBool(datum) ? "on" : "off"); /* Max retention duration */ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_submaxretention); - maxret = Int32GetDatum(datum); - if (maxret) - appendStringInfo(&buf, ", max_retention_duration = %d", maxret); + appendStringInfo(&buf, ", max_retention_duration = %lu", + Int32GetDatum(datum)); /* Finally close parenthesis and add semicolon to the statement */ appendStringInfoString(&buf, ");"); diff --git a/src/test/regress/expected/subscription_ddl.out b/src/test/regress/expected/subscription_ddl.out index 8a2f872cad9..0ffbb1f6c63 100644 --- a/src/test/regress/expected/subscription_ddl.out +++ b/src/test/regress/expected/subscription_ddl.out @@ -1,7 +1,8 @@ -- -- Get CREATE SUBSCRIPTION statement -- -CREATE ROLE sub_nonsup_user LOGIN; +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); @@ -9,9 +10,9 @@ 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); + 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 @@ -21,9 +22,9 @@ CREATE SUBSCRIPTION "TestSubddL2" CONNECTION 'host=unknown user=dvd password=pas 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); + 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 @@ -45,25 +46,43 @@ SELECT pg_get_subscription_ddl('testsub3'); 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 = on, max_retention_duration = 100); (1 row) --- Non-superuser can't see subscription ddl -SET SESSION AUTHORIZATION 'sub_nonsup_user'; +-- 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 for function pg_get_subscription_ddl +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 '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; -- Administrators can change who can access this function -GRANT EXECUTE ON FUNCTION pg_get_subscription_ddl TO sub_nonsup_user; -SET SESSION AUTHORIZATION 'sub_nonsup_user'; +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); + 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 EXECUTE ON FUNCTION pg_get_subscription_ddl FROM sub_nonsup_user; +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 sub_nonsup_user; +DROP ROLE createsub_role; +DROP ROLE readalldata_role; diff --git a/src/test/regress/sql/subscription_ddl.sql b/src/test/regress/sql/subscription_ddl.sql index 04a54364dff..0d6cc556d2d 100644 --- a/src/test/regress/sql/subscription_ddl.sql +++ b/src/test/regress/sql/subscription_ddl.sql @@ -2,7 +2,8 @@ -- Get CREATE SUBSCRIPTION statement -- -CREATE ROLE sub_nonsup_user LOGIN; +CREATE ROLE createsub_role LOGIN; +CREATE ROLE readalldata_role LOGIN; -- Create subscription with minimal options CREATE SUBSCRIPTION testsub1 CONNECTION 'dbname=db_doesnotexist' @@ -25,20 +26,30 @@ CREATE SUBSCRIPTION testsub3 CONNECTION 'host=unknown user=dvd password=pass12' retain_dead_tuples=true, max_retention_duration=100); SELECT pg_get_subscription_ddl('testsub3'); --- Non-superuser can't see subscription ddl -SET SESSION AUTHORIZATION 'sub_nonsup_user'; +-- 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 'createsub_role'; SELECT pg_get_subscription_ddl('TestSubddL2'); RESET SESSION AUTHORIZATION; -- Administrators can change who can access this function -GRANT EXECUTE ON FUNCTION pg_get_subscription_ddl TO sub_nonsup_user; -SET SESSION AUTHORIZATION 'sub_nonsup_user'; +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 EXECUTE ON FUNCTION pg_get_subscription_ddl FROM sub_nonsup_user; +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 sub_nonsup_user; +DROP ROLE createsub_role; +DROP ROLE readalldata_role; From b11a7da1e1051e6f78a44b0451fed8d6ca112da8 Mon Sep 17 00:00:00 2001 From: Vaibhav Dalvi Date: Thu, 9 Oct 2025 12:22:45 +0000 Subject: [PATCH 4/4] fix check-world failure The error ""wal_level" is insufficient to create the replication slot required by retain_dead_tuples" was there due to true "retain_dead_tuples". Set to false. Vaibhav Dalvi --- src/test/regress/expected/subscription_ddl.out | 15 ++++++--------- src/test/regress/sql/subscription_ddl.sql | 4 ++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/test/regress/expected/subscription_ddl.out b/src/test/regress/expected/subscription_ddl.out index 0ffbb1f6c63..1f4fa08c63c 100644 --- a/src/test/regress/expected/subscription_ddl.out +++ b/src/test/regress/expected/subscription_ddl.out @@ -33,17 +33,14 @@ CREATE SUBSCRIPTION testsub3 CONNECTION 'host=unknown user=dvd password=pass12' 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=true, max_retention_duration=100); -WARNING: commit timestamp and origin data required for detecting conflicts won't be retained -HINT: Consider setting "track_commit_timestamp" to true. -WARNING: deleted rows to detect conflicts would not be removed until the subscription is enabled -HINT: Consider setting retain_dead_tuples to false. + 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 = on, max_retention_duration = 100); + 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 @@ -53,7 +50,7 @@ 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 'createsub_role'; +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. diff --git a/src/test/regress/sql/subscription_ddl.sql b/src/test/regress/sql/subscription_ddl.sql index 0d6cc556d2d..6e8eecf010e 100644 --- a/src/test/regress/sql/subscription_ddl.sql +++ b/src/test/regress/sql/subscription_ddl.sql @@ -23,7 +23,7 @@ CREATE SUBSCRIPTION testsub3 CONNECTION 'host=unknown user=dvd password=pass12' 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=true, max_retention_duration=100); + 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 @@ -31,7 +31,7 @@ SELECT pg_get_subscription_ddl('testsub3'); SET SESSION AUTHORIZATION 'createsub_role'; SELECT pg_get_subscription_ddl('TestSubddL2'); RESET SESSION AUTHORIZATION; -SET SESSION AUTHORIZATION 'createsub_role'; +SET SESSION AUTHORIZATION 'readalldata_role'; SELECT pg_get_subscription_ddl('TestSubddL2'); RESET SESSION AUTHORIZATION; -- Administrators can change who can access this function