[PATCH] Add pg_get_subscription_ddl() function
Hi Hackers,
I am submitting a patch as a part of a larger Retail DDL functions
project described by Andrew Dunstan here
</messages/by-id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9@dunslane.net>
. This patch creates a
function pg_get_subscription_ddl, designed to retrieve the full DDL
statement for a subscription. Users can obtain the DDL by providing
the subscription name, like so:
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)
This patch includes documentation, comments, and regression tests.
Regards,
Vaibhav Dalvi
EnterpriseDB
Attachments:
v1-Add-pg_get_subscription_ddl-function.patchapplication/octet-stream; name=v1-Add-pg_get_subscription_ddl-function.patchDownload
From e49e9fc2f8e62a0826922889e4504a280ea76f23 Mon Sep 17 00:00:00 2001
From: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Date: Fri, 31 Oct 2025 10:15:13 +0000
Subject: [PATCH v1 1/1] 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.
This function is restricted to users that have the "pg_read_all_data" and/or
"pg_create_subscription" privilege. This is a security measure because
subscription connection strings often contain sensitive information, such as
passwords.
---
doc/src/sgml/func/func-info.sgml | 51 +++++
src/backend/catalog/pg_subscription.c | 4 +-
src/backend/utils/adt/ruleutils.c | 177 ++++++++++++++++++
src/include/catalog/pg_proc.dat | 3 +
src/include/catalog/pg_subscription.h | 2 +
.../regress/expected/subscription_ddl.out | 85 +++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/sql/subscription_ddl.sql | 55 ++++++
8 files changed, 375 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..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}
</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"/>
+ print the DDL statements for various database objects.
+ (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_subscription_ddl</primary>
+ </indexterm>
+ <function>pg_get_subscription_ddl</function> ( <parameter>subscription</parameter> <type>text</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a subscription.
+ The result is a complete <command>CREATE SUBSCRIPTION</command>
+ statement. The <literal>connect</literal> option set to
+ <literal>false</literal>.
+ </para>
+ <para>
+ This function is restricted to users that have the
+ <literal>pg_read_all_data</literal> and/or
+ <literal>pg_create_subscription</literal> privilege.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index 15b233a37d8..4a3e476cf63 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 79ec136231b..87c0b90534a 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"
@@ -13738,3 +13740,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 9121a382f76..4526853aad2 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..ce4b3138f81
--- /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 alter the subscription to refresh publications.
+-- 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 alter the subscription to refresh publications.
+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 alter the subscription to refresh publications.
+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 a0f5fab0f5d..cbe563413bb 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;
--
2.43.0
Hi Hackers,
Please find the revised patch for the `pg_get_subscription_ddl()` function
attached.
Based on feedback, this version of the function now supports calling the
DDL retrieval
using either the subscription name or the OID, as shown in the examples
below:
```sql
postgres=# 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)
postgres=# SELECT pg_get_subscription_ddl(16384);
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)
```
I request your review of the updated patch.
Regards,
Vaibhav Dalvi
EnterpriseDB
On Thu, Nov 6, 2025 at 7:26 PM Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
wrote:
Show quoted text
Hi Hackers,
Please find the revised patch for the `pg_get_subscription_ddl()` function
attached.Based on feedback, this version of the function now supports calling the
DDL retrieval
using either the subscription name or the OID, as shown in the examples
below:```sql
postgres=# 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)postgres=# SELECT pg_get_subscription_ddl(16384);
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)
```I request your review of the updated patch.
Regards,
Vaibhav Dalvi
EnterpriseDBOn Fri, Oct 31, 2025 at 4:27 PM Vaibhav Dalvi <
vaibhav.dalvi@enterprisedb.com> wrote:Hi Hackers,
I am submitting a patch as a part of a larger Retail DDL functions
project described by Andrew Dunstan here
</messages/by-id/945db7c5-be75-45bf-b55b-cb1e56f2e3e9@dunslane.net>
. This patch creates a
function pg_get_subscription_ddl, designed to retrieve the full DDL
statement for a subscription. Users can obtain the DDL by providing
the subscription name, like so: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)This patch includes documentation, comments, and regression tests.
Regards,
Vaibhav Dalvi
EnterpriseDB
Attachments:
v2-Add-pg_get_subscription_ddl-function.patchapplication/octet-stream; name=v2-Add-pg_get_subscription_ddl-function.patchDownload
From 1c3ff3e9e480444ff7386147d62cf5e841e36e3f Mon Sep 17 00:00:00 2001
From: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Date: Thu, 6 Nov 2025 13:38:42 +0000
Subject: [PATCH v2 1/1] Add pg_get_subscription_ddl() function
This new SQL-callable function returns the `CREATE SUBSCRIPTION`
statement for a given subscription name or oid.
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.
This function is restricted to users that have the "pg_read_all_data" and/or
"pg_create_subscription" privilege. This is a security measure because
subscription connection strings often contain sensitive information, such as
passwords.
Author: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Reviewers: Akshay Joshi, Ian Barwick
Discussion: https://www.postgresql.org/message-id/CA%2BvB%3DAGG0NoxWW%3D-947RBmba8Pzhj8j7op0Xkv8nLDzVMc2%3D7w%40mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 51 +++++
src/backend/catalog/pg_subscription.c | 4 +-
src/backend/utils/adt/ruleutils.c | 216 +++++++++++++++++++++
src/include/catalog/pg_proc.dat | 6 +
src/include/catalog/pg_subscription.h | 2 +
src/include/utils/ruleutils.h | 1 +
src/test/regress/expected/subscription.out | 95 +++++++++
src/test/regress/sql/subscription.sql | 60 ++++++
8 files changed, 432 insertions(+), 3 deletions(-)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..21d33ee1802 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}
</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"/>
+ print the DDL statements for various database objects.
+ (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_subscription_ddl</primary>
+ </indexterm>
+ <function>pg_get_subscription_ddl</function> ( <parameter>subscription</parameter> <type>text</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a subscription.
+ The result is a complete <command>CREATE SUBSCRIPTION</command>
+ statement. The <literal>connect</literal> option set to
+ <literal>false</literal>.
+ </para>
+ <para>
+ This function is restricted to users that have the
+ <literal>pg_read_all_data</literal> and/or
+ <literal>pg_create_subscription</literal> privilege.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index 1945627ed88..98dc15e0f41 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 556ab057e5a..52007bcbab7 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"
@@ -13743,3 +13745,217 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * build_subscription_ddl_string - Build CREATE SUBSCRIPTION statement for
+ * a subscription from its OID. This is internal version which helps
+ * pg_get_subscription_ddl_name() and pg_get_subscription_ddl_oid().
+ */
+char *
+build_subscription_ddl_string(const Oid suboid)
+{
+ Form_pg_subscription subForm;
+ 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 = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(suboid));
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("subscription with oid %d does not exist", suboid)));
+
+ /* Get subscription's details from its tuple */
+ subForm = (Form_pg_subscription) GETSTRUCT(tup);
+
+ initStringInfo(&buf);
+
+ /* Build the CREATE SUBSCRIPTION statement */
+ appendStringInfo(&buf, "CREATE SUBSCRIPTION %s ",
+ quote_identifier(NameStr(subForm->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);
+
+ return buf.data;
+}
+
+/*
+ * pg_get_subscription_ddl_name - Get CREATE SUBSCRIPTION statement for a
+ * subscription. This takes name as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_name(PG_FUNCTION_ARGS)
+{
+ Name subname = PG_GETARG_NAME(0);
+ Oid suboid;
+ char *ddl_stmt;
+
+ /* Get the OID of the subscription from its name */
+ suboid = get_subscription_oid(NameStr(*subname), false);
+
+ /* Get the CREATE SUBSCRIPTION DDL statement from its OID */
+ ddl_stmt = build_subscription_ddl_string(suboid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_subscription_ddl_oid - Get CREATE SUBSCRIPTION statement for a
+ * subscription. This takes oid as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_oid(PG_FUNCTION_ARGS)
+{
+ Oid suboid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE SUBSCRIPTION DDL statement from its OID */
+ ddl_stmt = build_subscription_ddl_string(suboid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 34b7fddb0e7..4b9efd141af 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3993,6 +3993,12 @@
{ 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 => 'name', prosrc => 'pg_get_subscription_ddl_name' },
+{ oid => '8002', descr => 'get CREATE statement for subscription',
+ proname => 'pg_get_subscription_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_subscription_ddl_oid' },
{ 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/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 7ba7d887914..f8f26284489 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -53,5 +53,6 @@ extern char *generate_opclass_name(Oid opclass);
extern char *get_range_partbound_string(List *bound_datums);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern char *build_subscription_ddl_string(const Oid suboid);
#endif /* RULEUTILS_H */
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index 327d1e7731f..1f2ad15c7ff 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -522,3 +522,98 @@ DROP ROLE regress_subscription_user;
DROP ROLE regress_subscription_user2;
DROP ROLE regress_subscription_user3;
DROP ROLE regress_subscription_user_dummy;
+--
+-- Test pg_get_subscription_ddl() by creating subscriptions with various
+-- configurations and checking the DDL.
+--
+CREATE ROLE createsub_role LOGIN;
+CREATE ROLE readalldata_role LOGIN;
+-- see the pg_get_subscription_ddl output for a NULL and empty input
+SELECT pg_get_subscription_ddl('');
+ERROR: subscription "" does not exist
+SELECT pg_get_subscription_ddl(NULL);
+ pg_get_subscription_ddl
+-------------------------
+
+(1 row)
+
+-- 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 alter the subscription to refresh publications.
+-- 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 alter the subscription to refresh publications.
+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 alter the subscription to refresh publications.
+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/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index ef0c298d2df..ef3c6c9ca16 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -370,3 +370,63 @@ DROP ROLE regress_subscription_user;
DROP ROLE regress_subscription_user2;
DROP ROLE regress_subscription_user3;
DROP ROLE regress_subscription_user_dummy;
+
+--
+-- Test pg_get_subscription_ddl() by creating subscriptions with various
+-- configurations and checking the DDL.
+--
+CREATE ROLE createsub_role LOGIN;
+CREATE ROLE readalldata_role LOGIN;
+
+-- see the pg_get_subscription_ddl output for a NULL and empty input
+SELECT pg_get_subscription_ddl('');
+SELECT pg_get_subscription_ddl(NULL);
+
+-- 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;
--
2.43.0
Import Notes
Reply to msg id not found: CA+vBAFCvDY80iENz1BNjR+fQ8hCYupUEBY8xwYiZiuTeauC+w@mail.gmail.com
Hello Vaibhav,
I wonder why is Subscription->publications a list of String rather than
a list of C strings. That's something you'd see in a Node structure,
but Subscription is not a node, so this seems wasteful and pointless.
This is of course not the fault of your patch, but the fact that your
patch feels the need to expose the textarray_to_stringlist() auxiliary
function made me wonder about it. I think that's not a great function
to expose, at least not from pg_subscription.h, so maybe we should
instead think about getting rid of the String nodes from there and see
about making this whole thing simpler. On the other hand, we already
have function strlist_to_textarray() declared in objectaddress.h, which
is kinda the inverse of this ... Looking further, I wonder why we have
publicationListToArray() when it seems strlist_to_textarray() is likely
to fit the bill, with a couple of tweaks --- assuming we want to keep
using String nodes in Subscription, which I doubt.
Oh, we also have textarray_to_strvaluelist() which is essentially
identical, but also static. If we're making one of them non-static,
then for sure let's remove the other one. But maybe what we really need
is a third one to use in ruleutils, and expose neither? (I think if we
get rid of the String around Subscription->publications, that's likely
what I'd do, since they'd be mostly trivial wrappers around
deconstruct_array_builtin.)
Anyway, I guess this is a long-winded way of saying that I don't think
making textarray_to_stringlist() non-static is a great idea. At least
not where you're doing it. I would start this with a 0001 patch that
gets rid of String usage there, and then the rest of your function on
top of that.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"La gente vulgar sólo piensa en pasar el tiempo;
el que tiene talento, en aprovecharlo"
Hi Alvaro,
Thanks for your input.
On Thu, Nov 6, 2025 at 9:18 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
Hello Vaibhav,
I wonder why is Subscription->publications a list of String rather than
a list of C strings. That's something you'd see in a Node structure,
but Subscription is not a node, so this seems wasteful and pointless.
I looked more into this and came to know that we can't make
Subscription->publications a list of C strings because input publications
list is also in the list of String from the parser:
CreateSubscriptionStmt:
CREATE SUBSCRIPTION name CONNECTION Sconst PUBLICATION name_list
opt_definition
{
CreateSubscriptionStmt *n =
makeNode(CreateSubscriptionStmt);
n->subname = $3;
n->conninfo = $5;
n->publication = $7;
n->options = $8;
$$ = (Node *) n;
};
name_list: name
{ $$ = list_make1(makeString($1)); }
| name_list ',' name
{ $$ = lappend($1, makeString($3)); };
Oh, we also have textarray_to_strvaluelist() which is essentially
identical, but also static. If we're making one of them non-static,
then for sure let's remove the other one. But maybe what we really need
is a third one to use in ruleutils, and expose neither? (I think if we
get rid of the String around Subscription->publications, that's likely
what I'd do, since they'd be mostly trivial wrappers around
deconstruct_array_builtin.)
I think we really need a third one to use in ruleutils, and expose neither.
Find the attached v3 patch which does the same.
Attachments:
v3-Add-pg_get_subscription_ddl-function.patchapplication/octet-stream; name=v3-Add-pg_get_subscription_ddl-function.patchDownload
From 170f3af9a4e29efa242692551435065b057b9ba8 Mon Sep 17 00:00:00 2001
From: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Date: Fri, 7 Nov 2025 10:14:54 +0000
Subject: [PATCH v3 1/1] Add pg_get_subscription_ddl() function
This new SQL-callable function returns the `CREATE SUBSCRIPTION`
statement for a given subscription name or oid.
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.
This function is restricted to users that have the "pg_read_all_data" and/or
"pg_create_subscription" privilege. This is a security measure because
subscription connection strings often contain sensitive information, such as
passwords.
Author: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Reviewers: Akshay Joshi, Nishant Sharma, Ian Barwick & Alvaro Herrera
Discussion: https://www.postgresql.org/message-id/CA%2BvB%3DAGG0NoxWW%3D-947RBmba8Pzhj8j7op0Xkv8nLDzVMc2%3D7w%40mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 51 +++++
src/backend/utils/adt/ruleutils.c | 246 +++++++++++++++++++++
src/include/catalog/pg_proc.dat | 6 +
src/include/utils/ruleutils.h | 1 +
src/test/regress/expected/subscription.out | 95 ++++++++
src/test/regress/sql/subscription.sql | 60 +++++
6 files changed, 459 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..21d33ee1802 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}
</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"/>
+ print the DDL statements for various database objects.
+ (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_subscription_ddl</primary>
+ </indexterm>
+ <function>pg_get_subscription_ddl</function> ( <parameter>subscription</parameter> <type>text</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a subscription.
+ The result is a complete <command>CREATE SUBSCRIPTION</command>
+ statement. The <literal>connect</literal> option set to
+ <literal>false</literal>.
+ </para>
+ <para>
+ This function is restricted to users that have the
+ <literal>pg_read_all_data</literal> and/or
+ <literal>pg_create_subscription</literal> privilege.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..f76f21f80dc 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"
@@ -546,6 +548,7 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
deparse_context *context,
bool showimplicit,
bool needcomma);
+static List *text_array_to_string_list(ArrayType *text_array);
#define only_marker(rte) ((rte)->inh ? "" : "ONLY ")
@@ -13743,3 +13746,246 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * build_subscription_ddl_string - Build CREATE SUBSCRIPTION statement for
+ * a subscription from its OID. This is internal version which helps
+ * pg_get_subscription_ddl_name() and pg_get_subscription_ddl_oid().
+ */
+char *
+build_subscription_ddl_string(const Oid suboid)
+{
+ Form_pg_subscription subForm;
+ 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 = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(suboid));
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("subscription with oid %d does not exist", suboid)));
+
+ /* Get subscription's details from its tuple */
+ subForm = (Form_pg_subscription) GETSTRUCT(tup);
+
+ initStringInfo(&buf);
+
+ /* Build the CREATE SUBSCRIPTION statement */
+ appendStringInfo(&buf, "CREATE SUBSCRIPTION %s ",
+ quote_identifier(NameStr(subForm->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 = text_array_to_string_list(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 = %d",
+ DatumGetInt32(datum));
+
+ /* Finally close parenthesis and add semicolon to the statement */
+ appendStringInfoString(&buf, ");");
+
+ ReleaseSysCache(tup);
+
+ return buf.data;
+}
+
+/*
+ * pg_get_subscription_ddl_name
+ * Get CREATE SUBSCRIPTION statement for a subscription.
+ *
+ * This takes name as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_name(PG_FUNCTION_ARGS)
+{
+ Name subname = PG_GETARG_NAME(0);
+ Oid suboid;
+ char *ddl_stmt;
+
+ /* Get the OID of the subscription from its name */
+ suboid = get_subscription_oid(NameStr(*subname), false);
+
+ /* Get the CREATE SUBSCRIPTION DDL statement from its OID */
+ ddl_stmt = build_subscription_ddl_string(suboid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_subscription_ddl_oid
+ * Get CREATE SUBSCRIPTION statement for a subscription.
+ *
+ * This takes oid as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_oid(PG_FUNCTION_ARGS)
+{
+ Oid suboid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE SUBSCRIPTION DDL statement from its OID */
+ ddl_stmt = build_subscription_ddl_string(suboid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * text_array_to_string_list
+ * Convert text array to list of strings.
+ *
+ * The resulting list of strings is pallocated here.
+ */
+static List *
+text_array_to_string_list(ArrayType *text_array)
+{
+ List *result = NIL;
+ Datum *elems;
+ int nelems,
+ i;
+
+ deconstruct_array_builtin(text_array, TEXTOID, &elems, NULL, &nelems);
+
+ if (nelems == 0)
+ return NIL;
+
+ for (i = 0; i < nelems; i++)
+ result = lappend(result, makeString(TextDatumGetCString(elems[i])));
+
+ return result;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 34b7fddb0e7..4b9efd141af 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3993,6 +3993,12 @@
{ 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 => 'name', prosrc => 'pg_get_subscription_ddl_name' },
+{ oid => '8002', descr => 'get CREATE statement for subscription',
+ proname => 'pg_get_subscription_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_subscription_ddl_oid' },
{ 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/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 7ba7d887914..f8f26284489 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -53,5 +53,6 @@ extern char *generate_opclass_name(Oid opclass);
extern char *get_range_partbound_string(List *bound_datums);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern char *build_subscription_ddl_string(const Oid suboid);
#endif /* RULEUTILS_H */
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index 327d1e7731f..1f2ad15c7ff 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -522,3 +522,98 @@ DROP ROLE regress_subscription_user;
DROP ROLE regress_subscription_user2;
DROP ROLE regress_subscription_user3;
DROP ROLE regress_subscription_user_dummy;
+--
+-- Test pg_get_subscription_ddl() by creating subscriptions with various
+-- configurations and checking the DDL.
+--
+CREATE ROLE createsub_role LOGIN;
+CREATE ROLE readalldata_role LOGIN;
+-- see the pg_get_subscription_ddl output for a NULL and empty input
+SELECT pg_get_subscription_ddl('');
+ERROR: subscription "" does not exist
+SELECT pg_get_subscription_ddl(NULL);
+ pg_get_subscription_ddl
+-------------------------
+
+(1 row)
+
+-- 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 alter the subscription to refresh publications.
+-- 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 alter the subscription to refresh publications.
+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 alter the subscription to refresh publications.
+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/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index ef0c298d2df..ef3c6c9ca16 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -370,3 +370,63 @@ DROP ROLE regress_subscription_user;
DROP ROLE regress_subscription_user2;
DROP ROLE regress_subscription_user3;
DROP ROLE regress_subscription_user_dummy;
+
+--
+-- Test pg_get_subscription_ddl() by creating subscriptions with various
+-- configurations and checking the DDL.
+--
+CREATE ROLE createsub_role LOGIN;
+CREATE ROLE readalldata_role LOGIN;
+
+-- see the pg_get_subscription_ddl output for a NULL and empty input
+SELECT pg_get_subscription_ddl('');
+SELECT pg_get_subscription_ddl(NULL);
+
+-- 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;
--
2.43.0
On 2025-Nov-07, Vaibhav Dalvi wrote:
On Thu, Nov 6, 2025 at 9:18 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
Hello Vaibhav,
I wonder why is Subscription->publications a list of String rather than
a list of C strings. That's something you'd see in a Node structure,
but Subscription is not a node, so this seems wasteful and pointless.I looked more into this and came to know that we can't make
Subscription->publications a list of C strings because input publications
list is also in the list of String from the parser:CreateSubscriptionStmt:
CREATE SUBSCRIPTION name CONNECTION Sconst PUBLICATION name_list
opt_definition
{
CreateSubscriptionStmt *n =
makeNode(CreateSubscriptionStmt);
n->subname = $3;
n->conninfo = $5;
n->publication = $7;
n->options = $8;
$$ = (Node *) n;
};
But this is a CreateSubscriptionStmt (which is indeed a Node), not a
Subscription (which isn't). Different thing.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"In fact, the basic problem with Perl 5's subroutines is that they're not
crufty enough, so the cruft leaks out into user-defined code instead, by
the Conservation of Cruft Principle." (Larry Wall, Apocalypse 6)
Hi Alvaro,
Thanks for the explanation.
I tried to get rid of String usage in 0001 patch.
Prepared 0002 patch for actual implementation of the
function p_get_subscription_ddl().
Please find attached patches.
Regards,
Vaibhav
On Fri, Nov 7, 2025 at 5:41 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
Show quoted text
On 2025-Nov-07, Vaibhav Dalvi wrote:
On Thu, Nov 6, 2025 at 9:18 PM Álvaro Herrera <alvherre@kurilemu.de>
wrote:
Hello Vaibhav,
I wonder why is Subscription->publications a list of String rather than
a list of C strings. That's something you'd see in a Node structure,
but Subscription is not a node, so this seems wasteful and pointless.I looked more into this and came to know that we can't make
Subscription->publications a list of C strings because input publications
list is also in the list of String from the parser:CreateSubscriptionStmt:
CREATE SUBSCRIPTION name CONNECTION Sconst PUBLICATION name_list
opt_definition
{
CreateSubscriptionStmt *n =
makeNode(CreateSubscriptionStmt);
n->subname = $3;
n->conninfo = $5;
n->publication = $7;
n->options = $8;
$$ = (Node *) n;
};But this is a CreateSubscriptionStmt (which is indeed a Node), not a
Subscription (which isn't). Different thing.--
Álvaro Herrera 48°01'N 7°57'E —
https://www.EnterpriseDB.com/
"In fact, the basic problem with Perl 5's subroutines is that they're not
crufty enough, so the cruft leaks out into user-defined code instead, by
the Conservation of Cruft Principle." (Larry Wall, Apocalypse 6)
Attachments:
v4-0001-Use-list-of-C-strings-for-publications.patchapplication/octet-stream; name=v4-0001-Use-list-of-C-strings-for-publications.patchDownload
From af7b37d2eff942e4a3a9563a9340fd3d7853e5ba Mon Sep 17 00:00:00 2001
From: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Date: Tue, 11 Nov 2025 04:58:59 +0000
Subject: [PATCH v4 1/2] Use list of C strings for publications
Currently, the structure Subscription has member
publications and it is list of String node rather
than a list of C strings. Subscription is not a
not a node, so this seems wasteful and pointless.
This patch tries to get rid of the String node
around Subscription->publications.
Vaibhav Dalvi
---
src/backend/catalog/pg_subscription.c | 15 ++++++++-----
src/backend/commands/subscriptioncmds.c | 21 +++++++++++++++----
.../libpqwalreceiver/libpqwalreceiver.c | 4 ++--
3 files changed, 29 insertions(+), 11 deletions(-)
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index 1945627ed88..09e9dc98167 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -32,7 +32,7 @@
#include "utils/rel.h"
#include "utils/syscache.h"
-static List *textarray_to_stringlist(ArrayType *textarray);
+static List *textarray_to_cstringlist(ArrayType *textarray);
/*
* Add a comma-separated list of publication names to the 'dest' string.
@@ -47,7 +47,12 @@ GetPublicationsStr(List *publications, StringInfo dest, bool quote_literal)
foreach(lc, publications)
{
- char *pubname = strVal(lfirst(lc));
+ char *pubname;
+
+ if (IsA((Node *) lfirst(lc), String))
+ pubname = strVal(lfirst(lc));
+ else
+ pubname = (char *) lfirst(lc);
if (first)
first = false;
@@ -133,7 +138,7 @@ GetSubscription(Oid subid, bool missing_ok)
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
tup,
Anum_pg_subscription_subpublications);
- sub->publications = textarray_to_stringlist(DatumGetArrayTypeP(datum));
+ sub->publications = textarray_to_cstringlist(DatumGetArrayTypeP(datum));
/* Get origin */
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
@@ -241,7 +246,7 @@ DisableSubscription(Oid subid)
* Note: the resulting list of strings is pallocated here.
*/
static List *
-textarray_to_stringlist(ArrayType *textarray)
+textarray_to_cstringlist(ArrayType *textarray)
{
Datum *elems;
int nelems,
@@ -254,7 +259,7 @@ textarray_to_stringlist(ArrayType *textarray)
return NIL;
for (i = 0; i < nelems; i++)
- res = lappend(res, makeString(TextDatumGetCString(elems[i])));
+ res = lappend(res, TextDatumGetCString(elems[i]));
return res;
}
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 3d29818badd..f55230292a6 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -3043,6 +3043,9 @@ ReportSlotConnectionError(List *rstates, Oid subid, char *slotname, char *err)
* Check for duplicates in the given list of publications and error out if
* found one. Add publications to datums as text datums, if datums is not
* NULL.
+ *
+ * Note that publications can be either list of string nodes or list of the
+ * C strings.
*/
static void
check_duplicates_in_publist(List *publist, Datum *datums)
@@ -3052,12 +3055,22 @@ check_duplicates_in_publist(List *publist, Datum *datums)
foreach(cell, publist)
{
- char *name = strVal(lfirst(cell));
+ char *name;
ListCell *pcell;
+ if (IsA((Node *) lfirst(cell), String))
+ name = strVal(lfirst(cell));
+ else
+ name = (char *) lfirst(cell);
+
foreach(pcell, publist)
{
- char *pname = strVal(lfirst(pcell));
+ char *pname;
+
+ if (IsA((Node *) lfirst(pcell), String))
+ pname = strVal(lfirst(pcell));
+ else
+ pname = (char *) lfirst(pcell);
if (pcell == cell)
break;
@@ -3101,7 +3114,7 @@ merge_publications(List *oldpublist, List *newpublist, bool addpub, const char *
foreach(lc2, oldpublist)
{
- char *pubname = strVal(lfirst(lc2));
+ char *pubname = (char *) (lfirst(lc2));
if (strcmp(name, pubname) == 0)
{
@@ -3119,7 +3132,7 @@ merge_publications(List *oldpublist, List *newpublist, bool addpub, const char *
}
if (addpub && !found)
- oldpublist = lappend(oldpublist, makeString(name));
+ oldpublist = lappend(oldpublist, name);
else if (!addpub && !found)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
index 239641bfbb6..8b97619db5c 100644
--- a/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
+++ b/src/backend/replication/libpqwalreceiver/libpqwalreceiver.c
@@ -1172,7 +1172,7 @@ libpqrcv_exec(WalReceiverConn *conn, const char *query,
}
/*
- * Given a List of strings, return it as single comma separated
+ * Given a List of C strings, return it as single comma separated
* string, quoting identifiers as needed.
*
* This is essentially the reverse of SplitIdentifierString.
@@ -1190,7 +1190,7 @@ stringlist_to_identifierstr(PGconn *conn, List *strings)
foreach(lc, strings)
{
- char *val = strVal(lfirst(lc));
+ char *val = (char *) (lfirst(lc));
char *val_escaped;
if (first)
--
2.43.0
v4-0002-Add-pg_get_subscription_ddl-function.patchapplication/octet-stream; name=v4-0002-Add-pg_get_subscription_ddl-function.patchDownload
From ff9e5d8cdf8944888d1d4c3dcf225dcb8e6a2ad5 Mon Sep 17 00:00:00 2001
From: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Date: Tue, 11 Nov 2025 05:43:21 +0000
Subject: [PATCH v4 2/2] Add pg_get_subscription_ddl() function
This new SQL-callable function returns the `CREATE SUBSCRIPTION`
statement for a given subscription name or oid.
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.
This function is restricted to users that have the "pg_read_all_data" and/or
"pg_create_subscription" privilege. This is a security measure because
subscription connection strings often contain sensitive information, such as
passwords.
Author: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Reviewers: Akshay Joshi, Nishant Sharma, Ian Barwick & Alvaro Herrera
Discussion: https://www.postgresql.org/message-id/CA%2BvB%3DAGG0NoxWW%3D-947RBmba8Pzhj8j7op0Xkv8nLDzVMc2%3D7w%40mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 51 +++++
src/backend/utils/adt/ruleutils.c | 244 +++++++++++++++++++++
src/include/catalog/pg_proc.dat | 6 +
src/include/utils/ruleutils.h | 1 +
src/test/regress/expected/subscription.out | 103 ++++++++-
src/test/regress/sql/subscription.sql | 68 +++++-
6 files changed, 465 insertions(+), 8 deletions(-)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..21d33ee1802 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}
</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"/>
+ print the DDL statements for various database objects.
+ (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_subscription_ddl</primary>
+ </indexterm>
+ <function>pg_get_subscription_ddl</function> ( <parameter>subscription</parameter> <type>text</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a subscription.
+ The result is a complete <command>CREATE SUBSCRIPTION</command>
+ statement. The <literal>connect</literal> option set to
+ <literal>false</literal>.
+ </para>
+ <para>
+ This function is restricted to users that have the
+ <literal>pg_read_all_data</literal> and/or
+ <literal>pg_create_subscription</literal> privilege.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..59affbd9d1c 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"
@@ -546,6 +548,7 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
deparse_context *context,
bool showimplicit,
bool needcomma);
+static List *text_array_to_cstring_list(ArrayType *text_array);
#define only_marker(rte) ((rte)->inh ? "" : "ONLY ")
@@ -13743,3 +13746,244 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * build_subscription_ddl_string - Build CREATE SUBSCRIPTION statement for
+ * a subscription from its OID. This is internal version which helps
+ * pg_get_subscription_ddl_name() and pg_get_subscription_ddl_oid().
+ */
+char *
+build_subscription_ddl_string(const Oid suboid)
+{
+ Form_pg_subscription subForm;
+ 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 = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(suboid));
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("subscription with oid %d does not exist", suboid)));
+
+ /* Get subscription's details from its tuple */
+ subForm = (Form_pg_subscription) GETSTRUCT(tup);
+
+ initStringInfo(&buf);
+
+ /* Build the CREATE SUBSCRIPTION statement */
+ appendStringInfo(&buf, "CREATE SUBSCRIPTION %s ",
+ quote_identifier(NameStr(subForm->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 = text_array_to_cstring_list(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 = %d",
+ DatumGetInt32(datum));
+
+ /* Finally close parenthesis and add semicolon to the statement */
+ appendStringInfoString(&buf, ");");
+
+ ReleaseSysCache(tup);
+
+ return buf.data;
+}
+
+/*
+ * pg_get_subscription_ddl_name
+ * Get CREATE SUBSCRIPTION statement for a subscription.
+ *
+ * This takes name as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_name(PG_FUNCTION_ARGS)
+{
+ Name subname = PG_GETARG_NAME(0);
+ Oid suboid;
+ char *ddl_stmt;
+
+ /* Get the OID of the subscription from its name */
+ suboid = get_subscription_oid(NameStr(*subname), false);
+
+ /* Get the CREATE SUBSCRIPTION DDL statement from its OID */
+ ddl_stmt = build_subscription_ddl_string(suboid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_subscription_ddl_oid
+ * Get CREATE SUBSCRIPTION statement for a subscription.
+ *
+ * This takes oid as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_oid(PG_FUNCTION_ARGS)
+{
+ Oid suboid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE SUBSCRIPTION DDL statement from its OID */
+ ddl_stmt = build_subscription_ddl_string(suboid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * text_array_to_string_list
+ * Convert text array to list of strings.
+ */
+static List *
+text_array_to_cstring_list(ArrayType *text_array)
+{
+ List *result = NIL;
+ Datum *elems;
+ int nelems,
+ i;
+
+ deconstruct_array_builtin(text_array, TEXTOID, &elems, NULL, &nelems);
+
+ if (nelems == 0)
+ return NIL;
+
+ for (i = 0; i < nelems; i++)
+ result = lappend(result, TextDatumGetCString(elems[i]));
+
+ return result;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 34b7fddb0e7..4b9efd141af 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3993,6 +3993,12 @@
{ 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 => 'name', prosrc => 'pg_get_subscription_ddl_name' },
+{ oid => '8002', descr => 'get CREATE statement for subscription',
+ proname => 'pg_get_subscription_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_subscription_ddl_oid' },
{ 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/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 7ba7d887914..f8f26284489 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -53,5 +53,6 @@ extern char *generate_opclass_name(Oid opclass);
extern char *get_range_partbound_string(List *bound_datums);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern char *build_subscription_ddl_string(const Oid suboid);
#endif /* RULEUTILS_H */
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index 327d1e7731f..9b267240a6e 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -100,13 +100,13 @@ ERROR: subscription with slot_name = NONE must also set create_slot = false
CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, create_slot = false);
ERROR: subscription with slot_name = NONE must also set enabled = false
-- ok - with slot_name = NONE
-CREATE SUBSCRIPTION regress_testsub3 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, connect = false);
+CREATE SUBSCRIPTION regress_regress_testsub3 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, 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 alter the subscription to refresh publications.
-- fail
-ALTER SUBSCRIPTION regress_testsub3 ENABLE;
+ALTER SUBSCRIPTION regress_regress_testsub3 ENABLE;
ERROR: cannot enable subscription that does not have a slot name
-ALTER SUBSCRIPTION regress_testsub3 REFRESH PUBLICATION;
+ALTER SUBSCRIPTION regress_regress_testsub3 REFRESH PUBLICATION;
ERROR: ALTER SUBSCRIPTION ... REFRESH PUBLICATION is not allowed for disabled subscriptions
-- fail - origin must be either none or any
CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, connect = false, origin = foo);
@@ -130,7 +130,7 @@ ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000
(1 row)
-DROP SUBSCRIPTION regress_testsub3;
+DROP SUBSCRIPTION regress_regress_testsub3;
DROP SUBSCRIPTION regress_testsub4;
-- fail, connection string does not parse
CREATE SUBSCRIPTION regress_testsub5 CONNECTION 'i_dont_exist=param' PUBLICATION testpub;
@@ -522,3 +522,98 @@ DROP ROLE regress_subscription_user;
DROP ROLE regress_subscription_user2;
DROP ROLE regress_subscription_user3;
DROP ROLE regress_subscription_user_dummy;
+--
+-- Test pg_get_subscription_ddl() by creating subscriptions with various
+-- configurations and checking the DDL.
+--
+CREATE ROLE regress_createsub_role LOGIN;
+CREATE ROLE regress_readalldata_role LOGIN;
+-- see the pg_get_subscription_ddl output for a NULL and empty input
+SELECT pg_get_subscription_ddl('');
+ERROR: subscription "" does not exist
+SELECT pg_get_subscription_ddl(NULL);
+ pg_get_subscription_ddl
+-------------------------
+
+(1 row)
+
+-- Create subscription with minimal options
+CREATE SUBSCRIPTION regress_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 alter the subscription to refresh publications.
+-- Check that the subscription ddl is correctly created
+SELECT pg_get_subscription_ddl('regress_testsub1');
+ pg_get_subscription_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION regress_testsub1 CONNECTION 'dbname=db_doesnotexist' PUBLICATION "testpub1" WITH (connect = false, slot_name = 'regress_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 "regress_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 alter the subscription to refresh publications.
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+ pg_get_subscription_ddl
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_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 regress_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 alter the subscription to refresh publications.
+SELECT pg_get_subscription_ddl('regress_testsub3');
+ pg_get_subscription_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION regress_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 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_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 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_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 regress_createsub_role;
+GRANT pg_read_all_data TO regress_readalldata_role;
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+ pg_get_subscription_ddl
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_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 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+ pg_get_subscription_ddl
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_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 regress_createsub_role;
+REVOKE pg_read_all_data FROM regress_readalldata_role;
+ALTER SUBSCRIPTION regress_testsub1 SET (slot_name=NONE);
+DROP SUBSCRIPTION regress_testsub1;
+ALTER SUBSCRIPTION "regress_TestSubddL2" SET (slot_name=NONE);
+DROP SUBSCRIPTION "regress_TestSubddL2";
+DROP SUBSCRIPTION regress_testsub3;
+DROP ROLE regress_createsub_role;
+DROP ROLE regress_readalldata_role;
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index ef0c298d2df..d2c05445b54 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -61,10 +61,10 @@ CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PU
CREATE SUBSCRIPTION regress_testsub2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, create_slot = false);
-- ok - with slot_name = NONE
-CREATE SUBSCRIPTION regress_testsub3 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, connect = false);
+CREATE SUBSCRIPTION regress_regress_testsub3 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, connect = false);
-- fail
-ALTER SUBSCRIPTION regress_testsub3 ENABLE;
-ALTER SUBSCRIPTION regress_testsub3 REFRESH PUBLICATION;
+ALTER SUBSCRIPTION regress_regress_testsub3 ENABLE;
+ALTER SUBSCRIPTION regress_regress_testsub3 REFRESH PUBLICATION;
-- fail - origin must be either none or any
CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (slot_name = NONE, connect = false, origin = foo);
@@ -75,7 +75,7 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU
ALTER SUBSCRIPTION regress_testsub4 SET (origin = any);
\dRs+ regress_testsub4
-DROP SUBSCRIPTION regress_testsub3;
+DROP SUBSCRIPTION regress_regress_testsub3;
DROP SUBSCRIPTION regress_testsub4;
-- fail, connection string does not parse
@@ -370,3 +370,63 @@ DROP ROLE regress_subscription_user;
DROP ROLE regress_subscription_user2;
DROP ROLE regress_subscription_user3;
DROP ROLE regress_subscription_user_dummy;
+
+--
+-- Test pg_get_subscription_ddl() by creating subscriptions with various
+-- configurations and checking the DDL.
+--
+CREATE ROLE regress_createsub_role LOGIN;
+CREATE ROLE regress_readalldata_role LOGIN;
+
+-- see the pg_get_subscription_ddl output for a NULL and empty input
+SELECT pg_get_subscription_ddl('');
+SELECT pg_get_subscription_ddl(NULL);
+
+-- Create subscription with minimal options
+CREATE SUBSCRIPTION regress_testsub1 CONNECTION 'dbname=db_doesnotexist'
+ PUBLICATION testpub1 WITH (connect=false);
+-- Check that the subscription ddl is correctly created
+SELECT pg_get_subscription_ddl('regress_testsub1');
+
+-- Create subscription with more options
+CREATE SUBSCRIPTION "regress_TestSubddL2" CONNECTION 'host=unknown user=dvd password=pass123'
+ PUBLICATION "testpub2", "TestPub3" WITH (connect=false, slot_name='slot1',
+ enabled=off);
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+
+-- Create subscription with all options
+CREATE SUBSCRIPTION regress_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('regress_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 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+RESET SESSION AUTHORIZATION;
+-- Administrators can change who can access this function
+GRANT pg_create_subscription TO regress_createsub_role;
+GRANT pg_read_all_data TO regress_readalldata_role;
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+
+RESET SESSION AUTHORIZATION;
+REVOKE pg_create_subscription FROM regress_createsub_role;
+REVOKE pg_read_all_data FROM regress_readalldata_role;
+ALTER SUBSCRIPTION regress_testsub1 SET (slot_name=NONE);
+DROP SUBSCRIPTION regress_testsub1;
+ALTER SUBSCRIPTION "regress_TestSubddL2" SET (slot_name=NONE);
+DROP SUBSCRIPTION "regress_TestSubddL2";
+DROP SUBSCRIPTION regress_testsub3;
+DROP ROLE regress_createsub_role;
+DROP ROLE regress_readalldata_role;
--
2.43.0
On 2025-Nov-11, Vaibhav Dalvi wrote:
Hi Alvaro,
Thanks for the explanation.
I tried to get rid of String usage in 0001 patch.
Prepared 0002 patch for actual implementation of the
function p_get_subscription_ddl().
OK, now I understand what you meant when you mentioned
CreateSubscriptionStmt: the problem is that the same code is being used
for the purposes of processing the subscription lists of both
Subscription and SubscriptionStmt. I think this whole code is a bit
ugly TBH -- not yours, but what was there before -- but now that I've
realized this, I think stripping those values out of the String wrapping
is the wrong direction to go in, because it leads to more contortions
rather than less.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"They proved that being American is not just for some people"
(George Takei)
Hi Alvaro,
I've realized this, I think stripping those values out of the String
wrapping
is the wrong direction to go in, because it leads to more contortions
rather than less.
I also agree with you. I couldn't explain it well so I thought of doing
the changes.
Please find the attached patch which introduces a new static function to
convert
text array to string list instead of making textarray_to_stringlist()
non-static.
Request to please review the attached v5 patch.
Regards,
Vaibhav
On Tue, Nov 11, 2025 at 5:04 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
Show quoted text
On 2025-Nov-11, Vaibhav Dalvi wrote:
Hi Alvaro,
Thanks for the explanation.
I tried to get rid of String usage in 0001 patch.
Prepared 0002 patch for actual implementation of the
function p_get_subscription_ddl().OK, now I understand what you meant when you mentioned
CreateSubscriptionStmt: the problem is that the same code is being used
for the purposes of processing the subscription lists of both
Subscription and SubscriptionStmt. I think this whole code is a bit
ugly TBH -- not yours, but what was there before -- but now that I've
realized this, I think stripping those values out of the String wrapping
is the wrong direction to go in, because it leads to more contortions
rather than less.--
Álvaro Herrera PostgreSQL Developer —
https://www.EnterpriseDB.com/
"They proved that being American is not just for some people"
(George Takei)
Attachments:
v5-Add-pg_get_subscription_ddl-function.patchapplication/octet-stream; name=v5-Add-pg_get_subscription_ddl-function.patchDownload
From 8799c9426d8e25f6b2dfd132d5acc092f34724ab Mon Sep 17 00:00:00 2001
From: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Date: Tue, 11 Nov 2025 15:40:55 +0000
Subject: [PATCH v5 1/1] Add pg_get_subscription_ddl() function
This new SQL-callable function returns the `CREATE SUBSCRIPTION`
statement for a given subscription name or oid.
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.
This function is restricted to users that have the "pg_read_all_data" and/or
"pg_create_subscription" privilege. This is a security measure because
subscription connection strings often contain sensitive information, such as
passwords.
Author: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Reviewers: Akshay Joshi, Nishant Sharma, Ian Barwick & Alvaro Herrera
Discussion: https://www.postgresql.org/message-id/CA%2BvB%3DAGG0NoxWW%3D-947RBmba8Pzhj8j7op0Xkv8nLDzVMc2%3D7w%40mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 51 +++++
src/backend/utils/adt/ruleutils.c | 244 +++++++++++++++++++++
src/include/catalog/pg_proc.dat | 6 +
src/include/utils/ruleutils.h | 1 +
src/test/regress/expected/subscription.out | 95 ++++++++
src/test/regress/sql/subscription.sql | 60 +++++
6 files changed, 457 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..21d33ee1802 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}
</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"/>
+ print the DDL statements for various database objects.
+ (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_subscription_ddl</primary>
+ </indexterm>
+ <function>pg_get_subscription_ddl</function> ( <parameter>subscription</parameter> <type>text</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Reconstructs the creating command for a subscription.
+ The result is a complete <command>CREATE SUBSCRIPTION</command>
+ statement. The <literal>connect</literal> option set to
+ <literal>false</literal>.
+ </para>
+ <para>
+ This function is restricted to users that have the
+ <literal>pg_read_all_data</literal> and/or
+ <literal>pg_create_subscription</literal> privilege.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..d1c3e0f7e38 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"
@@ -546,6 +548,7 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
deparse_context *context,
bool showimplicit,
bool needcomma);
+static List *text_array_to_string_list(ArrayType *text_array);
#define only_marker(rte) ((rte)->inh ? "" : "ONLY ")
@@ -13743,3 +13746,244 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * build_subscription_ddl_string - Build CREATE SUBSCRIPTION statement for
+ * a subscription from its OID. This is internal version which helps
+ * pg_get_subscription_ddl_name() and pg_get_subscription_ddl_oid().
+ */
+char *
+build_subscription_ddl_string(const Oid suboid)
+{
+ Form_pg_subscription subForm;
+ 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 = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(suboid));
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("subscription with oid %d does not exist", suboid)));
+
+ /* Get subscription's details from its tuple */
+ subForm = (Form_pg_subscription) GETSTRUCT(tup);
+
+ initStringInfo(&buf);
+
+ /* Build the CREATE SUBSCRIPTION statement */
+ appendStringInfo(&buf, "CREATE SUBSCRIPTION %s ",
+ quote_identifier(NameStr(subForm->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 = text_array_to_string_list(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 = %d",
+ DatumGetInt32(datum));
+
+ /* Finally close parenthesis and add semicolon to the statement */
+ appendStringInfoString(&buf, ");");
+
+ ReleaseSysCache(tup);
+
+ return buf.data;
+}
+
+/*
+ * pg_get_subscription_ddl_name
+ * Get CREATE SUBSCRIPTION statement for a subscription.
+ *
+ * This takes name as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_name(PG_FUNCTION_ARGS)
+{
+ Name subname = PG_GETARG_NAME(0);
+ Oid suboid;
+ char *ddl_stmt;
+
+ /* Get the OID of the subscription from its name */
+ suboid = get_subscription_oid(NameStr(*subname), false);
+
+ /* Get the CREATE SUBSCRIPTION DDL statement from its OID */
+ ddl_stmt = build_subscription_ddl_string(suboid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_subscription_ddl_oid
+ * Get CREATE SUBSCRIPTION statement for a subscription.
+ *
+ * This takes oid as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_oid(PG_FUNCTION_ARGS)
+{
+ Oid suboid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE SUBSCRIPTION DDL statement from its OID */
+ ddl_stmt = build_subscription_ddl_string(suboid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * text_array_to_string_list
+ * Convert text array to list of strings.
+ */
+static List *
+text_array_to_string_list(ArrayType *text_array)
+{
+ List *result = NIL;
+ Datum *elems;
+ int nelems,
+ i;
+
+ deconstruct_array_builtin(text_array, TEXTOID, &elems, NULL, &nelems);
+
+ if (nelems == 0)
+ return NIL;
+
+ for (i = 0; i < nelems; i++)
+ result = lappend(result, makeString(TextDatumGetCString(elems[i])));
+
+ return result;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5cf9e12fcb9..a35657ab9ae 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3993,6 +3993,12 @@
{ 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 => 'name', prosrc => 'pg_get_subscription_ddl_name' },
+{ oid => '8002', descr => 'get CREATE statement for subscription',
+ proname => 'pg_get_subscription_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_subscription_ddl_oid' },
{ 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/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 7ba7d887914..f8f26284489 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -53,5 +53,6 @@ extern char *generate_opclass_name(Oid opclass);
extern char *get_range_partbound_string(List *bound_datums);
extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern char *build_subscription_ddl_string(const Oid suboid);
#endif /* RULEUTILS_H */
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index 327d1e7731f..ec2ef27a4f1 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -522,3 +522,98 @@ DROP ROLE regress_subscription_user;
DROP ROLE regress_subscription_user2;
DROP ROLE regress_subscription_user3;
DROP ROLE regress_subscription_user_dummy;
+--
+-- Test pg_get_subscription_ddl() by creating subscriptions with various
+-- configurations and checking the DDL.
+--
+CREATE ROLE regress_createsub_role LOGIN;
+CREATE ROLE regress_readalldata_role LOGIN;
+-- see the pg_get_subscription_ddl output for a NULL and empty input
+SELECT pg_get_subscription_ddl('');
+ERROR: subscription "" does not exist
+SELECT pg_get_subscription_ddl(NULL);
+ pg_get_subscription_ddl
+-------------------------
+
+(1 row)
+
+-- Create subscription with minimal options
+CREATE SUBSCRIPTION regress_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 alter the subscription to refresh publications.
+-- Check that the subscription ddl is correctly created
+SELECT pg_get_subscription_ddl('regress_testsub1');
+ pg_get_subscription_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION regress_testsub1 CONNECTION 'dbname=db_doesnotexist' PUBLICATION "testpub1" WITH (connect = false, slot_name = 'regress_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 "regress_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 alter the subscription to refresh publications.
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+ pg_get_subscription_ddl
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_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 regress_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 alter the subscription to refresh publications.
+SELECT pg_get_subscription_ddl('regress_testsub3');
+ pg_get_subscription_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION regress_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 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_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 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_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 regress_createsub_role;
+GRANT pg_read_all_data TO regress_readalldata_role;
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+ pg_get_subscription_ddl
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_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 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+ pg_get_subscription_ddl
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_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 regress_createsub_role;
+REVOKE pg_read_all_data FROM regress_readalldata_role;
+ALTER SUBSCRIPTION regress_testsub1 SET (slot_name=NONE);
+DROP SUBSCRIPTION regress_testsub1;
+ALTER SUBSCRIPTION "regress_TestSubddL2" SET (slot_name=NONE);
+DROP SUBSCRIPTION "regress_TestSubddL2";
+DROP SUBSCRIPTION regress_testsub3;
+DROP ROLE regress_createsub_role;
+DROP ROLE regress_readalldata_role;
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index ef0c298d2df..43cf74967fe 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -370,3 +370,63 @@ DROP ROLE regress_subscription_user;
DROP ROLE regress_subscription_user2;
DROP ROLE regress_subscription_user3;
DROP ROLE regress_subscription_user_dummy;
+
+--
+-- Test pg_get_subscription_ddl() by creating subscriptions with various
+-- configurations and checking the DDL.
+--
+CREATE ROLE regress_createsub_role LOGIN;
+CREATE ROLE regress_readalldata_role LOGIN;
+
+-- see the pg_get_subscription_ddl output for a NULL and empty input
+SELECT pg_get_subscription_ddl('');
+SELECT pg_get_subscription_ddl(NULL);
+
+-- Create subscription with minimal options
+CREATE SUBSCRIPTION regress_testsub1 CONNECTION 'dbname=db_doesnotexist'
+ PUBLICATION testpub1 WITH (connect=false);
+-- Check that the subscription ddl is correctly created
+SELECT pg_get_subscription_ddl('regress_testsub1');
+
+-- Create subscription with more options
+CREATE SUBSCRIPTION "regress_TestSubddL2" CONNECTION 'host=unknown user=dvd password=pass123'
+ PUBLICATION "testpub2", "TestPub3" WITH (connect=false, slot_name='slot1',
+ enabled=off);
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+
+-- Create subscription with all options
+CREATE SUBSCRIPTION regress_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('regress_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 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+RESET SESSION AUTHORIZATION;
+-- Administrators can change who can access this function
+GRANT pg_create_subscription TO regress_createsub_role;
+GRANT pg_read_all_data TO regress_readalldata_role;
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
+
+RESET SESSION AUTHORIZATION;
+REVOKE pg_create_subscription FROM regress_createsub_role;
+REVOKE pg_read_all_data FROM regress_readalldata_role;
+ALTER SUBSCRIPTION regress_testsub1 SET (slot_name=NONE);
+DROP SUBSCRIPTION regress_testsub1;
+ALTER SUBSCRIPTION "regress_TestSubddL2" SET (slot_name=NONE);
+DROP SUBSCRIPTION "regress_TestSubddL2";
+DROP SUBSCRIPTION regress_testsub3;
+DROP ROLE regress_createsub_role;
+DROP ROLE regress_readalldata_role;
--
2.43.0
Hi Vaibhav.
Here are some review comments for v5-0001.
======
doc/src/sgml/func/func-info.sgml
1.
+ <title>Get Object DDL Functions</title>
Should the title just be "Object DDL Functions" (e.g. sans the "Get")?
~~~
2.
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ print the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
/print the DDL/return the DDL/
~~~
3.
+ <table id="functions-get-object-ddl-table">
+ <title>Get Object DDL Functions</title>
Ditto above ("Get" is not really needed).
~~~
4.
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_get_subscription_ddl</primary>
+ </indexterm>
+ <function>pg_get_subscription_ddl</function> (
<parameter>subscription</parameter> <type>text</type> )
+ <returnvalue>text</returnvalue>
+ </para>
The name of the parameter could be more descriptive:
/<parameter>subscription</parameter>/<parameter>sub_name</parameter>/
~~~
5.
+ Reconstructs the creating command for a subscription.
+ The result is a complete <command>CREATE SUBSCRIPTION</command>
+ statement. The <literal>connect</literal> option set to
+ <literal>false</literal>.
+ </para>
I felt those first two sentences could be combined:
SUGGESTION
Returns the <command>CREATE SUBSCRIPTION</command> command that would
create this subscription.
======
src/backend/utils/adt/ruleutils.c
build_subscription_ddl_string:
6.
+/*
+ * build_subscription_ddl_string - Build CREATE SUBSCRIPTION statement for
+ * a subscription from its OID. This is internal version which helps
+ * pg_get_subscription_ddl_name() and pg_get_subscription_ddl_oid().
+ */
+char *
+build_subscription_ddl_string(const Oid suboid)
Typo? "This is internal version"
Also, if it is only an internal helper, then why isn't it declared static?
~~~
7.
+ /* 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");
Mostly code is using ternary for the simple bool options. But some are
not. Consistent use of ternary for all of them might be better
~~~
8.
+ /* 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");
Here's another one that could have been a ternary.
~~~
9.
Is all the spacing between options and their values necessary? For
example, in "subscription.sql", you didn't put spaces everywhere in
the CREATE SUBSCRIPTION command. So why does the DDL generation put
them there? Anyway, I guess all the new DDL routines should be using
the same convention for option spacing, but I don't know what that is.
~~~
pg_get_subscription_ddl_name:
10.
+{
+ List *result = NIL;
+ Datum *elems;
+ int nelems,
+ i;
+
+ deconstruct_array_builtin(text_array, TEXTOID, &elems, NULL, &nelems);
+
+ if (nelems == 0)
+ return NIL;
+
+ for (i = 0; i < nelems; i++)
+ result = lappend(result, makeString(TextDatumGetCString(elems[i])));
+
+ return result;
+}
10a.
The early exit "if (nelems == 0)" is unnecessary; IMO just remove it.
The result = NIL already, and the loop does nothing when nelems is 0.
~
10b.
Variable 'i' can be declared as a for-loop variable
======
src/test/regress/expected/subscription.out
11.
+WARNING: subscription was created, but is not connected
+HINT: To initiate replication, you must manually create the
replication slot, enable the subscription, and alter the subscription
to refresh publications.
I wonder if the tests should suppress WARNINGS because you don't
really care about output like this, right?
======
src/test/regress/sql/subscription.sql
12.
+-- Non-superusers and which don't have pg_create_subscription and/or
+-- pg_read_all_data permission can't get ddl
typo: /and which/
Maybe something like this:
-- Non-superusers without pg_create_subscription and/or
pg_read_all_data permissions cannot retrieve the DDL.
~~~
13.
+CREATE SUBSCRIPTION "regress_TestSubddL2" CONNECTION 'host=unknown
user=dvd password=pass123'
+ PUBLICATION "testpub2", "TestPub3" WITH (connect=false, slot_name='slot1',
+ enabled=off);
+SELECT pg_get_subscription_ddl('regress_TestSubddL2');
Typo? Why the capital "L" in "regress_TestSubddL2"
Also, why is it called "regress_TestSubddL2"; there wasn't a
"regress_TestSubddL1"?
======
Kind Regards,
Peter Smith.
Fujitsu Australia
Hi Vaibhav,
I just reviewed the patch and got some comments:
On Nov 11, 2025, at 23:51, Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com> wrote:
<v5-Add-pg_get_subscription_ddl-function.patch>
1.
```
+
+/*
+ * build_subscription_ddl_string - Build CREATE SUBSCRIPTION statement for
+ * a subscription from its OID. This is internal version which helps
+ * pg_get_subscription_ddl_name() and pg_get_subscription_ddl_oid().
+ */
+char *
+build_subscription_ddl_string(const Oid suboid)
```
There are several existing similar functions that take an oid as input and return a string, for example:
* extern char *pg_get_indexdef_string(Oid indexrelid);
* extern char *pg_get_constraintdef_command(Oid constraintId);
So, can we keep the same naming convention and rename the function to pg_get_subscription_string().
2
```
+ publist = text_array_to_string_list(DatumGetArrayTypeP(datum));
+ pubnames = makeStringInfo();
```
Recently there are some efforts done to replace usages of StringInfo with StringInfoData, so I guess you may apply the practice as well. See [1]https://git.postgresql.org/cgit/postgresql.git/commit/?id=6d0eba66275b125bf634bbdffda90c70856e3f93.
3
```
+ /* 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”);
```
Logic of handling these two fields are the same, but you implement in two different ways, can we keep consistent?
4
```
+/*
+ * pg_get_subscription_ddl_name
+ * Get CREATE SUBSCRIPTION statement for a subscription.
+ *
+ * This takes name as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_name(PG_FUNCTION_ARGS)
+{
```
This function name is quite confusing, I think it should be pg_get_subscription_ddl_by_name().
5
```
+/*
+ * pg_get_subscription_ddl_oid
+ * Get CREATE SUBSCRIPTION statement for a subscription.
+ *
+ * This takes oid as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_oid(PG_FUNCTION_ARGS)
```
Similar to 4. I think the function name should be pg_get_subscription_ddl_by_oid().
6
```
+ errdetail("Only roles with privileges of the \"%s\" and/or \"%s\" role may get ddl.",
```
“May get ddl” maybe change to “may view subscription DDL”.
7
```
+ /* Append connection info to the CREATE SUBSCRIPTION statement */
+ appendStringInfo(&buf, "CONNECTION \'%s\'", conninfo);
```
A connection string contains a db access credential, and ROLE_PG_READ_ALL_DATA (not a high privilege) can view the DDL, is there a concern of leaking the secret? Should we redact the password in connection string?
8
```
+ appendStringInfo(&buf, ", slot_name = \'%s\'",
+ NameStr(*DatumGetName(datum)));
```
Instead of hardcode single-quotes, can we consider using quote_literal_cstr(), for example, in slotsync.c:
```
appendStringInfo(&cmd,
"SELECT pg_is_in_recovery(), count(*) = 1”
" FROM pg_catalog.pg_replication_slots”
" WHERE slot_type='physical' AND slot_name=%s”,
quote_literal_cstr(PrimarySlotName));
```
[1]: https://git.postgresql.org/cgit/postgresql.git/commit/?id=6d0eba66275b125bf634bbdffda90c70856e3f93
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
If build_subscription_ddl_string is "internal" as its comment claims,
why is it declared extern in ruleutils.h? I think it should be a static
function instead. If you want to make it extern, it should live in
src/backend/catalog/pg_subscription.c and its prototype in
src/include/catalog/pg_subscription.h. And if you do move it to
pg_subscription.c (but I don't necessarily agree with that), then you
don't need a third copy of textarray_to_stringlist.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"Find a bug in a program, and fix it, and the program will work today.
Show the program how to find and fix a bug, and the program
will work forever" (Oliver Silfridge)
Hi,
A couple more comments.
These seem like general questions that might apply to other DDL
funtion implementations -- not only this one.
======
1.
Question - is it deliberate to *always* return DLL with every possible
option assigned, even if those are just the option default values?
e.g. For something like the CREATE PUBLICATION command the string
returned could be only half the size if it accounts for default.
Following on from that, is there any plan for the function to take
some true/false flag param to tell it to return a full/condensed DDL?
~~~
2.
I was also wondering if it was really necessary to have so many
appendStringInfoString() calls to reconstruct the command.
I felt something like below (where every option value is assigned a
variable) is more readable:
SUGGESTION:
{
/* Get slotname */
datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup,
Anum_pg_subscription_subslotname, &isnull);
char *val_slot_name = isnull ? NULL : strdup(NameStr(*DatumGetName(datum)));
/* Get enabled option */
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
Anum_pg_subscription_subenabled);
/* Setting 'slot_name' to none must set 'enabled' to false as well */
bool val_enabled = DatumGetBool(datum) && (val_slot_name != NULL);
/* Get binary option */
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
Anum_pg_subscription_subbinary);
bool val_binary = DatumGetBool(datum);
/* Get streaming option */
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
Anum_pg_subscription_substream);
char val_streaming = DatumGetChar(datum);
/* Get sync commit option */
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
Anum_pg_subscription_subsynccommit);
char *val_synchronous_commit = strdup(TextDatumGetCString(datum));
/* Get two-phase commit option */
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
Anum_pg_subscription_subtwophasestate);
bool val_two_phase = DatumGetChar(datum) != LOGICALREP_TWOPHASE_STATE_DISABLED;
/* Disable on error? */
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
Anum_pg_subscription_subdisableonerr);
bool val_disable_on_error = DatumGetBool(datum);
/* Password required? */
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
Anum_pg_subscription_subpasswordrequired);
bool val_password_required = DatumGetBool(datum);
/* Run as owner? */
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
Anum_pg_subscription_subrunasowner);
bool val_run_as_owner = DatumGetBool(datum);
/* Get origin */
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
Anum_pg_subscription_suborigin);
char *val_origin = strdup(TextDatumGetCString(datum));
/* Failover? */
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
Anum_pg_subscription_subfailover);
bool val_failover = DatumGetBool(datum);
/* Retain dead tuples? */
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
Anum_pg_subscription_subretaindeadtuples);
bool val_retain_dead_tuples = DatumGetBool(datum);
/* Max retention duration */
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
Anum_pg_subscription_submaxretention);
int val_max_retention_duration = DatumGetInt32(datum);
appendStringInfo(&buf,
" WITH (connect = false"
", slot_name = %s"
", enabled = %s"
", binary = %s"
", streaming = %s"
", synchronous_commit = %s"
", two_phase = %s"
", disable_on_error = %s"
", password_required = %s"
", run_as_owner = %s"
", origin = %s"
", failover = %s"
", retain_dead_tuples = %s"
", max_retention_duration = %d"
");",
val_slot_name == NULL ? "none, create_slot = false" :
quote_literal_cstr(val_slot_name),
val_enabled ? "true" : "false",
val_binary ? "true" : "false",
val_streaming == LOGICALREP_STREAM_OFF ? "off" : val_streaming ==
LOGICALREP_STREAM_ON ? "on" : "parallel",
val_synchronous_commit,
val_two_phase ? "on" : "off",
val_disable_on_error ? "on" : "off",
val_password_required ? "on" : "off",
val_run_as_owner ? "on" : "off",
val_origin,
val_failover ? "on" : "off",
val_retain_dead_tuples ? "on" : "off",
val_max_retention_duration);
}
======
Kind Regards,
Peter Smith.
Fujitsu Australia
On 2025-Nov-13, Peter Smith wrote:
1. Question - is it deliberate to *always* return DLL with every
possible option assigned, even if those are just the option default
values? e.g. For something like the CREATE PUBLICATION command the
string returned could be only half the size if it accounts for
default.
Yeah, I was asking myself the same. I think we definitely want options
to be printed when there are GUCs that can affect the outcome (e.g.,
something that is considered default in this server but not on a
differently- configured one would give different results). But for
those that are just hardcoded defaults, omitting them would make sense.
2. I was also wondering if it was really necessary to have so many
appendStringInfoString() calls to reconstruct the command.
There are a couple of these patches that have an auxiliary
pretty-printing helper function to add newline-tabs instead of
individual spaces. I think that wouldn't work as nicely if you tried to
condense the printing in the way you suggest. On the other hand, if you
have a long format string, it's harder to visually match each specifier
to its corresponding argument. If this was performance-critical code I
would agree to use denser code and avoid function calls, but for this
usage I don't think we care much.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
Al principio era UNIX, y UNIX habló y dijo: "Hello world\n".
No dijo "Hello New Jersey\n", ni "Hello USA\n".
Hi Hackers,
Thank you Chao Li, Peter Smith and Alvaro for the review.
I have incorporated all your review comments except below ones:
7
``` + /* Append connection info to the CREATE SUBSCRIPTION statement */ + appendStringInfo(&buf, "CONNECTION \'%s\'", conninfo); ``` A connection string contains a db access credential, and ROLE_PG_READ_ALL_DATA (not a high privilege) can view the DDL, is there a concern of leaking the secret? Should we redact the password in connection string?If the user installs a password in the conninfo, I think they are being
dumb about it,
and it's not this function's job to educate them on that. Restricting the
function to
users that have the pg_read_all_data and/or pg_create_subscription privilege
(which applies to superusers, but also if the DBA grants that to other
users,
it'd work for those also) is a better idea.
1. Question - is it deliberate to *always* return DLL with every
possible option assigned, even if those are just the option default
values? e.g. For something like the CREATE PUBLICATION command the
string returned could be only half the size if it accounts for
default.Yeah, I was asking myself the same. I think we definitely want options
to be printed when there are GUCs that can affect the outcome (e.g.,
something that is considered default in this server but not on a
differently- configured one would give different results). But for
those that are just hardcoded defaults, omitting them would make sense.In future, the default value of any of the parameters may change so this
function
would create the wrong ddl. Also, having lengthy DDL doesn't create any
problem
and provides values for all the options.
2. I was also wondering if it was really necessary to have so many
appendStringInfoString() calls to reconstruct the command.
There are a couple of these patches that have an auxiliary
pretty-printing helper function to add newline-tabs instead of
individual spaces. I think that wouldn't work as nicely if you tried to
condense the printing in the way you suggest. On the other hand, if you
have a long format string, it's harder to visually match each specifier
to its corresponding argument. If this was performance-critical code I
would agree to use denser code and avoid function calls, but for this
usage I don't think we care much.+1.
Please find a revised patch.
Thanks,
Vaibhav Dalvi
EnterpriseDB
On Thu, Nov 13, 2025 at 1:50 PM Álvaro Herrera <alvherre@kurilemu.de> wrote:
Show quoted text
On 2025-Nov-13, Peter Smith wrote:
1. Question - is it deliberate to *always* return DLL with every
possible option assigned, even if those are just the option default
values? e.g. For something like the CREATE PUBLICATION command the
string returned could be only half the size if it accounts for
default.Yeah, I was asking myself the same. I think we definitely want options
to be printed when there are GUCs that can affect the outcome (e.g.,
something that is considered default in this server but not on a
differently- configured one would give different results). But for
those that are just hardcoded defaults, omitting them would make sense.2. I was also wondering if it was really necessary to have so many
appendStringInfoString() calls to reconstruct the command.There are a couple of these patches that have an auxiliary
pretty-printing helper function to add newline-tabs instead of
individual spaces. I think that wouldn't work as nicely if you tried to
condense the printing in the way you suggest. On the other hand, if you
have a long format string, it's harder to visually match each specifier
to its corresponding argument. If this was performance-critical code I
would agree to use denser code and avoid function calls, but for this
usage I don't think we care much.--
Álvaro Herrera Breisgau, Deutschland —
https://www.EnterpriseDB.com/
Al principio era UNIX, y UNIX habló y dijo: "Hello world\n".
No dijo "Hello New Jersey\n", ni "Hello USA\n".
Attachments:
v6-Add-pg_get_subscription_ddl-function.patchapplication/x-patch; name=v6-Add-pg_get_subscription_ddl-function.patchDownload
From 4d33dd0d5e6228457d4512f126e496888c3528c3 Mon Sep 17 00:00:00 2001
From: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Date: Mon, 17 Nov 2025 14:10:22 +0000
Subject: [PATCH v6 1/1] Add pg_get_subscription_ddl() function
This new SQL-callable function returns the `CREATE SUBSCRIPTION`
statement for a given subscription name or oid.
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.
This function is restricted to users that have the "pg_read_all_data" and/or
"pg_create_subscription" privilege. This is a security measure because
subscription connection strings often contain sensitive information, such as
passwords.
Author: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Reviewers: Akshay Joshi, Nishant Sharma, Ian Barwick, Chao Li, Peter Smith & Alvaro Herrera
Discussion: https://www.postgresql.org/message-id/CA%2BvB%3DAGG0NoxWW%3D-947RBmba8Pzhj8j7op0Xkv8nLDzVMc2%3D7w%40mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 50 +++++
src/backend/utils/adt/ruleutils.c | 239 +++++++++++++++++++++
src/include/catalog/pg_proc.dat | 6 +
src/test/regress/expected/subscription.out | 95 ++++++++
src/test/regress/sql/subscription.sql | 68 ++++++
5 files changed, 458 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..469c0b02d90 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}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ return the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>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_subscription_ddl</primary>
+ </indexterm>
+ <function>pg_get_subscription_ddl</function> ( <parameter>subscription</parameter> <type>name</type> or <type>oid</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the <command>CREATE SUBSCRIPTION</command> command that would
+ create this subscription. The <literal>connect</literal> option set to
+ <literal>false</literal>.
+ </para>
+ <para>
+ This function is restricted to users that have the
+ <literal>pg_read_all_data</literal> and/or
+ <literal>pg_create_subscription</literal> privilege.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..46e3c295766 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"
@@ -546,6 +548,8 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
deparse_context *context,
bool showimplicit,
bool needcomma);
+static char *pg_get_subscription_string(const Oid suboid);
+static List *text_array_to_string_list(ArrayType *text_array);
#define only_marker(rte) ((rte)->inh ? "" : "ONLY ")
@@ -13743,3 +13747,238 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * pg_get_subscription_string
+ * Build CREATE SUBSCRIPTION statement for a subscription from its OID.
+ *
+ * This is internal version which helps pg_get_subscription_ddl_by_name() and
+ * pg_get_subscription_ddl_by_oid().
+ */
+static char *
+pg_get_subscription_string(const Oid suboid)
+{
+ Form_pg_subscription subForm;
+ StringInfoData pubnames,
+ 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 view subscription DDL.",
+ "pg_create_subscription", "pg_read_all_data")));
+
+ /* Look up the subscription in pg_subscription */
+ tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(suboid));
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("subscription with oid %d does not exist", suboid)));
+
+ /* Get subscription's details from its tuple */
+ subForm = (Form_pg_subscription) GETSTRUCT(tup);
+
+ initStringInfo(&buf);
+
+ /* Build the CREATE SUBSCRIPTION statement */
+ appendStringInfo(&buf, "CREATE SUBSCRIPTION %s ",
+ quote_identifier(NameStr(subForm->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 = text_array_to_string_list(DatumGetArrayTypeP(datum));
+ initStringInfo(&pubnames);
+ 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",
+ quote_literal_cstr(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 */
+ appendStringInfo(&buf, ", enabled=%s",
+ (!DatumGetBool(datum) || isnull) ? "false" : "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);
+ appendStringInfo(&buf, ", two_phase=%s",
+ DatumGetChar(datum) == LOGICALREP_TWOPHASE_STATE_DISABLED ? "off" : "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=%d",
+ DatumGetInt32(datum));
+
+ /* Finally close parenthesis and add semicolon to the statement */
+ appendStringInfoString(&buf, ");");
+
+ ReleaseSysCache(tup);
+
+ return buf.data;
+}
+
+/*
+ * pg_get_subscription_ddl_by_name
+ * Get CREATE SUBSCRIPTION statement for a subscription.
+ *
+ * This takes name as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_by_name(PG_FUNCTION_ARGS)
+{
+ Name subname = PG_GETARG_NAME(0);
+ Oid suboid;
+ char *ddl_stmt;
+
+ /* Get the OID of the subscription from its name */
+ suboid = get_subscription_oid(NameStr(*subname), false);
+
+ /* Get the CREATE SUBSCRIPTION DDL statement from its OID */
+ ddl_stmt = pg_get_subscription_string(suboid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_subscription_ddl_by_oid
+ * Get CREATE SUBSCRIPTION statement for a subscription.
+ *
+ * This takes oid as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_by_oid(PG_FUNCTION_ARGS)
+{
+ Oid suboid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE SUBSCRIPTION DDL statement from its OID */
+ ddl_stmt = pg_get_subscription_string(suboid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * text_array_to_string_list
+ * Convert text array to list of strings.
+ */
+static List *
+text_array_to_string_list(ArrayType *text_array)
+{
+ List *result = NIL;
+ Datum *elems;
+ int nelems;
+
+ deconstruct_array_builtin(text_array, TEXTOID, &elems, NULL, &nelems);
+
+ for (int i = 0; i < nelems; i++)
+ result = lappend(result, makeString(TextDatumGetCString(elems[i])));
+
+ return result;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5cf9e12fcb9..cc6272b91af 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3993,6 +3993,12 @@
{ 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 => 'name', prosrc => 'pg_get_subscription_ddl_by_name' },
+{ oid => '8002', descr => 'get CREATE statement for subscription',
+ proname => 'pg_get_subscription_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_subscription_ddl_by_oid' },
{ 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/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index 327d1e7731f..be6a9bff7f3 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -522,3 +522,98 @@ DROP ROLE regress_subscription_user;
DROP ROLE regress_subscription_user2;
DROP ROLE regress_subscription_user3;
DROP ROLE regress_subscription_user_dummy;
+--
+-- Test pg_get_subscription_ddl() by creating subscriptions with various
+-- configurations and checking the DDL.
+--
+CREATE ROLE regress_createsub_role LOGIN;
+CREATE ROLE regress_readalldata_role LOGIN;
+-- see the pg_get_subscription_ddl output for a NULL and empty input
+SELECT pg_get_subscription_ddl('');
+ERROR: subscription "" does not exist
+SELECT pg_get_subscription_ddl(NULL);
+ pg_get_subscription_ddl
+-------------------------
+
+(1 row)
+
+-- Suppress WARNINGS when creating subscription
+SET client_min_messages = 'error';
+-- Create subscription with minimal options
+CREATE SUBSCRIPTION regress_testsub1 CONNECTION 'dbname=db_doesnotexist'
+ PUBLICATION testpub1 WITH (connect=false);
+RESET client_min_messages;
+-- Check that the subscription ddl is correctly created
+SELECT pg_get_subscription_ddl('regress_testsub1');
+ pg_get_subscription_ddl
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION regress_testsub1 CONNECTION 'dbname=db_doesnotexist' PUBLICATION "testpub1" WITH (connect=false, slot_name='regress_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
+SET client_min_messages = 'error';
+CREATE SUBSCRIPTION "regress_TestSub2" CONNECTION 'host=unknown user=dvd password=pass123'
+ PUBLICATION "testpub2", "TestPub3" WITH (connect=false, slot_name='slot1',
+ enabled=off);
+RESET client_min_messages;
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+ pg_get_subscription_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_TestSub2" 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
+SET client_min_messages = 'error';
+CREATE SUBSCRIPTION regress_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);
+RESET client_min_messages;
+SELECT pg_get_subscription_ddl('regress_testsub3');
+ pg_get_subscription_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION regress_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 without pg_create_subscription and/or pg_read_all_data
+-- permissions cannot retrieve the DDL.
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+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 view subscription DDL.
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+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 view subscription DDL.
+RESET SESSION AUTHORIZATION;
+-- Administrators can change who can access this function
+GRANT pg_create_subscription TO regress_createsub_role;
+GRANT pg_read_all_data TO regress_readalldata_role;
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+ pg_get_subscription_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_TestSub2" 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 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+ pg_get_subscription_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_TestSub2" 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 regress_createsub_role;
+REVOKE pg_read_all_data FROM regress_readalldata_role;
+ALTER SUBSCRIPTION regress_testsub1 SET (slot_name=NONE);
+DROP SUBSCRIPTION regress_testsub1;
+ALTER SUBSCRIPTION "regress_TestSub2" SET (slot_name=NONE);
+DROP SUBSCRIPTION "regress_TestSub2";
+DROP SUBSCRIPTION regress_testsub3;
+DROP ROLE regress_createsub_role;
+DROP ROLE regress_readalldata_role;
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index ef0c298d2df..4d1b95999cd 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -370,3 +370,71 @@ DROP ROLE regress_subscription_user;
DROP ROLE regress_subscription_user2;
DROP ROLE regress_subscription_user3;
DROP ROLE regress_subscription_user_dummy;
+
+--
+-- Test pg_get_subscription_ddl() by creating subscriptions with various
+-- configurations and checking the DDL.
+--
+CREATE ROLE regress_createsub_role LOGIN;
+CREATE ROLE regress_readalldata_role LOGIN;
+
+-- see the pg_get_subscription_ddl output for a NULL and empty input
+SELECT pg_get_subscription_ddl('');
+SELECT pg_get_subscription_ddl(NULL);
+
+-- Suppress WARNINGS when creating subscription
+SET client_min_messages = 'error';
+-- Create subscription with minimal options
+CREATE SUBSCRIPTION regress_testsub1 CONNECTION 'dbname=db_doesnotexist'
+ PUBLICATION testpub1 WITH (connect=false);
+RESET client_min_messages;
+
+-- Check that the subscription ddl is correctly created
+SELECT pg_get_subscription_ddl('regress_testsub1');
+
+-- Create subscription with more options
+SET client_min_messages = 'error';
+CREATE SUBSCRIPTION "regress_TestSub2" CONNECTION 'host=unknown user=dvd password=pass123'
+ PUBLICATION "testpub2", "TestPub3" WITH (connect=false, slot_name='slot1',
+ enabled=off);
+RESET client_min_messages;
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+
+-- Create subscription with all options
+SET client_min_messages = 'error';
+CREATE SUBSCRIPTION regress_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);
+RESET client_min_messages;
+SELECT pg_get_subscription_ddl('regress_testsub3');
+
+-- Non-superusers without pg_create_subscription and/or pg_read_all_data
+-- permissions cannot retrieve the DDL.
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+RESET SESSION AUTHORIZATION;
+-- Administrators can change who can access this function
+GRANT pg_create_subscription TO regress_createsub_role;
+GRANT pg_read_all_data TO regress_readalldata_role;
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+
+RESET SESSION AUTHORIZATION;
+REVOKE pg_create_subscription FROM regress_createsub_role;
+REVOKE pg_read_all_data FROM regress_readalldata_role;
+ALTER SUBSCRIPTION regress_testsub1 SET (slot_name=NONE);
+DROP SUBSCRIPTION regress_testsub1;
+ALTER SUBSCRIPTION "regress_TestSub2" SET (slot_name=NONE);
+DROP SUBSCRIPTION "regress_TestSub2";
+DROP SUBSCRIPTION regress_testsub3;
+DROP ROLE regress_createsub_role;
+DROP ROLE regress_readalldata_role;
--
2.43.0
Some review comments for v6.
======
src/backend/utils/adt/ruleutils.c
1.
+/*
+ * pg_get_subscription_string
+ * Build CREATE SUBSCRIPTION statement for a subscription from its OID.
+ *
+ * This is internal version which helps pg_get_subscription_ddl_by_name() and
+ * pg_get_subscription_ddl_by_oid().
+ */
+static char *
+pg_get_subscription_string(const Oid suboid)
The comment "This is internal" seemed awkward. Anyway, saying it is
"internal" is unnecessary now that it is static.
SUGGESTION
Helper for pg_get_subscription_ddl_by_name() and
pg_get_subscription_ddl_by_oid().
~~~
2.
+ /* Get enabled option */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subenabled);
+ /* Setting 'slot_name' to none must set 'enabled' to false as well */
+ appendStringInfo(&buf, ", enabled=%s",
+ (!DatumGetBool(datum) || isnull) ? "false" : "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);
+ appendStringInfo(&buf, ", two_phase=%s",
+ DatumGetChar(datum) == LOGICALREP_TWOPHASE_STATE_DISABLED ? "off" : "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=%d",
+ DatumGetInt32(datum));
2a.
It seems strange that almost everything is coded as "true/false" or
"on/off", except there are a couple ('enabled' and 'two_phase') that
are the other way around (false/true, off/on). There seemed to be no
particular reason for that. IMO consistency is better, so use
true/false for everything.
~
2b.
It's not clear to me how you decided when to use true/false versus
on/off. I know that it makes no functional difference, but it does
have the potential to make the result look strange unless there is
some consistent rule. This review comment would apply to *every* one
of the get_XXX_ddl() functions.
Personally, I think the DLL functions should only spit out
"true"/"false" for boolean parameters. It is easy and it is
predictable.
FWIW, my AI tool agrees with me. viz:
------
Why true/false for DDL:
* Consistent with PostgreSQL's boolean literals in SQL
* Matches what pg_dump produces
* Clear and unambiguous
* Parser accepts both, but true/false is standard output
Don't use:
* on/off in generated DDL (even though parser might accept it)
* 1/0
* yes/no
* t/f (internal catalog representation)
------
~
2c.
All those comments about the parameters should be consistent.
Currently they are:
* Get xxx
* xxx?
* xxx
Choose one common style to make them all look the same.
~
2d.
It's not clear what ordering was chosen for these parameters in the
generated DDL. Unless there is some meaningful order that eludes me, I
felt it would be best to generate them in the same order that they
appear in the documentation [1]https://www.postgresql.org/docs/devel/sql-createsubscription.html.
~~~
3.
+ /* Finally close parenthesis and add semicolon to the statement */
+ appendStringInfoString(&buf, ");");
+
This comment seems unnecessary.
======
src/test/regress/sql/subscription.sql
4.
+-- see the pg_get_subscription_ddl output for a NULL and empty input
/see/Check/
~~~
5.
+-- Check that the subscription ddl is correctly created
/ddl/DDL/
======
[1]: https://www.postgresql.org/docs/devel/sql-createsubscription.html
Kind Regards,
Peter Smith.
Fujitsu Australia
Hi,
Thank you for the review.
Please find the attached patch.
Regards,
Vaibhav
EnterpriseDB
On Tue, Nov 18, 2025 at 7:28 AM Peter Smith <smithpb2250@gmail.com> wrote:
Show quoted text
Some review comments for v6.
======
src/backend/utils/adt/ruleutils.c1. +/* + * pg_get_subscription_string + * Build CREATE SUBSCRIPTION statement for a subscription from its OID. + * + * This is internal version which helps pg_get_subscription_ddl_by_name() and + * pg_get_subscription_ddl_by_oid(). + */ +static char * +pg_get_subscription_string(const Oid suboid)The comment "This is internal" seemed awkward. Anyway, saying it is
"internal" is unnecessary now that it is static.SUGGESTION
Helper for pg_get_subscription_ddl_by_name() and
pg_get_subscription_ddl_by_oid().~~~
2. + /* Get enabled option */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subenabled); + /* Setting 'slot_name' to none must set 'enabled' to false as well */ + appendStringInfo(&buf, ", enabled=%s", + (!DatumGetBool(datum) || isnull) ? "false" : "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); + appendStringInfo(&buf, ", two_phase=%s", + DatumGetChar(datum) == LOGICALREP_TWOPHASE_STATE_DISABLED ? "off" : "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=%d", + DatumGetInt32(datum));2a.
It seems strange that almost everything is coded as "true/false" or
"on/off", except there are a couple ('enabled' and 'two_phase') that
are the other way around (false/true, off/on). There seemed to be no
particular reason for that. IMO consistency is better, so use
true/false for everything.~
2b.
It's not clear to me how you decided when to use true/false versus
on/off. I know that it makes no functional difference, but it does
have the potential to make the result look strange unless there is
some consistent rule. This review comment would apply to *every* one
of the get_XXX_ddl() functions.Personally, I think the DLL functions should only spit out
"true"/"false" for boolean parameters. It is easy and it is
predictable.FWIW, my AI tool agrees with me. viz:
------
Why true/false for DDL:
* Consistent with PostgreSQL's boolean literals in SQL
* Matches what pg_dump produces
* Clear and unambiguous
* Parser accepts both, but true/false is standard output
Don't use:
* on/off in generated DDL (even though parser might accept it)
* 1/0
* yes/no
* t/f (internal catalog representation)
------~
2c.
All those comments about the parameters should be consistent.
Currently they are:
* Get xxx
* xxx?
* xxxChoose one common style to make them all look the same.
~
2d.
It's not clear what ordering was chosen for these parameters in the
generated DDL. Unless there is some meaningful order that eludes me, I
felt it would be best to generate them in the same order that they
appear in the documentation [1].~~~
3. + /* Finally close parenthesis and add semicolon to the statement */ + appendStringInfoString(&buf, ");"); +This comment seems unnecessary.
======
src/test/regress/sql/subscription.sql4.
+-- see the pg_get_subscription_ddl output for a NULL and empty input/see/Check/
~~~
5.
+-- Check that the subscription ddl is correctly created/ddl/DDL/
======
[1] https://www.postgresql.org/docs/devel/sql-createsubscription.htmlKind Regards,
Peter Smith.
Fujitsu Australia
Attachments:
v7-Add-pg_get_subscription_ddl-function.patchapplication/octet-stream; name=v7-Add-pg_get_subscription_ddl-function.patchDownload
From 97252adc9426673c01eec3b6bd54f699bc0ae60e Mon Sep 17 00:00:00 2001
From: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Date: Tue, 18 Nov 2025 05:36:03 +0000
Subject: [PATCH v7 1/1] Add pg_get_subscription_ddl() function
This new SQL-callable function returns the `CREATE SUBSCRIPTION`
statement for a given subscription name or oid.
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.
This function is restricted to users that have the "pg_read_all_data" and/or
"pg_create_subscription" privilege. This is a security measure because
subscription connection strings often contain sensitive information, such as
passwords.
Author: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Reviewers: Akshay Joshi, Nishant Sharma, Ian Barwick, Chao Li, Peter Smith & Alvaro Herrera
Discussion: https://www.postgresql.org/message-id/CA%2BvB%3DAGG0NoxWW%3D-947RBmba8Pzhj8j7op0Xkv8nLDzVMc2%3D7w%40mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 50 +++++
src/backend/utils/adt/ruleutils.c | 238 +++++++++++++++++++++
src/include/catalog/pg_proc.dat | 6 +
src/test/regress/expected/subscription.out | 95 ++++++++
src/test/regress/sql/subscription.sql | 68 ++++++
5 files changed, 457 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..469c0b02d90 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}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ return the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>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_subscription_ddl</primary>
+ </indexterm>
+ <function>pg_get_subscription_ddl</function> ( <parameter>subscription</parameter> <type>name</type> or <type>oid</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the <command>CREATE SUBSCRIPTION</command> command that would
+ create this subscription. The <literal>connect</literal> option set to
+ <literal>false</literal>.
+ </para>
+ <para>
+ This function is restricted to users that have the
+ <literal>pg_read_all_data</literal> and/or
+ <literal>pg_create_subscription</literal> privilege.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..54e6c0dd831 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"
@@ -546,6 +548,8 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
deparse_context *context,
bool showimplicit,
bool needcomma);
+static char *pg_get_subscription_string(const Oid suboid);
+static List *text_array_to_string_list(ArrayType *text_array);
#define only_marker(rte) ((rte)->inh ? "" : "ONLY ")
@@ -13743,3 +13747,237 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * pg_get_subscription_string
+ * Build CREATE SUBSCRIPTION statement for a subscription from its OID.
+ *
+ * Helper for pg_get_subscription_ddl_by_name() and
+ * pg_get_subscription_ddl_by_oid().
+ */
+static char *
+pg_get_subscription_string(const Oid suboid)
+{
+ Form_pg_subscription subForm;
+ StringInfoData pubnames,
+ 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 view subscription DDL.",
+ "pg_create_subscription", "pg_read_all_data")));
+
+ /* Look up the subscription in pg_subscription */
+ tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(suboid));
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("subscription with oid %d does not exist", suboid)));
+
+ /* Get subscription's details from its tuple */
+ subForm = (Form_pg_subscription) GETSTRUCT(tup);
+
+ initStringInfo(&buf);
+
+ /* Build the CREATE SUBSCRIPTION statement */
+ appendStringInfo(&buf, "CREATE SUBSCRIPTION %s ",
+ quote_identifier(NameStr(subForm->subname)));
+
+ /* Get connection information */
+ 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 = text_array_to_string_list(DatumGetArrayTypeP(datum));
+ initStringInfo(&pubnames);
+ 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 enabled option */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subenabled);
+ /* Setting 'slot_name' to none must set 'enabled' to false as well */
+ appendStringInfo(&buf, ", enabled=%s",
+ (!DatumGetBool(datum) || isnull) ? "false" : "true");
+
+ /* Get slotname */
+ datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subslotname,
+ &isnull);
+ if (!isnull)
+ appendStringInfo(&buf, ", slot_name=%s",
+ quote_literal_cstr(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 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=false");
+ else if (DatumGetChar(datum) == LOGICALREP_STREAM_ON)
+ appendStringInfoString(&buf, ", streaming=true");
+ 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);
+ appendStringInfo(&buf, ", two_phase=%s",
+ DatumGetChar(datum) == LOGICALREP_TWOPHASE_STATE_DISABLED ? "false" : "true");
+
+ /* Get disable on error option */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subdisableonerr);
+ appendStringInfo(&buf, ", disable_on_error=%s",
+ DatumGetBool(datum) ? "true" : "false");
+
+ /* Get password required option */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subpasswordrequired);
+ appendStringInfo(&buf, ", password_required=%s",
+ DatumGetBool(datum) ? "true" : "false");
+
+ /* Get run as owner option */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subrunasowner);
+ appendStringInfo(&buf, ", run_as_owner=%s",
+ DatumGetBool(datum) ? "true" : "false");
+
+ /* Get origin */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_suborigin);
+ appendStringInfo(&buf, ", origin=%s", TextDatumGetCString(datum));
+
+ /* Get failover option */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subfailover);
+ appendStringInfo(&buf, ", failover=%s",
+ DatumGetBool(datum) ? "true" : "false");
+
+ /* Get retain dead tuples option */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subretaindeadtuples);
+ appendStringInfo(&buf, ", retain_dead_tuples=%s",
+ DatumGetBool(datum) ? "true" : "false");
+
+ /* Get max retention duration */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_submaxretention);
+ appendStringInfo(&buf, ", max_retention_duration=%d",
+ DatumGetInt32(datum));
+
+ appendStringInfoString(&buf, ");");
+
+ ReleaseSysCache(tup);
+
+ return buf.data;
+}
+
+/*
+ * pg_get_subscription_ddl_by_name
+ * Get CREATE SUBSCRIPTION statement for a subscription.
+ *
+ * This takes name as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_by_name(PG_FUNCTION_ARGS)
+{
+ Name subname = PG_GETARG_NAME(0);
+ Oid suboid;
+ char *ddl_stmt;
+
+ /* Get the OID of the subscription from its name */
+ suboid = get_subscription_oid(NameStr(*subname), false);
+
+ /* Get the CREATE SUBSCRIPTION DDL statement from its OID */
+ ddl_stmt = pg_get_subscription_string(suboid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_subscription_ddl_by_oid
+ * Get CREATE SUBSCRIPTION statement for a subscription.
+ *
+ * This takes oid as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_by_oid(PG_FUNCTION_ARGS)
+{
+ Oid suboid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE SUBSCRIPTION DDL statement from its OID */
+ ddl_stmt = pg_get_subscription_string(suboid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * text_array_to_string_list
+ * Convert text array to list of strings.
+ */
+static List *
+text_array_to_string_list(ArrayType *text_array)
+{
+ List *result = NIL;
+ Datum *elems;
+ int nelems;
+
+ deconstruct_array_builtin(text_array, TEXTOID, &elems, NULL, &nelems);
+
+ for (int i = 0; i < nelems; i++)
+ result = lappend(result, makeString(TextDatumGetCString(elems[i])));
+
+ return result;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index aaadfd8c748..7411bf611ca 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3993,6 +3993,12 @@
{ 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 => 'name', prosrc => 'pg_get_subscription_ddl_by_name' },
+{ oid => '8002', descr => 'get CREATE statement for subscription',
+ proname => 'pg_get_subscription_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_subscription_ddl_by_oid' },
{ 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/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index 327d1e7731f..6f6b5f115cc 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -522,3 +522,98 @@ DROP ROLE regress_subscription_user;
DROP ROLE regress_subscription_user2;
DROP ROLE regress_subscription_user3;
DROP ROLE regress_subscription_user_dummy;
+--
+-- Test pg_get_subscription_ddl() by creating subscriptions with various
+-- configurations and checking the DDL.
+--
+CREATE ROLE regress_createsub_role LOGIN;
+CREATE ROLE regress_readalldata_role LOGIN;
+-- Check the pg_get_subscription_ddl output for a NULL and empty input
+SELECT pg_get_subscription_ddl('');
+ERROR: subscription "" does not exist
+SELECT pg_get_subscription_ddl(NULL);
+ pg_get_subscription_ddl
+-------------------------
+
+(1 row)
+
+-- Suppress WARNINGS when creating subscription
+SET client_min_messages = 'error';
+-- Create subscription with minimal options
+CREATE SUBSCRIPTION regress_testsub1 CONNECTION 'dbname=db_doesnotexist'
+ PUBLICATION testpub1 WITH (connect=false);
+RESET client_min_messages;
+-- Check that the subscription DDL is correctly created
+SELECT pg_get_subscription_ddl('regress_testsub1');
+ pg_get_subscription_ddl
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION regress_testsub1 CONNECTION 'dbname=db_doesnotexist' PUBLICATION "testpub1" WITH (connect=false, enabled=false, slot_name='regress_testsub1', binary=false, streaming=parallel, synchronous_commit=off, two_phase=false, disable_on_error=false, password_required=true, run_as_owner=false, origin=any, failover=false, retain_dead_tuples=false, max_retention_duration=0);
+(1 row)
+
+-- Create subscription with more options
+SET client_min_messages = 'error';
+CREATE SUBSCRIPTION "regress_TestSub2" CONNECTION 'host=unknown user=dvd password=pass123'
+ PUBLICATION "testpub2", "TestPub3" WITH (connect=false, slot_name='slot1',
+ enabled=off);
+RESET client_min_messages;
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+ pg_get_subscription_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_TestSub2" CONNECTION 'host=unknown user=dvd password=pass123' PUBLICATION "testpub2", "TestPub3" WITH (connect=false, enabled=false, slot_name='slot1', binary=false, streaming=parallel, synchronous_commit=off, two_phase=false, disable_on_error=false, password_required=true, run_as_owner=false, origin=any, failover=false, retain_dead_tuples=false, max_retention_duration=0);
+(1 row)
+
+-- Create subscription with all options
+SET client_min_messages = 'error';
+CREATE SUBSCRIPTION regress_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);
+RESET client_min_messages;
+SELECT pg_get_subscription_ddl('regress_testsub3');
+ pg_get_subscription_ddl
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION regress_testsub3 CONNECTION 'host=unknown user=dvd password=pass12' PUBLICATION "testpub4" WITH (connect=false, enabled=false, slot_name=none, create_slot=false, binary=true, streaming=false, 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);
+(1 row)
+
+-- Non-superusers without pg_create_subscription and/or pg_read_all_data
+-- permissions cannot retrieve the DDL.
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+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 view subscription DDL.
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+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 view subscription DDL.
+RESET SESSION AUTHORIZATION;
+-- Administrators can change who can access this function
+GRANT pg_create_subscription TO regress_createsub_role;
+GRANT pg_read_all_data TO regress_readalldata_role;
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+ pg_get_subscription_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_TestSub2" CONNECTION 'host=unknown user=dvd password=pass123' PUBLICATION "testpub2", "TestPub3" WITH (connect=false, enabled=false, slot_name='slot1', binary=false, streaming=parallel, synchronous_commit=off, two_phase=false, disable_on_error=false, password_required=true, run_as_owner=false, origin=any, failover=false, retain_dead_tuples=false, max_retention_duration=0);
+(1 row)
+
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+ pg_get_subscription_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_TestSub2" CONNECTION 'host=unknown user=dvd password=pass123' PUBLICATION "testpub2", "TestPub3" WITH (connect=false, enabled=false, slot_name='slot1', binary=false, streaming=parallel, synchronous_commit=off, two_phase=false, disable_on_error=false, password_required=true, run_as_owner=false, origin=any, failover=false, retain_dead_tuples=false, max_retention_duration=0);
+(1 row)
+
+RESET SESSION AUTHORIZATION;
+REVOKE pg_create_subscription FROM regress_createsub_role;
+REVOKE pg_read_all_data FROM regress_readalldata_role;
+ALTER SUBSCRIPTION regress_testsub1 SET (slot_name=NONE);
+DROP SUBSCRIPTION regress_testsub1;
+ALTER SUBSCRIPTION "regress_TestSub2" SET (slot_name=NONE);
+DROP SUBSCRIPTION "regress_TestSub2";
+DROP SUBSCRIPTION regress_testsub3;
+DROP ROLE regress_createsub_role;
+DROP ROLE regress_readalldata_role;
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index ef0c298d2df..149c0e06cd3 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -370,3 +370,71 @@ DROP ROLE regress_subscription_user;
DROP ROLE regress_subscription_user2;
DROP ROLE regress_subscription_user3;
DROP ROLE regress_subscription_user_dummy;
+
+--
+-- Test pg_get_subscription_ddl() by creating subscriptions with various
+-- configurations and checking the DDL.
+--
+CREATE ROLE regress_createsub_role LOGIN;
+CREATE ROLE regress_readalldata_role LOGIN;
+
+-- Check the pg_get_subscription_ddl output for a NULL and empty input
+SELECT pg_get_subscription_ddl('');
+SELECT pg_get_subscription_ddl(NULL);
+
+-- Suppress WARNINGS when creating subscription
+SET client_min_messages = 'error';
+-- Create subscription with minimal options
+CREATE SUBSCRIPTION regress_testsub1 CONNECTION 'dbname=db_doesnotexist'
+ PUBLICATION testpub1 WITH (connect=false);
+RESET client_min_messages;
+
+-- Check that the subscription DDL is correctly created
+SELECT pg_get_subscription_ddl('regress_testsub1');
+
+-- Create subscription with more options
+SET client_min_messages = 'error';
+CREATE SUBSCRIPTION "regress_TestSub2" CONNECTION 'host=unknown user=dvd password=pass123'
+ PUBLICATION "testpub2", "TestPub3" WITH (connect=false, slot_name='slot1',
+ enabled=off);
+RESET client_min_messages;
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+
+-- Create subscription with all options
+SET client_min_messages = 'error';
+CREATE SUBSCRIPTION regress_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);
+RESET client_min_messages;
+SELECT pg_get_subscription_ddl('regress_testsub3');
+
+-- Non-superusers without pg_create_subscription and/or pg_read_all_data
+-- permissions cannot retrieve the DDL.
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+RESET SESSION AUTHORIZATION;
+-- Administrators can change who can access this function
+GRANT pg_create_subscription TO regress_createsub_role;
+GRANT pg_read_all_data TO regress_readalldata_role;
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+
+RESET SESSION AUTHORIZATION;
+REVOKE pg_create_subscription FROM regress_createsub_role;
+REVOKE pg_read_all_data FROM regress_readalldata_role;
+ALTER SUBSCRIPTION regress_testsub1 SET (slot_name=NONE);
+DROP SUBSCRIPTION regress_testsub1;
+ALTER SUBSCRIPTION "regress_TestSub2" SET (slot_name=NONE);
+DROP SUBSCRIPTION "regress_TestSub2";
+DROP SUBSCRIPTION regress_testsub3;
+DROP ROLE regress_createsub_role;
+DROP ROLE regress_readalldata_role;
--
2.43.0
Some more review comments for v7.
======
1.
+ * 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.
Is it consistent to just use defaults for these 2 parameters, and not
even explicitly return them in the DDL string, when IIRC earlier you
said we cannot use defaults for any of the others?
~~~
2.
+ /* Get enabled option */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subenabled);
+ /* Setting 'slot_name' to none must set 'enabled' to false as well */
+ appendStringInfo(&buf, ", enabled=%s",
+ (!DatumGetBool(datum) || isnull) ? "false" : "true");
+
+ /* Get slotname */
+ datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subslotname,
+ &isnull);
+ if (!isnull)
+ appendStringInfo(&buf, ", slot_name=%s",
+ quote_literal_cstr(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");
+ }
+
2a.
The 'enabled' condition should also be checking the slot name none
(aka 'null' check), so I think this code became broken in v7 when you
swapped the order of the parameters without handling the condition.
~
2b.
You'll need different logic to emit the 'create_slot' parameter in the
same order that it appears in the documentation. Previously, I had
suggested assigning all the parameters to variables up-front before
building your DDL string. If that were done, then it should be easy to
reshuffle the order of the DDL parameters without jumping through
hoops.
~~
3.
+ appendStringInfo(&buf, ", enabled=%s",
+ (!DatumGetBool(datum) || isnull) ? "false" : "true");
Can't you rearrange the ternary to be "true" : "false" like all the others?
~~~
4.
+ if (DatumGetChar(datum) == LOGICALREP_STREAM_OFF)
+ appendStringInfoString(&buf, ", streaming=false");
+ else if (DatumGetChar(datum) == LOGICALREP_STREAM_ON)
+ appendStringInfoString(&buf, ", streaming=true");
+ else
+ appendStringInfoString(&buf, ", streaming=parallel");
When I suggested changing all the boolean params to "true" and
"false", I wasn't expecting you would change this one, which is an
enum, not a boolean. The docs for this one refer to values "parallel",
"on" and "off", so it's better to use the same values as the
documentation.
~~
5.
+ appendStringInfo(&buf, ", two_phase=%s",
+ DatumGetChar(datum) == LOGICALREP_TWOPHASE_STATE_DISABLED ? "false" : "true");
Can't you rearrange the ternary to be "true" : "false" like all the others?
======
[1]: https://www.postgresql.org/docs/devel/sql-createsubscription.html
Kind Regards,
Peter Smith.
Fujitsu Australia
Thanks Peter for the close look.
1.
+ * 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. Is it consistent to just use defaults for these 2 parameters, and not even explicitly return them in the DDL string, when IIRC earlier you said we cannot use defaults for any of the others?It is not consistent but we don't have any other option because though we
explicitly
return them in the DDL string we have to use the default values because
we don't know the exact values for these two parameters. Using default to
explicitly
return them in the DDL string will be a problem because default value may
change
in the future, so better to not include in the ddl string and lets server
decide the
default value at the creation time.
2b.
You'll need different logic to emit the 'create_slot' parameter in the
same order that it appears in the documentation. Previously, I had
suggested assigning all the parameters to variables up-front before
building your DDL string. If that were done, then it should be easy to
reshuffle the order of the DDL parameters without jumping through
hoops.
I think it is fine to not follow the documentation order(at-least for these
two options)
because that's not a hard and fast rule and option order doesn't matter.
Please find a revised patch.
Regards,
Vaibhav
On Tue, Nov 18, 2025 at 12:42 PM Peter Smith <smithpb2250@gmail.com> wrote:
Show quoted text
Some more review comments for v7.
======
1. + * 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.Is it consistent to just use defaults for these 2 parameters, and not
even explicitly return them in the DDL string, when IIRC earlier you
said we cannot use defaults for any of the others?~~~
2. + /* Get enabled option */ + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subenabled); + /* Setting 'slot_name' to none must set 'enabled' to false as well */ + appendStringInfo(&buf, ", enabled=%s", + (!DatumGetBool(datum) || isnull) ? "false" : "true"); + + /* Get slotname */ + datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subslotname, + &isnull); + if (!isnull) + appendStringInfo(&buf, ", slot_name=%s", + quote_literal_cstr(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"); + } +2a.
The 'enabled' condition should also be checking the slot name none
(aka 'null' check), so I think this code became broken in v7 when you
swapped the order of the parameters without handling the condition.~
2b.
You'll need different logic to emit the 'create_slot' parameter in the
same order that it appears in the documentation. Previously, I had
suggested assigning all the parameters to variables up-front before
building your DDL string. If that were done, then it should be easy to
reshuffle the order of the DDL parameters without jumping through
hoops.~~
3. + appendStringInfo(&buf, ", enabled=%s", + (!DatumGetBool(datum) || isnull) ? "false" : "true");Can't you rearrange the ternary to be "true" : "false" like all the others?
~~~
4. + if (DatumGetChar(datum) == LOGICALREP_STREAM_OFF) + appendStringInfoString(&buf, ", streaming=false"); + else if (DatumGetChar(datum) == LOGICALREP_STREAM_ON) + appendStringInfoString(&buf, ", streaming=true"); + else + appendStringInfoString(&buf, ", streaming=parallel");When I suggested changing all the boolean params to "true" and
"false", I wasn't expecting you would change this one, which is an
enum, not a boolean. The docs for this one refer to values "parallel",
"on" and "off", so it's better to use the same values as the
documentation.~~
5. + appendStringInfo(&buf, ", two_phase=%s", + DatumGetChar(datum) == LOGICALREP_TWOPHASE_STATE_DISABLED ? "false" : "true");Can't you rearrange the ternary to be "true" : "false" like all the others?
======
[1] https://www.postgresql.org/docs/devel/sql-createsubscription.htmlKind Regards,
Peter Smith.
Fujitsu Australia
Attachments:
v8-Add-pg_get_subscription_ddl-function.patchapplication/octet-stream; name=v8-Add-pg_get_subscription_ddl-function.patchDownload
From d761e1f438c1e76b307de8c063f7f71e5c577de4 Mon Sep 17 00:00:00 2001
From: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Date: Tue, 18 Nov 2025 11:43:30 +0000
Subject: [PATCH v8 1/1] Add pg_get_subscription_ddl() function
This new SQL-callable function returns the `CREATE SUBSCRIPTION`
statement for a given subscription name or oid.
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.
This function is restricted to users that have the "pg_read_all_data" and/or
"pg_create_subscription" privilege. This is a security measure because
subscription connection strings often contain sensitive information, such as
passwords.
Author: Vaibhav Dalvi <vaibhav.dalvi@enterprisedb.com>
Reviewers: Akshay Joshi, Nishant Sharma, Ian Barwick, Chao Li, Peter Smith & Alvaro Herrera
Discussion: https://www.postgresql.org/message-id/CA%2BvB%3DAGG0NoxWW%3D-947RBmba8Pzhj8j7op0Xkv8nLDzVMc2%3D7w%40mail.gmail.com
---
doc/src/sgml/func/func-info.sgml | 50 +++++
src/backend/utils/adt/ruleutils.c | 239 +++++++++++++++++++++
src/include/catalog/pg_proc.dat | 6 +
src/test/regress/expected/subscription.out | 95 ++++++++
src/test/regress/sql/subscription.sql | 68 ++++++
5 files changed, 458 insertions(+)
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..469c0b02d90 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}
</sect2>
+ <sect2 id="functions-get-object-ddl">
+ <title>Object DDL Functions</title>
+
+ <para>
+ The functions shown in <xref linkend="functions-get-object-ddl-table"/>
+ return the DDL statements for various database objects.
+ (This is a decompiled reconstruction, not the original text
+ of the command.)
+ </para>
+
+ <table id="functions-get-object-ddl-table">
+ <title>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_subscription_ddl</primary>
+ </indexterm>
+ <function>pg_get_subscription_ddl</function> ( <parameter>subscription</parameter> <type>name</type> or <type>oid</type> )
+ <returnvalue>text</returnvalue>
+ </para>
+ <para>
+ Returns the <command>CREATE SUBSCRIPTION</command> command that would
+ create this subscription. The <literal>connect</literal> option set to
+ <literal>false</literal>.
+ </para>
+ <para>
+ This function is restricted to users that have the
+ <literal>pg_read_all_data</literal> and/or
+ <literal>pg_create_subscription</literal> privilege.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect2>
+
</sect1>
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 556ab057e5a..f416872565c 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"
@@ -546,6 +548,8 @@ static void get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
deparse_context *context,
bool showimplicit,
bool needcomma);
+static char *pg_get_subscription_string(const Oid suboid);
+static List *text_array_to_string_list(ArrayType *text_array);
#define only_marker(rte) ((rte)->inh ? "" : "ONLY ")
@@ -13743,3 +13747,238 @@ get_range_partbound_string(List *bound_datums)
return buf.data;
}
+
+/*
+ * pg_get_subscription_string
+ * Build CREATE SUBSCRIPTION statement for a subscription from its OID.
+ *
+ * Helper for pg_get_subscription_ddl_by_name() and
+ * pg_get_subscription_ddl_by_oid().
+ */
+static char *
+pg_get_subscription_string(const Oid suboid)
+{
+ Form_pg_subscription subForm;
+ StringInfoData pubnames,
+ 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 view subscription DDL.",
+ "pg_create_subscription", "pg_read_all_data")));
+
+ /* Look up the subscription in pg_subscription */
+ tup = SearchSysCache1(SUBSCRIPTIONOID, ObjectIdGetDatum(suboid));
+ if (!HeapTupleIsValid(tup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("subscription with oid %d does not exist", suboid)));
+
+ /* Get subscription's details from its tuple */
+ subForm = (Form_pg_subscription) GETSTRUCT(tup);
+
+ initStringInfo(&buf);
+
+ /* Build the CREATE SUBSCRIPTION statement */
+ appendStringInfo(&buf, "CREATE SUBSCRIPTION %s ",
+ quote_identifier(NameStr(subForm->subname)));
+
+ /* Get connection information */
+ 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 = text_array_to_string_list(DatumGetArrayTypeP(datum));
+ initStringInfo(&pubnames);
+ 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",
+ quote_literal_cstr(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 */
+ appendStringInfo(&buf, ", enabled=%s",
+ (DatumGetBool(datum) && !isnull) ? "true" : "false");
+
+ /* 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_ON)
+ appendStringInfoString(&buf, ", streaming=on");
+ else if (DatumGetChar(datum) == LOGICALREP_STREAM_OFF)
+ appendStringInfoString(&buf, ", streaming=off");
+ 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);
+ appendStringInfo(&buf, ", two_phase=%s",
+ DatumGetChar(datum) != LOGICALREP_TWOPHASE_STATE_DISABLED ? "true" : "false");
+
+ /* Get disable on error option */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subdisableonerr);
+ appendStringInfo(&buf, ", disable_on_error=%s",
+ DatumGetBool(datum) ? "true" : "false");
+
+ /* Get password required option */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subpasswordrequired);
+ appendStringInfo(&buf, ", password_required=%s",
+ DatumGetBool(datum) ? "true" : "false");
+
+ /* Get run as owner option */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subrunasowner);
+ appendStringInfo(&buf, ", run_as_owner=%s",
+ DatumGetBool(datum) ? "true" : "false");
+
+ /* Get origin */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_suborigin);
+ appendStringInfo(&buf, ", origin=%s", TextDatumGetCString(datum));
+
+ /* Get failover option */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subfailover);
+ appendStringInfo(&buf, ", failover=%s",
+ DatumGetBool(datum) ? "true" : "false");
+
+ /* Get retain dead tuples option */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_subretaindeadtuples);
+ appendStringInfo(&buf, ", retain_dead_tuples=%s",
+ DatumGetBool(datum) ? "true" : "false");
+
+ /* Get max retention duration */
+ datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup,
+ Anum_pg_subscription_submaxretention);
+ appendStringInfo(&buf, ", max_retention_duration=%d",
+ DatumGetInt32(datum));
+
+ appendStringInfoString(&buf, ");");
+
+ ReleaseSysCache(tup);
+
+ return buf.data;
+}
+
+/*
+ * pg_get_subscription_ddl_by_name
+ * Get CREATE SUBSCRIPTION statement for a subscription.
+ *
+ * This takes name as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_by_name(PG_FUNCTION_ARGS)
+{
+ Name subname = PG_GETARG_NAME(0);
+ Oid suboid;
+ char *ddl_stmt;
+
+ /* Get the OID of the subscription from its name */
+ suboid = get_subscription_oid(NameStr(*subname), false);
+
+ /* Get the CREATE SUBSCRIPTION DDL statement from its OID */
+ ddl_stmt = pg_get_subscription_string(suboid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * pg_get_subscription_ddl_by_oid
+ * Get CREATE SUBSCRIPTION statement for a subscription.
+ *
+ * This takes oid as parameter for pg_get_subscription_ddl().
+ */
+Datum
+pg_get_subscription_ddl_by_oid(PG_FUNCTION_ARGS)
+{
+ Oid suboid = PG_GETARG_OID(0);
+ char *ddl_stmt;
+
+ /* Get the CREATE SUBSCRIPTION DDL statement from its OID */
+ ddl_stmt = pg_get_subscription_string(suboid);
+
+ PG_RETURN_TEXT_P(string_to_text(ddl_stmt));
+}
+
+/*
+ * text_array_to_string_list
+ * Convert text array to list of strings.
+ */
+static List *
+text_array_to_string_list(ArrayType *text_array)
+{
+ List *result = NIL;
+ Datum *elems;
+ int nelems;
+
+ deconstruct_array_builtin(text_array, TEXTOID, &elems, NULL, &nelems);
+
+ for (int i = 0; i < nelems; i++)
+ result = lappend(result, makeString(TextDatumGetCString(elems[i])));
+
+ return result;
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index aaadfd8c748..7411bf611ca 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3993,6 +3993,12 @@
{ 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 => 'name', prosrc => 'pg_get_subscription_ddl_by_name' },
+{ oid => '8002', descr => 'get CREATE statement for subscription',
+ proname => 'pg_get_subscription_ddl', prorettype => 'text',
+ proargtypes => 'oid', prosrc => 'pg_get_subscription_ddl_by_oid' },
{ 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/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index 327d1e7731f..843481d1134 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -522,3 +522,98 @@ DROP ROLE regress_subscription_user;
DROP ROLE regress_subscription_user2;
DROP ROLE regress_subscription_user3;
DROP ROLE regress_subscription_user_dummy;
+--
+-- Test pg_get_subscription_ddl() by creating subscriptions with various
+-- configurations and checking the DDL.
+--
+CREATE ROLE regress_createsub_role LOGIN;
+CREATE ROLE regress_readalldata_role LOGIN;
+-- Check the pg_get_subscription_ddl output for a NULL and empty input
+SELECT pg_get_subscription_ddl('');
+ERROR: subscription "" does not exist
+SELECT pg_get_subscription_ddl(NULL);
+ pg_get_subscription_ddl
+-------------------------
+
+(1 row)
+
+-- Suppress WARNINGS when creating subscription
+SET client_min_messages = 'error';
+-- Create subscription with minimal options
+CREATE SUBSCRIPTION regress_testsub1 CONNECTION 'dbname=db_doesnotexist'
+ PUBLICATION testpub1 WITH (connect=false);
+RESET client_min_messages;
+-- Check that the subscription DDL is correctly created
+SELECT pg_get_subscription_ddl('regress_testsub1');
+ pg_get_subscription_ddl
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION regress_testsub1 CONNECTION 'dbname=db_doesnotexist' PUBLICATION "testpub1" WITH (connect=false, slot_name='regress_testsub1', enabled=false, binary=false, streaming=parallel, synchronous_commit=off, two_phase=false, disable_on_error=false, password_required=true, run_as_owner=false, origin=any, failover=false, retain_dead_tuples=false, max_retention_duration=0);
+(1 row)
+
+-- Create subscription with more options
+SET client_min_messages = 'error';
+CREATE SUBSCRIPTION "regress_TestSub2" CONNECTION 'host=unknown user=dvd password=pass123'
+ PUBLICATION "testpub2", "TestPub3" WITH (connect=false, slot_name='slot1',
+ enabled=off);
+RESET client_min_messages;
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+ pg_get_subscription_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_TestSub2" 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=false, disable_on_error=false, password_required=true, run_as_owner=false, origin=any, failover=false, retain_dead_tuples=false, max_retention_duration=0);
+(1 row)
+
+-- Create subscription with all options
+SET client_min_messages = 'error';
+CREATE SUBSCRIPTION regress_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);
+RESET client_min_messages;
+SELECT pg_get_subscription_ddl('regress_testsub3');
+ pg_get_subscription_ddl
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION regress_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=true, disable_on_error=true, password_required=false, run_as_owner=true, origin=none, failover=true, retain_dead_tuples=false, max_retention_duration=100);
+(1 row)
+
+-- Non-superusers without pg_create_subscription and/or pg_read_all_data
+-- permissions cannot retrieve the DDL.
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+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 view subscription DDL.
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+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 view subscription DDL.
+RESET SESSION AUTHORIZATION;
+-- Administrators can change who can access this function
+GRANT pg_create_subscription TO regress_createsub_role;
+GRANT pg_read_all_data TO regress_readalldata_role;
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+ pg_get_subscription_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_TestSub2" 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=false, disable_on_error=false, password_required=true, run_as_owner=false, origin=any, failover=false, retain_dead_tuples=false, max_retention_duration=0);
+(1 row)
+
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+ pg_get_subscription_ddl
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ CREATE SUBSCRIPTION "regress_TestSub2" 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=false, disable_on_error=false, password_required=true, run_as_owner=false, origin=any, failover=false, retain_dead_tuples=false, max_retention_duration=0);
+(1 row)
+
+RESET SESSION AUTHORIZATION;
+REVOKE pg_create_subscription FROM regress_createsub_role;
+REVOKE pg_read_all_data FROM regress_readalldata_role;
+ALTER SUBSCRIPTION regress_testsub1 SET (slot_name=NONE);
+DROP SUBSCRIPTION regress_testsub1;
+ALTER SUBSCRIPTION "regress_TestSub2" SET (slot_name=NONE);
+DROP SUBSCRIPTION "regress_TestSub2";
+DROP SUBSCRIPTION regress_testsub3;
+DROP ROLE regress_createsub_role;
+DROP ROLE regress_readalldata_role;
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index ef0c298d2df..149c0e06cd3 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -370,3 +370,71 @@ DROP ROLE regress_subscription_user;
DROP ROLE regress_subscription_user2;
DROP ROLE regress_subscription_user3;
DROP ROLE regress_subscription_user_dummy;
+
+--
+-- Test pg_get_subscription_ddl() by creating subscriptions with various
+-- configurations and checking the DDL.
+--
+CREATE ROLE regress_createsub_role LOGIN;
+CREATE ROLE regress_readalldata_role LOGIN;
+
+-- Check the pg_get_subscription_ddl output for a NULL and empty input
+SELECT pg_get_subscription_ddl('');
+SELECT pg_get_subscription_ddl(NULL);
+
+-- Suppress WARNINGS when creating subscription
+SET client_min_messages = 'error';
+-- Create subscription with minimal options
+CREATE SUBSCRIPTION regress_testsub1 CONNECTION 'dbname=db_doesnotexist'
+ PUBLICATION testpub1 WITH (connect=false);
+RESET client_min_messages;
+
+-- Check that the subscription DDL is correctly created
+SELECT pg_get_subscription_ddl('regress_testsub1');
+
+-- Create subscription with more options
+SET client_min_messages = 'error';
+CREATE SUBSCRIPTION "regress_TestSub2" CONNECTION 'host=unknown user=dvd password=pass123'
+ PUBLICATION "testpub2", "TestPub3" WITH (connect=false, slot_name='slot1',
+ enabled=off);
+RESET client_min_messages;
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+
+-- Create subscription with all options
+SET client_min_messages = 'error';
+CREATE SUBSCRIPTION regress_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);
+RESET client_min_messages;
+SELECT pg_get_subscription_ddl('regress_testsub3');
+
+-- Non-superusers without pg_create_subscription and/or pg_read_all_data
+-- permissions cannot retrieve the DDL.
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+RESET SESSION AUTHORIZATION;
+-- Administrators can change who can access this function
+GRANT pg_create_subscription TO regress_createsub_role;
+GRANT pg_read_all_data TO regress_readalldata_role;
+SET SESSION AUTHORIZATION 'regress_createsub_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION 'regress_readalldata_role';
+SELECT pg_get_subscription_ddl('regress_TestSub2');
+
+RESET SESSION AUTHORIZATION;
+REVOKE pg_create_subscription FROM regress_createsub_role;
+REVOKE pg_read_all_data FROM regress_readalldata_role;
+ALTER SUBSCRIPTION regress_testsub1 SET (slot_name=NONE);
+DROP SUBSCRIPTION regress_testsub1;
+ALTER SUBSCRIPTION "regress_TestSub2" SET (slot_name=NONE);
+DROP SUBSCRIPTION "regress_TestSub2";
+DROP SUBSCRIPTION regress_testsub3;
+DROP ROLE regress_createsub_role;
+DROP ROLE regress_readalldata_role;
--
2.43.0
Hi Vaibhav.
Thanks for the updates. I don't have any new review comments for v8 --
just a couple of the same ones as before.
On Wed, Nov 19, 2025 at 12:00 AM vDalvi <vaibhav.dalvi@enterprisedb.com> wrote:
Thanks Peter for the close look.
1.
+ * 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. Is it consistent to just use defaults for these 2 parameters, and not even explicitly return them in the DDL string, when IIRC earlier you said we cannot use defaults for any of the others?It is not consistent but we don't have any other option because though we explicitly
return them in the DDL string we have to use the default values because
we don't know the exact values for these two parameters. Using default to explicitly
return them in the DDL string will be a problem because default value may change
in the future, so better to not include in the ddl string and lets server decide the
default value at the creation time.
I don't understand "... will be a problem because default value may
change in the future". Why do we even care if some defaults might
change for some unknown future version of Postgres? Isn't the purpose
of the function to return a DDL string that, when executed, would
re-create the specified subscription *EXACTLY* as it is defined
here-and-now? That's why I thought even "copy_data" and "create_slot"
values ought to be in the DDL.
2b.
You'll need different logic to emit the 'create_slot' parameter in the
same order that it appears in the documentation. Previously, I had
suggested assigning all the parameters to variables up-front before
building your DDL string. If that were done, then it should be easy to
reshuffle the order of the DDL parameters without jumping through
hoops.I think it is fine to not follow the documentation order(at-least for these two options)
because that's not a hard and fast rule and option order doesn't matter.
Yeah, my comments about ordering are not really directed specifically
at this get_subscription_ddl() function implementation. I was thinking
more of the bigger picture -- AFAIK, there are going to be many of
these get_xxx_ddl() functions, so IMO you really do need to have in
place some common guidelines (e.g. "use the same option ordering as in
the docs"), and follow those rules even when they are not strictly
needed. Otherwise, consistency/predictability goes out the window when
every function gets implemented on the whim of different people. Each
function implementation taken individually may be fine, but I felt the
lack of consistency across many similar functions would not be a good
result. YMMV.
======
Kind Regards,
Peter Smith.
Fujitysu Australia
Hi Peter,
Thank you for your latest review comments.
I will let the committer decide on those two points.
To all reviewers, thank you for your feedback.
Given that the CFBot is green for v8 at
https://commitfest.postgresql.org/patch/6208/ (
https://commitfest.postgresql.org/patch/6208/),
I believe we are now in a position to move this patch to 'Ready for
Committer' status.
Hi Andrew,
We are moving this thread to 'Ready for Committer' for your final review
and patch finalization.
Thanks,
Vaibhav Dalvi
EnterpriseDB
On Wed, Nov 19, 2025 at 8:19 AM Peter Smith <smithpb2250@gmail.com> wrote:
Show quoted text
Hi Vaibhav.
Thanks for the updates. I don't have any new review comments for v8 --
just a couple of the same ones as before.On Wed, Nov 19, 2025 at 12:00 AM vDalvi <vaibhav.dalvi@enterprisedb.com>
wrote:Thanks Peter for the close look.
1.
+ * 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. Is it consistent to just use defaults for these 2 parameters, and not even explicitly return them in the DDL string, when IIRC earlier you said we cannot use defaults for any of the others?It is not consistent but we don't have any other option because though
we explicitly
return them in the DDL string we have to use the default values because
we don't know the exact values for these two parameters. Using defaultto explicitly
return them in the DDL string will be a problem because default value
may change
in the future, so better to not include in the ddl string and lets
server decide the
default value at the creation time.
I don't understand "... will be a problem because default value may
change in the future". Why do we even care if some defaults might
change for some unknown future version of Postgres? Isn't the purpose
of the function to return a DDL string that, when executed, would
re-create the specified subscription *EXACTLY* as it is defined
here-and-now? That's why I thought even "copy_data" and "create_slot"
values ought to be in the DDL.2b.
You'll need different logic to emit the 'create_slot' parameter in the
same order that it appears in the documentation. Previously, I had
suggested assigning all the parameters to variables up-front before
building your DDL string. If that were done, then it should be easy to
reshuffle the order of the DDL parameters without jumping through
hoops.I think it is fine to not follow the documentation order(at-least for
these two options)
because that's not a hard and fast rule and option order doesn't matter.
Yeah, my comments about ordering are not really directed specifically
at this get_subscription_ddl() function implementation. I was thinking
more of the bigger picture -- AFAIK, there are going to be many of
these get_xxx_ddl() functions, so IMO you really do need to have in
place some common guidelines (e.g. "use the same option ordering as in
the docs"), and follow those rules even when they are not strictly
needed. Otherwise, consistency/predictability goes out the window when
every function gets implemented on the whim of different people. Each
function implementation taken individually may be fine, but I felt the
lack of consistency across many similar functions would not be a good
result. YMMV.======
Kind Regards,
Peter Smith.
Fujitysu Australia