public inbox for [email protected]help / color / mirror / Atom feed
Re: [19] CREATE SUBSCRIPTION ... SERVER 17+ messages / 4 participants [nested] [flat]
* Re: [19] CREATE SUBSCRIPTION ... SERVER @ 2026-02-04 04:53 Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 0 siblings, 1 reply; 17+ messages in thread From: Masahiko Sawada @ 2026-02-04 04:53 UTC (permalink / raw) To: Jeff Davis <[email protected]>; +Cc: Shlok Kyal <[email protected]>; Ashutosh Bapat <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Sat, Jan 10, 2026 at 8:55 PM Jeff Davis <[email protected]> wrote: > > On Fri, 2025-12-26 at 13:52 -0800, Jeff Davis wrote: > > On Wed, 2025-04-02 at 17:58 +0530, Shlok Kyal wrote: > > > I reviewed the patch and I have a comment: > > > > > > If version is >=18, the query will have 'suboriginremotelsn', > > > 'subenabled', 'subfailover' twice. > > > > Thank you. Fixed and rebased. > > Attached new version with significant changes: > > - fixed several issues (including some improper merges in the last > rebase) > - refactored to share code between postgres_fdw_connection() and > connect_pg_server() > - added docs in postgres_fdw > - added tests in core > - bumped postgres_fdw version to 1.3 > I've reviewed the latest patch set. I understand the motivation behind this proposal and find it useful. Here are some comments: @@ -5580,6 +5580,8 @@ fdw_option: | NO HANDLER { $$ = makeDefElem("handler", NULL, @1); } | VALIDATOR handler_name { $$ = makeDefElem("validator", (Node *) $2, @1); } | NO VALIDATOR { $$ = makeDefElem("validator", NULL, @1); } + | CONNECTION handler_name { $$ = makeDefElem("connection", (Node *) $2, @1); } + | NO CONNECTION { $$ = makeDefElem("connection", NULL, @1); } ; The documentation for ALTER FOREIGN DATA WRAPPER needs to be updated. --- The security section[1] of logical replication chapter would also need to be updated. Currently we have: To create a subscription, the user must have the privileges of the pg_create_subscription role, as well as CREATE privileges on the database. IIUC if the user uses the SERVER clause, they must have the USAGE privilege on the foreign server too. --- We might want to mention in the documentation of CREATE SERVER[2] that a foreign server's name can be used to connect publication in CREATE SUBSCRIPTION as we have a similar description for dblink_connect(): When using the dblink module, a foreign server's name can be used as an argument of the dblink_connect function to indicate the connection parameters. It is necessary to have the USAGE privilege on the foreign server to be able to use it in this way. --- dblink_connect() function can retrieve the connection string from a foreign server specified in the second argument, which is a very similar use case to CREATE SUBSCRIPTION. Should we make dblink use the new function ForeignServerConnectionString() to get the connection string (in get_connect_string())? --- It would be better to enhance psql's \dRs command to show the server name specified in the subscription. Regards, [1] https://www.postgresql.org/docs/devel/logical-replication-security.html [2] https://www.postgresql.org/docs/devel/sql-createserver.html -- Masahiko Sawada Amazon Web Services: https://aws.amazon.com ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> @ 2026-02-26 19:12 ` Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 0 siblings, 1 reply; 17+ messages in thread From: Jeff Davis @ 2026-02-26 19:12 UTC (permalink / raw) To: Masahiko Sawada <[email protected]>; +Cc: Shlok Kyal <[email protected]>; Ashutosh Bapat <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Wed, 2026-02-04 at 13:53 +0900, Masahiko Sawada wrote: > I've reviewed the latest patch set. I understand the motivation > behind > this proposal and find it useful. Thank you, that's important feedback. > The documentation for ALTER FOREIGN DATA WRAPPER needs to be updated. Done. > --- > The security section[1] of logical replication chapter would also > need > to be updated. Done. > We might want to mention in the documentation of CREATE SERVER[2] > that > a foreign server's name can be used to connect publication in CREATE > SUBSCRIPTION as we have a similar description for dblink_connect(): Done. > --- > dblink_connect() function can retrieve the connection string from a > foreign server specified in the second argument, which is a very > similar use case to CREATE SUBSCRIPTION. Should we make dblink use > the > new function ForeignServerConnectionString() to get the connection > string (in get_connect_string())? ForeignServerConnectionString() goes through the new FDW connection_function, whereas dblink builds the string itself. Technically, changing that could break things, but overall it seems to make sense. I added this as a separate commit. > --- > It would be better to enhance psql's \dRs command to show the server > name specified in the subscription. Good idea, done. Regards, Jeff Davis Attachments: [text/x-patch] v17-0001-CREATE-SUBSCRIPTION-.-SERVER.patch (126.0K, 2-v17-0001-CREATE-SUBSCRIPTION-.-SERVER.patch) download | inline diff: From 3afc607764288b51c4385d22111f2de23ee630d9 Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Tue, 2 Jan 2024 13:42:48 -0800 Subject: [PATCH v17 1/2] CREATE SUBSCRIPTION ... SERVER. --- CATVERSION BUMP --- Allow specifying a foreign server for CREATE SUBSCRIPTION, rather than a raw connection string with CONNECTION. Using a foreign server as a layer of indirection improves management of multiple subscriptions to the same server. It also provides integration with user mappings in case different subscriptions have different owners or a subscription changes owners. Reviewed-by: Ashutosh Bapat <[email protected]> Reviewed-by: Shlok Kyal <[email protected]> Reviewed-by: Masahiko Sawada <[email protected]> Discussion: https://postgr.es/m/[email protected] --- contrib/postgres_fdw/Makefile | 2 +- contrib/postgres_fdw/connection.c | 299 +++++++++++------- .../postgres_fdw/expected/postgres_fdw.out | 8 + contrib/postgres_fdw/meson.build | 2 + .../postgres_fdw/postgres_fdw--1.2--1.3.sql | 12 + contrib/postgres_fdw/postgres_fdw.control | 2 +- contrib/postgres_fdw/sql/postgres_fdw.sql | 7 + contrib/postgres_fdw/t/010_subscription.pl | 71 +++++ doc/src/sgml/logical-replication.sgml | 4 +- doc/src/sgml/postgres-fdw.sgml | 26 ++ .../sgml/ref/alter_foreign_data_wrapper.sgml | 20 ++ doc/src/sgml/ref/alter_subscription.sgml | 18 +- .../sgml/ref/create_foreign_data_wrapper.sgml | 20 ++ doc/src/sgml/ref/create_server.sgml | 7 + doc/src/sgml/ref/create_subscription.sgml | 11 +- src/backend/catalog/pg_subscription.c | 38 ++- src/backend/commands/foreigncmds.c | 58 +++- src/backend/commands/subscriptioncmds.c | 168 +++++++++- src/backend/foreign/foreign.c | 81 +++++ src/backend/parser/gram.y | 22 ++ src/backend/replication/logical/worker.c | 16 +- src/bin/pg_dump/pg_dump.c | 39 ++- src/bin/pg_dump/pg_dump.h | 1 + src/bin/psql/describe.c | 6 +- src/bin/psql/tab-complete.in.c | 11 +- src/include/catalog/pg_foreign_data_wrapper.h | 3 + src/include/catalog/pg_subscription.h | 7 +- src/include/foreign/foreign.h | 3 + src/include/nodes/parsenodes.h | 3 + src/test/regress/expected/oidjoins.out | 1 + src/test/regress/expected/subscription.out | 199 ++++++------ src/test/regress/regress.c | 7 + src/test/regress/sql/subscription.sql | 26 ++ 33 files changed, 953 insertions(+), 245 deletions(-) create mode 100644 contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql create mode 100644 contrib/postgres_fdw/t/010_subscription.pl diff --git a/contrib/postgres_fdw/Makefile b/contrib/postgres_fdw/Makefile index 8eaf4d263b6..b8c78b58804 100644 --- a/contrib/postgres_fdw/Makefile +++ b/contrib/postgres_fdw/Makefile @@ -14,7 +14,7 @@ PG_CPPFLAGS = -I$(libpq_srcdir) SHLIB_LINK_INTERNAL = $(libpq) EXTENSION = postgres_fdw -DATA = postgres_fdw--1.0.sql postgres_fdw--1.0--1.1.sql postgres_fdw--1.1--1.2.sql +DATA = postgres_fdw--1.0.sql postgres_fdw--1.0--1.1.sql postgres_fdw--1.1--1.2.sql postgres_fdw--1.2--1.3.sql REGRESS = postgres_fdw query_cancel ISOLATION = eval_plan_qual diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c index add673a4776..a42631d4ca5 100644 --- a/contrib/postgres_fdw/connection.c +++ b/contrib/postgres_fdw/connection.c @@ -132,6 +132,7 @@ PG_FUNCTION_INFO_V1(postgres_fdw_get_connections); PG_FUNCTION_INFO_V1(postgres_fdw_get_connections_1_2); PG_FUNCTION_INFO_V1(postgres_fdw_disconnect); PG_FUNCTION_INFO_V1(postgres_fdw_disconnect_all); +PG_FUNCTION_INFO_V1(postgres_fdw_connection); /* prototypes of private functions */ static void make_new_connection(ConnCacheEntry *entry, UserMapping *user); @@ -477,141 +478,159 @@ pgfdw_security_check(const char **keywords, const char **values, UserMapping *us } /* - * Connect to remote server using specified server and user mapping properties. + * Construct connection params from generic options of ForeignServer and + * UserMapping. (Some of them might not be libpq options, in which case we'll + * just waste a few array slots.) */ -static PGconn * -connect_pg_server(ForeignServer *server, UserMapping *user) +static void +construct_connection_params(ForeignServer *server, UserMapping *user, + const char ***p_keywords, const char ***p_values, + char **p_appname) { - PGconn *volatile conn = NULL; + const char **keywords; + const char **values; + char *appname = NULL; + int n; /* - * Use PG_TRY block to ensure closing connection on error. + * Add 4 extra slots for application_name, fallback_application_name, + * client_encoding, end marker, and 3 extra slots for scram keys and + * required scram pass-through options. */ - PG_TRY(); - { - const char **keywords; - const char **values; - char *appname = NULL; - int n; + n = list_length(server->options) + list_length(user->options) + 4 + 3; + keywords = (const char **) palloc(n * sizeof(char *)); + values = (const char **) palloc(n * sizeof(char *)); - /* - * Construct connection params from generic options of ForeignServer - * and UserMapping. (Some of them might not be libpq options, in - * which case we'll just waste a few array slots.) Add 4 extra slots - * for application_name, fallback_application_name, client_encoding, - * end marker, and 3 extra slots for scram keys and required scram - * pass-through options. - */ - n = list_length(server->options) + list_length(user->options) + 4 + 3; - keywords = (const char **) palloc(n * sizeof(char *)); - values = (const char **) palloc(n * sizeof(char *)); + n = 0; + n += ExtractConnectionOptions(server->options, + keywords + n, values + n); + n += ExtractConnectionOptions(user->options, + keywords + n, values + n); - n = 0; - n += ExtractConnectionOptions(server->options, - keywords + n, values + n); - n += ExtractConnectionOptions(user->options, - keywords + n, values + n); - - /* - * Use pgfdw_application_name as application_name if set. - * - * PQconnectdbParams() processes the parameter arrays from start to - * end. If any key word is repeated, the last value is used. Therefore - * note that pgfdw_application_name must be added to the arrays after - * options of ForeignServer are, so that it can override - * application_name set in ForeignServer. - */ - if (pgfdw_application_name && *pgfdw_application_name != '\0') - { - keywords[n] = "application_name"; - values[n] = pgfdw_application_name; - n++; - } + /* + * Use pgfdw_application_name as application_name if set. + * + * PQconnectdbParams() processes the parameter arrays from start to end. + * If any key word is repeated, the last value is used. Therefore note + * that pgfdw_application_name must be added to the arrays after options + * of ForeignServer are, so that it can override application_name set in + * ForeignServer. + */ + if (pgfdw_application_name && *pgfdw_application_name != '\0') + { + keywords[n] = "application_name"; + values[n] = pgfdw_application_name; + n++; + } - /* - * Search the parameter arrays to find application_name setting, and - * replace escape sequences in it with status information if found. - * The arrays are searched backwards because the last value is used if - * application_name is repeatedly set. - */ - for (int i = n - 1; i >= 0; i--) + /* + * Search the parameter arrays to find application_name setting, and + * replace escape sequences in it with status information if found. The + * arrays are searched backwards because the last value is used if + * application_name is repeatedly set. + */ + for (int i = n - 1; i >= 0; i--) + { + if (strcmp(keywords[i], "application_name") == 0 && + *(values[i]) != '\0') { - if (strcmp(keywords[i], "application_name") == 0 && - *(values[i]) != '\0') + /* + * Use this application_name setting if it's not empty string even + * after any escape sequences in it are replaced. + */ + appname = process_pgfdw_appname(values[i]); + if (appname[0] != '\0') { - /* - * Use this application_name setting if it's not empty string - * even after any escape sequences in it are replaced. - */ - appname = process_pgfdw_appname(values[i]); - if (appname[0] != '\0') - { - values[i] = appname; - break; - } - - /* - * This empty application_name is not used, so we set - * values[i] to NULL and keep searching the array to find the - * next one. - */ - values[i] = NULL; - pfree(appname); - appname = NULL; + values[i] = appname; + break; } + + /* + * This empty application_name is not used, so we set values[i] to + * NULL and keep searching the array to find the next one. + */ + values[i] = NULL; + pfree(appname); + appname = NULL; } + } + + *p_appname = appname; - /* Use "postgres_fdw" as fallback_application_name */ - keywords[n] = "fallback_application_name"; - values[n] = "postgres_fdw"; + /* Use "postgres_fdw" as fallback_application_name */ + keywords[n] = "fallback_application_name"; + values[n] = "postgres_fdw"; + n++; + + /* Set client_encoding so that libpq can convert encoding properly. */ + keywords[n] = "client_encoding"; + values[n] = GetDatabaseEncodingName(); + n++; + + /* Add required SCRAM pass-through connection options if it's enabled. */ + if (MyProcPort != NULL && MyProcPort->has_scram_keys && UseScramPassthrough(server, user)) + { + int len; + int encoded_len; + + keywords[n] = "scram_client_key"; + len = pg_b64_enc_len(sizeof(MyProcPort->scram_ClientKey)); + /* don't forget the zero-terminator */ + values[n] = palloc0(len + 1); + encoded_len = pg_b64_encode(MyProcPort->scram_ClientKey, + sizeof(MyProcPort->scram_ClientKey), + (char *) values[n], len); + if (encoded_len < 0) + elog(ERROR, "could not encode SCRAM client key"); n++; - /* Set client_encoding so that libpq can convert encoding properly. */ - keywords[n] = "client_encoding"; - values[n] = GetDatabaseEncodingName(); + keywords[n] = "scram_server_key"; + len = pg_b64_enc_len(sizeof(MyProcPort->scram_ServerKey)); + /* don't forget the zero-terminator */ + values[n] = palloc0(len + 1); + encoded_len = pg_b64_encode(MyProcPort->scram_ServerKey, + sizeof(MyProcPort->scram_ServerKey), + (char *) values[n], len); + if (encoded_len < 0) + elog(ERROR, "could not encode SCRAM server key"); n++; - /* Add required SCRAM pass-through connection options if it's enabled. */ - if (MyProcPort != NULL && MyProcPort->has_scram_keys && UseScramPassthrough(server, user)) - { - int len; - int encoded_len; - - keywords[n] = "scram_client_key"; - len = pg_b64_enc_len(sizeof(MyProcPort->scram_ClientKey)); - /* don't forget the zero-terminator */ - values[n] = palloc0(len + 1); - encoded_len = pg_b64_encode(MyProcPort->scram_ClientKey, - sizeof(MyProcPort->scram_ClientKey), - (char *) values[n], len); - if (encoded_len < 0) - elog(ERROR, "could not encode SCRAM client key"); - n++; - - keywords[n] = "scram_server_key"; - len = pg_b64_enc_len(sizeof(MyProcPort->scram_ServerKey)); - /* don't forget the zero-terminator */ - values[n] = palloc0(len + 1); - encoded_len = pg_b64_encode(MyProcPort->scram_ServerKey, - sizeof(MyProcPort->scram_ServerKey), - (char *) values[n], len); - if (encoded_len < 0) - elog(ERROR, "could not encode SCRAM server key"); - n++; + /* + * Require scram-sha-256 to ensure that no other auth method is used + * when connecting with foreign server. + */ + keywords[n] = "require_auth"; + values[n] = "scram-sha-256"; + n++; + } - /* - * Require scram-sha-256 to ensure that no other auth method is - * used when connecting with foreign server. - */ - keywords[n] = "require_auth"; - values[n] = "scram-sha-256"; - n++; - } + keywords[n] = values[n] = NULL; + + /* Verify the set of connection parameters. */ + check_conn_params(keywords, values, user); - keywords[n] = values[n] = NULL; + *p_keywords = keywords; + *p_values = values; +} + +/* + * Connect to remote server using specified server and user mapping properties. + */ +static PGconn * +connect_pg_server(ForeignServer *server, UserMapping *user) +{ + PGconn *volatile conn = NULL; + + /* + * Use PG_TRY block to ensure closing connection on error. + */ + PG_TRY(); + { + const char **keywords; + const char **values; + char *appname; - /* Verify the set of connection parameters. */ - check_conn_params(keywords, values, user); + construct_connection_params(server, user, &keywords, &values, &appname); /* first time, allocate or get the custom wait event */ if (pgfdw_we_connect == 0) @@ -2309,6 +2328,56 @@ postgres_fdw_get_connections_internal(FunctionCallInfo fcinfo, } } +/* + * Values in connection strings must be enclosed in single quotes. Single + * quotes and backslashes must be escaped with backslash. NB: these rules are + * different from the rules for escaping a SQL literal. + */ +static void +appendEscapedValue(StringInfo str, const char *val) +{ + appendStringInfoChar(str, '\''); + for (int i = 0; val[i] != '\0'; i++) + { + if (val[i] == '\\' || val[i] == '\'') + appendStringInfoChar(str, '\\'); + appendStringInfoChar(str, val[i]); + } + appendStringInfoChar(str, '\''); +} + +Datum +postgres_fdw_connection(PG_FUNCTION_ARGS) +{ + Oid userid = PG_GETARG_OID(0); + Oid serverid = PG_GETARG_OID(1); + ForeignServer *server = GetForeignServer(serverid); + UserMapping *user = GetUserMapping(userid, serverid); + StringInfoData str; + const char **keywords; + const char **values; + char *appname; + char *sep = ""; + + construct_connection_params(server, user, &keywords, &values, &appname); + + initStringInfo(&str); + for (int i = 0; keywords[i] != NULL; i++) + { + if (values[i] == NULL) + continue; + appendStringInfo(&str, "%s%s = ", sep, keywords[i]); + appendEscapedValue(&str, values[i]); + sep = " "; + } + + if (appname != NULL) + pfree(appname); + pfree(keywords); + pfree(values); + PG_RETURN_TEXT_P(cstring_to_text(str.data)); +} + /* * List active foreign server connections. * diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 2ccb72c539a..0f5271d476e 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -255,6 +255,14 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again ANALYZE ft1; ALTER FOREIGN TABLE ft2 OPTIONS (use_remote_estimate 'true'); -- =================================================================== +-- test subscription +-- =================================================================== +CREATE SUBSCRIPTION regress_pgfdw_subscription SERVER testserver1 + PUBLICATION pub1 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. +DROP SUBSCRIPTION regress_pgfdw_subscription; +-- =================================================================== -- test error case for create publication on foreign table -- =================================================================== CREATE PUBLICATION testpub_ftbl FOR TABLE ft1; -- should fail diff --git a/contrib/postgres_fdw/meson.build b/contrib/postgres_fdw/meson.build index ea4cd9fcd46..3e2ed06b766 100644 --- a/contrib/postgres_fdw/meson.build +++ b/contrib/postgres_fdw/meson.build @@ -27,6 +27,7 @@ install_data( 'postgres_fdw--1.0.sql', 'postgres_fdw--1.0--1.1.sql', 'postgres_fdw--1.1--1.2.sql', + 'postgres_fdw--1.2--1.3.sql', kwargs: contrib_data_args, ) @@ -50,6 +51,7 @@ tests += { 'tap': { 'tests': [ 't/001_auth_scram.pl', + 't/010_subscription.pl', ], }, } diff --git a/contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql b/contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql new file mode 100644 index 00000000000..5bcf0ba2e09 --- /dev/null +++ b/contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql @@ -0,0 +1,12 @@ +/* contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION postgres_fdw UPDATE TO '1.3'" to load this file. \quit + +-- takes internal parameter to prevent calling from SQL +CREATE FUNCTION postgres_fdw_connection(oid, oid, internal) +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT PARALLEL RESTRICTED; + +ALTER FOREIGN DATA WRAPPER postgres_fdw CONNECTION postgres_fdw_connection; diff --git a/contrib/postgres_fdw/postgres_fdw.control b/contrib/postgres_fdw/postgres_fdw.control index a4b800be4fc..ae2963d480d 100644 --- a/contrib/postgres_fdw/postgres_fdw.control +++ b/contrib/postgres_fdw/postgres_fdw.control @@ -1,5 +1,5 @@ # postgres_fdw extension comment = 'foreign-data wrapper for remote PostgreSQL servers' -default_version = '1.2' +default_version = '1.3' module_pathname = '$libdir/postgres_fdw' relocatable = true diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 72d2d9c311b..49ed797e8ef 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -244,6 +244,13 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again ANALYZE ft1; ALTER FOREIGN TABLE ft2 OPTIONS (use_remote_estimate 'true'); +-- =================================================================== +-- test subscription +-- =================================================================== +CREATE SUBSCRIPTION regress_pgfdw_subscription SERVER testserver1 + PUBLICATION pub1 WITH (slot_name = NONE, connect = false); +DROP SUBSCRIPTION regress_pgfdw_subscription; + -- =================================================================== -- test error case for create publication on foreign table -- =================================================================== diff --git a/contrib/postgres_fdw/t/010_subscription.pl b/contrib/postgres_fdw/t/010_subscription.pl new file mode 100644 index 00000000000..a39e8fdbba4 --- /dev/null +++ b/contrib/postgres_fdw/t/010_subscription.pl @@ -0,0 +1,71 @@ + +# Copyright (c) 2021-2024, PostgreSQL Global Development Group + +# Basic logical replication test +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Initialize publisher node +my $node_publisher = PostgreSQL::Test::Cluster->new('publisher'); +$node_publisher->init(allows_streaming => 'logical'); +$node_publisher->start; + +# Create subscriber node +my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber'); +$node_subscriber->init; +$node_subscriber->start; + +# Create some preexisting content on publisher +$node_publisher->safe_psql('postgres', + "CREATE TABLE tab_ins AS SELECT a, a + 1 as b FROM generate_series(1,1002) AS a"); + +# Replicate the changes without columns +$node_publisher->safe_psql('postgres', "CREATE TABLE tab_no_col()"); +$node_publisher->safe_psql('postgres', + "INSERT INTO tab_no_col default VALUES"); + +# Setup structure on subscriber +$node_subscriber->safe_psql('postgres', "CREATE EXTENSION postgres_fdw"); +$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_ins (a int, b int)"); + +# Setup logical replication +my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres'; +$node_publisher->safe_psql('postgres', "CREATE PUBLICATION tap_pub FOR TABLE tab_ins"); + +my $publisher_host = $node_publisher->host; +my $publisher_port = $node_publisher->port; +$node_subscriber->safe_psql('postgres', + "CREATE SERVER tap_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '$publisher_host', port '$publisher_port', dbname 'postgres')" +); + +$node_subscriber->safe_psql('postgres', + "CREATE USER MAPPING FOR PUBLIC SERVER tap_server" +); + +$node_subscriber->safe_psql('postgres', + "CREATE FOREIGN TABLE f_tab_ins (a int, b int) SERVER tap_server OPTIONS(table_name 'tab_ins')" +); +$node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION tap_sub SERVER tap_server PUBLICATION tap_pub WITH (password_required=false)" +); + +# Wait for initial table sync to finish +$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); + +my $result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM (SELECT f.b = l.b as match FROM tab_ins l, f_tab_ins f WHERE l.a = f.a) WHERE match"); +is($result, qq(1002), 'check initial data was copied to subscriber'); + +$node_publisher->safe_psql('postgres', + "INSERT INTO tab_ins SELECT a, a + 1 FROM generate_series(1003,1050) a"); + +$node_publisher->wait_for_catchup('tap_sub'); + +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM (SELECT f.b = l.b as match FROM tab_ins l, f_tab_ins f WHERE l.a = f.a) WHERE match"); +is($result, qq(1050), 'check initial data was copied to subscriber'); + +done_testing(); diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index 5028fe9af09..b4e6d5ae3f9 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -2573,7 +2573,9 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER <para> To create a subscription, the user must have the privileges of the <literal>pg_create_subscription</literal> role, as well as - <literal>CREATE</literal> privileges on the database. + <literal>CREATE</literal> privileges on the database. If + <literal>SERVER</literal> is specified, the user also must have + <literal>USAGE</literal> privileges on the server. </para> <para> diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index fcf10e4317e..de69ddcdebc 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -1049,6 +1049,32 @@ postgres=# SELECT postgres_fdw_disconnect_all(); </para> </sect2> + <sect2 id="postgres-fdw-server-subscription"> + <title>Subscription Management</title> + + <para> + <filename>postgres_fdw</filename> supports subscription connections using + the same options described in <xref + linkend="postgres-fdw-options-connection"/>. + </para> + + <para> + For example, assuming the remote server <literal>foreign-host</literal> has + a publication <literal>testpub</literal>: +<programlisting> +CREATE SERVER subscription_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'foreign-host', dbname 'foreign_db'); +CREATE USER MAPPING FOR local_user SERVER subscription_server OPTIONS (user 'foreign_user', password 'password'); +CREATE SUBSCRIPTION my_subscription SERVER subscription_server PUBLICATION testpub; +</programlisting> + </para> + + <para> + To create a subscription, the user must be a member of the <xref + linkend="predefined-role-pg-create-subscription"/> role and have + <literal>USAGE</literal> privileges on the server. + </para> + </sect2> + <sect2 id="postgres-fdw-transaction-management"> <title>Transaction Management</title> diff --git a/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml b/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml index dc0957d965a..640c02893cf 100644 --- a/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml +++ b/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml @@ -24,6 +24,7 @@ PostgreSQL documentation ALTER FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> [ HANDLER <replaceable class="parameter">handler_function</replaceable> | NO HANDLER ] [ VALIDATOR <replaceable class="parameter">validator_function</replaceable> | NO VALIDATOR ] + [ CONNECTION <replaceable class="parameter">connection_function</replaceable> | NO CONNECTION ] [ OPTIONS ( [ ADD | SET | DROP ] <replaceable class="parameter">option</replaceable> ['<replaceable class="parameter">value</replaceable>'] [, ... ]) ] ALTER FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } ALTER FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> RENAME TO <replaceable>new_name</replaceable> @@ -112,6 +113,25 @@ ALTER FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> REN </listitem> </varlistentry> + <varlistentry> + <term><literal>CONNECTION <replaceable class="parameter">connection_function</replaceable></literal></term> + <listitem> + <para> + Specifies a new connection function for the foreign-data wrapper. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>NO CONNECTION</literal></term> + <listitem> + <para> + This is used to specify that the foreign-data wrapper should no + longer have a connection function. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>OPTIONS ( [ ADD | SET | DROP ] <replaceable class="parameter">option</replaceable> ['<replaceable class="parameter">value</replaceable>'] [, ... ] )</literal></term> <listitem> diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml index 5318998e80c..f215fb0e5a2 100644 --- a/doc/src/sgml/ref/alter_subscription.sgml +++ b/doc/src/sgml/ref/alter_subscription.sgml @@ -21,6 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> +ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> SERVER <replaceable>servername</replaceable> ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> CONNECTION '<replaceable>conninfo</replaceable>' ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> SET PUBLICATION <replaceable class="parameter">publication_name</replaceable> [, ...] [ WITH ( <replaceable class="parameter">publication_option</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ] ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> ADD PUBLICATION <replaceable class="parameter">publication_name</replaceable> [, ...] [ WITH ( <replaceable class="parameter">publication_option</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ] @@ -102,13 +103,24 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO < </listitem> </varlistentry> + <varlistentry id="sql-altersubscription-params-server"> + <term><literal>SERVER <replaceable class="parameter">servername</replaceable></literal></term> + <listitem> + <para> + This clause replaces the foreign server or connection string originally + set by <xref linkend="sql-createsubscription"/> with the foreign server + <replaceable>servername</replaceable>. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-altersubscription-params-connection"> <term><literal>CONNECTION '<replaceable class="parameter">conninfo</replaceable>'</literal></term> <listitem> <para> - This clause replaces the connection string originally set by - <xref linkend="sql-createsubscription"/>. See there for more - information. + This clause replaces the foreign server or connection string originally + set by <xref linkend="sql-createsubscription"/> with the connection + string <replaceable>conninfo</replaceable>. </para> </listitem> </varlistentry> diff --git a/doc/src/sgml/ref/create_foreign_data_wrapper.sgml b/doc/src/sgml/ref/create_foreign_data_wrapper.sgml index 0fcba18a347..7b83f500b25 100644 --- a/doc/src/sgml/ref/create_foreign_data_wrapper.sgml +++ b/doc/src/sgml/ref/create_foreign_data_wrapper.sgml @@ -24,6 +24,7 @@ PostgreSQL documentation CREATE FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> [ HANDLER <replaceable class="parameter">handler_function</replaceable> | NO HANDLER ] [ VALIDATOR <replaceable class="parameter">validator_function</replaceable> | NO VALIDATOR ] + [ CONNECTION <replaceable class="parameter">connection_function</replaceable> | NO CONNECTION ] [ OPTIONS ( <replaceable class="parameter">option</replaceable> '<replaceable class="parameter">value</replaceable>' [, ... ] ) ] </synopsis> </refsynopsisdiv> @@ -99,6 +100,25 @@ CREATE FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> </listitem> </varlistentry> + <varlistentry> + <term><literal>CONNECTION <replaceable class="parameter">connection_function</replaceable></literal></term> + <listitem> + <para> + <replaceable class="parameter">connection_function</replaceable> is the + name of a previously registered function that will be called to generate + the postgres connection string when a foreign server is used as part of + <xref linkend="sql-createsubscription"/>. If no connection function or + <literal>NO CONNECTION</literal> is specified, then servers using this + foreign data wrapper cannot be used for <literal>CREATE + SUBSCRIPTION</literal>. The connection function must take three + arguments: one of type <type>oid</type> for the user, one of type + <type>oid</type> for the server, and an unused third argument of type + <type>internal</type> (which prevents calling the function in other + contexts). + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>OPTIONS ( <replaceable class="parameter">option</replaceable> '<replaceable class="parameter">value</replaceable>' [, ... ] )</literal></term> <listitem> diff --git a/doc/src/sgml/ref/create_server.sgml b/doc/src/sgml/ref/create_server.sgml index 05f4019453b..ce4a064eabb 100644 --- a/doc/src/sgml/ref/create_server.sgml +++ b/doc/src/sgml/ref/create_server.sgml @@ -42,6 +42,13 @@ CREATE SERVER [ IF NOT EXISTS ] <replaceable class="parameter">server_name</repl means of user mappings. </para> + <para> + If the foreign data wrapper <replaceable>fdw_name</replaceable> is + specified with a <literal>CONNECTION</literal> clause, then <xref + linkend="sql-createsubscription"/> may use this foreign server for + connection information. + </para> + <para> The server name must be unique within the database. </para> diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml index eb0cc645d8f..2ca7e0e6826 100644 --- a/doc/src/sgml/ref/create_subscription.sgml +++ b/doc/src/sgml/ref/create_subscription.sgml @@ -22,7 +22,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceable> - CONNECTION '<replaceable class="parameter">conninfo</replaceable>' + { SERVER <replaceable class="parameter">servername</replaceable> | CONNECTION '<replaceable class="parameter">conninfo</replaceable>' } PUBLICATION <replaceable class="parameter">publication_name</replaceable> [, ...] [ WITH ( <replaceable class="parameter">subscription_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ] </synopsis> @@ -77,6 +77,15 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl </listitem> </varlistentry> + <varlistentry id="sql-createsubscription-params-server"> + <term><literal>SERVER <replaceable class="parameter">servername</replaceable></literal></term> + <listitem> + <para> + A foreign server to use for the connection. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-createsubscription-params-connection"> <term><literal>CONNECTION '<replaceable class="parameter">conninfo</replaceable>'</literal></term> <listitem> diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c index acf42b853ed..3673d4f0bc1 100644 --- a/src/backend/catalog/pg_subscription.c +++ b/src/backend/catalog/pg_subscription.c @@ -19,11 +19,14 @@ #include "access/htup_details.h" #include "access/tableam.h" #include "catalog/indexing.h" +#include "catalog/pg_foreign_server.h" #include "catalog/pg_subscription.h" #include "catalog/pg_subscription_rel.h" #include "catalog/pg_type.h" +#include "foreign/foreign.h" #include "miscadmin.h" #include "storage/lmgr.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -69,7 +72,7 @@ GetPublicationsStr(List *publications, StringInfo dest, bool quote_literal) * Fetch the subscription from the syscache. */ Subscription * -GetSubscription(Oid subid, bool missing_ok) +GetSubscription(Oid subid, bool missing_ok, bool aclcheck) { HeapTuple tup; Subscription *sub; @@ -108,10 +111,35 @@ GetSubscription(Oid subid, bool missing_ok) sub->retentionactive = subform->subretentionactive; /* Get conninfo */ - datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, - tup, - Anum_pg_subscription_subconninfo); - sub->conninfo = TextDatumGetCString(datum); + if (OidIsValid(subform->subserver)) + { + AclResult aclresult; + + /* recheck ACL if requested */ + if (aclcheck) + { + aclresult = object_aclcheck(ForeignServerRelationId, + subform->subserver, + subform->subowner, ACL_USAGE); + + if (aclresult != ACLCHECK_OK) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", + GetUserNameFromId(subform->subowner, false), + ForeignServerName(subform->subserver)))); + } + + sub->conninfo = ForeignServerConnectionString(subform->subowner, + subform->subserver); + } + else + { + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, + tup, + Anum_pg_subscription_subconninfo); + sub->conninfo = TextDatumGetCString(datum); + } /* Get slotname */ datum = SysCacheGetAttr(SUBSCRIPTIONOID, diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c index b56d1ad6785..45681235782 100644 --- a/src/backend/commands/foreigncmds.c +++ b/src/backend/commands/foreigncmds.c @@ -522,21 +522,53 @@ lookup_fdw_validator_func(DefElem *validator) /* validator's return value is ignored, so we don't check the type */ } +/* + * Convert a connection string function name passed from the parser to an Oid. + */ +static Oid +lookup_fdw_connection_func(DefElem *connection) +{ + Oid connectionOid; + Oid funcargtypes[3]; + + if (connection == NULL || connection->arg == NULL) + return InvalidOid; + + /* connection string functions take user oid, server oid */ + funcargtypes[0] = OIDOID; + funcargtypes[1] = OIDOID; + funcargtypes[2] = INTERNALOID; + + connectionOid = LookupFuncName((List *) connection->arg, 3, funcargtypes, false); + + /* check that connection string function has correct return type */ + if (get_func_rettype(connectionOid) != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("function %s must return type %s", + NameListToString((List *) connection->arg), "text"))); + + return connectionOid; +} + /* * Process function options of CREATE/ALTER FDW */ static void parse_func_options(ParseState *pstate, List *func_options, bool *handler_given, Oid *fdwhandler, - bool *validator_given, Oid *fdwvalidator) + bool *validator_given, Oid *fdwvalidator, + bool *connection_given, Oid *fdwconnection) { ListCell *cell; *handler_given = false; *validator_given = false; + *connection_given = false; /* return InvalidOid if not given */ *fdwhandler = InvalidOid; *fdwvalidator = InvalidOid; + *fdwconnection = InvalidOid; foreach(cell, func_options) { @@ -556,6 +588,13 @@ parse_func_options(ParseState *pstate, List *func_options, *validator_given = true; *fdwvalidator = lookup_fdw_validator_func(def); } + else if (strcmp(def->defname, "connection") == 0) + { + if (*connection_given) + errorConflictingDefElem(def, pstate); + *connection_given = true; + *fdwconnection = lookup_fdw_connection_func(def); + } else elog(ERROR, "option \"%s\" not recognized", def->defname); @@ -575,8 +614,10 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) Oid fdwId; bool handler_given; bool validator_given; + bool connection_given; Oid fdwhandler; Oid fdwvalidator; + Oid fdwconnection; Datum fdwoptions; Oid ownerId; ObjectAddress myself; @@ -620,10 +661,12 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) /* Lookup handler and validator functions, if given */ parse_func_options(pstate, stmt->func_options, &handler_given, &fdwhandler, - &validator_given, &fdwvalidator); + &validator_given, &fdwvalidator, + &connection_given, &fdwconnection); values[Anum_pg_foreign_data_wrapper_fdwhandler - 1] = ObjectIdGetDatum(fdwhandler); values[Anum_pg_foreign_data_wrapper_fdwvalidator - 1] = ObjectIdGetDatum(fdwvalidator); + values[Anum_pg_foreign_data_wrapper_fdwconnection - 1] = ObjectIdGetDatum(fdwconnection); nulls[Anum_pg_foreign_data_wrapper_fdwacl - 1] = true; @@ -695,8 +738,10 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) Datum datum; bool handler_given; bool validator_given; + bool connection_given; Oid fdwhandler; Oid fdwvalidator; + Oid fdwconnection; ObjectAddress myself; rel = table_open(ForeignDataWrapperRelationId, RowExclusiveLock); @@ -726,7 +771,8 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) parse_func_options(pstate, stmt->func_options, &handler_given, &fdwhandler, - &validator_given, &fdwvalidator); + &validator_given, &fdwvalidator, + &connection_given, &fdwconnection); if (handler_given) { @@ -764,6 +810,12 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) fdwvalidator = fdwForm->fdwvalidator; } + if (connection_given) + { + repl_val[Anum_pg_foreign_data_wrapper_fdwconnection - 1] = ObjectIdGetDatum(fdwconnection); + repl_repl[Anum_pg_foreign_data_wrapper_fdwconnection - 1] = true; + } + /* * If options specified, validate and update. */ diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index 5e3c0964d38..091e7b7372d 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -27,13 +27,16 @@ #include "catalog/objectaddress.h" #include "catalog/pg_authid_d.h" #include "catalog/pg_database_d.h" +#include "catalog/pg_foreign_server.h" #include "catalog/pg_subscription.h" #include "catalog/pg_subscription_rel.h" #include "catalog/pg_type.h" +#include "catalog/pg_user_mapping.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/subscriptioncmds.h" #include "executor/executor.h" +#include "foreign/foreign.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "pgstat.h" @@ -619,6 +622,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, Datum values[Natts_pg_subscription]; Oid owner = GetUserId(); HeapTuple tup; + Oid serverid; char *conninfo; char originname[NAMEDATALEN]; List *publications; @@ -730,15 +734,40 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, if (opts.wal_receiver_timeout == NULL) opts.wal_receiver_timeout = "-1"; - conninfo = stmt->conninfo; - publications = stmt->publication; - /* Load the library providing us libpq calls. */ load_file("libpqwalreceiver", false); + if (stmt->servername) + { + ForeignServer *server; + + Assert(!stmt->conninfo); + conninfo = NULL; + + server = GetForeignServerByName(stmt->servername, false); + aclresult = object_aclcheck(ForeignServerRelationId, server->serverid, owner, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_FOREIGN_SERVER, server->servername); + + /* make sure a user mapping exists */ + GetUserMapping(owner, server->serverid); + + serverid = server->serverid; + conninfo = ForeignServerConnectionString(owner, serverid); + } + else + { + Assert(stmt->conninfo); + + serverid = InvalidOid; + conninfo = stmt->conninfo; + } + /* Check the connection info string. */ walrcv_check_conninfo(conninfo, opts.passwordrequired && !superuser()); + publications = stmt->publication; + /* Everything ok, form a new tuple. */ memset(values, 0, sizeof(values)); memset(nulls, false, sizeof(nulls)); @@ -768,8 +797,12 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, Int32GetDatum(opts.maxretention); values[Anum_pg_subscription_subretentionactive - 1] = Int32GetDatum(opts.retaindeadtuples); - values[Anum_pg_subscription_subconninfo - 1] = - CStringGetTextDatum(conninfo); + values[Anum_pg_subscription_subserver - 1] = serverid; + if (!OidIsValid(serverid)) + values[Anum_pg_subscription_subconninfo - 1] = + CStringGetTextDatum(conninfo); + else + nulls[Anum_pg_subscription_subconninfo - 1] = true; if (opts.slot_name) values[Anum_pg_subscription_subslotname - 1] = DirectFunctionCall1(namein, CStringGetDatum(opts.slot_name)); @@ -792,6 +825,18 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, recordDependencyOnOwner(SubscriptionRelationId, subid, owner); + ObjectAddressSet(myself, SubscriptionRelationId, subid); + + if (stmt->servername) + { + ObjectAddress referenced; + + Assert(OidIsValid(serverid)); + + ObjectAddressSet(referenced, ForeignServerRelationId, serverid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + } + /* * A replication origin is currently created for all subscriptions, * including those that only contain sequences or are otherwise empty. @@ -945,8 +990,6 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, if (opts.enabled || opts.retaindeadtuples) ApplyLauncherWakeupAtCommit(); - ObjectAddressSet(myself, SubscriptionRelationId, subid); - InvokeObjectPostCreateHook(SubscriptionRelationId, subid, 0); return myself; @@ -1410,7 +1453,14 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION, stmt->subname); - sub = GetSubscription(subid, false); + /* + * Skip ACL checks on the subscription's foreign server, if any. If + * changing the server (or replacing it with a raw connection), then the + * old one will be removed anyway. If changing something unrelated, + * there's no need to do an additional ACL check here; that will be done + * by the subscription worker anyway. + */ + sub = GetSubscription(subid, false, false); retain_dead_tuples = sub->retaindeadtuples; origin = sub->origin; @@ -1435,6 +1485,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, memset(nulls, false, sizeof(nulls)); memset(replaces, false, sizeof(replaces)); + ObjectAddressSet(myself, SubscriptionRelationId, subid); + switch (stmt->kind) { case ALTER_SUBSCRIPTION_OPTIONS: @@ -1753,7 +1805,79 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, break; } + case ALTER_SUBSCRIPTION_SERVER: + { + ForeignServer *new_server; + ObjectAddress referenced; + AclResult aclresult; + char *conninfo; + + /* + * Remove what was there before, either another foreign server + * or a connection string. + */ + if (form->subserver) + { + deleteDependencyRecordsForSpecific(SubscriptionRelationId, form->oid, + DEPENDENCY_NORMAL, + ForeignServerRelationId, form->subserver); + } + else + { + nulls[Anum_pg_subscription_subconninfo - 1] = true; + replaces[Anum_pg_subscription_subconninfo - 1] = true; + } + + /* + * Find the new server and user mapping. Check ACL of server + * based on current user ID, but find the user mapping based + * on the subscription owner. + */ + new_server = GetForeignServerByName(stmt->servername, false); + aclresult = object_aclcheck(ForeignServerRelationId, + new_server->serverid, + form->subowner, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", + GetUserNameFromId(form->subowner, false), + ForeignServerName(new_server->serverid)))); + + /* make sure a user mapping exists */ + GetUserMapping(form->subowner, new_server->serverid); + + conninfo = ForeignServerConnectionString(form->subowner, + new_server->serverid); + + /* Load the library providing us libpq calls. */ + load_file("libpqwalreceiver", false); + /* Check the connection info string. */ + walrcv_check_conninfo(conninfo, + sub->passwordrequired && !sub->ownersuperuser); + + values[Anum_pg_subscription_subserver - 1] = new_server->serverid; + replaces[Anum_pg_subscription_subserver - 1] = true; + + ObjectAddressSet(referenced, ForeignServerRelationId, new_server->serverid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + + update_tuple = true; + } + break; + case ALTER_SUBSCRIPTION_CONNECTION: + /* remove reference to foreign server and dependencies, if present */ + if (form->subserver) + { + deleteDependencyRecordsForSpecific(SubscriptionRelationId, form->oid, + DEPENDENCY_NORMAL, + ForeignServerRelationId, form->subserver); + + values[Anum_pg_subscription_subserver - 1] = InvalidOid; + replaces[Anum_pg_subscription_subserver - 1] = true; + } + /* Load the library providing us libpq calls. */ load_file("libpqwalreceiver", false); /* Check the connection info string. */ @@ -2038,8 +2162,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, table_close(rel, RowExclusiveLock); - ObjectAddressSet(myself, SubscriptionRelationId, subid); - InvokeObjectPostAlterHook(SubscriptionRelationId, subid, 0); /* Wake up related replication workers to handle this change quickly. */ @@ -2126,9 +2248,28 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) subname = pstrdup(NameStr(*DatumGetName(datum))); /* Get conninfo */ - datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, - Anum_pg_subscription_subconninfo); - conninfo = TextDatumGetCString(datum); + if (OidIsValid(form->subserver)) + { + AclResult aclresult; + + aclresult = object_aclcheck(ForeignServerRelationId, form->subserver, + form->subowner, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", + GetUserNameFromId(form->subowner, false), + ForeignServerName(form->subserver)))); + + conninfo = ForeignServerConnectionString(form->subowner, + form->subserver); + } + else + { + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subconninfo); + conninfo = TextDatumGetCString(datum); + } /* Get slotname */ datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup, @@ -2227,6 +2368,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) } /* Clean up dependencies */ + deleteDependencyRecordsFor(SubscriptionRelationId, subid, false); deleteSharedDependencyRecordsFor(SubscriptionRelationId, subid, 0); /* Remove any associated relation synchronization states. */ diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c index b912a06dd15..4e18bade409 100644 --- a/src/backend/foreign/foreign.c +++ b/src/backend/foreign/foreign.c @@ -72,6 +72,7 @@ GetForeignDataWrapperExtended(Oid fdwid, bits16 flags) fdw->fdwname = pstrdup(NameStr(fdwform->fdwname)); fdw->fdwhandler = fdwform->fdwhandler; fdw->fdwvalidator = fdwform->fdwvalidator; + fdw->fdwconnection = fdwform->fdwconnection; /* Extract the fdwoptions */ datum = SysCacheGetAttr(FOREIGNDATAWRAPPEROID, @@ -176,6 +177,31 @@ GetForeignServerExtended(Oid serverid, bits16 flags) } +/* + * ForeignServerName - get name of foreign server. + */ +char * +ForeignServerName(Oid serverid) +{ + Form_pg_foreign_server serverform; + char *servername; + HeapTuple tp; + + tp = SearchSysCache1(FOREIGNSERVEROID, ObjectIdGetDatum(serverid)); + + if (!HeapTupleIsValid(tp)) + elog(ERROR, "cache lookup failed for foreign server %u", serverid); + + serverform = (Form_pg_foreign_server) GETSTRUCT(tp); + + servername = pstrdup(NameStr(serverform->srvname)); + + ReleaseSysCache(tp); + + return servername; +} + + /* * GetForeignServerByName - look up the foreign server definition by name. */ @@ -191,6 +217,61 @@ GetForeignServerByName(const char *srvname, bool missing_ok) } +/* + * Retrieve connection string from server's FDW. + */ +char * +ForeignServerConnectionString(Oid userid, Oid serverid) +{ + static MemoryContext tempContext = NULL; + MemoryContext oldcxt; + ForeignServer *server; + ForeignDataWrapper *fdw; + Datum connection_datum; + text *connection_text; + char *result; + + /* + * GetForeignServer, GetForeignDataWrapper, and the connection function + * itself all leak memory into CurrentMemoryContext. Switch to a temporary + * context for easy cleanup. + */ + if (tempContext == NULL) + { + tempContext = AllocSetContextCreate(TopMemoryContext, + "FDWConnectionContext", + ALLOCSET_DEFAULT_SIZES); + } + + oldcxt = MemoryContextSwitchTo(tempContext); + + server = GetForeignServer(serverid); + fdw = GetForeignDataWrapper(server->fdwid); + + if (!OidIsValid(fdw->fdwconnection)) + ereport(ERROR, + (errmsg("foreign data wrapper \"%s\" does not support subscription connections", + fdw->fdwname), + errdetail("Foreign data wrapper must be defined with CONNECTION specified."))); + + + connection_datum = OidFunctionCall3(fdw->fdwconnection, + ObjectIdGetDatum(userid), + ObjectIdGetDatum(serverid), + PointerGetDatum(NULL)); + + connection_text = DatumGetTextPP(connection_datum); + + MemoryContextSwitchTo(oldcxt); + + result = text_to_cstring(connection_text); + + MemoryContextReset(tempContext); + + return result; +} + + /* * GetUserMapping - look up the user mapping. * diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index c567252acc4..014a3ec3783 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -5580,6 +5580,8 @@ fdw_option: | NO HANDLER { $$ = makeDefElem("handler", NULL, @1); } | VALIDATOR handler_name { $$ = makeDefElem("validator", (Node *) $2, @1); } | NO VALIDATOR { $$ = makeDefElem("validator", NULL, @1); } + | CONNECTION handler_name { $$ = makeDefElem("connection", (Node *) $2, @1); } + | NO CONNECTION { $$ = makeDefElem("connection", NULL, @1); } ; fdw_options: @@ -11030,6 +11032,16 @@ CreateSubscriptionStmt: n->options = $8; $$ = (Node *) n; } + | CREATE SUBSCRIPTION name SERVER name PUBLICATION name_list opt_definition + { + CreateSubscriptionStmt *n = + makeNode(CreateSubscriptionStmt); + n->subname = $3; + n->servername = $5; + n->publication = $7; + n->options = $8; + $$ = (Node *) n; + } ; /***************************************************************************** @@ -11059,6 +11071,16 @@ AlterSubscriptionStmt: n->conninfo = $5; $$ = (Node *) n; } + | ALTER SUBSCRIPTION name SERVER name + { + AlterSubscriptionStmt *n = + makeNode(AlterSubscriptionStmt); + + n->kind = ALTER_SUBSCRIPTION_SERVER; + n->subname = $3; + n->servername = $5; + $$ = (Node *) n; + } | ALTER SUBSCRIPTION name REFRESH PUBLICATION opt_definition { AlterSubscriptionStmt *n = diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index bae8c011390..d5e836fa118 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -5057,7 +5057,7 @@ maybe_reread_subscription(void) /* Ensure allocations in permanent context. */ oldctx = MemoryContextSwitchTo(ApplyContext); - newsub = GetSubscription(MyLogicalRepWorker->subid, true); + newsub = GetSubscription(MyLogicalRepWorker->subid, true, true); /* * Exit if the subscription was removed. This normally should not happen @@ -5199,7 +5199,9 @@ set_wal_receiver_timeout(void) } /* - * Callback from subscription syscache invalidation. + * Callback from subscription syscache invalidation. Also needed for server or + * user mapping invalidation, which can change the connection information for + * subscriptions that connect using a server object. */ static void subscription_change_cb(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) @@ -5804,7 +5806,7 @@ InitializeLogRepWorker(void) */ LockSharedObject(SubscriptionRelationId, MyLogicalRepWorker->subid, 0, AccessShareLock); - MySubscription = GetSubscription(MyLogicalRepWorker->subid, true); + MySubscription = GetSubscription(MyLogicalRepWorker->subid, true, true); if (!MySubscription) { ereport(LOG, @@ -5869,6 +5871,14 @@ InitializeLogRepWorker(void) CacheRegisterSyscacheCallback(SUBSCRIPTIONOID, subscription_change_cb, (Datum) 0); + /* Keep us informed about subscription changes. */ + CacheRegisterSyscacheCallback(FOREIGNSERVEROID, + subscription_change_cb, + (Datum) 0); + /* Keep us informed about subscription changes. */ + CacheRegisterSyscacheCallback(USERMAPPINGOID, + subscription_change_cb, + (Datum) 0); CacheRegisterSyscacheCallback(AUTHOID, subscription_change_cb, diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index dd8adef0a3e..0e94edeec20 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5109,6 +5109,7 @@ getSubscriptions(Archive *fout) int i_subdisableonerr; int i_subpasswordrequired; int i_subrunasowner; + int i_subservername; int i_subconninfo; int i_subslotname; int i_subsynccommit; @@ -5213,14 +5214,24 @@ getSubscriptions(Archive *fout) if (fout->remoteVersion >= 190000) appendPQExpBufferStr(query, - " s.subwalrcvtimeout\n"); + " s.subwalrcvtimeout,\n"); else appendPQExpBufferStr(query, - " '-1' AS subwalrcvtimeout\n"); + " '-1' AS subwalrcvtimeout,\n"); + + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, " fs.srvname AS subservername\n"); + else + appendPQExpBufferStr(query, " NULL AS subservername\n"); appendPQExpBufferStr(query, "FROM pg_subscription s\n"); + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, + "LEFT JOIN pg_catalog.pg_foreign_server fs \n" + " ON fs.oid = s.subserver \n"); + if (dopt->binary_upgrade && fout->remoteVersion >= 170000) appendPQExpBufferStr(query, "LEFT JOIN pg_catalog.pg_replication_origin_status o \n" @@ -5252,6 +5263,7 @@ getSubscriptions(Archive *fout) i_subfailover = PQfnumber(res, "subfailover"); i_subretaindeadtuples = PQfnumber(res, "subretaindeadtuples"); i_submaxretention = PQfnumber(res, "submaxretention"); + i_subservername = PQfnumber(res, "subservername"); i_subconninfo = PQfnumber(res, "subconninfo"); i_subslotname = PQfnumber(res, "subslotname"); i_subsynccommit = PQfnumber(res, "subsynccommit"); @@ -5274,6 +5286,10 @@ getSubscriptions(Archive *fout) subinfo[i].subenabled = (strcmp(PQgetvalue(res, i, i_subenabled), "t") == 0); + if (PQgetisnull(res, i, i_subservername)) + subinfo[i].subservername = NULL; + else + subinfo[i].subservername = pg_strdup(PQgetvalue(res, i, i_subservername)); subinfo[i].subbinary = (strcmp(PQgetvalue(res, i, i_subbinary), "t") == 0); subinfo[i].substream = *(PQgetvalue(res, i, i_substream)); @@ -5290,8 +5306,11 @@ getSubscriptions(Archive *fout) (strcmp(PQgetvalue(res, i, i_subretaindeadtuples), "t") == 0); subinfo[i].submaxretention = atoi(PQgetvalue(res, i, i_submaxretention)); - subinfo[i].subconninfo = - pg_strdup(PQgetvalue(res, i, i_subconninfo)); + if (PQgetisnull(res, i, i_subconninfo)) + subinfo[i].subconninfo = NULL; + else + subinfo[i].subconninfo = + pg_strdup(PQgetvalue(res, i, i_subconninfo)); if (PQgetisnull(res, i, i_subslotname)) subinfo[i].subslotname = NULL; else @@ -5502,9 +5521,17 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo) appendPQExpBuffer(delq, "DROP SUBSCRIPTION %s;\n", qsubname); - appendPQExpBuffer(query, "CREATE SUBSCRIPTION %s CONNECTION ", + appendPQExpBuffer(query, "CREATE SUBSCRIPTION %s ", qsubname); - appendStringLiteralAH(query, subinfo->subconninfo, fout); + if (subinfo->subservername) + { + appendPQExpBuffer(query, "SERVER %s", fmtId(subinfo->subservername)); + } + else + { + appendPQExpBuffer(query, "CONNECTION "); + appendStringLiteralAH(query, subinfo->subconninfo, fout); + } /* Build list of quoted publications and append them to query. */ if (!parsePGArray(subinfo->subpublications, &pubnames, &npubnames)) diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 6deceef23f3..41ed470969c 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -719,6 +719,7 @@ typedef struct _SubscriptionInfo bool subfailover; bool subretaindeadtuples; int submaxretention; + char *subservername; char *subconninfo; char *subslotname; char *subsynccommit; diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 571a6a003d5..d0af22cb78d 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -6806,7 +6806,7 @@ describeSubscriptions(const char *pattern, bool verbose) printQueryOpt myopt = pset.popt; static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false, false, false, false, - false, false, false, false, false}; + false, false, false, false, false, false}; if (pset.sversion < 100000) { @@ -6876,6 +6876,10 @@ describeSubscriptions(const char *pattern, bool verbose) gettext_noop("Failover")); if (pset.sversion >= 190000) { + appendPQExpBuffer(&buf, + ", (select srvname from pg_foreign_server where oid=subserver) AS \"%s\"\n", + gettext_noop("Server")); + appendPQExpBuffer(&buf, ", subretaindeadtuples AS \"%s\"\n", gettext_noop("Retain dead tuples")); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 8b91bc00062..c587e9ba948 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -2332,7 +2332,7 @@ match_previous_words(int pattern_id, else if (Matches("ALTER", "SUBSCRIPTION", MatchAny)) COMPLETE_WITH("CONNECTION", "ENABLE", "DISABLE", "OWNER TO", "RENAME TO", "REFRESH PUBLICATION", "REFRESH SEQUENCES", - "SET", "SKIP (", "ADD PUBLICATION", "DROP PUBLICATION"); + "SERVER", "SET", "SKIP (", "ADD PUBLICATION", "DROP PUBLICATION"); /* ALTER SUBSCRIPTION <name> REFRESH */ else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, MatchAnyN, "REFRESH")) COMPLETE_WITH("PUBLICATION", "SEQUENCES"); @@ -3848,9 +3848,16 @@ match_previous_words(int pattern_id, /* CREATE SUBSCRIPTION */ else if (Matches("CREATE", "SUBSCRIPTION", MatchAny)) - COMPLETE_WITH("CONNECTION"); + COMPLETE_WITH("SERVER", "CONNECTION"); + else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "SERVER", MatchAny)) + COMPLETE_WITH("PUBLICATION"); else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION", MatchAny)) COMPLETE_WITH("PUBLICATION"); + else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "SERVER", + MatchAny, "PUBLICATION")) + { + /* complete with nothing here as this refers to remote publications */ + } else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION", MatchAny, "PUBLICATION")) { diff --git a/src/include/catalog/pg_foreign_data_wrapper.h b/src/include/catalog/pg_foreign_data_wrapper.h index e6009069e82..3d8389de65e 100644 --- a/src/include/catalog/pg_foreign_data_wrapper.h +++ b/src/include/catalog/pg_foreign_data_wrapper.h @@ -38,6 +38,9 @@ CATALOG(pg_foreign_data_wrapper,2328,ForeignDataWrapperRelationId) Oid fdwvalidator BKI_LOOKUP_OPT(pg_proc); /* option validation * function, or 0 if * none */ + Oid fdwconnection BKI_LOOKUP_OPT(pg_proc); /* connection string + * function, or 0 if + * none */ #ifdef CATALOG_VARLEN /* variable-length fields start here */ aclitem fdwacl[1]; /* access permissions */ diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h index c369b5abfb3..bba7a0b68a6 100644 --- a/src/include/catalog/pg_subscription.h +++ b/src/include/catalog/pg_subscription.h @@ -92,9 +92,11 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW * exceeded max_retention_duration, when * defined */ + Oid subserver; /* Set if connecting with server */ + #ifdef CATALOG_VARLEN /* variable-length fields start here */ /* Connection string to the publisher */ - text subconninfo BKI_FORCE_NOT_NULL; + text subconninfo; /* Set if connecting with connection string */ /* Slot name on publisher */ NameData subslotname BKI_FORCE_NULL; @@ -207,7 +209,8 @@ typedef struct Subscription #endif /* EXPOSE_TO_CLIENT_CODE */ -extern Subscription *GetSubscription(Oid subid, bool missing_ok); +extern Subscription *GetSubscription(Oid subid, bool missing_ok, + bool aclcheck); extern void FreeSubscription(Subscription *sub); extern void DisableSubscription(Oid subid); diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h index c185d1458a2..65ed9a7f987 100644 --- a/src/include/foreign/foreign.h +++ b/src/include/foreign/foreign.h @@ -28,6 +28,7 @@ typedef struct ForeignDataWrapper char *fdwname; /* Name of the FDW */ Oid fdwhandler; /* Oid of handler function, or 0 */ Oid fdwvalidator; /* Oid of validator function, or 0 */ + Oid fdwconnection; /* Oid of connection string function, or 0 */ List *options; /* fdwoptions as DefElem list */ } ForeignDataWrapper; @@ -65,10 +66,12 @@ typedef struct ForeignTable extern ForeignServer *GetForeignServer(Oid serverid); +extern char *ForeignServerName(Oid serverid); extern ForeignServer *GetForeignServerExtended(Oid serverid, bits16 flags); extern ForeignServer *GetForeignServerByName(const char *srvname, bool missing_ok); +extern char *ForeignServerConnectionString(Oid userid, Oid serverid); extern UserMapping *GetUserMapping(Oid userid, Oid serverid); extern ForeignDataWrapper *GetForeignDataWrapper(Oid fdwid); extern ForeignDataWrapper *GetForeignDataWrapperExtended(Oid fdwid, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index f37131835be..12e357e8316 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4380,6 +4380,7 @@ typedef struct CreateSubscriptionStmt { NodeTag type; char *subname; /* Name of the subscription */ + char *servername; /* Server name of publisher */ char *conninfo; /* Connection string to publisher */ List *publication; /* One or more publication to subscribe to */ List *options; /* List of DefElem nodes */ @@ -4388,6 +4389,7 @@ typedef struct CreateSubscriptionStmt typedef enum AlterSubscriptionType { ALTER_SUBSCRIPTION_OPTIONS, + ALTER_SUBSCRIPTION_SERVER, ALTER_SUBSCRIPTION_CONNECTION, ALTER_SUBSCRIPTION_SET_PUBLICATION, ALTER_SUBSCRIPTION_ADD_PUBLICATION, @@ -4403,6 +4405,7 @@ typedef struct AlterSubscriptionStmt NodeTag type; AlterSubscriptionType kind; /* ALTER_SUBSCRIPTION_OPTIONS, etc */ char *subname; /* Name of the subscription */ + char *servername; /* Server name of publisher */ char *conninfo; /* Connection string to publisher */ List *publication; /* One or more publication to subscribe to */ List *options; /* List of DefElem nodes */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 25aaae8d05a..8768ce30b6a 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -224,6 +224,7 @@ NOTICE: checking pg_extension {extconfig} => pg_class {oid} NOTICE: checking pg_foreign_data_wrapper {fdwowner} => pg_authid {oid} NOTICE: checking pg_foreign_data_wrapper {fdwhandler} => pg_proc {oid} NOTICE: checking pg_foreign_data_wrapper {fdwvalidator} => pg_proc {oid} +NOTICE: checking pg_foreign_data_wrapper {fdwconnection} => pg_proc {oid} NOTICE: checking pg_foreign_server {srvowner} => pg_authid {oid} NOTICE: checking pg_foreign_server {srvfdw} => pg_foreign_data_wrapper {oid} NOTICE: checking pg_user_mapping {umuser} => pg_authid {oid} diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out index 3a0637772c7..2095bfc463e 100644 --- a/src/test/regress/expected/subscription.out +++ b/src/test/regress/expected/subscription.out @@ -1,6 +1,14 @@ -- -- SUBSCRIPTION -- +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX +\set regresslib :libdir '/regress' :dlsuffix +CREATE FUNCTION test_fdw_connection(oid, oid, internal) + RETURNS text + AS :'regresslib', 'test_fdw_connection' + LANGUAGE C; CREATE ROLE regress_subscription_user LOGIN SUPERUSER; CREATE ROLE regress_subscription_user2; CREATE ROLE regress_subscription_user3 IN ROLE pg_create_subscription; @@ -116,18 +124,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU 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. \dRs+ regress_testsub4 - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN -------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | none | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | none | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) ALTER SUBSCRIPTION regress_testsub4 SET (origin = any); \dRs+ regress_testsub4 - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN -------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) DROP SUBSCRIPTION regress_testsub3; @@ -140,15 +148,30 @@ ERROR: invalid connection string syntax: invalid connection option "i_dont_exis -- connecting, so this is reliable and safe) CREATE SUBSCRIPTION regress_testsub5 CONNECTION 'port=-1' PUBLICATION testpub; ERROR: subscription "regress_testsub5" could not connect to the publisher: invalid port number: "-1" +CREATE FOREIGN DATA WRAPPER test_fdw; +CREATE SERVER test_server FOREIGN DATA WRAPPER test_fdw; +CREATE USER MAPPING FOR regress_subscription_user SERVER test_server; +-- fail, need CONNECTION clause +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); +ERROR: foreign data wrapper "test_fdw" does not support subscription connections +DETAIL: Foreign data wrapper must be defined with CONNECTION specified. +ALTER FOREIGN DATA WRAPPER test_fdw CONNECTION test_fdw_connection; +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server 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. +DROP SUBSCRIPTION regress_testsub6; +DROP USER MAPPING FOR regress_subscription_user SERVER test_server; +DROP SERVER test_server; +DROP FOREIGN DATA WRAPPER test_fdw; -- fail - invalid connection string during ALTER ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar'; ERROR: invalid connection string syntax: missing "=" after "foobar" in connection info string \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false); @@ -157,10 +180,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname'); ALTER SUBSCRIPTION regress_testsub SET (password_required = false); ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | f | t | f | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | f | t | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 (1 row) ALTER SUBSCRIPTION regress_testsub SET (password_required = true); @@ -176,10 +199,10 @@ ERROR: unrecognized subscription parameter: "create_slot" -- ok ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345'); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00012345 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00012345 (1 row) -- ok - with lsn = NONE @@ -188,10 +211,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE); ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0'); ERROR: invalid WAL location (LSN): 0/0 \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 (1 row) BEGIN; @@ -227,10 +250,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (wal_receiver_timeout = '80s'); ALTER SUBSCRIPTION regress_testsub_foo SET (wal_receiver_timeout = 'foobar'); ERROR: invalid value for parameter "wal_receiver_timeout": "foobar" \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------ - regress_testsub_foo | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | local | dbname=regress_doesnotexist2 | 80s | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------ + regress_testsub_foo | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | local | dbname=regress_doesnotexist2 | 80s | 0/00000000 (1 row) -- rename back to keep the rest simple @@ -259,19 +282,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | t | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | t | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) ALTER SUBSCRIPTION regress_testsub SET (binary = false); ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) DROP SUBSCRIPTION regress_testsub; @@ -283,27 +306,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | f | on | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | f | on | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) ALTER SUBSCRIPTION regress_testsub SET (streaming = false); ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) -- fail - publication already exists @@ -318,10 +341,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false); ERROR: publication "testpub1" is already in subscription "regress_testsub" \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub,testpub1,testpub2} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub,testpub1,testpub2} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) -- fail - publication used more than once @@ -336,10 +359,10 @@ ERROR: publication "testpub3" is not in subscription "regress_testsub" -- ok - delete publications ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) DROP SUBSCRIPTION regress_testsub; @@ -375,19 +398,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) -- we can alter streaming when two_phase enabled ALTER SUBSCRIPTION regress_testsub SET (streaming = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -397,10 +420,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -413,18 +436,18 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | t | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | t | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -437,10 +460,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -454,19 +477,19 @@ NOTICE: max_retention_duration is ineffective when retain_dead_tuples is disabl 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 1000 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 1000 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) -- ok ALTER SUBSCRIPTION regress_testsub SET (max_retention_duration = 0); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------ + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c index a02f41c9727..158c7b7a4c0 100644 --- a/src/test/regress/regress.c +++ b/src/test/regress/regress.c @@ -729,6 +729,13 @@ test_fdw_handler(PG_FUNCTION_ARGS) PG_RETURN_NULL(); } +PG_FUNCTION_INFO_V1(test_fdw_connection); +Datum +test_fdw_connection(PG_FUNCTION_ARGS) +{ + PG_RETURN_TEXT_P(cstring_to_text("dbname=regress_doesnotexist")); +} + PG_FUNCTION_INFO_V1(is_catalog_text_unique_index_oid); Datum is_catalog_text_unique_index_oid(PG_FUNCTION_ARGS) diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql index d93cbc279d9..990d75f1749 100644 --- a/src/test/regress/sql/subscription.sql +++ b/src/test/regress/sql/subscription.sql @@ -2,6 +2,17 @@ -- SUBSCRIPTION -- +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX + +\set regresslib :libdir '/regress' :dlsuffix + +CREATE FUNCTION test_fdw_connection(oid, oid, internal) + RETURNS text + AS :'regresslib', 'test_fdw_connection' + LANGUAGE C; + CREATE ROLE regress_subscription_user LOGIN SUPERUSER; CREATE ROLE regress_subscription_user2; CREATE ROLE regress_subscription_user3 IN ROLE pg_create_subscription; @@ -85,6 +96,21 @@ CREATE SUBSCRIPTION regress_testsub5 CONNECTION 'i_dont_exist=param' PUBLICATION -- connecting, so this is reliable and safe) CREATE SUBSCRIPTION regress_testsub5 CONNECTION 'port=-1' PUBLICATION testpub; +CREATE FOREIGN DATA WRAPPER test_fdw; +CREATE SERVER test_server FOREIGN DATA WRAPPER test_fdw; +CREATE USER MAPPING FOR regress_subscription_user SERVER test_server; + +-- fail, need CONNECTION clause +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); + +ALTER FOREIGN DATA WRAPPER test_fdw CONNECTION test_fdw_connection; +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); +DROP SUBSCRIPTION regress_testsub6; + +DROP USER MAPPING FOR regress_subscription_user SERVER test_server; +DROP SERVER test_server; +DROP FOREIGN DATA WRAPPER test_fdw; + -- fail - invalid connection string during ALTER ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar'; -- 2.43.0 [text/x-patch] v17-0002-dblink-support-foreign-data-wrapper-CONNECTION-c.patch (9.3K, 3-v17-0002-dblink-support-foreign-data-wrapper-CONNECTION-c.patch) download | inline diff: From a778d1fd8a6dca1737770043321f7ebc17cec811 Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Thu, 26 Feb 2026 10:42:08 -0800 Subject: [PATCH v17 2/2] dblink: support foreign data wrapper CONNECTION clause. --- contrib/dblink/Makefile | 2 +- contrib/dblink/dblink--1.2--1.3.sql | 12 ++ .../{dblink--1.2.sql => dblink--1.3.sql} | 11 +- contrib/dblink/dblink.c | 163 +++++++++--------- contrib/dblink/dblink.control | 2 +- contrib/dblink/meson.build | 3 +- 6 files changed, 109 insertions(+), 84 deletions(-) create mode 100644 contrib/dblink/dblink--1.2--1.3.sql rename contrib/dblink/{dblink--1.2.sql => dblink--1.3.sql} (96%) diff --git a/contrib/dblink/Makefile b/contrib/dblink/Makefile index fde0b49ddbb..caa76c9cb27 100644 --- a/contrib/dblink/Makefile +++ b/contrib/dblink/Makefile @@ -8,7 +8,7 @@ PG_CPPFLAGS = -I$(libpq_srcdir) SHLIB_LINK_INTERNAL = $(libpq) EXTENSION = dblink -DATA = dblink--1.2.sql dblink--1.1--1.2.sql dblink--1.0--1.1.sql +DATA = dblink--1.3.sql dblink--1.2--1.3.sql dblink--1.1--1.2.sql dblink--1.0--1.1.sql PGFILEDESC = "dblink - connect to other PostgreSQL databases" REGRESS = dblink diff --git a/contrib/dblink/dblink--1.2--1.3.sql b/contrib/dblink/dblink--1.2--1.3.sql new file mode 100644 index 00000000000..77928a9e656 --- /dev/null +++ b/contrib/dblink/dblink--1.2--1.3.sql @@ -0,0 +1,12 @@ +/* contrib/dblink/dblink--1.2--1.3.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION dblink UPDATE TO '1.3'" to load this file. \quit + +-- takes internal parameter to prevent calling from SQL +CREATE FUNCTION dblink_fdw_connection(oid, oid, internal) +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT PARALLEL RESTRICTED; + +ALTER FOREIGN DATA WRAPPER dblink_fdw CONNECTION dblink_fdw_connection; diff --git a/contrib/dblink/dblink--1.2.sql b/contrib/dblink/dblink--1.3.sql similarity index 96% rename from contrib/dblink/dblink--1.2.sql rename to contrib/dblink/dblink--1.3.sql index 405eccb0ff9..22e4ea2061e 100644 --- a/contrib/dblink/dblink--1.2.sql +++ b/contrib/dblink/dblink--1.3.sql @@ -1,4 +1,4 @@ -/* contrib/dblink/dblink--1.2.sql */ +/* contrib/dblink/dblink--1.3.sql */ -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION dblink" to load this file. \quit @@ -232,4 +232,11 @@ RETURNS void AS 'MODULE_PATHNAME', 'dblink_fdw_validator' LANGUAGE C STRICT PARALLEL SAFE; -CREATE FOREIGN DATA WRAPPER dblink_fdw VALIDATOR dblink_fdw_validator; +-- takes internal parameter to prevent calling from SQL +CREATE FUNCTION dblink_fdw_connection(oid, oid, internal) +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT PARALLEL RESTRICTED; + +CREATE FOREIGN DATA WRAPPER dblink_fdw VALIDATOR dblink_fdw_validator + CONNECTION dblink_fdw_connection; diff --git a/contrib/dblink/dblink.c b/contrib/dblink/dblink.c index 2498d80c8e7..3a4b307ff64 100644 --- a/contrib/dblink/dblink.c +++ b/contrib/dblink/dblink.c @@ -1993,6 +1993,87 @@ dblink_fdw_validator(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } +/* + * Implement FDW CONNECTION clause. + */ +PG_FUNCTION_INFO_V1(dblink_fdw_connection); +Datum +dblink_fdw_connection(PG_FUNCTION_ARGS) +{ + Oid userid = PG_GETARG_OID(0); + Oid serverid = PG_GETARG_OID(1); + ForeignServer *foreign_server = GetForeignServer(serverid); + UserMapping *user_mapping = GetUserMapping(userid, serverid); + ForeignDataWrapper *fdw = GetForeignDataWrapper(foreign_server->fdwid); + AclResult aclresult; + ListCell *cell; + StringInfoData buf; + + static const PQconninfoOption *options = NULL; + + initStringInfo(&buf); + + /* + * Get list of valid libpq options. + * + * To avoid unnecessary work, we get the list once and use it throughout + * the lifetime of this backend process. We don't need to care about + * memory context issues, because PQconndefaults allocates with malloc. + */ + if (!options) + { + options = PQconndefaults(); + if (!options) /* assume reason for failure is OOM */ + ereport(ERROR, + (errcode(ERRCODE_FDW_OUT_OF_MEMORY), + errmsg("out of memory"), + errdetail("Could not get libpq's default connection options."))); + } + + /* Check permissions, user must have usage on the server. */ + aclresult = object_aclcheck(ForeignServerRelationId, serverid, userid, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_FOREIGN_SERVER, foreign_server->servername); + + /* + * First append hardcoded options needed for SCRAM pass-through, so if the + * user overwrites these options we can ereport on dblink_connstr_check + * and dblink_security_check. + */ + if (MyProcPort != NULL && MyProcPort->has_scram_keys && UseScramPassthrough(foreign_server, user_mapping)) + appendSCRAMKeysInfo(&buf); + + foreach(cell, fdw->options) + { + DefElem *def = lfirst(cell); + + if (is_valid_dblink_option(options, def->defname, ForeignDataWrapperRelationId)) + appendStringInfo(&buf, "%s='%s' ", def->defname, + escape_param_str(strVal(def->arg))); + } + + foreach(cell, foreign_server->options) + { + DefElem *def = lfirst(cell); + + if (is_valid_dblink_option(options, def->defname, ForeignServerRelationId)) + appendStringInfo(&buf, "%s='%s' ", def->defname, + escape_param_str(strVal(def->arg))); + } + + foreach(cell, user_mapping->options) + { + + DefElem *def = lfirst(cell); + + if (is_valid_dblink_option(options, def->defname, UserMappingRelationId)) + appendStringInfo(&buf, "%s='%s' ", def->defname, + escape_param_str(strVal(def->arg))); + } + + PG_RETURN_TEXT_P(cstring_to_text(buf.data)); +} + /************************************************************* * internal functions @@ -2855,93 +2936,17 @@ static char * get_connect_string(const char *servername) { ForeignServer *foreign_server = NULL; - UserMapping *user_mapping; - ListCell *cell; - StringInfoData buf; - ForeignDataWrapper *fdw; - AclResult aclresult; char *srvname; - static const PQconninfoOption *options = NULL; - - initStringInfo(&buf); - - /* - * Get list of valid libpq options. - * - * To avoid unnecessary work, we get the list once and use it throughout - * the lifetime of this backend process. We don't need to care about - * memory context issues, because PQconndefaults allocates with malloc. - */ - if (!options) - { - options = PQconndefaults(); - if (!options) /* assume reason for failure is OOM */ - ereport(ERROR, - (errcode(ERRCODE_FDW_OUT_OF_MEMORY), - errmsg("out of memory"), - errdetail("Could not get libpq's default connection options."))); - } - /* first gather the server connstr options */ srvname = pstrdup(servername); truncate_identifier(srvname, strlen(srvname), false); foreign_server = GetForeignServerByName(srvname, true); - if (foreign_server) - { - Oid serverid = foreign_server->serverid; - Oid fdwid = foreign_server->fdwid; - Oid userid = GetUserId(); - - user_mapping = GetUserMapping(userid, serverid); - fdw = GetForeignDataWrapper(fdwid); - - /* Check permissions, user must have usage on the server. */ - aclresult = object_aclcheck(ForeignServerRelationId, serverid, userid, ACL_USAGE); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_FOREIGN_SERVER, foreign_server->servername); - - /* - * First append hardcoded options needed for SCRAM pass-through, so if - * the user overwrites these options we can ereport on - * dblink_connstr_check and dblink_security_check. - */ - if (MyProcPort != NULL && MyProcPort->has_scram_keys && UseScramPassthrough(foreign_server, user_mapping)) - appendSCRAMKeysInfo(&buf); - - foreach(cell, fdw->options) - { - DefElem *def = lfirst(cell); - - if (is_valid_dblink_option(options, def->defname, ForeignDataWrapperRelationId)) - appendStringInfo(&buf, "%s='%s' ", def->defname, - escape_param_str(strVal(def->arg))); - } - - foreach(cell, foreign_server->options) - { - DefElem *def = lfirst(cell); - - if (is_valid_dblink_option(options, def->defname, ForeignServerRelationId)) - appendStringInfo(&buf, "%s='%s' ", def->defname, - escape_param_str(strVal(def->arg))); - } - - foreach(cell, user_mapping->options) - { - - DefElem *def = lfirst(cell); - - if (is_valid_dblink_option(options, def->defname, UserMappingRelationId)) - appendStringInfo(&buf, "%s='%s' ", def->defname, - escape_param_str(strVal(def->arg))); - } - - return buf.data; - } - else + if (!foreign_server) return NULL; + + return ForeignServerConnectionString(GetUserId(), foreign_server->serverid); } /* diff --git a/contrib/dblink/dblink.control b/contrib/dblink/dblink.control index bdd17d28a4b..816d19f4483 100644 --- a/contrib/dblink/dblink.control +++ b/contrib/dblink/dblink.control @@ -1,5 +1,5 @@ # dblink extension comment = 'connect to other PostgreSQL databases from within a database' -default_version = '1.2' +default_version = '1.3' module_pathname = '$libdir/dblink' relocatable = true diff --git a/contrib/dblink/meson.build b/contrib/dblink/meson.build index e2489f41229..fc91b4a918d 100644 --- a/contrib/dblink/meson.build +++ b/contrib/dblink/meson.build @@ -22,7 +22,8 @@ install_data( 'dblink.control', 'dblink--1.0--1.1.sql', 'dblink--1.1--1.2.sql', - 'dblink--1.2.sql', + 'dblink--1.2--1.3.sql', + 'dblink--1.3.sql', kwargs: contrib_data_args, ) -- 2.43.0 ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> @ 2026-03-02 21:34 ` Jeff Davis <[email protected]> 2026-03-03 18:19 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-03-05 03:51 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 0 siblings, 2 replies; 17+ messages in thread From: Jeff Davis @ 2026-03-02 21:34 UTC (permalink / raw) To: Masahiko Sawada <[email protected]>; +Cc: Shlok Kyal <[email protected]>; Ashutosh Bapat <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Thu, 2026-02-26 at 11:12 -0800, Jeff Davis wrote: > On Wed, 2026-02-04 at 13:53 +0900, Masahiko Sawada wrote: > > I've reviewed the latest patch set. I understand the motivation > > behind > > this proposal and find it useful. > > Thank you, that's important feedback. Attached v18: * rebase * Changed ForeignServerConnectionString() to use a local variable rather than a static. It's not very performance-sensitive, so it's OK to create a memory context for each invocation, which will be deleted. I'm not aware of an actual problem in the previous code, but it seemed a bit less safe. I plan to commit the main patch (v18-0001) soon, after rechecking some details (like the postgres_fdw upgrade). v18-0002 could use some review first. Regards, Jeff Davis Attachments: [text/x-patch] v18-0001-CREATE-SUBSCRIPTION-.-SERVER.patch (128.1K, 2-v18-0001-CREATE-SUBSCRIPTION-.-SERVER.patch) download | inline diff: From c94f3e31aeb2663349a1a5ab1136410d38e3131c Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Tue, 2 Jan 2024 13:42:48 -0800 Subject: [PATCH v18 1/2] CREATE SUBSCRIPTION ... SERVER. --- CATVERSION BUMP --- Allow specifying a foreign server for CREATE SUBSCRIPTION, rather than a raw connection string with CONNECTION. Using a foreign server as a layer of indirection improves management of multiple subscriptions to the same server. It also provides integration with user mappings in case different subscriptions have different owners or a subscription changes owners. Reviewed-by: Ashutosh Bapat <[email protected]> Reviewed-by: Shlok Kyal <[email protected]> Reviewed-by: Masahiko Sawada <[email protected]> Discussion: https://postgr.es/m/[email protected] --- contrib/postgres_fdw/Makefile | 2 +- contrib/postgres_fdw/connection.c | 299 +++++++++++------- .../postgres_fdw/expected/postgres_fdw.out | 8 + contrib/postgres_fdw/meson.build | 2 + .../postgres_fdw/postgres_fdw--1.2--1.3.sql | 12 + contrib/postgres_fdw/postgres_fdw.control | 2 +- contrib/postgres_fdw/sql/postgres_fdw.sql | 7 + contrib/postgres_fdw/t/010_subscription.pl | 71 +++++ doc/src/sgml/logical-replication.sgml | 4 +- doc/src/sgml/postgres-fdw.sgml | 26 ++ .../sgml/ref/alter_foreign_data_wrapper.sgml | 20 ++ doc/src/sgml/ref/alter_subscription.sgml | 18 +- .../sgml/ref/create_foreign_data_wrapper.sgml | 20 ++ doc/src/sgml/ref/create_server.sgml | 7 + doc/src/sgml/ref/create_subscription.sgml | 11 +- src/backend/catalog/pg_subscription.c | 38 ++- src/backend/commands/foreigncmds.c | 58 +++- src/backend/commands/subscriptioncmds.c | 168 +++++++++- src/backend/foreign/foreign.c | 86 +++++ src/backend/parser/gram.y | 22 ++ src/backend/replication/logical/worker.c | 16 +- src/bin/pg_dump/pg_dump.c | 39 ++- src/bin/pg_dump/pg_dump.h | 1 + src/bin/psql/describe.c | 6 +- src/bin/psql/tab-complete.in.c | 11 +- src/include/catalog/pg_foreign_data_wrapper.h | 3 + src/include/catalog/pg_subscription.h | 7 +- src/include/foreign/foreign.h | 3 + src/include/nodes/parsenodes.h | 3 + src/test/regress/expected/oidjoins.out | 1 + src/test/regress/expected/subscription.out | 199 ++++++------ src/test/regress/regress.c | 7 + src/test/regress/sql/subscription.sql | 26 ++ 33 files changed, 958 insertions(+), 245 deletions(-) create mode 100644 contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql create mode 100644 contrib/postgres_fdw/t/010_subscription.pl diff --git a/contrib/postgres_fdw/Makefile b/contrib/postgres_fdw/Makefile index 8eaf4d263b6..b8c78b58804 100644 --- a/contrib/postgres_fdw/Makefile +++ b/contrib/postgres_fdw/Makefile @@ -14,7 +14,7 @@ PG_CPPFLAGS = -I$(libpq_srcdir) SHLIB_LINK_INTERNAL = $(libpq) EXTENSION = postgres_fdw -DATA = postgres_fdw--1.0.sql postgres_fdw--1.0--1.1.sql postgres_fdw--1.1--1.2.sql +DATA = postgres_fdw--1.0.sql postgres_fdw--1.0--1.1.sql postgres_fdw--1.1--1.2.sql postgres_fdw--1.2--1.3.sql REGRESS = postgres_fdw query_cancel ISOLATION = eval_plan_qual diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c index 311936406f2..7e2b822d161 100644 --- a/contrib/postgres_fdw/connection.c +++ b/contrib/postgres_fdw/connection.c @@ -132,6 +132,7 @@ PG_FUNCTION_INFO_V1(postgres_fdw_get_connections); PG_FUNCTION_INFO_V1(postgres_fdw_get_connections_1_2); PG_FUNCTION_INFO_V1(postgres_fdw_disconnect); PG_FUNCTION_INFO_V1(postgres_fdw_disconnect_all); +PG_FUNCTION_INFO_V1(postgres_fdw_connection); /* prototypes of private functions */ static void make_new_connection(ConnCacheEntry *entry, UserMapping *user); @@ -477,141 +478,159 @@ pgfdw_security_check(const char **keywords, const char **values, UserMapping *us } /* - * Connect to remote server using specified server and user mapping properties. + * Construct connection params from generic options of ForeignServer and + * UserMapping. (Some of them might not be libpq options, in which case we'll + * just waste a few array slots.) */ -static PGconn * -connect_pg_server(ForeignServer *server, UserMapping *user) +static void +construct_connection_params(ForeignServer *server, UserMapping *user, + const char ***p_keywords, const char ***p_values, + char **p_appname) { - PGconn *volatile conn = NULL; + const char **keywords; + const char **values; + char *appname = NULL; + int n; /* - * Use PG_TRY block to ensure closing connection on error. + * Add 4 extra slots for application_name, fallback_application_name, + * client_encoding, end marker, and 3 extra slots for scram keys and + * required scram pass-through options. */ - PG_TRY(); - { - const char **keywords; - const char **values; - char *appname = NULL; - int n; + n = list_length(server->options) + list_length(user->options) + 4 + 3; + keywords = (const char **) palloc(n * sizeof(char *)); + values = (const char **) palloc(n * sizeof(char *)); - /* - * Construct connection params from generic options of ForeignServer - * and UserMapping. (Some of them might not be libpq options, in - * which case we'll just waste a few array slots.) Add 4 extra slots - * for application_name, fallback_application_name, client_encoding, - * end marker, and 3 extra slots for scram keys and required scram - * pass-through options. - */ - n = list_length(server->options) + list_length(user->options) + 4 + 3; - keywords = (const char **) palloc(n * sizeof(char *)); - values = (const char **) palloc(n * sizeof(char *)); + n = 0; + n += ExtractConnectionOptions(server->options, + keywords + n, values + n); + n += ExtractConnectionOptions(user->options, + keywords + n, values + n); - n = 0; - n += ExtractConnectionOptions(server->options, - keywords + n, values + n); - n += ExtractConnectionOptions(user->options, - keywords + n, values + n); - - /* - * Use pgfdw_application_name as application_name if set. - * - * PQconnectdbParams() processes the parameter arrays from start to - * end. If any key word is repeated, the last value is used. Therefore - * note that pgfdw_application_name must be added to the arrays after - * options of ForeignServer are, so that it can override - * application_name set in ForeignServer. - */ - if (pgfdw_application_name && *pgfdw_application_name != '\0') - { - keywords[n] = "application_name"; - values[n] = pgfdw_application_name; - n++; - } + /* + * Use pgfdw_application_name as application_name if set. + * + * PQconnectdbParams() processes the parameter arrays from start to end. + * If any key word is repeated, the last value is used. Therefore note + * that pgfdw_application_name must be added to the arrays after options + * of ForeignServer are, so that it can override application_name set in + * ForeignServer. + */ + if (pgfdw_application_name && *pgfdw_application_name != '\0') + { + keywords[n] = "application_name"; + values[n] = pgfdw_application_name; + n++; + } - /* - * Search the parameter arrays to find application_name setting, and - * replace escape sequences in it with status information if found. - * The arrays are searched backwards because the last value is used if - * application_name is repeatedly set. - */ - for (int i = n - 1; i >= 0; i--) + /* + * Search the parameter arrays to find application_name setting, and + * replace escape sequences in it with status information if found. The + * arrays are searched backwards because the last value is used if + * application_name is repeatedly set. + */ + for (int i = n - 1; i >= 0; i--) + { + if (strcmp(keywords[i], "application_name") == 0 && + *(values[i]) != '\0') { - if (strcmp(keywords[i], "application_name") == 0 && - *(values[i]) != '\0') + /* + * Use this application_name setting if it's not empty string even + * after any escape sequences in it are replaced. + */ + appname = process_pgfdw_appname(values[i]); + if (appname[0] != '\0') { - /* - * Use this application_name setting if it's not empty string - * even after any escape sequences in it are replaced. - */ - appname = process_pgfdw_appname(values[i]); - if (appname[0] != '\0') - { - values[i] = appname; - break; - } - - /* - * This empty application_name is not used, so we set - * values[i] to NULL and keep searching the array to find the - * next one. - */ - values[i] = NULL; - pfree(appname); - appname = NULL; + values[i] = appname; + break; } + + /* + * This empty application_name is not used, so we set values[i] to + * NULL and keep searching the array to find the next one. + */ + values[i] = NULL; + pfree(appname); + appname = NULL; } + } + + *p_appname = appname; - /* Use "postgres_fdw" as fallback_application_name */ - keywords[n] = "fallback_application_name"; - values[n] = "postgres_fdw"; + /* Use "postgres_fdw" as fallback_application_name */ + keywords[n] = "fallback_application_name"; + values[n] = "postgres_fdw"; + n++; + + /* Set client_encoding so that libpq can convert encoding properly. */ + keywords[n] = "client_encoding"; + values[n] = GetDatabaseEncodingName(); + n++; + + /* Add required SCRAM pass-through connection options if it's enabled. */ + if (MyProcPort != NULL && MyProcPort->has_scram_keys && UseScramPassthrough(server, user)) + { + int len; + int encoded_len; + + keywords[n] = "scram_client_key"; + len = pg_b64_enc_len(sizeof(MyProcPort->scram_ClientKey)); + /* don't forget the zero-terminator */ + values[n] = palloc0(len + 1); + encoded_len = pg_b64_encode(MyProcPort->scram_ClientKey, + sizeof(MyProcPort->scram_ClientKey), + (char *) values[n], len); + if (encoded_len < 0) + elog(ERROR, "could not encode SCRAM client key"); n++; - /* Set client_encoding so that libpq can convert encoding properly. */ - keywords[n] = "client_encoding"; - values[n] = GetDatabaseEncodingName(); + keywords[n] = "scram_server_key"; + len = pg_b64_enc_len(sizeof(MyProcPort->scram_ServerKey)); + /* don't forget the zero-terminator */ + values[n] = palloc0(len + 1); + encoded_len = pg_b64_encode(MyProcPort->scram_ServerKey, + sizeof(MyProcPort->scram_ServerKey), + (char *) values[n], len); + if (encoded_len < 0) + elog(ERROR, "could not encode SCRAM server key"); n++; - /* Add required SCRAM pass-through connection options if it's enabled. */ - if (MyProcPort != NULL && MyProcPort->has_scram_keys && UseScramPassthrough(server, user)) - { - int len; - int encoded_len; - - keywords[n] = "scram_client_key"; - len = pg_b64_enc_len(sizeof(MyProcPort->scram_ClientKey)); - /* don't forget the zero-terminator */ - values[n] = palloc0(len + 1); - encoded_len = pg_b64_encode(MyProcPort->scram_ClientKey, - sizeof(MyProcPort->scram_ClientKey), - (char *) values[n], len); - if (encoded_len < 0) - elog(ERROR, "could not encode SCRAM client key"); - n++; - - keywords[n] = "scram_server_key"; - len = pg_b64_enc_len(sizeof(MyProcPort->scram_ServerKey)); - /* don't forget the zero-terminator */ - values[n] = palloc0(len + 1); - encoded_len = pg_b64_encode(MyProcPort->scram_ServerKey, - sizeof(MyProcPort->scram_ServerKey), - (char *) values[n], len); - if (encoded_len < 0) - elog(ERROR, "could not encode SCRAM server key"); - n++; + /* + * Require scram-sha-256 to ensure that no other auth method is used + * when connecting with foreign server. + */ + keywords[n] = "require_auth"; + values[n] = "scram-sha-256"; + n++; + } - /* - * Require scram-sha-256 to ensure that no other auth method is - * used when connecting with foreign server. - */ - keywords[n] = "require_auth"; - values[n] = "scram-sha-256"; - n++; - } + keywords[n] = values[n] = NULL; + + /* Verify the set of connection parameters. */ + check_conn_params(keywords, values, user); - keywords[n] = values[n] = NULL; + *p_keywords = keywords; + *p_values = values; +} + +/* + * Connect to remote server using specified server and user mapping properties. + */ +static PGconn * +connect_pg_server(ForeignServer *server, UserMapping *user) +{ + PGconn *volatile conn = NULL; + + /* + * Use PG_TRY block to ensure closing connection on error. + */ + PG_TRY(); + { + const char **keywords; + const char **values; + char *appname; - /* Verify the set of connection parameters. */ - check_conn_params(keywords, values, user); + construct_connection_params(server, user, &keywords, &values, &appname); /* first time, allocate or get the custom wait event */ if (pgfdw_we_connect == 0) @@ -2310,6 +2329,56 @@ postgres_fdw_get_connections_internal(FunctionCallInfo fcinfo, } } +/* + * Values in connection strings must be enclosed in single quotes. Single + * quotes and backslashes must be escaped with backslash. NB: these rules are + * different from the rules for escaping a SQL literal. + */ +static void +appendEscapedValue(StringInfo str, const char *val) +{ + appendStringInfoChar(str, '\''); + for (int i = 0; val[i] != '\0'; i++) + { + if (val[i] == '\\' || val[i] == '\'') + appendStringInfoChar(str, '\\'); + appendStringInfoChar(str, val[i]); + } + appendStringInfoChar(str, '\''); +} + +Datum +postgres_fdw_connection(PG_FUNCTION_ARGS) +{ + Oid userid = PG_GETARG_OID(0); + Oid serverid = PG_GETARG_OID(1); + ForeignServer *server = GetForeignServer(serverid); + UserMapping *user = GetUserMapping(userid, serverid); + StringInfoData str; + const char **keywords; + const char **values; + char *appname; + char *sep = ""; + + construct_connection_params(server, user, &keywords, &values, &appname); + + initStringInfo(&str); + for (int i = 0; keywords[i] != NULL; i++) + { + if (values[i] == NULL) + continue; + appendStringInfo(&str, "%s%s = ", sep, keywords[i]); + appendEscapedValue(&str, values[i]); + sep = " "; + } + + if (appname != NULL) + pfree(appname); + pfree(keywords); + pfree(values); + PG_RETURN_TEXT_P(cstring_to_text(str.data)); +} + /* * List active foreign server connections. * diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 2ccb72c539a..0f5271d476e 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -255,6 +255,14 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again ANALYZE ft1; ALTER FOREIGN TABLE ft2 OPTIONS (use_remote_estimate 'true'); -- =================================================================== +-- test subscription +-- =================================================================== +CREATE SUBSCRIPTION regress_pgfdw_subscription SERVER testserver1 + PUBLICATION pub1 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. +DROP SUBSCRIPTION regress_pgfdw_subscription; +-- =================================================================== -- test error case for create publication on foreign table -- =================================================================== CREATE PUBLICATION testpub_ftbl FOR TABLE ft1; -- should fail diff --git a/contrib/postgres_fdw/meson.build b/contrib/postgres_fdw/meson.build index ea4cd9fcd46..3e2ed06b766 100644 --- a/contrib/postgres_fdw/meson.build +++ b/contrib/postgres_fdw/meson.build @@ -27,6 +27,7 @@ install_data( 'postgres_fdw--1.0.sql', 'postgres_fdw--1.0--1.1.sql', 'postgres_fdw--1.1--1.2.sql', + 'postgres_fdw--1.2--1.3.sql', kwargs: contrib_data_args, ) @@ -50,6 +51,7 @@ tests += { 'tap': { 'tests': [ 't/001_auth_scram.pl', + 't/010_subscription.pl', ], }, } diff --git a/contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql b/contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql new file mode 100644 index 00000000000..5bcf0ba2e09 --- /dev/null +++ b/contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql @@ -0,0 +1,12 @@ +/* contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION postgres_fdw UPDATE TO '1.3'" to load this file. \quit + +-- takes internal parameter to prevent calling from SQL +CREATE FUNCTION postgres_fdw_connection(oid, oid, internal) +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT PARALLEL RESTRICTED; + +ALTER FOREIGN DATA WRAPPER postgres_fdw CONNECTION postgres_fdw_connection; diff --git a/contrib/postgres_fdw/postgres_fdw.control b/contrib/postgres_fdw/postgres_fdw.control index a4b800be4fc..ae2963d480d 100644 --- a/contrib/postgres_fdw/postgres_fdw.control +++ b/contrib/postgres_fdw/postgres_fdw.control @@ -1,5 +1,5 @@ # postgres_fdw extension comment = 'foreign-data wrapper for remote PostgreSQL servers' -default_version = '1.2' +default_version = '1.3' module_pathname = '$libdir/postgres_fdw' relocatable = true diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 72d2d9c311b..49ed797e8ef 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -244,6 +244,13 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again ANALYZE ft1; ALTER FOREIGN TABLE ft2 OPTIONS (use_remote_estimate 'true'); +-- =================================================================== +-- test subscription +-- =================================================================== +CREATE SUBSCRIPTION regress_pgfdw_subscription SERVER testserver1 + PUBLICATION pub1 WITH (slot_name = NONE, connect = false); +DROP SUBSCRIPTION regress_pgfdw_subscription; + -- =================================================================== -- test error case for create publication on foreign table -- =================================================================== diff --git a/contrib/postgres_fdw/t/010_subscription.pl b/contrib/postgres_fdw/t/010_subscription.pl new file mode 100644 index 00000000000..a39e8fdbba4 --- /dev/null +++ b/contrib/postgres_fdw/t/010_subscription.pl @@ -0,0 +1,71 @@ + +# Copyright (c) 2021-2024, PostgreSQL Global Development Group + +# Basic logical replication test +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Initialize publisher node +my $node_publisher = PostgreSQL::Test::Cluster->new('publisher'); +$node_publisher->init(allows_streaming => 'logical'); +$node_publisher->start; + +# Create subscriber node +my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber'); +$node_subscriber->init; +$node_subscriber->start; + +# Create some preexisting content on publisher +$node_publisher->safe_psql('postgres', + "CREATE TABLE tab_ins AS SELECT a, a + 1 as b FROM generate_series(1,1002) AS a"); + +# Replicate the changes without columns +$node_publisher->safe_psql('postgres', "CREATE TABLE tab_no_col()"); +$node_publisher->safe_psql('postgres', + "INSERT INTO tab_no_col default VALUES"); + +# Setup structure on subscriber +$node_subscriber->safe_psql('postgres', "CREATE EXTENSION postgres_fdw"); +$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_ins (a int, b int)"); + +# Setup logical replication +my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres'; +$node_publisher->safe_psql('postgres', "CREATE PUBLICATION tap_pub FOR TABLE tab_ins"); + +my $publisher_host = $node_publisher->host; +my $publisher_port = $node_publisher->port; +$node_subscriber->safe_psql('postgres', + "CREATE SERVER tap_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '$publisher_host', port '$publisher_port', dbname 'postgres')" +); + +$node_subscriber->safe_psql('postgres', + "CREATE USER MAPPING FOR PUBLIC SERVER tap_server" +); + +$node_subscriber->safe_psql('postgres', + "CREATE FOREIGN TABLE f_tab_ins (a int, b int) SERVER tap_server OPTIONS(table_name 'tab_ins')" +); +$node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION tap_sub SERVER tap_server PUBLICATION tap_pub WITH (password_required=false)" +); + +# Wait for initial table sync to finish +$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); + +my $result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM (SELECT f.b = l.b as match FROM tab_ins l, f_tab_ins f WHERE l.a = f.a) WHERE match"); +is($result, qq(1002), 'check initial data was copied to subscriber'); + +$node_publisher->safe_psql('postgres', + "INSERT INTO tab_ins SELECT a, a + 1 FROM generate_series(1003,1050) a"); + +$node_publisher->wait_for_catchup('tap_sub'); + +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM (SELECT f.b = l.b as match FROM tab_ins l, f_tab_ins f WHERE l.a = f.a) WHERE match"); +is($result, qq(1050), 'check initial data was copied to subscriber'); + +done_testing(); diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index 5028fe9af09..b4e6d5ae3f9 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -2573,7 +2573,9 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER <para> To create a subscription, the user must have the privileges of the <literal>pg_create_subscription</literal> role, as well as - <literal>CREATE</literal> privileges on the database. + <literal>CREATE</literal> privileges on the database. If + <literal>SERVER</literal> is specified, the user also must have + <literal>USAGE</literal> privileges on the server. </para> <para> diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index fcf10e4317e..de69ddcdebc 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -1049,6 +1049,32 @@ postgres=# SELECT postgres_fdw_disconnect_all(); </para> </sect2> + <sect2 id="postgres-fdw-server-subscription"> + <title>Subscription Management</title> + + <para> + <filename>postgres_fdw</filename> supports subscription connections using + the same options described in <xref + linkend="postgres-fdw-options-connection"/>. + </para> + + <para> + For example, assuming the remote server <literal>foreign-host</literal> has + a publication <literal>testpub</literal>: +<programlisting> +CREATE SERVER subscription_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'foreign-host', dbname 'foreign_db'); +CREATE USER MAPPING FOR local_user SERVER subscription_server OPTIONS (user 'foreign_user', password 'password'); +CREATE SUBSCRIPTION my_subscription SERVER subscription_server PUBLICATION testpub; +</programlisting> + </para> + + <para> + To create a subscription, the user must be a member of the <xref + linkend="predefined-role-pg-create-subscription"/> role and have + <literal>USAGE</literal> privileges on the server. + </para> + </sect2> + <sect2 id="postgres-fdw-transaction-management"> <title>Transaction Management</title> diff --git a/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml b/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml index dc0957d965a..640c02893cf 100644 --- a/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml +++ b/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml @@ -24,6 +24,7 @@ PostgreSQL documentation ALTER FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> [ HANDLER <replaceable class="parameter">handler_function</replaceable> | NO HANDLER ] [ VALIDATOR <replaceable class="parameter">validator_function</replaceable> | NO VALIDATOR ] + [ CONNECTION <replaceable class="parameter">connection_function</replaceable> | NO CONNECTION ] [ OPTIONS ( [ ADD | SET | DROP ] <replaceable class="parameter">option</replaceable> ['<replaceable class="parameter">value</replaceable>'] [, ... ]) ] ALTER FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } ALTER FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> RENAME TO <replaceable>new_name</replaceable> @@ -112,6 +113,25 @@ ALTER FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> REN </listitem> </varlistentry> + <varlistentry> + <term><literal>CONNECTION <replaceable class="parameter">connection_function</replaceable></literal></term> + <listitem> + <para> + Specifies a new connection function for the foreign-data wrapper. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>NO CONNECTION</literal></term> + <listitem> + <para> + This is used to specify that the foreign-data wrapper should no + longer have a connection function. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>OPTIONS ( [ ADD | SET | DROP ] <replaceable class="parameter">option</replaceable> ['<replaceable class="parameter">value</replaceable>'] [, ... ] )</literal></term> <listitem> diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml index 5318998e80c..f215fb0e5a2 100644 --- a/doc/src/sgml/ref/alter_subscription.sgml +++ b/doc/src/sgml/ref/alter_subscription.sgml @@ -21,6 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> +ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> SERVER <replaceable>servername</replaceable> ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> CONNECTION '<replaceable>conninfo</replaceable>' ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> SET PUBLICATION <replaceable class="parameter">publication_name</replaceable> [, ...] [ WITH ( <replaceable class="parameter">publication_option</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ] ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> ADD PUBLICATION <replaceable class="parameter">publication_name</replaceable> [, ...] [ WITH ( <replaceable class="parameter">publication_option</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ] @@ -102,13 +103,24 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO < </listitem> </varlistentry> + <varlistentry id="sql-altersubscription-params-server"> + <term><literal>SERVER <replaceable class="parameter">servername</replaceable></literal></term> + <listitem> + <para> + This clause replaces the foreign server or connection string originally + set by <xref linkend="sql-createsubscription"/> with the foreign server + <replaceable>servername</replaceable>. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-altersubscription-params-connection"> <term><literal>CONNECTION '<replaceable class="parameter">conninfo</replaceable>'</literal></term> <listitem> <para> - This clause replaces the connection string originally set by - <xref linkend="sql-createsubscription"/>. See there for more - information. + This clause replaces the foreign server or connection string originally + set by <xref linkend="sql-createsubscription"/> with the connection + string <replaceable>conninfo</replaceable>. </para> </listitem> </varlistentry> diff --git a/doc/src/sgml/ref/create_foreign_data_wrapper.sgml b/doc/src/sgml/ref/create_foreign_data_wrapper.sgml index 0fcba18a347..7b83f500b25 100644 --- a/doc/src/sgml/ref/create_foreign_data_wrapper.sgml +++ b/doc/src/sgml/ref/create_foreign_data_wrapper.sgml @@ -24,6 +24,7 @@ PostgreSQL documentation CREATE FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> [ HANDLER <replaceable class="parameter">handler_function</replaceable> | NO HANDLER ] [ VALIDATOR <replaceable class="parameter">validator_function</replaceable> | NO VALIDATOR ] + [ CONNECTION <replaceable class="parameter">connection_function</replaceable> | NO CONNECTION ] [ OPTIONS ( <replaceable class="parameter">option</replaceable> '<replaceable class="parameter">value</replaceable>' [, ... ] ) ] </synopsis> </refsynopsisdiv> @@ -99,6 +100,25 @@ CREATE FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> </listitem> </varlistentry> + <varlistentry> + <term><literal>CONNECTION <replaceable class="parameter">connection_function</replaceable></literal></term> + <listitem> + <para> + <replaceable class="parameter">connection_function</replaceable> is the + name of a previously registered function that will be called to generate + the postgres connection string when a foreign server is used as part of + <xref linkend="sql-createsubscription"/>. If no connection function or + <literal>NO CONNECTION</literal> is specified, then servers using this + foreign data wrapper cannot be used for <literal>CREATE + SUBSCRIPTION</literal>. The connection function must take three + arguments: one of type <type>oid</type> for the user, one of type + <type>oid</type> for the server, and an unused third argument of type + <type>internal</type> (which prevents calling the function in other + contexts). + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>OPTIONS ( <replaceable class="parameter">option</replaceable> '<replaceable class="parameter">value</replaceable>' [, ... ] )</literal></term> <listitem> diff --git a/doc/src/sgml/ref/create_server.sgml b/doc/src/sgml/ref/create_server.sgml index 05f4019453b..ce4a064eabb 100644 --- a/doc/src/sgml/ref/create_server.sgml +++ b/doc/src/sgml/ref/create_server.sgml @@ -42,6 +42,13 @@ CREATE SERVER [ IF NOT EXISTS ] <replaceable class="parameter">server_name</repl means of user mappings. </para> + <para> + If the foreign data wrapper <replaceable>fdw_name</replaceable> is + specified with a <literal>CONNECTION</literal> clause, then <xref + linkend="sql-createsubscription"/> may use this foreign server for + connection information. + </para> + <para> The server name must be unique within the database. </para> diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml index eb0cc645d8f..2ca7e0e6826 100644 --- a/doc/src/sgml/ref/create_subscription.sgml +++ b/doc/src/sgml/ref/create_subscription.sgml @@ -22,7 +22,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceable> - CONNECTION '<replaceable class="parameter">conninfo</replaceable>' + { SERVER <replaceable class="parameter">servername</replaceable> | CONNECTION '<replaceable class="parameter">conninfo</replaceable>' } PUBLICATION <replaceable class="parameter">publication_name</replaceable> [, ...] [ WITH ( <replaceable class="parameter">subscription_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ] </synopsis> @@ -77,6 +77,15 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl </listitem> </varlistentry> + <varlistentry id="sql-createsubscription-params-server"> + <term><literal>SERVER <replaceable class="parameter">servername</replaceable></literal></term> + <listitem> + <para> + A foreign server to use for the connection. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-createsubscription-params-connection"> <term><literal>CONNECTION '<replaceable class="parameter">conninfo</replaceable>'</literal></term> <listitem> diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c index acf42b853ed..3673d4f0bc1 100644 --- a/src/backend/catalog/pg_subscription.c +++ b/src/backend/catalog/pg_subscription.c @@ -19,11 +19,14 @@ #include "access/htup_details.h" #include "access/tableam.h" #include "catalog/indexing.h" +#include "catalog/pg_foreign_server.h" #include "catalog/pg_subscription.h" #include "catalog/pg_subscription_rel.h" #include "catalog/pg_type.h" +#include "foreign/foreign.h" #include "miscadmin.h" #include "storage/lmgr.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -69,7 +72,7 @@ GetPublicationsStr(List *publications, StringInfo dest, bool quote_literal) * Fetch the subscription from the syscache. */ Subscription * -GetSubscription(Oid subid, bool missing_ok) +GetSubscription(Oid subid, bool missing_ok, bool aclcheck) { HeapTuple tup; Subscription *sub; @@ -108,10 +111,35 @@ GetSubscription(Oid subid, bool missing_ok) sub->retentionactive = subform->subretentionactive; /* Get conninfo */ - datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, - tup, - Anum_pg_subscription_subconninfo); - sub->conninfo = TextDatumGetCString(datum); + if (OidIsValid(subform->subserver)) + { + AclResult aclresult; + + /* recheck ACL if requested */ + if (aclcheck) + { + aclresult = object_aclcheck(ForeignServerRelationId, + subform->subserver, + subform->subowner, ACL_USAGE); + + if (aclresult != ACLCHECK_OK) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", + GetUserNameFromId(subform->subowner, false), + ForeignServerName(subform->subserver)))); + } + + sub->conninfo = ForeignServerConnectionString(subform->subowner, + subform->subserver); + } + else + { + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, + tup, + Anum_pg_subscription_subconninfo); + sub->conninfo = TextDatumGetCString(datum); + } /* Get slotname */ datum = SysCacheGetAttr(SUBSCRIPTIONOID, diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c index b56d1ad6785..45681235782 100644 --- a/src/backend/commands/foreigncmds.c +++ b/src/backend/commands/foreigncmds.c @@ -522,21 +522,53 @@ lookup_fdw_validator_func(DefElem *validator) /* validator's return value is ignored, so we don't check the type */ } +/* + * Convert a connection string function name passed from the parser to an Oid. + */ +static Oid +lookup_fdw_connection_func(DefElem *connection) +{ + Oid connectionOid; + Oid funcargtypes[3]; + + if (connection == NULL || connection->arg == NULL) + return InvalidOid; + + /* connection string functions take user oid, server oid */ + funcargtypes[0] = OIDOID; + funcargtypes[1] = OIDOID; + funcargtypes[2] = INTERNALOID; + + connectionOid = LookupFuncName((List *) connection->arg, 3, funcargtypes, false); + + /* check that connection string function has correct return type */ + if (get_func_rettype(connectionOid) != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("function %s must return type %s", + NameListToString((List *) connection->arg), "text"))); + + return connectionOid; +} + /* * Process function options of CREATE/ALTER FDW */ static void parse_func_options(ParseState *pstate, List *func_options, bool *handler_given, Oid *fdwhandler, - bool *validator_given, Oid *fdwvalidator) + bool *validator_given, Oid *fdwvalidator, + bool *connection_given, Oid *fdwconnection) { ListCell *cell; *handler_given = false; *validator_given = false; + *connection_given = false; /* return InvalidOid if not given */ *fdwhandler = InvalidOid; *fdwvalidator = InvalidOid; + *fdwconnection = InvalidOid; foreach(cell, func_options) { @@ -556,6 +588,13 @@ parse_func_options(ParseState *pstate, List *func_options, *validator_given = true; *fdwvalidator = lookup_fdw_validator_func(def); } + else if (strcmp(def->defname, "connection") == 0) + { + if (*connection_given) + errorConflictingDefElem(def, pstate); + *connection_given = true; + *fdwconnection = lookup_fdw_connection_func(def); + } else elog(ERROR, "option \"%s\" not recognized", def->defname); @@ -575,8 +614,10 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) Oid fdwId; bool handler_given; bool validator_given; + bool connection_given; Oid fdwhandler; Oid fdwvalidator; + Oid fdwconnection; Datum fdwoptions; Oid ownerId; ObjectAddress myself; @@ -620,10 +661,12 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) /* Lookup handler and validator functions, if given */ parse_func_options(pstate, stmt->func_options, &handler_given, &fdwhandler, - &validator_given, &fdwvalidator); + &validator_given, &fdwvalidator, + &connection_given, &fdwconnection); values[Anum_pg_foreign_data_wrapper_fdwhandler - 1] = ObjectIdGetDatum(fdwhandler); values[Anum_pg_foreign_data_wrapper_fdwvalidator - 1] = ObjectIdGetDatum(fdwvalidator); + values[Anum_pg_foreign_data_wrapper_fdwconnection - 1] = ObjectIdGetDatum(fdwconnection); nulls[Anum_pg_foreign_data_wrapper_fdwacl - 1] = true; @@ -695,8 +738,10 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) Datum datum; bool handler_given; bool validator_given; + bool connection_given; Oid fdwhandler; Oid fdwvalidator; + Oid fdwconnection; ObjectAddress myself; rel = table_open(ForeignDataWrapperRelationId, RowExclusiveLock); @@ -726,7 +771,8 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) parse_func_options(pstate, stmt->func_options, &handler_given, &fdwhandler, - &validator_given, &fdwvalidator); + &validator_given, &fdwvalidator, + &connection_given, &fdwconnection); if (handler_given) { @@ -764,6 +810,12 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) fdwvalidator = fdwForm->fdwvalidator; } + if (connection_given) + { + repl_val[Anum_pg_foreign_data_wrapper_fdwconnection - 1] = ObjectIdGetDatum(fdwconnection); + repl_repl[Anum_pg_foreign_data_wrapper_fdwconnection - 1] = true; + } + /* * If options specified, validate and update. */ diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index 5e3c0964d38..091e7b7372d 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -27,13 +27,16 @@ #include "catalog/objectaddress.h" #include "catalog/pg_authid_d.h" #include "catalog/pg_database_d.h" +#include "catalog/pg_foreign_server.h" #include "catalog/pg_subscription.h" #include "catalog/pg_subscription_rel.h" #include "catalog/pg_type.h" +#include "catalog/pg_user_mapping.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/subscriptioncmds.h" #include "executor/executor.h" +#include "foreign/foreign.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "pgstat.h" @@ -619,6 +622,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, Datum values[Natts_pg_subscription]; Oid owner = GetUserId(); HeapTuple tup; + Oid serverid; char *conninfo; char originname[NAMEDATALEN]; List *publications; @@ -730,15 +734,40 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, if (opts.wal_receiver_timeout == NULL) opts.wal_receiver_timeout = "-1"; - conninfo = stmt->conninfo; - publications = stmt->publication; - /* Load the library providing us libpq calls. */ load_file("libpqwalreceiver", false); + if (stmt->servername) + { + ForeignServer *server; + + Assert(!stmt->conninfo); + conninfo = NULL; + + server = GetForeignServerByName(stmt->servername, false); + aclresult = object_aclcheck(ForeignServerRelationId, server->serverid, owner, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_FOREIGN_SERVER, server->servername); + + /* make sure a user mapping exists */ + GetUserMapping(owner, server->serverid); + + serverid = server->serverid; + conninfo = ForeignServerConnectionString(owner, serverid); + } + else + { + Assert(stmt->conninfo); + + serverid = InvalidOid; + conninfo = stmt->conninfo; + } + /* Check the connection info string. */ walrcv_check_conninfo(conninfo, opts.passwordrequired && !superuser()); + publications = stmt->publication; + /* Everything ok, form a new tuple. */ memset(values, 0, sizeof(values)); memset(nulls, false, sizeof(nulls)); @@ -768,8 +797,12 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, Int32GetDatum(opts.maxretention); values[Anum_pg_subscription_subretentionactive - 1] = Int32GetDatum(opts.retaindeadtuples); - values[Anum_pg_subscription_subconninfo - 1] = - CStringGetTextDatum(conninfo); + values[Anum_pg_subscription_subserver - 1] = serverid; + if (!OidIsValid(serverid)) + values[Anum_pg_subscription_subconninfo - 1] = + CStringGetTextDatum(conninfo); + else + nulls[Anum_pg_subscription_subconninfo - 1] = true; if (opts.slot_name) values[Anum_pg_subscription_subslotname - 1] = DirectFunctionCall1(namein, CStringGetDatum(opts.slot_name)); @@ -792,6 +825,18 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, recordDependencyOnOwner(SubscriptionRelationId, subid, owner); + ObjectAddressSet(myself, SubscriptionRelationId, subid); + + if (stmt->servername) + { + ObjectAddress referenced; + + Assert(OidIsValid(serverid)); + + ObjectAddressSet(referenced, ForeignServerRelationId, serverid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + } + /* * A replication origin is currently created for all subscriptions, * including those that only contain sequences or are otherwise empty. @@ -945,8 +990,6 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, if (opts.enabled || opts.retaindeadtuples) ApplyLauncherWakeupAtCommit(); - ObjectAddressSet(myself, SubscriptionRelationId, subid); - InvokeObjectPostCreateHook(SubscriptionRelationId, subid, 0); return myself; @@ -1410,7 +1453,14 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION, stmt->subname); - sub = GetSubscription(subid, false); + /* + * Skip ACL checks on the subscription's foreign server, if any. If + * changing the server (or replacing it with a raw connection), then the + * old one will be removed anyway. If changing something unrelated, + * there's no need to do an additional ACL check here; that will be done + * by the subscription worker anyway. + */ + sub = GetSubscription(subid, false, false); retain_dead_tuples = sub->retaindeadtuples; origin = sub->origin; @@ -1435,6 +1485,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, memset(nulls, false, sizeof(nulls)); memset(replaces, false, sizeof(replaces)); + ObjectAddressSet(myself, SubscriptionRelationId, subid); + switch (stmt->kind) { case ALTER_SUBSCRIPTION_OPTIONS: @@ -1753,7 +1805,79 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, break; } + case ALTER_SUBSCRIPTION_SERVER: + { + ForeignServer *new_server; + ObjectAddress referenced; + AclResult aclresult; + char *conninfo; + + /* + * Remove what was there before, either another foreign server + * or a connection string. + */ + if (form->subserver) + { + deleteDependencyRecordsForSpecific(SubscriptionRelationId, form->oid, + DEPENDENCY_NORMAL, + ForeignServerRelationId, form->subserver); + } + else + { + nulls[Anum_pg_subscription_subconninfo - 1] = true; + replaces[Anum_pg_subscription_subconninfo - 1] = true; + } + + /* + * Find the new server and user mapping. Check ACL of server + * based on current user ID, but find the user mapping based + * on the subscription owner. + */ + new_server = GetForeignServerByName(stmt->servername, false); + aclresult = object_aclcheck(ForeignServerRelationId, + new_server->serverid, + form->subowner, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", + GetUserNameFromId(form->subowner, false), + ForeignServerName(new_server->serverid)))); + + /* make sure a user mapping exists */ + GetUserMapping(form->subowner, new_server->serverid); + + conninfo = ForeignServerConnectionString(form->subowner, + new_server->serverid); + + /* Load the library providing us libpq calls. */ + load_file("libpqwalreceiver", false); + /* Check the connection info string. */ + walrcv_check_conninfo(conninfo, + sub->passwordrequired && !sub->ownersuperuser); + + values[Anum_pg_subscription_subserver - 1] = new_server->serverid; + replaces[Anum_pg_subscription_subserver - 1] = true; + + ObjectAddressSet(referenced, ForeignServerRelationId, new_server->serverid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + + update_tuple = true; + } + break; + case ALTER_SUBSCRIPTION_CONNECTION: + /* remove reference to foreign server and dependencies, if present */ + if (form->subserver) + { + deleteDependencyRecordsForSpecific(SubscriptionRelationId, form->oid, + DEPENDENCY_NORMAL, + ForeignServerRelationId, form->subserver); + + values[Anum_pg_subscription_subserver - 1] = InvalidOid; + replaces[Anum_pg_subscription_subserver - 1] = true; + } + /* Load the library providing us libpq calls. */ load_file("libpqwalreceiver", false); /* Check the connection info string. */ @@ -2038,8 +2162,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, table_close(rel, RowExclusiveLock); - ObjectAddressSet(myself, SubscriptionRelationId, subid); - InvokeObjectPostAlterHook(SubscriptionRelationId, subid, 0); /* Wake up related replication workers to handle this change quickly. */ @@ -2126,9 +2248,28 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) subname = pstrdup(NameStr(*DatumGetName(datum))); /* Get conninfo */ - datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, - Anum_pg_subscription_subconninfo); - conninfo = TextDatumGetCString(datum); + if (OidIsValid(form->subserver)) + { + AclResult aclresult; + + aclresult = object_aclcheck(ForeignServerRelationId, form->subserver, + form->subowner, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", + GetUserNameFromId(form->subowner, false), + ForeignServerName(form->subserver)))); + + conninfo = ForeignServerConnectionString(form->subowner, + form->subserver); + } + else + { + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subconninfo); + conninfo = TextDatumGetCString(datum); + } /* Get slotname */ datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup, @@ -2227,6 +2368,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) } /* Clean up dependencies */ + deleteDependencyRecordsFor(SubscriptionRelationId, subid, false); deleteSharedDependencyRecordsFor(SubscriptionRelationId, subid, 0); /* Remove any associated relation synchronization states. */ diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c index b912a06dd15..c53699959ea 100644 --- a/src/backend/foreign/foreign.c +++ b/src/backend/foreign/foreign.c @@ -72,6 +72,7 @@ GetForeignDataWrapperExtended(Oid fdwid, bits16 flags) fdw->fdwname = pstrdup(NameStr(fdwform->fdwname)); fdw->fdwhandler = fdwform->fdwhandler; fdw->fdwvalidator = fdwform->fdwvalidator; + fdw->fdwconnection = fdwform->fdwconnection; /* Extract the fdwoptions */ datum = SysCacheGetAttr(FOREIGNDATAWRAPPEROID, @@ -176,6 +177,31 @@ GetForeignServerExtended(Oid serverid, bits16 flags) } +/* + * ForeignServerName - get name of foreign server. + */ +char * +ForeignServerName(Oid serverid) +{ + Form_pg_foreign_server serverform; + char *servername; + HeapTuple tp; + + tp = SearchSysCache1(FOREIGNSERVEROID, ObjectIdGetDatum(serverid)); + + if (!HeapTupleIsValid(tp)) + elog(ERROR, "cache lookup failed for foreign server %u", serverid); + + serverform = (Form_pg_foreign_server) GETSTRUCT(tp); + + servername = pstrdup(NameStr(serverform->srvname)); + + ReleaseSysCache(tp); + + return servername; +} + + /* * GetForeignServerByName - look up the foreign server definition by name. */ @@ -191,6 +217,66 @@ GetForeignServerByName(const char *srvname, bool missing_ok) } +/* + * Retrieve connection string from server's FDW. + */ +char * +ForeignServerConnectionString(Oid userid, Oid serverid) +{ + MemoryContext tempContext; + MemoryContext oldcxt; + volatile text *connection_text = NULL; + char *result = NULL; + + /* + * GetForeignServer, GetForeignDataWrapper, and the connection function + * itself all leak memory into CurrentMemoryContext. Switch to a temporary + * context for easy cleanup. + */ + tempContext = AllocSetContextCreate(CurrentMemoryContext, + "FDWConnectionContext", + ALLOCSET_SMALL_SIZES); + + oldcxt = MemoryContextSwitchTo(tempContext); + + PG_TRY(); + { + ForeignServer *server; + ForeignDataWrapper *fdw; + Datum connection_datum; + + server = GetForeignServer(serverid); + fdw = GetForeignDataWrapper(server->fdwid); + + if (!OidIsValid(fdw->fdwconnection)) + ereport(ERROR, + (errmsg("foreign data wrapper \"%s\" does not support subscription connections", + fdw->fdwname), + errdetail("Foreign data wrapper must be defined with CONNECTION specified."))); + + + connection_datum = OidFunctionCall3(fdw->fdwconnection, + ObjectIdGetDatum(userid), + ObjectIdGetDatum(serverid), + PointerGetDatum(NULL)); + + connection_text = DatumGetTextPP(connection_datum); + } + PG_FINALLY(); + { + MemoryContextSwitchTo(oldcxt); + + if (connection_text) + result = text_to_cstring((text *) connection_text); + + MemoryContextDelete(tempContext); + } + PG_END_TRY(); + + return result; +} + + /* * GetUserMapping - look up the user mapping. * diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index c567252acc4..014a3ec3783 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -5580,6 +5580,8 @@ fdw_option: | NO HANDLER { $$ = makeDefElem("handler", NULL, @1); } | VALIDATOR handler_name { $$ = makeDefElem("validator", (Node *) $2, @1); } | NO VALIDATOR { $$ = makeDefElem("validator", NULL, @1); } + | CONNECTION handler_name { $$ = makeDefElem("connection", (Node *) $2, @1); } + | NO CONNECTION { $$ = makeDefElem("connection", NULL, @1); } ; fdw_options: @@ -11030,6 +11032,16 @@ CreateSubscriptionStmt: n->options = $8; $$ = (Node *) n; } + | CREATE SUBSCRIPTION name SERVER name PUBLICATION name_list opt_definition + { + CreateSubscriptionStmt *n = + makeNode(CreateSubscriptionStmt); + n->subname = $3; + n->servername = $5; + n->publication = $7; + n->options = $8; + $$ = (Node *) n; + } ; /***************************************************************************** @@ -11059,6 +11071,16 @@ AlterSubscriptionStmt: n->conninfo = $5; $$ = (Node *) n; } + | ALTER SUBSCRIPTION name SERVER name + { + AlterSubscriptionStmt *n = + makeNode(AlterSubscriptionStmt); + + n->kind = ALTER_SUBSCRIPTION_SERVER; + n->subname = $3; + n->servername = $5; + $$ = (Node *) n; + } | ALTER SUBSCRIPTION name REFRESH PUBLICATION opt_definition { AlterSubscriptionStmt *n = diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index f9c4b484754..7ac986eb07a 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -5058,7 +5058,7 @@ maybe_reread_subscription(void) /* Ensure allocations in permanent context. */ oldctx = MemoryContextSwitchTo(ApplyContext); - newsub = GetSubscription(MyLogicalRepWorker->subid, true); + newsub = GetSubscription(MyLogicalRepWorker->subid, true, true); /* * Exit if the subscription was removed. This normally should not happen @@ -5200,7 +5200,9 @@ set_wal_receiver_timeout(void) } /* - * Callback from subscription syscache invalidation. + * Callback from subscription syscache invalidation. Also needed for server or + * user mapping invalidation, which can change the connection information for + * subscriptions that connect using a server object. */ static void subscription_change_cb(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) @@ -5805,7 +5807,7 @@ InitializeLogRepWorker(void) */ LockSharedObject(SubscriptionRelationId, MyLogicalRepWorker->subid, 0, AccessShareLock); - MySubscription = GetSubscription(MyLogicalRepWorker->subid, true); + MySubscription = GetSubscription(MyLogicalRepWorker->subid, true, true); if (!MySubscription) { ereport(LOG, @@ -5870,6 +5872,14 @@ InitializeLogRepWorker(void) CacheRegisterSyscacheCallback(SUBSCRIPTIONOID, subscription_change_cb, (Datum) 0); + /* Keep us informed about subscription changes. */ + CacheRegisterSyscacheCallback(FOREIGNSERVEROID, + subscription_change_cb, + (Datum) 0); + /* Keep us informed about subscription changes. */ + CacheRegisterSyscacheCallback(USERMAPPINGOID, + subscription_change_cb, + (Datum) 0); CacheRegisterSyscacheCallback(AUTHOID, subscription_change_cb, diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index dd8adef0a3e..0e94edeec20 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5109,6 +5109,7 @@ getSubscriptions(Archive *fout) int i_subdisableonerr; int i_subpasswordrequired; int i_subrunasowner; + int i_subservername; int i_subconninfo; int i_subslotname; int i_subsynccommit; @@ -5213,14 +5214,24 @@ getSubscriptions(Archive *fout) if (fout->remoteVersion >= 190000) appendPQExpBufferStr(query, - " s.subwalrcvtimeout\n"); + " s.subwalrcvtimeout,\n"); else appendPQExpBufferStr(query, - " '-1' AS subwalrcvtimeout\n"); + " '-1' AS subwalrcvtimeout,\n"); + + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, " fs.srvname AS subservername\n"); + else + appendPQExpBufferStr(query, " NULL AS subservername\n"); appendPQExpBufferStr(query, "FROM pg_subscription s\n"); + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, + "LEFT JOIN pg_catalog.pg_foreign_server fs \n" + " ON fs.oid = s.subserver \n"); + if (dopt->binary_upgrade && fout->remoteVersion >= 170000) appendPQExpBufferStr(query, "LEFT JOIN pg_catalog.pg_replication_origin_status o \n" @@ -5252,6 +5263,7 @@ getSubscriptions(Archive *fout) i_subfailover = PQfnumber(res, "subfailover"); i_subretaindeadtuples = PQfnumber(res, "subretaindeadtuples"); i_submaxretention = PQfnumber(res, "submaxretention"); + i_subservername = PQfnumber(res, "subservername"); i_subconninfo = PQfnumber(res, "subconninfo"); i_subslotname = PQfnumber(res, "subslotname"); i_subsynccommit = PQfnumber(res, "subsynccommit"); @@ -5274,6 +5286,10 @@ getSubscriptions(Archive *fout) subinfo[i].subenabled = (strcmp(PQgetvalue(res, i, i_subenabled), "t") == 0); + if (PQgetisnull(res, i, i_subservername)) + subinfo[i].subservername = NULL; + else + subinfo[i].subservername = pg_strdup(PQgetvalue(res, i, i_subservername)); subinfo[i].subbinary = (strcmp(PQgetvalue(res, i, i_subbinary), "t") == 0); subinfo[i].substream = *(PQgetvalue(res, i, i_substream)); @@ -5290,8 +5306,11 @@ getSubscriptions(Archive *fout) (strcmp(PQgetvalue(res, i, i_subretaindeadtuples), "t") == 0); subinfo[i].submaxretention = atoi(PQgetvalue(res, i, i_submaxretention)); - subinfo[i].subconninfo = - pg_strdup(PQgetvalue(res, i, i_subconninfo)); + if (PQgetisnull(res, i, i_subconninfo)) + subinfo[i].subconninfo = NULL; + else + subinfo[i].subconninfo = + pg_strdup(PQgetvalue(res, i, i_subconninfo)); if (PQgetisnull(res, i, i_subslotname)) subinfo[i].subslotname = NULL; else @@ -5502,9 +5521,17 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo) appendPQExpBuffer(delq, "DROP SUBSCRIPTION %s;\n", qsubname); - appendPQExpBuffer(query, "CREATE SUBSCRIPTION %s CONNECTION ", + appendPQExpBuffer(query, "CREATE SUBSCRIPTION %s ", qsubname); - appendStringLiteralAH(query, subinfo->subconninfo, fout); + if (subinfo->subservername) + { + appendPQExpBuffer(query, "SERVER %s", fmtId(subinfo->subservername)); + } + else + { + appendPQExpBuffer(query, "CONNECTION "); + appendStringLiteralAH(query, subinfo->subconninfo, fout); + } /* Build list of quoted publications and append them to query. */ if (!parsePGArray(subinfo->subpublications, &pubnames, &npubnames)) diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 6deceef23f3..41ed470969c 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -719,6 +719,7 @@ typedef struct _SubscriptionInfo bool subfailover; bool subretaindeadtuples; int submaxretention; + char *subservername; char *subconninfo; char *subslotname; char *subsynccommit; diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index ab13c90ed33..cada709b995 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -6816,7 +6816,7 @@ describeSubscriptions(const char *pattern, bool verbose) printQueryOpt myopt = pset.popt; static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false, false, false, false, - false, false, false, false, false, false}; + false, false, false, false, false, false, false}; if (pset.sversion < 100000) { @@ -6886,6 +6886,10 @@ describeSubscriptions(const char *pattern, bool verbose) gettext_noop("Failover")); if (pset.sversion >= 190000) { + appendPQExpBuffer(&buf, + ", (select srvname from pg_foreign_server where oid=subserver) AS \"%s\"\n", + gettext_noop("Server")); + appendPQExpBuffer(&buf, ", subretaindeadtuples AS \"%s\"\n", gettext_noop("Retain dead tuples")); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 987cce820b9..e3697c17502 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -2332,7 +2332,7 @@ match_previous_words(int pattern_id, else if (Matches("ALTER", "SUBSCRIPTION", MatchAny)) COMPLETE_WITH("CONNECTION", "ENABLE", "DISABLE", "OWNER TO", "RENAME TO", "REFRESH PUBLICATION", "REFRESH SEQUENCES", - "SET", "SKIP (", "ADD PUBLICATION", "DROP PUBLICATION"); + "SERVER", "SET", "SKIP (", "ADD PUBLICATION", "DROP PUBLICATION"); /* ALTER SUBSCRIPTION <name> REFRESH */ else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, MatchAnyN, "REFRESH")) COMPLETE_WITH("PUBLICATION", "SEQUENCES"); @@ -3860,9 +3860,16 @@ match_previous_words(int pattern_id, /* CREATE SUBSCRIPTION */ else if (Matches("CREATE", "SUBSCRIPTION", MatchAny)) - COMPLETE_WITH("CONNECTION"); + COMPLETE_WITH("SERVER", "CONNECTION"); + else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "SERVER", MatchAny)) + COMPLETE_WITH("PUBLICATION"); else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION", MatchAny)) COMPLETE_WITH("PUBLICATION"); + else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "SERVER", + MatchAny, "PUBLICATION")) + { + /* complete with nothing here as this refers to remote publications */ + } else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION", MatchAny, "PUBLICATION")) { diff --git a/src/include/catalog/pg_foreign_data_wrapper.h b/src/include/catalog/pg_foreign_data_wrapper.h index e6009069e82..3d8389de65e 100644 --- a/src/include/catalog/pg_foreign_data_wrapper.h +++ b/src/include/catalog/pg_foreign_data_wrapper.h @@ -38,6 +38,9 @@ CATALOG(pg_foreign_data_wrapper,2328,ForeignDataWrapperRelationId) Oid fdwvalidator BKI_LOOKUP_OPT(pg_proc); /* option validation * function, or 0 if * none */ + Oid fdwconnection BKI_LOOKUP_OPT(pg_proc); /* connection string + * function, or 0 if + * none */ #ifdef CATALOG_VARLEN /* variable-length fields start here */ aclitem fdwacl[1]; /* access permissions */ diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h index c369b5abfb3..bba7a0b68a6 100644 --- a/src/include/catalog/pg_subscription.h +++ b/src/include/catalog/pg_subscription.h @@ -92,9 +92,11 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW * exceeded max_retention_duration, when * defined */ + Oid subserver; /* Set if connecting with server */ + #ifdef CATALOG_VARLEN /* variable-length fields start here */ /* Connection string to the publisher */ - text subconninfo BKI_FORCE_NOT_NULL; + text subconninfo; /* Set if connecting with connection string */ /* Slot name on publisher */ NameData subslotname BKI_FORCE_NULL; @@ -207,7 +209,8 @@ typedef struct Subscription #endif /* EXPOSE_TO_CLIENT_CODE */ -extern Subscription *GetSubscription(Oid subid, bool missing_ok); +extern Subscription *GetSubscription(Oid subid, bool missing_ok, + bool aclcheck); extern void FreeSubscription(Subscription *sub); extern void DisableSubscription(Oid subid); diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h index c185d1458a2..65ed9a7f987 100644 --- a/src/include/foreign/foreign.h +++ b/src/include/foreign/foreign.h @@ -28,6 +28,7 @@ typedef struct ForeignDataWrapper char *fdwname; /* Name of the FDW */ Oid fdwhandler; /* Oid of handler function, or 0 */ Oid fdwvalidator; /* Oid of validator function, or 0 */ + Oid fdwconnection; /* Oid of connection string function, or 0 */ List *options; /* fdwoptions as DefElem list */ } ForeignDataWrapper; @@ -65,10 +66,12 @@ typedef struct ForeignTable extern ForeignServer *GetForeignServer(Oid serverid); +extern char *ForeignServerName(Oid serverid); extern ForeignServer *GetForeignServerExtended(Oid serverid, bits16 flags); extern ForeignServer *GetForeignServerByName(const char *srvname, bool missing_ok); +extern char *ForeignServerConnectionString(Oid userid, Oid serverid); extern UserMapping *GetUserMapping(Oid userid, Oid serverid); extern ForeignDataWrapper *GetForeignDataWrapper(Oid fdwid); extern ForeignDataWrapper *GetForeignDataWrapperExtended(Oid fdwid, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index f37131835be..12e357e8316 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4380,6 +4380,7 @@ typedef struct CreateSubscriptionStmt { NodeTag type; char *subname; /* Name of the subscription */ + char *servername; /* Server name of publisher */ char *conninfo; /* Connection string to publisher */ List *publication; /* One or more publication to subscribe to */ List *options; /* List of DefElem nodes */ @@ -4388,6 +4389,7 @@ typedef struct CreateSubscriptionStmt typedef enum AlterSubscriptionType { ALTER_SUBSCRIPTION_OPTIONS, + ALTER_SUBSCRIPTION_SERVER, ALTER_SUBSCRIPTION_CONNECTION, ALTER_SUBSCRIPTION_SET_PUBLICATION, ALTER_SUBSCRIPTION_ADD_PUBLICATION, @@ -4403,6 +4405,7 @@ typedef struct AlterSubscriptionStmt NodeTag type; AlterSubscriptionType kind; /* ALTER_SUBSCRIPTION_OPTIONS, etc */ char *subname; /* Name of the subscription */ + char *servername; /* Server name of publisher */ char *conninfo; /* Connection string to publisher */ List *publication; /* One or more publication to subscribe to */ List *options; /* List of DefElem nodes */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 25aaae8d05a..8768ce30b6a 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -224,6 +224,7 @@ NOTICE: checking pg_extension {extconfig} => pg_class {oid} NOTICE: checking pg_foreign_data_wrapper {fdwowner} => pg_authid {oid} NOTICE: checking pg_foreign_data_wrapper {fdwhandler} => pg_proc {oid} NOTICE: checking pg_foreign_data_wrapper {fdwvalidator} => pg_proc {oid} +NOTICE: checking pg_foreign_data_wrapper {fdwconnection} => pg_proc {oid} NOTICE: checking pg_foreign_server {srvowner} => pg_authid {oid} NOTICE: checking pg_foreign_server {srvfdw} => pg_foreign_data_wrapper {oid} NOTICE: checking pg_user_mapping {umuser} => pg_authid {oid} diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out index a5fdfe68a0e..3fd8a18b73a 100644 --- a/src/test/regress/expected/subscription.out +++ b/src/test/regress/expected/subscription.out @@ -1,6 +1,14 @@ -- -- SUBSCRIPTION -- +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX +\set regresslib :libdir '/regress' :dlsuffix +CREATE FUNCTION test_fdw_connection(oid, oid, internal) + RETURNS text + AS :'regresslib', 'test_fdw_connection' + LANGUAGE C; CREATE ROLE regress_subscription_user LOGIN SUPERUSER; CREATE ROLE regress_subscription_user2; CREATE ROLE regress_subscription_user3 IN ROLE pg_create_subscription; @@ -116,18 +124,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU 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. \dRs+ regress_testsub4 - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description -------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | none | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | none | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub4 SET (origin = any); \dRs+ regress_testsub4 - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description -------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) DROP SUBSCRIPTION regress_testsub3; @@ -140,15 +148,30 @@ ERROR: invalid connection string syntax: invalid connection option "i_dont_exis -- connecting, so this is reliable and safe) CREATE SUBSCRIPTION regress_testsub5 CONNECTION 'port=-1' PUBLICATION testpub; ERROR: subscription "regress_testsub5" could not connect to the publisher: invalid port number: "-1" +CREATE FOREIGN DATA WRAPPER test_fdw; +CREATE SERVER test_server FOREIGN DATA WRAPPER test_fdw; +CREATE USER MAPPING FOR regress_subscription_user SERVER test_server; +-- fail, need CONNECTION clause +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); +ERROR: foreign data wrapper "test_fdw" does not support subscription connections +DETAIL: Foreign data wrapper must be defined with CONNECTION specified. +ALTER FOREIGN DATA WRAPPER test_fdw CONNECTION test_fdw_connection; +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server 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. +DROP SUBSCRIPTION regress_testsub6; +DROP USER MAPPING FOR regress_subscription_user SERVER test_server; +DROP SERVER test_server; +DROP FOREIGN DATA WRAPPER test_fdw; -- fail - invalid connection string during ALTER ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar'; ERROR: invalid connection string syntax: missing "=" after "foobar" in connection info string \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | test subscription + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | test subscription (1 row) ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false); @@ -157,10 +180,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname'); ALTER SUBSCRIPTION regress_testsub SET (password_required = false); ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | f | t | f | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 | test subscription + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | f | t | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 | test subscription (1 row) ALTER SUBSCRIPTION regress_testsub SET (password_required = true); @@ -176,10 +199,10 @@ ERROR: unrecognized subscription parameter: "create_slot" -- ok ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345'); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00012345 | test subscription + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00012345 | test subscription (1 row) -- ok - with lsn = NONE @@ -188,10 +211,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE); ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0'); ERROR: invalid WAL location (LSN): 0/0 \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 | test subscription + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 | test subscription (1 row) BEGIN; @@ -227,10 +250,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (wal_receiver_timeout = '80s'); ALTER SUBSCRIPTION regress_testsub_foo SET (wal_receiver_timeout = 'foobar'); ERROR: invalid value for parameter "wal_receiver_timeout": "foobar" \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- - regress_testsub_foo | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | local | dbname=regress_doesnotexist2 | 80s | 0/00000000 | test subscription + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub_foo | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | local | dbname=regress_doesnotexist2 | 80s | 0/00000000 | test subscription (1 row) -- rename back to keep the rest simple @@ -259,19 +282,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | t | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | t | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (binary = false); ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) DROP SUBSCRIPTION regress_testsub; @@ -283,27 +306,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | on | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (streaming = false); ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- fail - publication already exists @@ -318,10 +341,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false); ERROR: publication "testpub1" is already in subscription "regress_testsub" \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub,testpub1,testpub2} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub,testpub1,testpub2} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- fail - publication used more than once @@ -336,10 +359,10 @@ ERROR: publication "testpub3" is not in subscription "regress_testsub" -- ok - delete publications ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) DROP SUBSCRIPTION regress_testsub; @@ -375,19 +398,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- we can alter streaming when two_phase enabled ALTER SUBSCRIPTION regress_testsub SET (streaming = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -397,10 +420,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -413,18 +436,18 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | t | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | t | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -437,10 +460,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -454,19 +477,19 @@ NOTICE: max_retention_duration is ineffective when retain_dead_tuples is disabl 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 1000 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 1000 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- ok ALTER SUBSCRIPTION regress_testsub SET (max_retention_duration = 0); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c index a02f41c9727..158c7b7a4c0 100644 --- a/src/test/regress/regress.c +++ b/src/test/regress/regress.c @@ -729,6 +729,13 @@ test_fdw_handler(PG_FUNCTION_ARGS) PG_RETURN_NULL(); } +PG_FUNCTION_INFO_V1(test_fdw_connection); +Datum +test_fdw_connection(PG_FUNCTION_ARGS) +{ + PG_RETURN_TEXT_P(cstring_to_text("dbname=regress_doesnotexist")); +} + PG_FUNCTION_INFO_V1(is_catalog_text_unique_index_oid); Datum is_catalog_text_unique_index_oid(PG_FUNCTION_ARGS) diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql index d93cbc279d9..990d75f1749 100644 --- a/src/test/regress/sql/subscription.sql +++ b/src/test/regress/sql/subscription.sql @@ -2,6 +2,17 @@ -- SUBSCRIPTION -- +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX + +\set regresslib :libdir '/regress' :dlsuffix + +CREATE FUNCTION test_fdw_connection(oid, oid, internal) + RETURNS text + AS :'regresslib', 'test_fdw_connection' + LANGUAGE C; + CREATE ROLE regress_subscription_user LOGIN SUPERUSER; CREATE ROLE regress_subscription_user2; CREATE ROLE regress_subscription_user3 IN ROLE pg_create_subscription; @@ -85,6 +96,21 @@ CREATE SUBSCRIPTION regress_testsub5 CONNECTION 'i_dont_exist=param' PUBLICATION -- connecting, so this is reliable and safe) CREATE SUBSCRIPTION regress_testsub5 CONNECTION 'port=-1' PUBLICATION testpub; +CREATE FOREIGN DATA WRAPPER test_fdw; +CREATE SERVER test_server FOREIGN DATA WRAPPER test_fdw; +CREATE USER MAPPING FOR regress_subscription_user SERVER test_server; + +-- fail, need CONNECTION clause +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); + +ALTER FOREIGN DATA WRAPPER test_fdw CONNECTION test_fdw_connection; +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); +DROP SUBSCRIPTION regress_testsub6; + +DROP USER MAPPING FOR regress_subscription_user SERVER test_server; +DROP SERVER test_server; +DROP FOREIGN DATA WRAPPER test_fdw; + -- fail - invalid connection string during ALTER ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar'; -- 2.43.0 [text/x-patch] v18-0002-dblink-support-foreign-data-wrapper-CONNECTION-c.patch (9.3K, 3-v18-0002-dblink-support-foreign-data-wrapper-CONNECTION-c.patch) download | inline diff: From f49c1d57dd26f0c7c76b794df14963aae5226181 Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Thu, 26 Feb 2026 10:42:08 -0800 Subject: [PATCH v18 2/2] dblink: support foreign data wrapper CONNECTION clause. --- contrib/dblink/Makefile | 2 +- contrib/dblink/dblink--1.2--1.3.sql | 12 ++ .../{dblink--1.2.sql => dblink--1.3.sql} | 11 +- contrib/dblink/dblink.c | 163 +++++++++--------- contrib/dblink/dblink.control | 2 +- contrib/dblink/meson.build | 3 +- 6 files changed, 109 insertions(+), 84 deletions(-) create mode 100644 contrib/dblink/dblink--1.2--1.3.sql rename contrib/dblink/{dblink--1.2.sql => dblink--1.3.sql} (96%) diff --git a/contrib/dblink/Makefile b/contrib/dblink/Makefile index fde0b49ddbb..caa76c9cb27 100644 --- a/contrib/dblink/Makefile +++ b/contrib/dblink/Makefile @@ -8,7 +8,7 @@ PG_CPPFLAGS = -I$(libpq_srcdir) SHLIB_LINK_INTERNAL = $(libpq) EXTENSION = dblink -DATA = dblink--1.2.sql dblink--1.1--1.2.sql dblink--1.0--1.1.sql +DATA = dblink--1.3.sql dblink--1.2--1.3.sql dblink--1.1--1.2.sql dblink--1.0--1.1.sql PGFILEDESC = "dblink - connect to other PostgreSQL databases" REGRESS = dblink diff --git a/contrib/dblink/dblink--1.2--1.3.sql b/contrib/dblink/dblink--1.2--1.3.sql new file mode 100644 index 00000000000..77928a9e656 --- /dev/null +++ b/contrib/dblink/dblink--1.2--1.3.sql @@ -0,0 +1,12 @@ +/* contrib/dblink/dblink--1.2--1.3.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION dblink UPDATE TO '1.3'" to load this file. \quit + +-- takes internal parameter to prevent calling from SQL +CREATE FUNCTION dblink_fdw_connection(oid, oid, internal) +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT PARALLEL RESTRICTED; + +ALTER FOREIGN DATA WRAPPER dblink_fdw CONNECTION dblink_fdw_connection; diff --git a/contrib/dblink/dblink--1.2.sql b/contrib/dblink/dblink--1.3.sql similarity index 96% rename from contrib/dblink/dblink--1.2.sql rename to contrib/dblink/dblink--1.3.sql index 405eccb0ff9..22e4ea2061e 100644 --- a/contrib/dblink/dblink--1.2.sql +++ b/contrib/dblink/dblink--1.3.sql @@ -1,4 +1,4 @@ -/* contrib/dblink/dblink--1.2.sql */ +/* contrib/dblink/dblink--1.3.sql */ -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION dblink" to load this file. \quit @@ -232,4 +232,11 @@ RETURNS void AS 'MODULE_PATHNAME', 'dblink_fdw_validator' LANGUAGE C STRICT PARALLEL SAFE; -CREATE FOREIGN DATA WRAPPER dblink_fdw VALIDATOR dblink_fdw_validator; +-- takes internal parameter to prevent calling from SQL +CREATE FUNCTION dblink_fdw_connection(oid, oid, internal) +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT PARALLEL RESTRICTED; + +CREATE FOREIGN DATA WRAPPER dblink_fdw VALIDATOR dblink_fdw_validator + CONNECTION dblink_fdw_connection; diff --git a/contrib/dblink/dblink.c b/contrib/dblink/dblink.c index 2498d80c8e7..3a4b307ff64 100644 --- a/contrib/dblink/dblink.c +++ b/contrib/dblink/dblink.c @@ -1993,6 +1993,87 @@ dblink_fdw_validator(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } +/* + * Implement FDW CONNECTION clause. + */ +PG_FUNCTION_INFO_V1(dblink_fdw_connection); +Datum +dblink_fdw_connection(PG_FUNCTION_ARGS) +{ + Oid userid = PG_GETARG_OID(0); + Oid serverid = PG_GETARG_OID(1); + ForeignServer *foreign_server = GetForeignServer(serverid); + UserMapping *user_mapping = GetUserMapping(userid, serverid); + ForeignDataWrapper *fdw = GetForeignDataWrapper(foreign_server->fdwid); + AclResult aclresult; + ListCell *cell; + StringInfoData buf; + + static const PQconninfoOption *options = NULL; + + initStringInfo(&buf); + + /* + * Get list of valid libpq options. + * + * To avoid unnecessary work, we get the list once and use it throughout + * the lifetime of this backend process. We don't need to care about + * memory context issues, because PQconndefaults allocates with malloc. + */ + if (!options) + { + options = PQconndefaults(); + if (!options) /* assume reason for failure is OOM */ + ereport(ERROR, + (errcode(ERRCODE_FDW_OUT_OF_MEMORY), + errmsg("out of memory"), + errdetail("Could not get libpq's default connection options."))); + } + + /* Check permissions, user must have usage on the server. */ + aclresult = object_aclcheck(ForeignServerRelationId, serverid, userid, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_FOREIGN_SERVER, foreign_server->servername); + + /* + * First append hardcoded options needed for SCRAM pass-through, so if the + * user overwrites these options we can ereport on dblink_connstr_check + * and dblink_security_check. + */ + if (MyProcPort != NULL && MyProcPort->has_scram_keys && UseScramPassthrough(foreign_server, user_mapping)) + appendSCRAMKeysInfo(&buf); + + foreach(cell, fdw->options) + { + DefElem *def = lfirst(cell); + + if (is_valid_dblink_option(options, def->defname, ForeignDataWrapperRelationId)) + appendStringInfo(&buf, "%s='%s' ", def->defname, + escape_param_str(strVal(def->arg))); + } + + foreach(cell, foreign_server->options) + { + DefElem *def = lfirst(cell); + + if (is_valid_dblink_option(options, def->defname, ForeignServerRelationId)) + appendStringInfo(&buf, "%s='%s' ", def->defname, + escape_param_str(strVal(def->arg))); + } + + foreach(cell, user_mapping->options) + { + + DefElem *def = lfirst(cell); + + if (is_valid_dblink_option(options, def->defname, UserMappingRelationId)) + appendStringInfo(&buf, "%s='%s' ", def->defname, + escape_param_str(strVal(def->arg))); + } + + PG_RETURN_TEXT_P(cstring_to_text(buf.data)); +} + /************************************************************* * internal functions @@ -2855,93 +2936,17 @@ static char * get_connect_string(const char *servername) { ForeignServer *foreign_server = NULL; - UserMapping *user_mapping; - ListCell *cell; - StringInfoData buf; - ForeignDataWrapper *fdw; - AclResult aclresult; char *srvname; - static const PQconninfoOption *options = NULL; - - initStringInfo(&buf); - - /* - * Get list of valid libpq options. - * - * To avoid unnecessary work, we get the list once and use it throughout - * the lifetime of this backend process. We don't need to care about - * memory context issues, because PQconndefaults allocates with malloc. - */ - if (!options) - { - options = PQconndefaults(); - if (!options) /* assume reason for failure is OOM */ - ereport(ERROR, - (errcode(ERRCODE_FDW_OUT_OF_MEMORY), - errmsg("out of memory"), - errdetail("Could not get libpq's default connection options."))); - } - /* first gather the server connstr options */ srvname = pstrdup(servername); truncate_identifier(srvname, strlen(srvname), false); foreign_server = GetForeignServerByName(srvname, true); - if (foreign_server) - { - Oid serverid = foreign_server->serverid; - Oid fdwid = foreign_server->fdwid; - Oid userid = GetUserId(); - - user_mapping = GetUserMapping(userid, serverid); - fdw = GetForeignDataWrapper(fdwid); - - /* Check permissions, user must have usage on the server. */ - aclresult = object_aclcheck(ForeignServerRelationId, serverid, userid, ACL_USAGE); - if (aclresult != ACLCHECK_OK) - aclcheck_error(aclresult, OBJECT_FOREIGN_SERVER, foreign_server->servername); - - /* - * First append hardcoded options needed for SCRAM pass-through, so if - * the user overwrites these options we can ereport on - * dblink_connstr_check and dblink_security_check. - */ - if (MyProcPort != NULL && MyProcPort->has_scram_keys && UseScramPassthrough(foreign_server, user_mapping)) - appendSCRAMKeysInfo(&buf); - - foreach(cell, fdw->options) - { - DefElem *def = lfirst(cell); - - if (is_valid_dblink_option(options, def->defname, ForeignDataWrapperRelationId)) - appendStringInfo(&buf, "%s='%s' ", def->defname, - escape_param_str(strVal(def->arg))); - } - - foreach(cell, foreign_server->options) - { - DefElem *def = lfirst(cell); - - if (is_valid_dblink_option(options, def->defname, ForeignServerRelationId)) - appendStringInfo(&buf, "%s='%s' ", def->defname, - escape_param_str(strVal(def->arg))); - } - - foreach(cell, user_mapping->options) - { - - DefElem *def = lfirst(cell); - - if (is_valid_dblink_option(options, def->defname, UserMappingRelationId)) - appendStringInfo(&buf, "%s='%s' ", def->defname, - escape_param_str(strVal(def->arg))); - } - - return buf.data; - } - else + if (!foreign_server) return NULL; + + return ForeignServerConnectionString(GetUserId(), foreign_server->serverid); } /* diff --git a/contrib/dblink/dblink.control b/contrib/dblink/dblink.control index bdd17d28a4b..816d19f4483 100644 --- a/contrib/dblink/dblink.control +++ b/contrib/dblink/dblink.control @@ -1,5 +1,5 @@ # dblink extension comment = 'connect to other PostgreSQL databases from within a database' -default_version = '1.2' +default_version = '1.3' module_pathname = '$libdir/dblink' relocatable = true diff --git a/contrib/dblink/meson.build b/contrib/dblink/meson.build index e2489f41229..fc91b4a918d 100644 --- a/contrib/dblink/meson.build +++ b/contrib/dblink/meson.build @@ -22,7 +22,8 @@ install_data( 'dblink.control', 'dblink--1.0--1.1.sql', 'dblink--1.1--1.2.sql', - 'dblink--1.2.sql', + 'dblink--1.2--1.3.sql', + 'dblink--1.3.sql', kwargs: contrib_data_args, ) -- 2.43.0 ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> @ 2026-03-03 18:19 ` Masahiko Sawada <[email protected]> 1 sibling, 0 replies; 17+ messages in thread From: Masahiko Sawada @ 2026-03-03 18:19 UTC (permalink / raw) To: Jeff Davis <[email protected]>; +Cc: Shlok Kyal <[email protected]>; Ashutosh Bapat <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Mon, Mar 2, 2026 at 1:34 PM Jeff Davis <[email protected]> wrote: > > On Thu, 2026-02-26 at 11:12 -0800, Jeff Davis wrote: > > On Wed, 2026-02-04 at 13:53 +0900, Masahiko Sawada wrote: > > > I've reviewed the latest patch set. I understand the motivation > > > behind > > > this proposal and find it useful. > > > > Thank you, that's important feedback. > > Attached v18: > > * rebase > * Changed ForeignServerConnectionString() to use a local variable > rather than a static. It's not very performance-sensitive, so it's OK > to create a memory context for each invocation, which will be deleted. > I'm not aware of an actual problem in the previous code, but it seemed > a bit less safe. > > I plan to commit the main patch (v18-0001) soon, after rechecking some > details (like the postgres_fdw upgrade). I have a few minor comments: + Oid subserver; /* Set if connecting with server */ + Do we want to add BKI_LOOKUP(pg_foreign_data_wrapper) here? --- + +# Copyright (c) 2021-2024, PostgreSQL Global Development Group + Need to update the copyright year. The rest looks good to me. > v18-0002 could use some review > first. Thank you for making this patch. I'll look at this patch too. FYI interestingly, dblink_fdw can also be used for subscription connections like postgres_fdw. It made me think that it might be interesting to implement a FDW that supports only the libpq connection (i.e., NO HANDLER, NO VALIDATOR, and CONNECTION) as it provides the connection management capability useful for subscriptions while users can avoid any security risks in postgres_fdw that users might be concerned about. Regards, -- Masahiko Sawada Amazon Web Services: https://aws.amazon.com ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> @ 2026-03-05 03:51 ` Amit Kapila <[email protected]> 2026-03-05 08:52 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 1 sibling, 1 reply; 17+ messages in thread From: Amit Kapila @ 2026-03-05 03:51 UTC (permalink / raw) To: Jeff Davis <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Shlok Kyal <[email protected]>; Ashutosh Bapat <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Tue, Mar 3, 2026 at 3:04 AM Jeff Davis <[email protected]> wrote: > > Attached v18: > I haven't checked the details but while glancing at the patch, I have few observations: 1. @@ -92,9 +92,11 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW * exceeded max_retention_duration, when * defined */ + Oid subserver; /* Set if connecting with server */ + #ifdef CATALOG_VARLEN /* variable-length fields start here */ /* Connection string to the publisher */ - text subconninfo BKI_FORCE_NOT_NULL; + text subconninfo; /* Set if connecting with connection string */ We revoke view rights on subconninfo from the public. See below [A] in system_views.sql. Do we want to do the same for subserver or is it okay for users to see it? I think the following comment and some place in docs needs to be updated. [A] -- All columns of pg_subscription except subconninfo are publicly readable. REVOKE ALL ON pg_subscription FROM public; GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled, subbinary, substream, subtwophasestate, subdisableonerr, subpasswordrequired, subrunasowner, subfailover, subretaindeadtuples, submaxretention, subretentionactive, subslotname, subsynccommit, subpublications, suborigin) ON pg_subscription TO public; 2. We may want to update the following text in pg_dump docs about the new way of connecting to hosts. See [B] (When dumping logical replication subscriptions, pg_dump will generate CREATE SUBSCRIPTION commands that use the connect = false option, so that restoring the subscription does not make remote connections for creating a replication slot or for initial table copy. That way, the dump can be restored without requiring network access to the remote servers. It is then up to the user to reactivate the subscriptions in a suitable way. If the involved hosts have changed, the connection information might have to be changed.) [B] - https://www.postgresql.org/docs/devel/app-pgdump.html -- With Regards, Amit Kapila. ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-05 03:51 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> @ 2026-03-05 08:52 ` Jeff Davis <[email protected]> 2026-03-06 16:19 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Ashutosh Bapat <[email protected]> 2026-03-07 07:01 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 0 siblings, 2 replies; 17+ messages in thread From: Jeff Davis @ 2026-03-05 08:52 UTC (permalink / raw) To: Amit Kapila <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Shlok Kyal <[email protected]>; Ashutosh Bapat <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Thu, 2026-03-05 at 09:21 +0530, Amit Kapila wrote: > We revoke view rights on subconninfo from the public. See below [A] > in > system_views.sql. Do we want to do the same for subserver or is it > okay for users to see it? I can't think of a reason that the server name should be secret, but let me know if you think so. > I think the following comment and some place > in docs needs to be updated. > [A] > -- All columns of pg_subscription except subconninfo are publicly > readable. > REVOKE ALL ON pg_subscription FROM public; > GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, Good catch! Thank you. > 2. We may want to update the following text in pg_dump docs about the > new way of connecting to hosts. See [B] (When dumping logical > replication subscriptions, pg_dump will generate CREATE SUBSCRIPTION > commands that use the connect = false option, so that restoring the > subscription does not make remote connections for creating a > replication slot or for initial table copy. That way, the dump can be > restored without requiring network access to the remote servers. It > is > then up to the user to reactivate the subscriptions in a suitable > way. > If the involved hosts have changed, the connection information might > have to be changed.) > > [B] - https://www.postgresql.org/docs/devel/app-pgdump.html > I think the above comment is still correct -- it would be a bit easier to deal with servers rather than raw connection strings, but the comment already says "...might have to be changed" which is just a reminder to look. Attached a new patch that also addressed the review comments from here: https://www.postgresql.org/message-id/[email protected]... Additionally, I ran into a problem that's worth highlighting: DROP SERVER ... CASCADE was broken, because the subscription is dependent on it but that's in a global catalog, which is not handled by doDeletion(). The subscription is conceptually a per-database object, but it's in a shared catalog with a subdbid field. I solved that problem by adding a guard to findDependentObjects() to check for the referenced object belonging to a shared catalog, and if so it just throws an error (so CASCADE is not supported for servers used in subscriptions). That's a simple but not a very satisfying solution, so let me know if you see a problem with that. Regards, Jeff Davis Attachments: [text/x-patch] v19-0001-CREATE-SUBSCRIPTION-.-SERVER.patch (131.6K, 2-v19-0001-CREATE-SUBSCRIPTION-.-SERVER.patch) download | inline diff: From 752b8a1e07d9541de91873259bb3f7a74fadc0a6 Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Tue, 2 Jan 2024 13:42:48 -0800 Subject: [PATCH v19] CREATE SUBSCRIPTION ... SERVER. Allow CREATE SUBSCRIPTION to accept a foreign server using the SERVER clause instead of a raw connection string using the CONNECTION clause. * Enables a user with sufficient privileges to create a subscription using a foreign server by name without specifying the connection details. * Integrates with user mappings (and other FDW infrastructure) using the subscription owner. * Provides a layer of indirection to manage multiple subscriptions to the same remote server more easily. Also add CREATE FOREIGN DATA WRAPPER ... CONNECTION clause to specify a connection_function. To be eligible for a subscription, the foreign server's foreign data wrapper must specify a connection_function. Add connection_function support to postgres_fdw, and bump postgres_fdw version to 1.3. Bump catversion. Reviewed-by: Ashutosh Bapat <[email protected]> Reviewed-by: Shlok Kyal <[email protected]> Reviewed-by: Masahiko Sawada <[email protected]> Discussion: https://postgr.es/m/[email protected] --- contrib/postgres_fdw/Makefile | 2 +- contrib/postgres_fdw/connection.c | 299 +++++++++++------- .../postgres_fdw/expected/postgres_fdw.out | 8 + contrib/postgres_fdw/meson.build | 2 + .../postgres_fdw/postgres_fdw--1.2--1.3.sql | 12 + contrib/postgres_fdw/postgres_fdw.control | 2 +- contrib/postgres_fdw/sql/postgres_fdw.sql | 7 + contrib/postgres_fdw/t/010_subscription.pl | 71 +++++ doc/src/sgml/logical-replication.sgml | 4 +- doc/src/sgml/postgres-fdw.sgml | 26 ++ .../sgml/ref/alter_foreign_data_wrapper.sgml | 20 ++ doc/src/sgml/ref/alter_subscription.sgml | 18 +- .../sgml/ref/create_foreign_data_wrapper.sgml | 20 ++ doc/src/sgml/ref/create_server.sgml | 7 + doc/src/sgml/ref/create_subscription.sgml | 16 +- src/backend/catalog/dependency.c | 11 + src/backend/catalog/pg_subscription.c | 38 ++- src/backend/catalog/system_views.sql | 2 +- src/backend/commands/foreigncmds.c | 58 +++- src/backend/commands/subscriptioncmds.c | 168 +++++++++- src/backend/foreign/foreign.c | 86 +++++ src/backend/parser/gram.y | 22 ++ src/backend/replication/logical/worker.c | 24 +- src/bin/pg_dump/pg_dump.c | 39 ++- src/bin/pg_dump/pg_dump.h | 1 + src/bin/psql/describe.c | 6 +- src/bin/psql/tab-complete.in.c | 11 +- src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_foreign_data_wrapper.h | 3 + src/include/catalog/pg_subscription.h | 8 +- src/include/foreign/foreign.h | 3 + src/include/nodes/parsenodes.h | 3 + src/test/regress/expected/oidjoins.out | 2 + src/test/regress/expected/subscription.out | 199 ++++++------ src/test/regress/regress.c | 7 + src/test/regress/sql/subscription.sql | 26 ++ 36 files changed, 986 insertions(+), 247 deletions(-) create mode 100644 contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql create mode 100644 contrib/postgres_fdw/t/010_subscription.pl diff --git a/contrib/postgres_fdw/Makefile b/contrib/postgres_fdw/Makefile index 8eaf4d263b6..b8c78b58804 100644 --- a/contrib/postgres_fdw/Makefile +++ b/contrib/postgres_fdw/Makefile @@ -14,7 +14,7 @@ PG_CPPFLAGS = -I$(libpq_srcdir) SHLIB_LINK_INTERNAL = $(libpq) EXTENSION = postgres_fdw -DATA = postgres_fdw--1.0.sql postgres_fdw--1.0--1.1.sql postgres_fdw--1.1--1.2.sql +DATA = postgres_fdw--1.0.sql postgres_fdw--1.0--1.1.sql postgres_fdw--1.1--1.2.sql postgres_fdw--1.2--1.3.sql REGRESS = postgres_fdw query_cancel ISOLATION = eval_plan_qual diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c index 311936406f2..7e2b822d161 100644 --- a/contrib/postgres_fdw/connection.c +++ b/contrib/postgres_fdw/connection.c @@ -132,6 +132,7 @@ PG_FUNCTION_INFO_V1(postgres_fdw_get_connections); PG_FUNCTION_INFO_V1(postgres_fdw_get_connections_1_2); PG_FUNCTION_INFO_V1(postgres_fdw_disconnect); PG_FUNCTION_INFO_V1(postgres_fdw_disconnect_all); +PG_FUNCTION_INFO_V1(postgres_fdw_connection); /* prototypes of private functions */ static void make_new_connection(ConnCacheEntry *entry, UserMapping *user); @@ -477,141 +478,159 @@ pgfdw_security_check(const char **keywords, const char **values, UserMapping *us } /* - * Connect to remote server using specified server and user mapping properties. + * Construct connection params from generic options of ForeignServer and + * UserMapping. (Some of them might not be libpq options, in which case we'll + * just waste a few array slots.) */ -static PGconn * -connect_pg_server(ForeignServer *server, UserMapping *user) +static void +construct_connection_params(ForeignServer *server, UserMapping *user, + const char ***p_keywords, const char ***p_values, + char **p_appname) { - PGconn *volatile conn = NULL; + const char **keywords; + const char **values; + char *appname = NULL; + int n; /* - * Use PG_TRY block to ensure closing connection on error. + * Add 4 extra slots for application_name, fallback_application_name, + * client_encoding, end marker, and 3 extra slots for scram keys and + * required scram pass-through options. */ - PG_TRY(); - { - const char **keywords; - const char **values; - char *appname = NULL; - int n; + n = list_length(server->options) + list_length(user->options) + 4 + 3; + keywords = (const char **) palloc(n * sizeof(char *)); + values = (const char **) palloc(n * sizeof(char *)); - /* - * Construct connection params from generic options of ForeignServer - * and UserMapping. (Some of them might not be libpq options, in - * which case we'll just waste a few array slots.) Add 4 extra slots - * for application_name, fallback_application_name, client_encoding, - * end marker, and 3 extra slots for scram keys and required scram - * pass-through options. - */ - n = list_length(server->options) + list_length(user->options) + 4 + 3; - keywords = (const char **) palloc(n * sizeof(char *)); - values = (const char **) palloc(n * sizeof(char *)); + n = 0; + n += ExtractConnectionOptions(server->options, + keywords + n, values + n); + n += ExtractConnectionOptions(user->options, + keywords + n, values + n); - n = 0; - n += ExtractConnectionOptions(server->options, - keywords + n, values + n); - n += ExtractConnectionOptions(user->options, - keywords + n, values + n); - - /* - * Use pgfdw_application_name as application_name if set. - * - * PQconnectdbParams() processes the parameter arrays from start to - * end. If any key word is repeated, the last value is used. Therefore - * note that pgfdw_application_name must be added to the arrays after - * options of ForeignServer are, so that it can override - * application_name set in ForeignServer. - */ - if (pgfdw_application_name && *pgfdw_application_name != '\0') - { - keywords[n] = "application_name"; - values[n] = pgfdw_application_name; - n++; - } + /* + * Use pgfdw_application_name as application_name if set. + * + * PQconnectdbParams() processes the parameter arrays from start to end. + * If any key word is repeated, the last value is used. Therefore note + * that pgfdw_application_name must be added to the arrays after options + * of ForeignServer are, so that it can override application_name set in + * ForeignServer. + */ + if (pgfdw_application_name && *pgfdw_application_name != '\0') + { + keywords[n] = "application_name"; + values[n] = pgfdw_application_name; + n++; + } - /* - * Search the parameter arrays to find application_name setting, and - * replace escape sequences in it with status information if found. - * The arrays are searched backwards because the last value is used if - * application_name is repeatedly set. - */ - for (int i = n - 1; i >= 0; i--) + /* + * Search the parameter arrays to find application_name setting, and + * replace escape sequences in it with status information if found. The + * arrays are searched backwards because the last value is used if + * application_name is repeatedly set. + */ + for (int i = n - 1; i >= 0; i--) + { + if (strcmp(keywords[i], "application_name") == 0 && + *(values[i]) != '\0') { - if (strcmp(keywords[i], "application_name") == 0 && - *(values[i]) != '\0') + /* + * Use this application_name setting if it's not empty string even + * after any escape sequences in it are replaced. + */ + appname = process_pgfdw_appname(values[i]); + if (appname[0] != '\0') { - /* - * Use this application_name setting if it's not empty string - * even after any escape sequences in it are replaced. - */ - appname = process_pgfdw_appname(values[i]); - if (appname[0] != '\0') - { - values[i] = appname; - break; - } - - /* - * This empty application_name is not used, so we set - * values[i] to NULL and keep searching the array to find the - * next one. - */ - values[i] = NULL; - pfree(appname); - appname = NULL; + values[i] = appname; + break; } + + /* + * This empty application_name is not used, so we set values[i] to + * NULL and keep searching the array to find the next one. + */ + values[i] = NULL; + pfree(appname); + appname = NULL; } + } + + *p_appname = appname; - /* Use "postgres_fdw" as fallback_application_name */ - keywords[n] = "fallback_application_name"; - values[n] = "postgres_fdw"; + /* Use "postgres_fdw" as fallback_application_name */ + keywords[n] = "fallback_application_name"; + values[n] = "postgres_fdw"; + n++; + + /* Set client_encoding so that libpq can convert encoding properly. */ + keywords[n] = "client_encoding"; + values[n] = GetDatabaseEncodingName(); + n++; + + /* Add required SCRAM pass-through connection options if it's enabled. */ + if (MyProcPort != NULL && MyProcPort->has_scram_keys && UseScramPassthrough(server, user)) + { + int len; + int encoded_len; + + keywords[n] = "scram_client_key"; + len = pg_b64_enc_len(sizeof(MyProcPort->scram_ClientKey)); + /* don't forget the zero-terminator */ + values[n] = palloc0(len + 1); + encoded_len = pg_b64_encode(MyProcPort->scram_ClientKey, + sizeof(MyProcPort->scram_ClientKey), + (char *) values[n], len); + if (encoded_len < 0) + elog(ERROR, "could not encode SCRAM client key"); n++; - /* Set client_encoding so that libpq can convert encoding properly. */ - keywords[n] = "client_encoding"; - values[n] = GetDatabaseEncodingName(); + keywords[n] = "scram_server_key"; + len = pg_b64_enc_len(sizeof(MyProcPort->scram_ServerKey)); + /* don't forget the zero-terminator */ + values[n] = palloc0(len + 1); + encoded_len = pg_b64_encode(MyProcPort->scram_ServerKey, + sizeof(MyProcPort->scram_ServerKey), + (char *) values[n], len); + if (encoded_len < 0) + elog(ERROR, "could not encode SCRAM server key"); n++; - /* Add required SCRAM pass-through connection options if it's enabled. */ - if (MyProcPort != NULL && MyProcPort->has_scram_keys && UseScramPassthrough(server, user)) - { - int len; - int encoded_len; - - keywords[n] = "scram_client_key"; - len = pg_b64_enc_len(sizeof(MyProcPort->scram_ClientKey)); - /* don't forget the zero-terminator */ - values[n] = palloc0(len + 1); - encoded_len = pg_b64_encode(MyProcPort->scram_ClientKey, - sizeof(MyProcPort->scram_ClientKey), - (char *) values[n], len); - if (encoded_len < 0) - elog(ERROR, "could not encode SCRAM client key"); - n++; - - keywords[n] = "scram_server_key"; - len = pg_b64_enc_len(sizeof(MyProcPort->scram_ServerKey)); - /* don't forget the zero-terminator */ - values[n] = palloc0(len + 1); - encoded_len = pg_b64_encode(MyProcPort->scram_ServerKey, - sizeof(MyProcPort->scram_ServerKey), - (char *) values[n], len); - if (encoded_len < 0) - elog(ERROR, "could not encode SCRAM server key"); - n++; + /* + * Require scram-sha-256 to ensure that no other auth method is used + * when connecting with foreign server. + */ + keywords[n] = "require_auth"; + values[n] = "scram-sha-256"; + n++; + } - /* - * Require scram-sha-256 to ensure that no other auth method is - * used when connecting with foreign server. - */ - keywords[n] = "require_auth"; - values[n] = "scram-sha-256"; - n++; - } + keywords[n] = values[n] = NULL; + + /* Verify the set of connection parameters. */ + check_conn_params(keywords, values, user); - keywords[n] = values[n] = NULL; + *p_keywords = keywords; + *p_values = values; +} + +/* + * Connect to remote server using specified server and user mapping properties. + */ +static PGconn * +connect_pg_server(ForeignServer *server, UserMapping *user) +{ + PGconn *volatile conn = NULL; + + /* + * Use PG_TRY block to ensure closing connection on error. + */ + PG_TRY(); + { + const char **keywords; + const char **values; + char *appname; - /* Verify the set of connection parameters. */ - check_conn_params(keywords, values, user); + construct_connection_params(server, user, &keywords, &values, &appname); /* first time, allocate or get the custom wait event */ if (pgfdw_we_connect == 0) @@ -2310,6 +2329,56 @@ postgres_fdw_get_connections_internal(FunctionCallInfo fcinfo, } } +/* + * Values in connection strings must be enclosed in single quotes. Single + * quotes and backslashes must be escaped with backslash. NB: these rules are + * different from the rules for escaping a SQL literal. + */ +static void +appendEscapedValue(StringInfo str, const char *val) +{ + appendStringInfoChar(str, '\''); + for (int i = 0; val[i] != '\0'; i++) + { + if (val[i] == '\\' || val[i] == '\'') + appendStringInfoChar(str, '\\'); + appendStringInfoChar(str, val[i]); + } + appendStringInfoChar(str, '\''); +} + +Datum +postgres_fdw_connection(PG_FUNCTION_ARGS) +{ + Oid userid = PG_GETARG_OID(0); + Oid serverid = PG_GETARG_OID(1); + ForeignServer *server = GetForeignServer(serverid); + UserMapping *user = GetUserMapping(userid, serverid); + StringInfoData str; + const char **keywords; + const char **values; + char *appname; + char *sep = ""; + + construct_connection_params(server, user, &keywords, &values, &appname); + + initStringInfo(&str); + for (int i = 0; keywords[i] != NULL; i++) + { + if (values[i] == NULL) + continue; + appendStringInfo(&str, "%s%s = ", sep, keywords[i]); + appendEscapedValue(&str, values[i]); + sep = " "; + } + + if (appname != NULL) + pfree(appname); + pfree(keywords); + pfree(values); + PG_RETURN_TEXT_P(cstring_to_text(str.data)); +} + /* * List active foreign server connections. * diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 2ccb72c539a..0f5271d476e 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -255,6 +255,14 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again ANALYZE ft1; ALTER FOREIGN TABLE ft2 OPTIONS (use_remote_estimate 'true'); -- =================================================================== +-- test subscription +-- =================================================================== +CREATE SUBSCRIPTION regress_pgfdw_subscription SERVER testserver1 + PUBLICATION pub1 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. +DROP SUBSCRIPTION regress_pgfdw_subscription; +-- =================================================================== -- test error case for create publication on foreign table -- =================================================================== CREATE PUBLICATION testpub_ftbl FOR TABLE ft1; -- should fail diff --git a/contrib/postgres_fdw/meson.build b/contrib/postgres_fdw/meson.build index ea4cd9fcd46..3e2ed06b766 100644 --- a/contrib/postgres_fdw/meson.build +++ b/contrib/postgres_fdw/meson.build @@ -27,6 +27,7 @@ install_data( 'postgres_fdw--1.0.sql', 'postgres_fdw--1.0--1.1.sql', 'postgres_fdw--1.1--1.2.sql', + 'postgres_fdw--1.2--1.3.sql', kwargs: contrib_data_args, ) @@ -50,6 +51,7 @@ tests += { 'tap': { 'tests': [ 't/001_auth_scram.pl', + 't/010_subscription.pl', ], }, } diff --git a/contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql b/contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql new file mode 100644 index 00000000000..5bcf0ba2e09 --- /dev/null +++ b/contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql @@ -0,0 +1,12 @@ +/* contrib/postgres_fdw/postgres_fdw--1.2--1.3.sql */ + +-- complain if script is sourced in psql, rather than via ALTER EXTENSION +\echo Use "ALTER EXTENSION postgres_fdw UPDATE TO '1.3'" to load this file. \quit + +-- takes internal parameter to prevent calling from SQL +CREATE FUNCTION postgres_fdw_connection(oid, oid, internal) +RETURNS text +AS 'MODULE_PATHNAME' +LANGUAGE C STRICT PARALLEL RESTRICTED; + +ALTER FOREIGN DATA WRAPPER postgres_fdw CONNECTION postgres_fdw_connection; diff --git a/contrib/postgres_fdw/postgres_fdw.control b/contrib/postgres_fdw/postgres_fdw.control index a4b800be4fc..ae2963d480d 100644 --- a/contrib/postgres_fdw/postgres_fdw.control +++ b/contrib/postgres_fdw/postgres_fdw.control @@ -1,5 +1,5 @@ # postgres_fdw extension comment = 'foreign-data wrapper for remote PostgreSQL servers' -default_version = '1.2' +default_version = '1.3' module_pathname = '$libdir/postgres_fdw' relocatable = true diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index 72d2d9c311b..49ed797e8ef 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -244,6 +244,13 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again ANALYZE ft1; ALTER FOREIGN TABLE ft2 OPTIONS (use_remote_estimate 'true'); +-- =================================================================== +-- test subscription +-- =================================================================== +CREATE SUBSCRIPTION regress_pgfdw_subscription SERVER testserver1 + PUBLICATION pub1 WITH (slot_name = NONE, connect = false); +DROP SUBSCRIPTION regress_pgfdw_subscription; + -- =================================================================== -- test error case for create publication on foreign table -- =================================================================== diff --git a/contrib/postgres_fdw/t/010_subscription.pl b/contrib/postgres_fdw/t/010_subscription.pl new file mode 100644 index 00000000000..1e41091badc --- /dev/null +++ b/contrib/postgres_fdw/t/010_subscription.pl @@ -0,0 +1,71 @@ + +# Copyright (c) 2021-2026, PostgreSQL Global Development Group + +# Basic logical replication test +use strict; +use warnings FATAL => 'all'; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# Initialize publisher node +my $node_publisher = PostgreSQL::Test::Cluster->new('publisher'); +$node_publisher->init(allows_streaming => 'logical'); +$node_publisher->start; + +# Create subscriber node +my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber'); +$node_subscriber->init; +$node_subscriber->start; + +# Create some preexisting content on publisher +$node_publisher->safe_psql('postgres', + "CREATE TABLE tab_ins AS SELECT a, a + 1 as b FROM generate_series(1,1002) AS a"); + +# Replicate the changes without columns +$node_publisher->safe_psql('postgres', "CREATE TABLE tab_no_col()"); +$node_publisher->safe_psql('postgres', + "INSERT INTO tab_no_col default VALUES"); + +# Setup structure on subscriber +$node_subscriber->safe_psql('postgres', "CREATE EXTENSION postgres_fdw"); +$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_ins (a int, b int)"); + +# Setup logical replication +my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres'; +$node_publisher->safe_psql('postgres', "CREATE PUBLICATION tap_pub FOR TABLE tab_ins"); + +my $publisher_host = $node_publisher->host; +my $publisher_port = $node_publisher->port; +$node_subscriber->safe_psql('postgres', + "CREATE SERVER tap_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '$publisher_host', port '$publisher_port', dbname 'postgres')" +); + +$node_subscriber->safe_psql('postgres', + "CREATE USER MAPPING FOR PUBLIC SERVER tap_server" +); + +$node_subscriber->safe_psql('postgres', + "CREATE FOREIGN TABLE f_tab_ins (a int, b int) SERVER tap_server OPTIONS(table_name 'tab_ins')" +); +$node_subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION tap_sub SERVER tap_server PUBLICATION tap_pub WITH (password_required=false)" +); + +# Wait for initial table sync to finish +$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); + +my $result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM (SELECT f.b = l.b as match FROM tab_ins l, f_tab_ins f WHERE l.a = f.a) WHERE match"); +is($result, qq(1002), 'check that initial data was copied to subscriber'); + +$node_publisher->safe_psql('postgres', + "INSERT INTO tab_ins SELECT a, a + 1 FROM generate_series(1003,1050) a"); + +$node_publisher->wait_for_catchup('tap_sub'); + +$result = + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM (SELECT f.b = l.b as match FROM tab_ins l, f_tab_ins f WHERE l.a = f.a) WHERE match"); +is($result, qq(1050), 'check that inserted data was copied to subscriber'); + +done_testing(); diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index bcb473c078b..72c8d3d59bd 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -2577,7 +2577,9 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER <para> To create a subscription, the user must have the privileges of the <literal>pg_create_subscription</literal> role, as well as - <literal>CREATE</literal> privileges on the database. + <literal>CREATE</literal> privileges on the database. If + <literal>SERVER</literal> is specified, the user also must have + <literal>USAGE</literal> privileges on the server. </para> <para> diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index fcf10e4317e..de69ddcdebc 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -1049,6 +1049,32 @@ postgres=# SELECT postgres_fdw_disconnect_all(); </para> </sect2> + <sect2 id="postgres-fdw-server-subscription"> + <title>Subscription Management</title> + + <para> + <filename>postgres_fdw</filename> supports subscription connections using + the same options described in <xref + linkend="postgres-fdw-options-connection"/>. + </para> + + <para> + For example, assuming the remote server <literal>foreign-host</literal> has + a publication <literal>testpub</literal>: +<programlisting> +CREATE SERVER subscription_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'foreign-host', dbname 'foreign_db'); +CREATE USER MAPPING FOR local_user SERVER subscription_server OPTIONS (user 'foreign_user', password 'password'); +CREATE SUBSCRIPTION my_subscription SERVER subscription_server PUBLICATION testpub; +</programlisting> + </para> + + <para> + To create a subscription, the user must be a member of the <xref + linkend="predefined-role-pg-create-subscription"/> role and have + <literal>USAGE</literal> privileges on the server. + </para> + </sect2> + <sect2 id="postgres-fdw-transaction-management"> <title>Transaction Management</title> diff --git a/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml b/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml index dc0957d965a..640c02893cf 100644 --- a/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml +++ b/doc/src/sgml/ref/alter_foreign_data_wrapper.sgml @@ -24,6 +24,7 @@ PostgreSQL documentation ALTER FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> [ HANDLER <replaceable class="parameter">handler_function</replaceable> | NO HANDLER ] [ VALIDATOR <replaceable class="parameter">validator_function</replaceable> | NO VALIDATOR ] + [ CONNECTION <replaceable class="parameter">connection_function</replaceable> | NO CONNECTION ] [ OPTIONS ( [ ADD | SET | DROP ] <replaceable class="parameter">option</replaceable> ['<replaceable class="parameter">value</replaceable>'] [, ... ]) ] ALTER FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> OWNER TO { <replaceable>new_owner</replaceable> | CURRENT_ROLE | CURRENT_USER | SESSION_USER } ALTER FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> RENAME TO <replaceable>new_name</replaceable> @@ -112,6 +113,25 @@ ALTER FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> REN </listitem> </varlistentry> + <varlistentry> + <term><literal>CONNECTION <replaceable class="parameter">connection_function</replaceable></literal></term> + <listitem> + <para> + Specifies a new connection function for the foreign-data wrapper. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>NO CONNECTION</literal></term> + <listitem> + <para> + This is used to specify that the foreign-data wrapper should no + longer have a connection function. + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>OPTIONS ( [ ADD | SET | DROP ] <replaceable class="parameter">option</replaceable> ['<replaceable class="parameter">value</replaceable>'] [, ... ] )</literal></term> <listitem> diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml index 5318998e80c..f215fb0e5a2 100644 --- a/doc/src/sgml/ref/alter_subscription.sgml +++ b/doc/src/sgml/ref/alter_subscription.sgml @@ -21,6 +21,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> +ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> SERVER <replaceable>servername</replaceable> ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> CONNECTION '<replaceable>conninfo</replaceable>' ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> SET PUBLICATION <replaceable class="parameter">publication_name</replaceable> [, ...] [ WITH ( <replaceable class="parameter">publication_option</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ] ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> ADD PUBLICATION <replaceable class="parameter">publication_name</replaceable> [, ...] [ WITH ( <replaceable class="parameter">publication_option</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ] @@ -102,13 +103,24 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO < </listitem> </varlistentry> + <varlistentry id="sql-altersubscription-params-server"> + <term><literal>SERVER <replaceable class="parameter">servername</replaceable></literal></term> + <listitem> + <para> + This clause replaces the foreign server or connection string originally + set by <xref linkend="sql-createsubscription"/> with the foreign server + <replaceable>servername</replaceable>. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-altersubscription-params-connection"> <term><literal>CONNECTION '<replaceable class="parameter">conninfo</replaceable>'</literal></term> <listitem> <para> - This clause replaces the connection string originally set by - <xref linkend="sql-createsubscription"/>. See there for more - information. + This clause replaces the foreign server or connection string originally + set by <xref linkend="sql-createsubscription"/> with the connection + string <replaceable>conninfo</replaceable>. </para> </listitem> </varlistentry> diff --git a/doc/src/sgml/ref/create_foreign_data_wrapper.sgml b/doc/src/sgml/ref/create_foreign_data_wrapper.sgml index 0fcba18a347..7b83f500b25 100644 --- a/doc/src/sgml/ref/create_foreign_data_wrapper.sgml +++ b/doc/src/sgml/ref/create_foreign_data_wrapper.sgml @@ -24,6 +24,7 @@ PostgreSQL documentation CREATE FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> [ HANDLER <replaceable class="parameter">handler_function</replaceable> | NO HANDLER ] [ VALIDATOR <replaceable class="parameter">validator_function</replaceable> | NO VALIDATOR ] + [ CONNECTION <replaceable class="parameter">connection_function</replaceable> | NO CONNECTION ] [ OPTIONS ( <replaceable class="parameter">option</replaceable> '<replaceable class="parameter">value</replaceable>' [, ... ] ) ] </synopsis> </refsynopsisdiv> @@ -99,6 +100,25 @@ CREATE FOREIGN DATA WRAPPER <replaceable class="parameter">name</replaceable> </listitem> </varlistentry> + <varlistentry> + <term><literal>CONNECTION <replaceable class="parameter">connection_function</replaceable></literal></term> + <listitem> + <para> + <replaceable class="parameter">connection_function</replaceable> is the + name of a previously registered function that will be called to generate + the postgres connection string when a foreign server is used as part of + <xref linkend="sql-createsubscription"/>. If no connection function or + <literal>NO CONNECTION</literal> is specified, then servers using this + foreign data wrapper cannot be used for <literal>CREATE + SUBSCRIPTION</literal>. The connection function must take three + arguments: one of type <type>oid</type> for the user, one of type + <type>oid</type> for the server, and an unused third argument of type + <type>internal</type> (which prevents calling the function in other + contexts). + </para> + </listitem> + </varlistentry> + <varlistentry> <term><literal>OPTIONS ( <replaceable class="parameter">option</replaceable> '<replaceable class="parameter">value</replaceable>' [, ... ] )</literal></term> <listitem> diff --git a/doc/src/sgml/ref/create_server.sgml b/doc/src/sgml/ref/create_server.sgml index 05f4019453b..ce4a064eabb 100644 --- a/doc/src/sgml/ref/create_server.sgml +++ b/doc/src/sgml/ref/create_server.sgml @@ -42,6 +42,13 @@ CREATE SERVER [ IF NOT EXISTS ] <replaceable class="parameter">server_name</repl means of user mappings. </para> + <para> + If the foreign data wrapper <replaceable>fdw_name</replaceable> is + specified with a <literal>CONNECTION</literal> clause, then <xref + linkend="sql-createsubscription"/> may use this foreign server for + connection information. + </para> + <para> The server name must be unique within the database. </para> diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml index eb0cc645d8f..07d5b1bd77c 100644 --- a/doc/src/sgml/ref/create_subscription.sgml +++ b/doc/src/sgml/ref/create_subscription.sgml @@ -22,7 +22,7 @@ PostgreSQL documentation <refsynopsisdiv> <synopsis> CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceable> - CONNECTION '<replaceable class="parameter">conninfo</replaceable>' + { SERVER <replaceable class="parameter">servername</replaceable> | CONNECTION '<replaceable class="parameter">conninfo</replaceable>' } PUBLICATION <replaceable class="parameter">publication_name</replaceable> [, ...] [ WITH ( <replaceable class="parameter">subscription_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ] </synopsis> @@ -77,6 +77,20 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl </listitem> </varlistentry> + <varlistentry id="sql-createsubscription-params-server"> + <term><literal>SERVER <replaceable class="parameter">servername</replaceable></literal></term> + <listitem> + <para> + A foreign server to use for the connection. The server's foreign data + wrapper must have a <replaceable>connection_function</replaceable> + registered, and a user mapping for the subscription owner on the server + must exist. Additionally, the subscription owner must have + <literal>USAGE</literal> privileges on + <replaceable>servername</replaceable>. + </para> + </listitem> + </varlistentry> + <varlistentry id="sql-createsubscription-params-connection"> <term><literal>CONNECTION '<replaceable class="parameter">conninfo</replaceable>'</literal></term> <listitem> diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index 570c434ede8..09575278de3 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -895,6 +895,17 @@ findDependentObjects(const ObjectAddress *object, object->objectSubId == 0) continue; + /* + * Check that the dependent object is not in a shared catalog, which + * is not supported by doDeletion(). + */ + if (IsSharedRelation(otherObject.classId)) + ereport(ERROR, + (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST), + errmsg("cannot drop %s because %s depends on it", + getObjectDescription(object, false), + getObjectDescription(&otherObject, false)))); + /* * Must lock the dependent object before recursing to it. */ diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c index acf42b853ed..3673d4f0bc1 100644 --- a/src/backend/catalog/pg_subscription.c +++ b/src/backend/catalog/pg_subscription.c @@ -19,11 +19,14 @@ #include "access/htup_details.h" #include "access/tableam.h" #include "catalog/indexing.h" +#include "catalog/pg_foreign_server.h" #include "catalog/pg_subscription.h" #include "catalog/pg_subscription_rel.h" #include "catalog/pg_type.h" +#include "foreign/foreign.h" #include "miscadmin.h" #include "storage/lmgr.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -69,7 +72,7 @@ GetPublicationsStr(List *publications, StringInfo dest, bool quote_literal) * Fetch the subscription from the syscache. */ Subscription * -GetSubscription(Oid subid, bool missing_ok) +GetSubscription(Oid subid, bool missing_ok, bool aclcheck) { HeapTuple tup; Subscription *sub; @@ -108,10 +111,35 @@ GetSubscription(Oid subid, bool missing_ok) sub->retentionactive = subform->subretentionactive; /* Get conninfo */ - datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, - tup, - Anum_pg_subscription_subconninfo); - sub->conninfo = TextDatumGetCString(datum); + if (OidIsValid(subform->subserver)) + { + AclResult aclresult; + + /* recheck ACL if requested */ + if (aclcheck) + { + aclresult = object_aclcheck(ForeignServerRelationId, + subform->subserver, + subform->subowner, ACL_USAGE); + + if (aclresult != ACLCHECK_OK) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", + GetUserNameFromId(subform->subowner, false), + ForeignServerName(subform->subserver)))); + } + + sub->conninfo = ForeignServerConnectionString(subform->subowner, + subform->subserver); + } + else + { + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, + tup, + Anum_pg_subscription_subconninfo); + sub->conninfo = TextDatumGetCString(datum); + } /* Get slotname */ datum = SysCacheGetAttr(SUBSCRIPTIONOID, diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 1ea8f1faa9e..dcf0f8ef4fe 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1442,7 +1442,7 @@ GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled, subbinary, substream, subtwophasestate, subdisableonerr, subpasswordrequired, subrunasowner, subfailover, subretaindeadtuples, submaxretention, subretentionactive, - subslotname, subsynccommit, subpublications, suborigin) + subserver, subslotname, subsynccommit, subpublications, suborigin) ON pg_subscription TO public; CREATE VIEW pg_stat_subscription_stats AS diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c index b56d1ad6785..45681235782 100644 --- a/src/backend/commands/foreigncmds.c +++ b/src/backend/commands/foreigncmds.c @@ -522,21 +522,53 @@ lookup_fdw_validator_func(DefElem *validator) /* validator's return value is ignored, so we don't check the type */ } +/* + * Convert a connection string function name passed from the parser to an Oid. + */ +static Oid +lookup_fdw_connection_func(DefElem *connection) +{ + Oid connectionOid; + Oid funcargtypes[3]; + + if (connection == NULL || connection->arg == NULL) + return InvalidOid; + + /* connection string functions take user oid, server oid */ + funcargtypes[0] = OIDOID; + funcargtypes[1] = OIDOID; + funcargtypes[2] = INTERNALOID; + + connectionOid = LookupFuncName((List *) connection->arg, 3, funcargtypes, false); + + /* check that connection string function has correct return type */ + if (get_func_rettype(connectionOid) != TEXTOID) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("function %s must return type %s", + NameListToString((List *) connection->arg), "text"))); + + return connectionOid; +} + /* * Process function options of CREATE/ALTER FDW */ static void parse_func_options(ParseState *pstate, List *func_options, bool *handler_given, Oid *fdwhandler, - bool *validator_given, Oid *fdwvalidator) + bool *validator_given, Oid *fdwvalidator, + bool *connection_given, Oid *fdwconnection) { ListCell *cell; *handler_given = false; *validator_given = false; + *connection_given = false; /* return InvalidOid if not given */ *fdwhandler = InvalidOid; *fdwvalidator = InvalidOid; + *fdwconnection = InvalidOid; foreach(cell, func_options) { @@ -556,6 +588,13 @@ parse_func_options(ParseState *pstate, List *func_options, *validator_given = true; *fdwvalidator = lookup_fdw_validator_func(def); } + else if (strcmp(def->defname, "connection") == 0) + { + if (*connection_given) + errorConflictingDefElem(def, pstate); + *connection_given = true; + *fdwconnection = lookup_fdw_connection_func(def); + } else elog(ERROR, "option \"%s\" not recognized", def->defname); @@ -575,8 +614,10 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) Oid fdwId; bool handler_given; bool validator_given; + bool connection_given; Oid fdwhandler; Oid fdwvalidator; + Oid fdwconnection; Datum fdwoptions; Oid ownerId; ObjectAddress myself; @@ -620,10 +661,12 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) /* Lookup handler and validator functions, if given */ parse_func_options(pstate, stmt->func_options, &handler_given, &fdwhandler, - &validator_given, &fdwvalidator); + &validator_given, &fdwvalidator, + &connection_given, &fdwconnection); values[Anum_pg_foreign_data_wrapper_fdwhandler - 1] = ObjectIdGetDatum(fdwhandler); values[Anum_pg_foreign_data_wrapper_fdwvalidator - 1] = ObjectIdGetDatum(fdwvalidator); + values[Anum_pg_foreign_data_wrapper_fdwconnection - 1] = ObjectIdGetDatum(fdwconnection); nulls[Anum_pg_foreign_data_wrapper_fdwacl - 1] = true; @@ -695,8 +738,10 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) Datum datum; bool handler_given; bool validator_given; + bool connection_given; Oid fdwhandler; Oid fdwvalidator; + Oid fdwconnection; ObjectAddress myself; rel = table_open(ForeignDataWrapperRelationId, RowExclusiveLock); @@ -726,7 +771,8 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) parse_func_options(pstate, stmt->func_options, &handler_given, &fdwhandler, - &validator_given, &fdwvalidator); + &validator_given, &fdwvalidator, + &connection_given, &fdwconnection); if (handler_given) { @@ -764,6 +810,12 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) fdwvalidator = fdwForm->fdwvalidator; } + if (connection_given) + { + repl_val[Anum_pg_foreign_data_wrapper_fdwconnection - 1] = ObjectIdGetDatum(fdwconnection); + repl_repl[Anum_pg_foreign_data_wrapper_fdwconnection - 1] = true; + } + /* * If options specified, validate and update. */ diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index 5e3c0964d38..091e7b7372d 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -27,13 +27,16 @@ #include "catalog/objectaddress.h" #include "catalog/pg_authid_d.h" #include "catalog/pg_database_d.h" +#include "catalog/pg_foreign_server.h" #include "catalog/pg_subscription.h" #include "catalog/pg_subscription_rel.h" #include "catalog/pg_type.h" +#include "catalog/pg_user_mapping.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/subscriptioncmds.h" #include "executor/executor.h" +#include "foreign/foreign.h" #include "miscadmin.h" #include "nodes/makefuncs.h" #include "pgstat.h" @@ -619,6 +622,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, Datum values[Natts_pg_subscription]; Oid owner = GetUserId(); HeapTuple tup; + Oid serverid; char *conninfo; char originname[NAMEDATALEN]; List *publications; @@ -730,15 +734,40 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, if (opts.wal_receiver_timeout == NULL) opts.wal_receiver_timeout = "-1"; - conninfo = stmt->conninfo; - publications = stmt->publication; - /* Load the library providing us libpq calls. */ load_file("libpqwalreceiver", false); + if (stmt->servername) + { + ForeignServer *server; + + Assert(!stmt->conninfo); + conninfo = NULL; + + server = GetForeignServerByName(stmt->servername, false); + aclresult = object_aclcheck(ForeignServerRelationId, server->serverid, owner, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + aclcheck_error(aclresult, OBJECT_FOREIGN_SERVER, server->servername); + + /* make sure a user mapping exists */ + GetUserMapping(owner, server->serverid); + + serverid = server->serverid; + conninfo = ForeignServerConnectionString(owner, serverid); + } + else + { + Assert(stmt->conninfo); + + serverid = InvalidOid; + conninfo = stmt->conninfo; + } + /* Check the connection info string. */ walrcv_check_conninfo(conninfo, opts.passwordrequired && !superuser()); + publications = stmt->publication; + /* Everything ok, form a new tuple. */ memset(values, 0, sizeof(values)); memset(nulls, false, sizeof(nulls)); @@ -768,8 +797,12 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, Int32GetDatum(opts.maxretention); values[Anum_pg_subscription_subretentionactive - 1] = Int32GetDatum(opts.retaindeadtuples); - values[Anum_pg_subscription_subconninfo - 1] = - CStringGetTextDatum(conninfo); + values[Anum_pg_subscription_subserver - 1] = serverid; + if (!OidIsValid(serverid)) + values[Anum_pg_subscription_subconninfo - 1] = + CStringGetTextDatum(conninfo); + else + nulls[Anum_pg_subscription_subconninfo - 1] = true; if (opts.slot_name) values[Anum_pg_subscription_subslotname - 1] = DirectFunctionCall1(namein, CStringGetDatum(opts.slot_name)); @@ -792,6 +825,18 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, recordDependencyOnOwner(SubscriptionRelationId, subid, owner); + ObjectAddressSet(myself, SubscriptionRelationId, subid); + + if (stmt->servername) + { + ObjectAddress referenced; + + Assert(OidIsValid(serverid)); + + ObjectAddressSet(referenced, ForeignServerRelationId, serverid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + } + /* * A replication origin is currently created for all subscriptions, * including those that only contain sequences or are otherwise empty. @@ -945,8 +990,6 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, if (opts.enabled || opts.retaindeadtuples) ApplyLauncherWakeupAtCommit(); - ObjectAddressSet(myself, SubscriptionRelationId, subid); - InvokeObjectPostCreateHook(SubscriptionRelationId, subid, 0); return myself; @@ -1410,7 +1453,14 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_SUBSCRIPTION, stmt->subname); - sub = GetSubscription(subid, false); + /* + * Skip ACL checks on the subscription's foreign server, if any. If + * changing the server (or replacing it with a raw connection), then the + * old one will be removed anyway. If changing something unrelated, + * there's no need to do an additional ACL check here; that will be done + * by the subscription worker anyway. + */ + sub = GetSubscription(subid, false, false); retain_dead_tuples = sub->retaindeadtuples; origin = sub->origin; @@ -1435,6 +1485,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, memset(nulls, false, sizeof(nulls)); memset(replaces, false, sizeof(replaces)); + ObjectAddressSet(myself, SubscriptionRelationId, subid); + switch (stmt->kind) { case ALTER_SUBSCRIPTION_OPTIONS: @@ -1753,7 +1805,79 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, break; } + case ALTER_SUBSCRIPTION_SERVER: + { + ForeignServer *new_server; + ObjectAddress referenced; + AclResult aclresult; + char *conninfo; + + /* + * Remove what was there before, either another foreign server + * or a connection string. + */ + if (form->subserver) + { + deleteDependencyRecordsForSpecific(SubscriptionRelationId, form->oid, + DEPENDENCY_NORMAL, + ForeignServerRelationId, form->subserver); + } + else + { + nulls[Anum_pg_subscription_subconninfo - 1] = true; + replaces[Anum_pg_subscription_subconninfo - 1] = true; + } + + /* + * Find the new server and user mapping. Check ACL of server + * based on current user ID, but find the user mapping based + * on the subscription owner. + */ + new_server = GetForeignServerByName(stmt->servername, false); + aclresult = object_aclcheck(ForeignServerRelationId, + new_server->serverid, + form->subowner, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", + GetUserNameFromId(form->subowner, false), + ForeignServerName(new_server->serverid)))); + + /* make sure a user mapping exists */ + GetUserMapping(form->subowner, new_server->serverid); + + conninfo = ForeignServerConnectionString(form->subowner, + new_server->serverid); + + /* Load the library providing us libpq calls. */ + load_file("libpqwalreceiver", false); + /* Check the connection info string. */ + walrcv_check_conninfo(conninfo, + sub->passwordrequired && !sub->ownersuperuser); + + values[Anum_pg_subscription_subserver - 1] = new_server->serverid; + replaces[Anum_pg_subscription_subserver - 1] = true; + + ObjectAddressSet(referenced, ForeignServerRelationId, new_server->serverid); + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + + update_tuple = true; + } + break; + case ALTER_SUBSCRIPTION_CONNECTION: + /* remove reference to foreign server and dependencies, if present */ + if (form->subserver) + { + deleteDependencyRecordsForSpecific(SubscriptionRelationId, form->oid, + DEPENDENCY_NORMAL, + ForeignServerRelationId, form->subserver); + + values[Anum_pg_subscription_subserver - 1] = InvalidOid; + replaces[Anum_pg_subscription_subserver - 1] = true; + } + /* Load the library providing us libpq calls. */ load_file("libpqwalreceiver", false); /* Check the connection info string. */ @@ -2038,8 +2162,6 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, table_close(rel, RowExclusiveLock); - ObjectAddressSet(myself, SubscriptionRelationId, subid); - InvokeObjectPostAlterHook(SubscriptionRelationId, subid, 0); /* Wake up related replication workers to handle this change quickly. */ @@ -2126,9 +2248,28 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) subname = pstrdup(NameStr(*DatumGetName(datum))); /* Get conninfo */ - datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, - Anum_pg_subscription_subconninfo); - conninfo = TextDatumGetCString(datum); + if (OidIsValid(form->subserver)) + { + AclResult aclresult; + + aclresult = object_aclcheck(ForeignServerRelationId, form->subserver, + form->subowner, ACL_USAGE); + if (aclresult != ACLCHECK_OK) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", + GetUserNameFromId(form->subowner, false), + ForeignServerName(form->subserver)))); + + conninfo = ForeignServerConnectionString(form->subowner, + form->subserver); + } + else + { + datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, + Anum_pg_subscription_subconninfo); + conninfo = TextDatumGetCString(datum); + } /* Get slotname */ datum = SysCacheGetAttr(SUBSCRIPTIONOID, tup, @@ -2227,6 +2368,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) } /* Clean up dependencies */ + deleteDependencyRecordsFor(SubscriptionRelationId, subid, false); deleteSharedDependencyRecordsFor(SubscriptionRelationId, subid, 0); /* Remove any associated relation synchronization states. */ diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c index b912a06dd15..c53699959ea 100644 --- a/src/backend/foreign/foreign.c +++ b/src/backend/foreign/foreign.c @@ -72,6 +72,7 @@ GetForeignDataWrapperExtended(Oid fdwid, bits16 flags) fdw->fdwname = pstrdup(NameStr(fdwform->fdwname)); fdw->fdwhandler = fdwform->fdwhandler; fdw->fdwvalidator = fdwform->fdwvalidator; + fdw->fdwconnection = fdwform->fdwconnection; /* Extract the fdwoptions */ datum = SysCacheGetAttr(FOREIGNDATAWRAPPEROID, @@ -176,6 +177,31 @@ GetForeignServerExtended(Oid serverid, bits16 flags) } +/* + * ForeignServerName - get name of foreign server. + */ +char * +ForeignServerName(Oid serverid) +{ + Form_pg_foreign_server serverform; + char *servername; + HeapTuple tp; + + tp = SearchSysCache1(FOREIGNSERVEROID, ObjectIdGetDatum(serverid)); + + if (!HeapTupleIsValid(tp)) + elog(ERROR, "cache lookup failed for foreign server %u", serverid); + + serverform = (Form_pg_foreign_server) GETSTRUCT(tp); + + servername = pstrdup(NameStr(serverform->srvname)); + + ReleaseSysCache(tp); + + return servername; +} + + /* * GetForeignServerByName - look up the foreign server definition by name. */ @@ -191,6 +217,66 @@ GetForeignServerByName(const char *srvname, bool missing_ok) } +/* + * Retrieve connection string from server's FDW. + */ +char * +ForeignServerConnectionString(Oid userid, Oid serverid) +{ + MemoryContext tempContext; + MemoryContext oldcxt; + volatile text *connection_text = NULL; + char *result = NULL; + + /* + * GetForeignServer, GetForeignDataWrapper, and the connection function + * itself all leak memory into CurrentMemoryContext. Switch to a temporary + * context for easy cleanup. + */ + tempContext = AllocSetContextCreate(CurrentMemoryContext, + "FDWConnectionContext", + ALLOCSET_SMALL_SIZES); + + oldcxt = MemoryContextSwitchTo(tempContext); + + PG_TRY(); + { + ForeignServer *server; + ForeignDataWrapper *fdw; + Datum connection_datum; + + server = GetForeignServer(serverid); + fdw = GetForeignDataWrapper(server->fdwid); + + if (!OidIsValid(fdw->fdwconnection)) + ereport(ERROR, + (errmsg("foreign data wrapper \"%s\" does not support subscription connections", + fdw->fdwname), + errdetail("Foreign data wrapper must be defined with CONNECTION specified."))); + + + connection_datum = OidFunctionCall3(fdw->fdwconnection, + ObjectIdGetDatum(userid), + ObjectIdGetDatum(serverid), + PointerGetDatum(NULL)); + + connection_text = DatumGetTextPP(connection_datum); + } + PG_FINALLY(); + { + MemoryContextSwitchTo(oldcxt); + + if (connection_text) + result = text_to_cstring((text *) connection_text); + + MemoryContextDelete(tempContext); + } + PG_END_TRY(); + + return result; +} + + /* * GetUserMapping - look up the user mapping. * diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 3c3e24324a8..9cbe8eafc45 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -5583,6 +5583,8 @@ fdw_option: | NO HANDLER { $$ = makeDefElem("handler", NULL, @1); } | VALIDATOR handler_name { $$ = makeDefElem("validator", (Node *) $2, @1); } | NO VALIDATOR { $$ = makeDefElem("validator", NULL, @1); } + | CONNECTION handler_name { $$ = makeDefElem("connection", (Node *) $2, @1); } + | NO CONNECTION { $$ = makeDefElem("connection", NULL, @1); } ; fdw_options: @@ -11057,6 +11059,16 @@ CreateSubscriptionStmt: n->options = $8; $$ = (Node *) n; } + | CREATE SUBSCRIPTION name SERVER name PUBLICATION name_list opt_definition + { + CreateSubscriptionStmt *n = + makeNode(CreateSubscriptionStmt); + n->subname = $3; + n->servername = $5; + n->publication = $7; + n->options = $8; + $$ = (Node *) n; + } ; /***************************************************************************** @@ -11086,6 +11098,16 @@ AlterSubscriptionStmt: n->conninfo = $5; $$ = (Node *) n; } + | ALTER SUBSCRIPTION name SERVER name + { + AlterSubscriptionStmt *n = + makeNode(AlterSubscriptionStmt); + + n->kind = ALTER_SUBSCRIPTION_SERVER; + n->subname = $3; + n->servername = $5; + $$ = (Node *) n; + } | ALTER SUBSCRIPTION name REFRESH PUBLICATION opt_definition { AlterSubscriptionStmt *n = diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index f9c4b484754..566393d1b65 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -5058,7 +5058,7 @@ maybe_reread_subscription(void) /* Ensure allocations in permanent context. */ oldctx = MemoryContextSwitchTo(ApplyContext); - newsub = GetSubscription(MyLogicalRepWorker->subid, true); + newsub = GetSubscription(MyLogicalRepWorker->subid, true, true); /* * Exit if the subscription was removed. This normally should not happen @@ -5200,7 +5200,9 @@ set_wal_receiver_timeout(void) } /* - * Callback from subscription syscache invalidation. + * Callback from subscription syscache invalidation. Also needed for server or + * user mapping invalidation, which can change the connection information for + * subscriptions that connect using a server object. */ static void subscription_change_cb(Datum arg, SysCacheIdentifier cacheid, uint32 hashvalue) @@ -5805,7 +5807,7 @@ InitializeLogRepWorker(void) */ LockSharedObject(SubscriptionRelationId, MyLogicalRepWorker->subid, 0, AccessShareLock); - MySubscription = GetSubscription(MyLogicalRepWorker->subid, true); + MySubscription = GetSubscription(MyLogicalRepWorker->subid, true, true); if (!MySubscription) { ereport(LOG, @@ -5870,6 +5872,22 @@ InitializeLogRepWorker(void) CacheRegisterSyscacheCallback(SUBSCRIPTIONOID, subscription_change_cb, (Datum) 0); + /* Changes to foreign servers may affect subscriptions using SERVER. */ + CacheRegisterSyscacheCallback(FOREIGNSERVEROID, + subscription_change_cb, + (Datum) 0); + /* Changes to user mappings may affect subscriptions using SERVER. */ + CacheRegisterSyscacheCallback(USERMAPPINGOID, + subscription_change_cb, + (Datum) 0); + + /* + * Changes to FDW connection_function may affect subscriptions using + * SERVER. + */ + CacheRegisterSyscacheCallback(FOREIGNDATAWRAPPEROID, + subscription_change_cb, + (Datum) 0); CacheRegisterSyscacheCallback(AUTHOID, subscription_change_cb, diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 1035bba72ce..88e0a55f8f1 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5182,6 +5182,7 @@ getSubscriptions(Archive *fout) int i_subdisableonerr; int i_subpasswordrequired; int i_subrunasowner; + int i_subservername; int i_subconninfo; int i_subslotname; int i_subsynccommit; @@ -5286,14 +5287,24 @@ getSubscriptions(Archive *fout) if (fout->remoteVersion >= 190000) appendPQExpBufferStr(query, - " s.subwalrcvtimeout\n"); + " s.subwalrcvtimeout,\n"); else appendPQExpBufferStr(query, - " '-1' AS subwalrcvtimeout\n"); + " '-1' AS subwalrcvtimeout,\n"); + + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, " fs.srvname AS subservername\n"); + else + appendPQExpBufferStr(query, " NULL AS subservername\n"); appendPQExpBufferStr(query, "FROM pg_subscription s\n"); + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, + "LEFT JOIN pg_catalog.pg_foreign_server fs \n" + " ON fs.oid = s.subserver \n"); + if (dopt->binary_upgrade && fout->remoteVersion >= 170000) appendPQExpBufferStr(query, "LEFT JOIN pg_catalog.pg_replication_origin_status o \n" @@ -5325,6 +5336,7 @@ getSubscriptions(Archive *fout) i_subfailover = PQfnumber(res, "subfailover"); i_subretaindeadtuples = PQfnumber(res, "subretaindeadtuples"); i_submaxretention = PQfnumber(res, "submaxretention"); + i_subservername = PQfnumber(res, "subservername"); i_subconninfo = PQfnumber(res, "subconninfo"); i_subslotname = PQfnumber(res, "subslotname"); i_subsynccommit = PQfnumber(res, "subsynccommit"); @@ -5347,6 +5359,10 @@ getSubscriptions(Archive *fout) subinfo[i].subenabled = (strcmp(PQgetvalue(res, i, i_subenabled), "t") == 0); + if (PQgetisnull(res, i, i_subservername)) + subinfo[i].subservername = NULL; + else + subinfo[i].subservername = pg_strdup(PQgetvalue(res, i, i_subservername)); subinfo[i].subbinary = (strcmp(PQgetvalue(res, i, i_subbinary), "t") == 0); subinfo[i].substream = *(PQgetvalue(res, i, i_substream)); @@ -5363,8 +5379,11 @@ getSubscriptions(Archive *fout) (strcmp(PQgetvalue(res, i, i_subretaindeadtuples), "t") == 0); subinfo[i].submaxretention = atoi(PQgetvalue(res, i, i_submaxretention)); - subinfo[i].subconninfo = - pg_strdup(PQgetvalue(res, i, i_subconninfo)); + if (PQgetisnull(res, i, i_subconninfo)) + subinfo[i].subconninfo = NULL; + else + subinfo[i].subconninfo = + pg_strdup(PQgetvalue(res, i, i_subconninfo)); if (PQgetisnull(res, i, i_subslotname)) subinfo[i].subslotname = NULL; else @@ -5575,9 +5594,17 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo) appendPQExpBuffer(delq, "DROP SUBSCRIPTION %s;\n", qsubname); - appendPQExpBuffer(query, "CREATE SUBSCRIPTION %s CONNECTION ", + appendPQExpBuffer(query, "CREATE SUBSCRIPTION %s ", qsubname); - appendStringLiteralAH(query, subinfo->subconninfo, fout); + if (subinfo->subservername) + { + appendPQExpBuffer(query, "SERVER %s", fmtId(subinfo->subservername)); + } + else + { + appendPQExpBuffer(query, "CONNECTION "); + appendStringLiteralAH(query, subinfo->subconninfo, fout); + } /* Build list of quoted publications and append them to query. */ if (!parsePGArray(subinfo->subpublications, &pubnames, &npubnames)) diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index e138ef1276c..1c11a79083f 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -720,6 +720,7 @@ typedef struct _SubscriptionInfo bool subfailover; bool subretaindeadtuples; int submaxretention; + char *subservername; char *subconninfo; char *subslotname; char *subsynccommit; diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index a94eade282f..211d8f3b1ec 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -6895,7 +6895,7 @@ describeSubscriptions(const char *pattern, bool verbose) printQueryOpt myopt = pset.popt; static const bool translate_columns[] = {false, false, false, false, false, false, false, false, false, false, false, false, false, false, - false, false, false, false, false, false}; + false, false, false, false, false, false, false}; if (pset.sversion < 100000) { @@ -6965,6 +6965,10 @@ describeSubscriptions(const char *pattern, bool verbose) gettext_noop("Failover")); if (pset.sversion >= 190000) { + appendPQExpBuffer(&buf, + ", (select srvname from pg_foreign_server where oid=subserver) AS \"%s\"\n", + gettext_noop("Server")); + appendPQExpBuffer(&buf, ", subretaindeadtuples AS \"%s\"\n", gettext_noop("Retain dead tuples")); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index f8c0865ca89..6484c6a3dd4 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -2332,7 +2332,7 @@ match_previous_words(int pattern_id, else if (Matches("ALTER", "SUBSCRIPTION", MatchAny)) COMPLETE_WITH("CONNECTION", "ENABLE", "DISABLE", "OWNER TO", "RENAME TO", "REFRESH PUBLICATION", "REFRESH SEQUENCES", - "SET", "SKIP (", "ADD PUBLICATION", "DROP PUBLICATION"); + "SERVER", "SET", "SKIP (", "ADD PUBLICATION", "DROP PUBLICATION"); /* ALTER SUBSCRIPTION <name> REFRESH */ else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, MatchAnyN, "REFRESH")) COMPLETE_WITH("PUBLICATION", "SEQUENCES"); @@ -3870,9 +3870,16 @@ match_previous_words(int pattern_id, /* CREATE SUBSCRIPTION */ else if (Matches("CREATE", "SUBSCRIPTION", MatchAny)) - COMPLETE_WITH("CONNECTION"); + COMPLETE_WITH("SERVER", "CONNECTION"); + else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "SERVER", MatchAny)) + COMPLETE_WITH("PUBLICATION"); else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION", MatchAny)) COMPLETE_WITH("PUBLICATION"); + else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "SERVER", + MatchAny, "PUBLICATION")) + { + /* complete with nothing here as this refers to remote publications */ + } else if (Matches("CREATE", "SUBSCRIPTION", MatchAny, "CONNECTION", MatchAny, "PUBLICATION")) { diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index f164bf8767f..c39189237fe 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202603041 +#define CATALOG_VERSION_NO 202603042 #endif diff --git a/src/include/catalog/pg_foreign_data_wrapper.h b/src/include/catalog/pg_foreign_data_wrapper.h index e6009069e82..3d8389de65e 100644 --- a/src/include/catalog/pg_foreign_data_wrapper.h +++ b/src/include/catalog/pg_foreign_data_wrapper.h @@ -38,6 +38,9 @@ CATALOG(pg_foreign_data_wrapper,2328,ForeignDataWrapperRelationId) Oid fdwvalidator BKI_LOOKUP_OPT(pg_proc); /* option validation * function, or 0 if * none */ + Oid fdwconnection BKI_LOOKUP_OPT(pg_proc); /* connection string + * function, or 0 if + * none */ #ifdef CATALOG_VARLEN /* variable-length fields start here */ aclitem fdwacl[1]; /* access permissions */ diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h index c369b5abfb3..0058d9387d7 100644 --- a/src/include/catalog/pg_subscription.h +++ b/src/include/catalog/pg_subscription.h @@ -92,9 +92,12 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW * exceeded max_retention_duration, when * defined */ + Oid subserver BKI_LOOKUP_OPT(pg_foreign_server); /* If connection uses + * server */ + #ifdef CATALOG_VARLEN /* variable-length fields start here */ /* Connection string to the publisher */ - text subconninfo BKI_FORCE_NOT_NULL; + text subconninfo; /* Set if connecting with connection string */ /* Slot name on publisher */ NameData subslotname BKI_FORCE_NULL; @@ -207,7 +210,8 @@ typedef struct Subscription #endif /* EXPOSE_TO_CLIENT_CODE */ -extern Subscription *GetSubscription(Oid subid, bool missing_ok); +extern Subscription *GetSubscription(Oid subid, bool missing_ok, + bool aclcheck); extern void FreeSubscription(Subscription *sub); extern void DisableSubscription(Oid subid); diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h index c185d1458a2..65ed9a7f987 100644 --- a/src/include/foreign/foreign.h +++ b/src/include/foreign/foreign.h @@ -28,6 +28,7 @@ typedef struct ForeignDataWrapper char *fdwname; /* Name of the FDW */ Oid fdwhandler; /* Oid of handler function, or 0 */ Oid fdwvalidator; /* Oid of validator function, or 0 */ + Oid fdwconnection; /* Oid of connection string function, or 0 */ List *options; /* fdwoptions as DefElem list */ } ForeignDataWrapper; @@ -65,10 +66,12 @@ typedef struct ForeignTable extern ForeignServer *GetForeignServer(Oid serverid); +extern char *ForeignServerName(Oid serverid); extern ForeignServer *GetForeignServerExtended(Oid serverid, bits16 flags); extern ForeignServer *GetForeignServerByName(const char *srvname, bool missing_ok); +extern char *ForeignServerConnectionString(Oid userid, Oid serverid); extern UserMapping *GetUserMapping(Oid userid, Oid serverid); extern ForeignDataWrapper *GetForeignDataWrapper(Oid fdwid); extern ForeignDataWrapper *GetForeignDataWrapperExtended(Oid fdwid, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index ff41943a6db..4ee092206b0 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4383,6 +4383,7 @@ typedef struct CreateSubscriptionStmt { NodeTag type; char *subname; /* Name of the subscription */ + char *servername; /* Server name of publisher */ char *conninfo; /* Connection string to publisher */ List *publication; /* One or more publication to subscribe to */ List *options; /* List of DefElem nodes */ @@ -4391,6 +4392,7 @@ typedef struct CreateSubscriptionStmt typedef enum AlterSubscriptionType { ALTER_SUBSCRIPTION_OPTIONS, + ALTER_SUBSCRIPTION_SERVER, ALTER_SUBSCRIPTION_CONNECTION, ALTER_SUBSCRIPTION_SET_PUBLICATION, ALTER_SUBSCRIPTION_ADD_PUBLICATION, @@ -4406,6 +4408,7 @@ typedef struct AlterSubscriptionStmt NodeTag type; AlterSubscriptionType kind; /* ALTER_SUBSCRIPTION_OPTIONS, etc */ char *subname; /* Name of the subscription */ + char *servername; /* Server name of publisher */ char *conninfo; /* Connection string to publisher */ List *publication; /* One or more publication to subscribe to */ List *options; /* List of DefElem nodes */ diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 25aaae8d05a..51b9608a668 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -224,6 +224,7 @@ NOTICE: checking pg_extension {extconfig} => pg_class {oid} NOTICE: checking pg_foreign_data_wrapper {fdwowner} => pg_authid {oid} NOTICE: checking pg_foreign_data_wrapper {fdwhandler} => pg_proc {oid} NOTICE: checking pg_foreign_data_wrapper {fdwvalidator} => pg_proc {oid} +NOTICE: checking pg_foreign_data_wrapper {fdwconnection} => pg_proc {oid} NOTICE: checking pg_foreign_server {srvowner} => pg_authid {oid} NOTICE: checking pg_foreign_server {srvfdw} => pg_foreign_data_wrapper {oid} NOTICE: checking pg_user_mapping {umuser} => pg_authid {oid} @@ -269,5 +270,6 @@ NOTICE: checking pg_publication_rel {prpubid} => pg_publication {oid} NOTICE: checking pg_publication_rel {prrelid} => pg_class {oid} NOTICE: checking pg_subscription {subdbid} => pg_database {oid} NOTICE: checking pg_subscription {subowner} => pg_authid {oid} +NOTICE: checking pg_subscription {subserver} => pg_foreign_server {oid} NOTICE: checking pg_subscription_rel {srsubid} => pg_subscription {oid} NOTICE: checking pg_subscription_rel {srrelid} => pg_class {oid} diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out index a5fdfe68a0e..3fd8a18b73a 100644 --- a/src/test/regress/expected/subscription.out +++ b/src/test/regress/expected/subscription.out @@ -1,6 +1,14 @@ -- -- SUBSCRIPTION -- +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX +\set regresslib :libdir '/regress' :dlsuffix +CREATE FUNCTION test_fdw_connection(oid, oid, internal) + RETURNS text + AS :'regresslib', 'test_fdw_connection' + LANGUAGE C; CREATE ROLE regress_subscription_user LOGIN SUPERUSER; CREATE ROLE regress_subscription_user2; CREATE ROLE regress_subscription_user3 IN ROLE pg_create_subscription; @@ -116,18 +124,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU 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. \dRs+ regress_testsub4 - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description -------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | none | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | none | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub4 SET (origin = any); \dRs+ regress_testsub4 - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description -------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) DROP SUBSCRIPTION regress_testsub3; @@ -140,15 +148,30 @@ ERROR: invalid connection string syntax: invalid connection option "i_dont_exis -- connecting, so this is reliable and safe) CREATE SUBSCRIPTION regress_testsub5 CONNECTION 'port=-1' PUBLICATION testpub; ERROR: subscription "regress_testsub5" could not connect to the publisher: invalid port number: "-1" +CREATE FOREIGN DATA WRAPPER test_fdw; +CREATE SERVER test_server FOREIGN DATA WRAPPER test_fdw; +CREATE USER MAPPING FOR regress_subscription_user SERVER test_server; +-- fail, need CONNECTION clause +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); +ERROR: foreign data wrapper "test_fdw" does not support subscription connections +DETAIL: Foreign data wrapper must be defined with CONNECTION specified. +ALTER FOREIGN DATA WRAPPER test_fdw CONNECTION test_fdw_connection; +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server 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. +DROP SUBSCRIPTION regress_testsub6; +DROP USER MAPPING FOR regress_subscription_user SERVER test_server; +DROP SERVER test_server; +DROP FOREIGN DATA WRAPPER test_fdw; -- fail - invalid connection string during ALTER ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar'; ERROR: invalid connection string syntax: missing "=" after "foobar" in connection info string \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | test subscription + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | test subscription (1 row) ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false); @@ -157,10 +180,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname'); ALTER SUBSCRIPTION regress_testsub SET (password_required = false); ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | f | t | f | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 | test subscription + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | f | t | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 | test subscription (1 row) ALTER SUBSCRIPTION regress_testsub SET (password_required = true); @@ -176,10 +199,10 @@ ERROR: unrecognized subscription parameter: "create_slot" -- ok ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345'); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00012345 | test subscription + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00012345 | test subscription (1 row) -- ok - with lsn = NONE @@ -188,10 +211,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE); ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0'); ERROR: invalid WAL location (LSN): 0/0 \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 | test subscription + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 | test subscription (1 row) BEGIN; @@ -227,10 +250,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (wal_receiver_timeout = '80s'); ALTER SUBSCRIPTION regress_testsub_foo SET (wal_receiver_timeout = 'foobar'); ERROR: invalid value for parameter "wal_receiver_timeout": "foobar" \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- - regress_testsub_foo | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | local | dbname=regress_doesnotexist2 | 80s | 0/00000000 | test subscription + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub_foo | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | local | dbname=regress_doesnotexist2 | 80s | 0/00000000 | test subscription (1 row) -- rename back to keep the rest simple @@ -259,19 +282,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | t | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | t | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (binary = false); ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) DROP SUBSCRIPTION regress_testsub; @@ -283,27 +306,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | on | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (streaming = false); ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- fail - publication already exists @@ -318,10 +341,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false); ERROR: publication "testpub1" is already in subscription "regress_testsub" \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub,testpub1,testpub2} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub,testpub1,testpub2} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- fail - publication used more than once @@ -336,10 +359,10 @@ ERROR: publication "testpub3" is not in subscription "regress_testsub" -- ok - delete publications ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) DROP SUBSCRIPTION regress_testsub; @@ -375,19 +398,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- we can alter streaming when two_phase enabled ALTER SUBSCRIPTION regress_testsub SET (streaming = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -397,10 +420,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -413,18 +436,18 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | t | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | t | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -437,10 +460,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -454,19 +477,19 @@ NOTICE: max_retention_duration is ineffective when retain_dead_tuples is disabl 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. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 1000 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 1000 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- ok ALTER SUBSCRIPTION regress_testsub SET (max_retention_duration = 0); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); diff --git a/src/test/regress/regress.c b/src/test/regress/regress.c index a02f41c9727..158c7b7a4c0 100644 --- a/src/test/regress/regress.c +++ b/src/test/regress/regress.c @@ -729,6 +729,13 @@ test_fdw_handler(PG_FUNCTION_ARGS) PG_RETURN_NULL(); } +PG_FUNCTION_INFO_V1(test_fdw_connection); +Datum +test_fdw_connection(PG_FUNCTION_ARGS) +{ + PG_RETURN_TEXT_P(cstring_to_text("dbname=regress_doesnotexist")); +} + PG_FUNCTION_INFO_V1(is_catalog_text_unique_index_oid); Datum is_catalog_text_unique_index_oid(PG_FUNCTION_ARGS) diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql index d93cbc279d9..990d75f1749 100644 --- a/src/test/regress/sql/subscription.sql +++ b/src/test/regress/sql/subscription.sql @@ -2,6 +2,17 @@ -- SUBSCRIPTION -- +-- directory paths and dlsuffix are passed to us in environment variables +\getenv libdir PG_LIBDIR +\getenv dlsuffix PG_DLSUFFIX + +\set regresslib :libdir '/regress' :dlsuffix + +CREATE FUNCTION test_fdw_connection(oid, oid, internal) + RETURNS text + AS :'regresslib', 'test_fdw_connection' + LANGUAGE C; + CREATE ROLE regress_subscription_user LOGIN SUPERUSER; CREATE ROLE regress_subscription_user2; CREATE ROLE regress_subscription_user3 IN ROLE pg_create_subscription; @@ -85,6 +96,21 @@ CREATE SUBSCRIPTION regress_testsub5 CONNECTION 'i_dont_exist=param' PUBLICATION -- connecting, so this is reliable and safe) CREATE SUBSCRIPTION regress_testsub5 CONNECTION 'port=-1' PUBLICATION testpub; +CREATE FOREIGN DATA WRAPPER test_fdw; +CREATE SERVER test_server FOREIGN DATA WRAPPER test_fdw; +CREATE USER MAPPING FOR regress_subscription_user SERVER test_server; + +-- fail, need CONNECTION clause +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); + +ALTER FOREIGN DATA WRAPPER test_fdw CONNECTION test_fdw_connection; +CREATE SUBSCRIPTION regress_testsub6 SERVER test_server PUBLICATION testpub WITH (slot_name = NONE, connect = false); +DROP SUBSCRIPTION regress_testsub6; + +DROP USER MAPPING FOR regress_subscription_user SERVER test_server; +DROP SERVER test_server; +DROP FOREIGN DATA WRAPPER test_fdw; + -- fail - invalid connection string during ALTER ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar'; -- 2.43.0 ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-05 03:51 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-05 08:52 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> @ 2026-03-06 16:19 ` Ashutosh Bapat <[email protected]> 2026-03-07 07:05 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-14 22:55 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 1 sibling, 2 replies; 17+ messages in thread From: Ashutosh Bapat @ 2026-03-06 16:19 UTC (permalink / raw) To: Jeff Davis <[email protected]>; +Cc: Amit Kapila <[email protected]>; Masahiko Sawada <[email protected]>; Shlok Kyal <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Thu, Mar 5, 2026 at 2:23 PM Jeff Davis <[email protected]> wrote: > > On Thu, 2026-03-05 at 09:21 +0530, Amit Kapila wrote: > > We revoke view rights on subconninfo from the public. See below [A] > > in > > system_views.sql. Do we want to do the same for subserver or is it > > okay for users to see it? > > I can't think of a reason that the server name should be secret, but > let me know if you think so. > > > I think the following comment and some place > > in docs needs to be updated. > > [A] > > -- All columns of pg_subscription except subconninfo are publicly > > readable. > > REVOKE ALL ON pg_subscription FROM public; > > GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, > > Good catch! Thank you. > > > 2. We may want to update the following text in pg_dump docs about the > > new way of connecting to hosts. See [B] (When dumping logical > > replication subscriptions, pg_dump will generate CREATE SUBSCRIPTION > > commands that use the connect = false option, so that restoring the > > subscription does not make remote connections for creating a > > replication slot or for initial table copy. That way, the dump can be > > restored without requiring network access to the remote servers. It > > is > > then up to the user to reactivate the subscriptions in a suitable > > way. > > If the involved hosts have changed, the connection information might > > have to be changed.) > > > > [B] - https://www.postgresql.org/docs/devel/app-pgdump.html > > > > I think the above comment is still correct -- it would be a bit easier > to deal with servers rather than raw connection strings, but the > comment already says "...might have to be changed" which is just a > reminder to look. > > > Attached a new patch that also addressed the review comments from here: > > https://www.postgresql.org/message-id/[email protected]... > > Additionally, I ran into a problem that's worth highlighting: > > DROP SERVER ... CASCADE was broken, because the subscription is > dependent on it but that's in a global catalog, which is not handled by > doDeletion(). The subscription is conceptually a per-database object, > but it's in a shared catalog with a subdbid field. I solved that > problem by adding a guard to findDependentObjects() to check for the > referenced object belonging to a shared catalog, and if so it just > throws an error (so CASCADE is not supported for servers used in > subscriptions). That's a simple but not a very satisfying solution, so > let me know if you see a problem with that. I shared the awkwardness, but don't have any better ideas. However, it does raise a question as to why do we need an FDW to be database specific or for that matter a SERVER database specific. That might be because it requires an extension which is database specific. Probably we should support extensions which are database agnostic. However that's way beyond the scope of this patch. Other way around why do we need subscriptions to be shared objects? Again probably beyond the scope of this patch. I also see some code duplicated across Create and Alter subscription code paths. Even without this patch the code was duplicated, but with this patch the amount of duplication has increased. Can we deduplicate some of the code? I don't think we need a separate ForeignServerName function. In AlterSubscription() we already have ForeignSever object which has server name in it. Other two callers invoke ForeignServerConnectionString() which in turn fetches ForeignServer object. Those callers instead may fetch ForeignServer object themselves and pass it to ForeignServerConnectionString() and use it in the error message. The patch has changes to pg_dump.c but there is no corresponding test. But I don't think we need a separate test. If the objects created in regress/*.sql tests are not dropped, 002_pg_upgrade.pl would test dump/restore of subscriptions with server. I think we need tests for testing changes in connection when ALTER SUBSCRIPTION ... SERVER is executed and also those for switching between SERVER and CONNECTION. -- Best Wishes, Ashutosh Bapat ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-05 03:51 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-05 08:52 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-06 16:19 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Ashutosh Bapat <[email protected]> @ 2026-03-07 07:05 ` Amit Kapila <[email protected]> 1 sibling, 0 replies; 17+ messages in thread From: Amit Kapila @ 2026-03-07 07:05 UTC (permalink / raw) To: Ashutosh Bapat <[email protected]>; +Cc: Jeff Davis <[email protected]>; Masahiko Sawada <[email protected]>; Shlok Kyal <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Fri, Mar 6, 2026 at 9:49 PM Ashutosh Bapat <[email protected]> wrote: > > On Thu, Mar 5, 2026 at 2:23 PM Jeff Davis <[email protected]> wrote: > > > > On Thu, 2026-03-05 at 09:21 +0530, Amit Kapila wrote: > > > > Additionally, I ran into a problem that's worth highlighting: > > > > DROP SERVER ... CASCADE was broken, because the subscription is > > dependent on it but that's in a global catalog, which is not handled by > > doDeletion(). The subscription is conceptually a per-database object, > > but it's in a shared catalog with a subdbid field. I solved that > > problem by adding a guard to findDependentObjects() to check for the > > referenced object belonging to a shared catalog, and if so it just > > throws an error (so CASCADE is not supported for servers used in > > subscriptions). That's a simple but not a very satisfying solution, so > > let me know if you see a problem with that. > > I shared the awkwardness, but don't have any better ideas. However, it > does raise a question as to why do we need an FDW to be database > specific or for that matter a SERVER database specific. That might be > because it requires an extension which is database specific. Probably > we should support extensions which are database agnostic. However > that's way beyond the scope of this patch. Other way around why do we > need subscriptions to be shared objects? > It is because the launcher process needs to traverse all subscriptions to start workers. -- With Regards, Amit Kapila. ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-05 03:51 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-05 08:52 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-06 16:19 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Ashutosh Bapat <[email protected]> @ 2026-03-14 22:55 ` Jeff Davis <[email protected]> 2026-03-16 05:38 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-18 19:06 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 1 sibling, 2 replies; 17+ messages in thread From: Jeff Davis @ 2026-03-14 22:55 UTC (permalink / raw) To: Ashutosh Bapat <[email protected]>; +Cc: Amit Kapila <[email protected]>; Masahiko Sawada <[email protected]>; Shlok Kyal <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Fri, 2026-03-06 at 21:49 +0530, Ashutosh Bapat wrote: > I don't think we need a separate ForeignServerName function. In > AlterSubscription() we already have ForeignSever object which has > server name in it. Other two callers invoke > ForeignServerConnectionString() which in turn fetches ForeignServer > object. Those callers instead may fetch ForeignServer object > themselves and pass it to ForeignServerConnectionString() and use it > in the error message. Done. > The patch has changes to pg_dump.c but there is no corresponding > test. > But I don't think we need a separate test. If the objects created in > regress/*.sql tests are not dropped, 002_pg_upgrade.pl would test > dump/restore of subscriptions with server. It seems that foreign_data.sql expects there to be zero FDWs, servers, and user mappings, so it's not quite that simple. I'm not entirely sure why that is, but I suppose it's meant to be tested in 002_pg_dump.pl instead. I wrote the tests there (attached), which revealed that CREATE FOREIGN DATA WRAPPER ... CONNECTION wasn't being dumped properly. I attached a separate fix for that. Unfortunately I don't think we can rely on regress.so being available when 002_pg_dump.pl runs. Do you have an idea how I can effectively test the FDW (which is needed to test the server and subscription)? I suppose I could make it a built-in function, and that wouldn't be so bad, but not ideal. Right now this test is failing for CI on debian autoconf. > I think we need tests for testing changes in connection when ALTER > SUBSCRIPTION ... SERVER is executed and also those for switching > between SERVER and CONNECTION. Done. Attached series including patches to address Andres's and Amit's comments, too. Thank you! Regards, Jeff Davis Attachments: [text/x-patch] v22-0001-Clean-up-postgres_fdw-t-010_subscription.pl.patch (3.0K, 2-v22-0001-Clean-up-postgres_fdw-t-010_subscription.pl.patch) download | inline diff: From 7726c05c6338bfea1ccac68aaf8288fe8a165107 Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Sat, 14 Mar 2026 11:03:53 -0700 Subject: [PATCH v22 1/6] Clean up postgres_fdw/t/010_subscription.pl. The test was based on test/subscription/002_rep_changes.pl, but had some leftover copy+paste problems that were useless and/or distracted from the point of the test. Discussion: https://postgr.es/m/CAA4eK1+=V_UFNHwcoMFqzy0F4AtS9_GyXhQDUzizgieQPWr=0A@mail.gmail.com Reported-by: Amit Kapila <[email protected]> --- contrib/postgres_fdw/t/010_subscription.pl | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/contrib/postgres_fdw/t/010_subscription.pl b/contrib/postgres_fdw/t/010_subscription.pl index 1e41091badc..a04d64bb78c 100644 --- a/contrib/postgres_fdw/t/010_subscription.pl +++ b/contrib/postgres_fdw/t/010_subscription.pl @@ -1,7 +1,8 @@ # Copyright (c) 2021-2026, PostgreSQL Global Development Group -# Basic logical replication test +# Test postgres_fdw foreign server for use with a subscription. + use strict; use warnings FATAL => 'all'; use PostgreSQL::Test::Cluster; @@ -22,11 +23,6 @@ $node_subscriber->start; $node_publisher->safe_psql('postgres', "CREATE TABLE tab_ins AS SELECT a, a + 1 as b FROM generate_series(1,1002) AS a"); -# Replicate the changes without columns -$node_publisher->safe_psql('postgres', "CREATE TABLE tab_no_col()"); -$node_publisher->safe_psql('postgres', - "INSERT INTO tab_no_col default VALUES"); - # Setup structure on subscriber $node_subscriber->safe_psql('postgres', "CREATE EXTENSION postgres_fdw"); $node_subscriber->safe_psql('postgres', "CREATE TABLE tab_ins (a int, b int)"); @@ -45,9 +41,6 @@ $node_subscriber->safe_psql('postgres', "CREATE USER MAPPING FOR PUBLIC SERVER tap_server" ); -$node_subscriber->safe_psql('postgres', - "CREATE FOREIGN TABLE f_tab_ins (a int, b int) SERVER tap_server OPTIONS(table_name 'tab_ins')" -); $node_subscriber->safe_psql('postgres', "CREATE SUBSCRIPTION tap_sub SERVER tap_server PUBLICATION tap_pub WITH (password_required=false)" ); @@ -56,7 +49,7 @@ $node_subscriber->safe_psql('postgres', $node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); my $result = - $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM (SELECT f.b = l.b as match FROM tab_ins l, f_tab_ins f WHERE l.a = f.a) WHERE match"); + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab_ins"); is($result, qq(1002), 'check that initial data was copied to subscriber'); $node_publisher->safe_psql('postgres', @@ -65,7 +58,7 @@ $node_publisher->safe_psql('postgres', $node_publisher->wait_for_catchup('tap_sub'); $result = - $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM (SELECT f.b = l.b as match FROM tab_ins l, f_tab_ins f WHERE l.a = f.a) WHERE match"); + $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab_ins"); is($result, qq(1050), 'check that inserted data was copied to subscriber'); done_testing(); -- 2.43.0 [text/x-patch] v22-0002-ALTER-SUBSCRIPTION-.-SERVER-test.patch (2.9K, 3-v22-0002-ALTER-SUBSCRIPTION-.-SERVER-test.patch) download | inline diff: From 08001be72c54ce6f1bd4729546260b28821a46a1 Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Sat, 14 Mar 2026 12:50:45 -0700 Subject: [PATCH v22 2/6] ALTER SUBSCRIPTION ... SERVER test. Test ALTER SUBSCRIPTION ... SERVER and ALTER SUBSCRIPTION ... CONNECTION, including invalidation. Discussion: https://postgr.es/m/CAExHW5vV5znEvecX=ra2-v7UBj9-M6qvdDzuB78M-TxbYD1PEA@mail.gmail.com Suggested-by: Ashutosh Bapat <[email protected]> --- contrib/postgres_fdw/t/010_subscription.pl | 38 ++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/contrib/postgres_fdw/t/010_subscription.pl b/contrib/postgres_fdw/t/010_subscription.pl index a04d64bb78c..50eac4c3bdb 100644 --- a/contrib/postgres_fdw/t/010_subscription.pl +++ b/contrib/postgres_fdw/t/010_subscription.pl @@ -49,7 +49,7 @@ $node_subscriber->safe_psql('postgres', $node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub'); my $result = - $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab_ins"); + $node_subscriber->safe_psql('postgres', "SELECT MAX(a) FROM tab_ins"); is($result, qq(1002), 'check that initial data was copied to subscriber'); $node_publisher->safe_psql('postgres', @@ -58,7 +58,41 @@ $node_publisher->safe_psql('postgres', $node_publisher->wait_for_catchup('tap_sub'); $result = - $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM tab_ins"); + $node_subscriber->safe_psql('postgres', "SELECT MAX(a) FROM tab_ins"); is($result, qq(1050), 'check that inserted data was copied to subscriber'); +# change to CONNECTION and confirm invalidation +my $log_offset = -s $node_subscriber->logfile; +$node_subscriber->safe_psql('postgres', + "ALTER SUBSCRIPTION tap_sub CONNECTION '$publisher_connstr'"); +$node_subscriber->wait_for_log( + qr/logical replication worker for subscription "tap_sub" will restart because of a parameter change/, + $log_offset); + +$node_publisher->safe_psql('postgres', + "INSERT INTO tab_ins SELECT a, a + 1 FROM generate_series(1051,1057) a"); + +$node_publisher->wait_for_catchup('tap_sub'); + +$result = + $node_subscriber->safe_psql('postgres', "SELECT MAX(a) FROM tab_ins"); +is($result, qq(1057), 'check subscription after ALTER SUBSCRIPTION ... CONNECTION'); + +# change back to SERVER and confirm invalidation +$log_offset = -s $node_subscriber->logfile; +$node_subscriber->safe_psql('postgres', + "ALTER SUBSCRIPTION tap_sub SERVER tap_server"); +$node_subscriber->wait_for_log( + qr/logical replication worker for subscription "tap_sub" will restart because of a parameter change/, + $log_offset); + +$node_publisher->safe_psql('postgres', + "INSERT INTO tab_ins SELECT a, a + 1 FROM generate_series(1058,1073) a"); + +$node_publisher->wait_for_catchup('tap_sub'); + +$result = + $node_subscriber->safe_psql('postgres', "SELECT MAX(a) FROM tab_ins"); +is($result, qq(1073), 'check subscription after ALTER SUBSCRIPTION ... SERVER'); + done_testing(); -- 2.43.0 [text/x-patch] v22-0003-Temp-context-for-maybe_reread_subscription.patch (6.9K, 4-v22-0003-Temp-context-for-maybe_reread_subscription.patch) download | inline diff: From 305de8a5dd4e960e372cddb1055fbd7d0a1717a3 Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Thu, 12 Mar 2026 18:04:35 -0700 Subject: [PATCH v22 3/6] Temp context for maybe_reread_subscription(). Move temp context from ForeignServerConnectionString() to maybe_reread_subscription(), so that it prevents more invalidation-related leaks. Remove PG_TRY()/PG_FINALLY() from ForeignServerConnectionString(). Suggested-by: Andres Freund <[email protected]> Discussion: https://postgr.es/m/xvdjrdqnpap3uq7owbaox3r7p5gf7sv62aaqf2ju3vb6yglatr%40kvvwhoudrlxq --- src/backend/catalog/pg_subscription.c | 14 ----- src/backend/foreign/foreign.c | 66 +++++++----------------- src/backend/replication/logical/worker.c | 30 +++++++++-- src/include/catalog/pg_subscription.h | 1 - 4 files changed, 43 insertions(+), 68 deletions(-) diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c index 3673d4f0bc1..ca053c152cf 100644 --- a/src/backend/catalog/pg_subscription.c +++ b/src/backend/catalog/pg_subscription.c @@ -216,20 +216,6 @@ CountDBSubscriptions(Oid dbid) return nsubs; } -/* - * Free memory allocated by subscription struct. - */ -void -FreeSubscription(Subscription *sub) -{ - pfree(sub->name); - pfree(sub->conninfo); - if (sub->slotname) - pfree(sub->slotname); - list_free_deep(sub->publications); - pfree(sub); -} - /* * Disable the given subscription. */ diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c index 160cf6f51c9..f437b447282 100644 --- a/src/backend/foreign/foreign.c +++ b/src/backend/foreign/foreign.c @@ -219,62 +219,32 @@ GetForeignServerByName(const char *srvname, bool missing_ok) /* * Retrieve connection string from server's FDW. + * + * NB: leaks into CurrentMemoryContext. */ char * ForeignServerConnectionString(Oid userid, Oid serverid) { - MemoryContext tempContext; - MemoryContext oldcxt; - text *volatile connection_text = NULL; - char *result = NULL; - - /* - * GetForeignServer, GetForeignDataWrapper, and the connection function - * itself all leak memory into CurrentMemoryContext. Switch to a temporary - * context for easy cleanup. - */ - tempContext = AllocSetContextCreate(CurrentMemoryContext, - "FDWConnectionContext", - ALLOCSET_SMALL_SIZES); - - oldcxt = MemoryContextSwitchTo(tempContext); - - PG_TRY(); - { - ForeignServer *server; - ForeignDataWrapper *fdw; - Datum connection_datum; - - server = GetForeignServer(serverid); - fdw = GetForeignDataWrapper(server->fdwid); - - if (!OidIsValid(fdw->fdwconnection)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("foreign data wrapper \"%s\" does not support subscription connections", - fdw->fdwname), - errdetail("Foreign data wrapper must be defined with CONNECTION specified."))); - - - connection_datum = OidFunctionCall3(fdw->fdwconnection, - ObjectIdGetDatum(userid), - ObjectIdGetDatum(serverid), - PointerGetDatum(NULL)); + ForeignServer *server; + ForeignDataWrapper *fdw; + Datum connection_datum; - connection_text = DatumGetTextPP(connection_datum); - } - PG_FINALLY(); - { - MemoryContextSwitchTo(oldcxt); + server = GetForeignServer(serverid); + fdw = GetForeignDataWrapper(server->fdwid); - if (connection_text) - result = text_to_cstring((text *) connection_text); + if (!OidIsValid(fdw->fdwconnection)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("foreign data wrapper \"%s\" does not support subscription connections", + fdw->fdwname), + errdetail("Foreign data wrapper must be defined with CONNECTION specified."))); - MemoryContextDelete(tempContext); - } - PG_END_TRY(); + connection_datum = OidFunctionCall3(fdw->fdwconnection, + ObjectIdGetDatum(userid), + ObjectIdGetDatum(serverid), + PointerGetDatum(NULL)); - return result; + return text_to_cstring(DatumGetTextPP(connection_datum)); } diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index 033858752d9..4ea65a61fb4 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -479,6 +479,7 @@ static MemoryContext LogicalStreamingContext = NULL; WalReceiverConn *LogRepWorkerWalRcvConn = NULL; Subscription *MySubscription = NULL; +static MemoryContext MySubscriptionCtx = NULL; static bool MySubscriptionValid = false; static List *on_commit_wakeup_workers_subids = NIL; @@ -5042,6 +5043,7 @@ void maybe_reread_subscription(void) { MemoryContext oldctx; + MemoryContext newctx; Subscription *newsub; bool started_tx = false; @@ -5056,8 +5058,15 @@ maybe_reread_subscription(void) started_tx = true; } - /* Ensure allocations in permanent context. */ - oldctx = MemoryContextSwitchTo(ApplyContext); + newctx = AllocSetContextCreate(ApplyContext, + "Subscription Context", + ALLOCSET_SMALL_SIZES); + + /* + * GetSubscription() leaks a number of small allocations, so use a + * subcontext for each call. + */ + oldctx = MemoryContextSwitchTo(newctx); newsub = GetSubscription(MyLogicalRepWorker->subid, true, true); @@ -5149,7 +5158,8 @@ maybe_reread_subscription(void) } /* Clean old subscription info and switch to new one. */ - FreeSubscription(MySubscription); + MemoryContextDelete(MySubscriptionCtx); + MySubscriptionCtx = newctx; MySubscription = newsub; MemoryContextSwitchTo(oldctx); @@ -5794,12 +5804,19 @@ InitializeLogRepWorker(void) */ SetConfigOption("search_path", "", PGC_SUSET, PGC_S_OVERRIDE); - /* Load the subscription into persistent memory context. */ ApplyContext = AllocSetContextCreate(TopMemoryContext, "ApplyContext", ALLOCSET_DEFAULT_SIZES); + + /* + * GetSubscription() leaks a number of small allocations, so use a + * subcontext for each call. + */ + MySubscriptionCtx = AllocSetContextCreate(ApplyContext, + "Subscription Context", + ALLOCSET_SMALL_SIZES); + StartTransactionCommand(); - oldctx = MemoryContextSwitchTo(ApplyContext); /* * Lock the subscription to prevent it from being concurrently dropped, @@ -5808,7 +5825,10 @@ InitializeLogRepWorker(void) */ LockSharedObject(SubscriptionRelationId, MyLogicalRepWorker->subid, 0, AccessShareLock); + + oldctx = MemoryContextSwitchTo(MySubscriptionCtx); MySubscription = GetSubscription(MyLogicalRepWorker->subid, true, true); + if (!MySubscription) { ereport(LOG, diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h index 0058d9387d7..2f6f7b57698 100644 --- a/src/include/catalog/pg_subscription.h +++ b/src/include/catalog/pg_subscription.h @@ -212,7 +212,6 @@ typedef struct Subscription extern Subscription *GetSubscription(Oid subid, bool missing_ok, bool aclcheck); -extern void FreeSubscription(Subscription *sub); extern void DisableSubscription(Oid subid); extern int CountDBSubscriptions(Oid dbid); -- 2.43.0 [text/x-patch] v22-0004-Refactor-to-remove-ForeignServerName.patch (7.3K, 5-v22-0004-Refactor-to-remove-ForeignServerName.patch) download | inline diff: From 9cc1598f8a6686015a0c8167cab56888729395b0 Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Fri, 13 Mar 2026 19:37:21 -0700 Subject: [PATCH v22 4/6] Refactor to remove ForeignServerName(). Callers either have a ForeignServer object or can readily construct one. Also simplify ForeignServerConnectionString() by accepting a ForeignServer rather than its OID. Discussion: https://postgr.es/m/CAExHW5vV5znEvecX=ra2-v7UBj9-M6qvdDzuB78M-TxbYD1PEA@mail.gmail.com Suggested-by: Ashutosh Bapat <[email protected]> --- src/backend/catalog/pg_subscription.c | 7 ++++-- src/backend/commands/subscriptioncmds.c | 20 +++++++++------- src/backend/foreign/foreign.c | 31 ++----------------------- src/include/foreign/foreign.h | 4 ++-- 4 files changed, 20 insertions(+), 42 deletions(-) diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c index ca053c152cf..d9e220172e9 100644 --- a/src/backend/catalog/pg_subscription.c +++ b/src/backend/catalog/pg_subscription.c @@ -114,6 +114,9 @@ GetSubscription(Oid subid, bool missing_ok, bool aclcheck) if (OidIsValid(subform->subserver)) { AclResult aclresult; + ForeignServer *server; + + server = GetForeignServer(subform->subserver); /* recheck ACL if requested */ if (aclcheck) @@ -127,11 +130,11 @@ GetSubscription(Oid subid, bool missing_ok, bool aclcheck) (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", GetUserNameFromId(subform->subowner, false), - ForeignServerName(subform->subserver)))); + server->servername))); } sub->conninfo = ForeignServerConnectionString(subform->subowner, - subform->subserver); + server); } else { diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index 724637cff5b..7375e214cb4 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -753,7 +753,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, GetUserMapping(owner, server->serverid); serverid = server->serverid; - conninfo = ForeignServerConnectionString(owner, serverid); + conninfo = ForeignServerConnectionString(owner, server); } else { @@ -1841,13 +1841,13 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", GetUserNameFromId(form->subowner, false), - ForeignServerName(new_server->serverid))); + new_server->servername)); /* make sure a user mapping exists */ GetUserMapping(form->subowner, new_server->serverid); conninfo = ForeignServerConnectionString(form->subowner, - new_server->serverid); + new_server); /* Load the library providing us libpq calls. */ load_file("libpqwalreceiver", false); @@ -2250,7 +2250,9 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) if (OidIsValid(form->subserver)) { AclResult aclresult; + ForeignServer *server; + server = GetForeignServer(form->subserver); aclresult = object_aclcheck(ForeignServerRelationId, form->subserver, form->subowner, ACL_USAGE); if (aclresult != ACLCHECK_OK) @@ -2263,12 +2265,12 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) */ err = psprintf(_("subscription owner \"%s\" does not have permission on foreign server \"%s\""), GetUserNameFromId(form->subowner, false), - ForeignServerName(form->subserver)); + server->servername); conninfo = NULL; } else conninfo = ForeignServerConnectionString(form->subowner, - form->subserver); + server); } else { @@ -2593,18 +2595,18 @@ AlterSubscriptionOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId) */ if (OidIsValid(form->subserver)) { - Oid serverid = form->subserver; + ForeignServer *server = GetForeignServer(form->subserver); - aclresult = object_aclcheck(ForeignServerRelationId, serverid, newOwnerId, ACL_USAGE); + aclresult = object_aclcheck(ForeignServerRelationId, server->serverid, newOwnerId, ACL_USAGE); if (aclresult != ACLCHECK_OK) ereport(ERROR, errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("new subscription owner \"%s\" does not have permission on foreign server \"%s\"", GetUserNameFromId(newOwnerId, false), - ForeignServerName(serverid))); + server->servername)); /* make sure a user mapping exists */ - GetUserMapping(newOwnerId, serverid); + GetUserMapping(newOwnerId, server->serverid); } form->subowner = newOwnerId; diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c index f437b447282..5e9a1ac8514 100644 --- a/src/backend/foreign/foreign.c +++ b/src/backend/foreign/foreign.c @@ -177,31 +177,6 @@ GetForeignServerExtended(Oid serverid, bits16 flags) } -/* - * ForeignServerName - get name of foreign server. - */ -char * -ForeignServerName(Oid serverid) -{ - Form_pg_foreign_server serverform; - char *servername; - HeapTuple tp; - - tp = SearchSysCache1(FOREIGNSERVEROID, ObjectIdGetDatum(serverid)); - - if (!HeapTupleIsValid(tp)) - elog(ERROR, "cache lookup failed for foreign server %u", serverid); - - serverform = (Form_pg_foreign_server) GETSTRUCT(tp); - - servername = pstrdup(NameStr(serverform->srvname)); - - ReleaseSysCache(tp); - - return servername; -} - - /* * GetForeignServerByName - look up the foreign server definition by name. */ @@ -223,13 +198,11 @@ GetForeignServerByName(const char *srvname, bool missing_ok) * NB: leaks into CurrentMemoryContext. */ char * -ForeignServerConnectionString(Oid userid, Oid serverid) +ForeignServerConnectionString(Oid userid, ForeignServer *server) { - ForeignServer *server; ForeignDataWrapper *fdw; Datum connection_datum; - server = GetForeignServer(serverid); fdw = GetForeignDataWrapper(server->fdwid); if (!OidIsValid(fdw->fdwconnection)) @@ -241,7 +214,7 @@ ForeignServerConnectionString(Oid userid, Oid serverid) connection_datum = OidFunctionCall3(fdw->fdwconnection, ObjectIdGetDatum(userid), - ObjectIdGetDatum(serverid), + ObjectIdGetDatum(server->serverid), PointerGetDatum(NULL)); return text_to_cstring(DatumGetTextPP(connection_datum)); diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h index 65ed9a7f987..564c3cc1b7f 100644 --- a/src/include/foreign/foreign.h +++ b/src/include/foreign/foreign.h @@ -66,12 +66,12 @@ typedef struct ForeignTable extern ForeignServer *GetForeignServer(Oid serverid); -extern char *ForeignServerName(Oid serverid); extern ForeignServer *GetForeignServerExtended(Oid serverid, bits16 flags); extern ForeignServer *GetForeignServerByName(const char *srvname, bool missing_ok); -extern char *ForeignServerConnectionString(Oid userid, Oid serverid); +extern char *ForeignServerConnectionString(Oid userid, + ForeignServer *server); extern UserMapping *GetUserMapping(Oid userid, Oid serverid); extern ForeignDataWrapper *GetForeignDataWrapper(Oid fdwid); extern ForeignDataWrapper *GetForeignDataWrapperExtended(Oid fdwid, -- 2.43.0 [text/x-patch] v22-0005-Fix-pg_dump-for-CREATE-FOREIGN-DATA-WRAPPER-.-CO.patch (2.9K, 6-v22-0005-Fix-pg_dump-for-CREATE-FOREIGN-DATA-WRAPPER-.-CO.patch) download | inline diff: From d5496c2cdb21cc083a7b844d5ef312d3107941b0 Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Sat, 14 Mar 2026 15:07:27 -0700 Subject: [PATCH v22 5/6] Fix pg_dump for CREATE FOREIGN DATA WRAPPER ... CONNECTION. --- src/bin/pg_dump/pg_dump.c | 15 ++++++++++++++- src/bin/pg_dump/pg_dump.h | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index 137161aa5e0..17fa533086c 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -10525,6 +10525,7 @@ getForeignDataWrappers(Archive *fout) int i_fdwowner; int i_fdwhandler; int i_fdwvalidator; + int i_fdwconnection; int i_fdwacl; int i_acldefault; int i_fdwoptions; @@ -10534,7 +10535,14 @@ getForeignDataWrappers(Archive *fout) appendPQExpBufferStr(query, "SELECT tableoid, oid, fdwname, " "fdwowner, " "fdwhandler::pg_catalog.regproc, " - "fdwvalidator::pg_catalog.regproc, " + "fdwvalidator::pg_catalog.regproc, "); + + if (fout->remoteVersion >= 190000) + appendPQExpBufferStr(query, "fdwconnection::pg_catalog.regproc, "); + else + appendPQExpBufferStr(query, "'-' AS fdwconnection, "); + + appendPQExpBufferStr(query, "fdwacl, " "acldefault('F', fdwowner) AS acldefault, " "array_to_string(ARRAY(" @@ -10557,6 +10565,7 @@ getForeignDataWrappers(Archive *fout) i_fdwowner = PQfnumber(res, "fdwowner"); i_fdwhandler = PQfnumber(res, "fdwhandler"); i_fdwvalidator = PQfnumber(res, "fdwvalidator"); + i_fdwconnection = PQfnumber(res, "fdwconnection"); i_fdwacl = PQfnumber(res, "fdwacl"); i_acldefault = PQfnumber(res, "acldefault"); i_fdwoptions = PQfnumber(res, "fdwoptions"); @@ -10576,6 +10585,7 @@ getForeignDataWrappers(Archive *fout) fdwinfo[i].rolname = getRoleName(PQgetvalue(res, i, i_fdwowner)); fdwinfo[i].fdwhandler = pg_strdup(PQgetvalue(res, i, i_fdwhandler)); fdwinfo[i].fdwvalidator = pg_strdup(PQgetvalue(res, i, i_fdwvalidator)); + fdwinfo[i].fdwconnection = pg_strdup(PQgetvalue(res, i, i_fdwconnection)); fdwinfo[i].fdwoptions = pg_strdup(PQgetvalue(res, i, i_fdwoptions)); /* Decide whether we want to dump it */ @@ -16179,6 +16189,9 @@ dumpForeignDataWrapper(Archive *fout, const FdwInfo *fdwinfo) if (strcmp(fdwinfo->fdwvalidator, "-") != 0) appendPQExpBuffer(q, " VALIDATOR %s", fdwinfo->fdwvalidator); + if (strcmp(fdwinfo->fdwconnection, "-") != 0) + appendPQExpBuffer(q, " CONNECTION %s", fdwinfo->fdwconnection); + if (strlen(fdwinfo->fdwoptions) > 0) appendPQExpBuffer(q, " OPTIONS (\n %s\n)", fdwinfo->fdwoptions); diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h index 1c11a79083f..b150d736db1 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -604,6 +604,7 @@ typedef struct _fdwInfo const char *rolname; char *fdwhandler; char *fdwvalidator; + char *fdwconnection; char *fdwoptions; } FdwInfo; -- 2.43.0 [text/x-patch] v22-0006-Add-pg_dump-tests-related-to-CREATE-SUBSCRIPTION.patch (3.0K, 7-v22-0006-Add-pg_dump-tests-related-to-CREATE-SUBSCRIPTION.patch) download | inline diff: From 2b28e4bc147bbf47bc6912986b4400d634f17131 Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Sat, 14 Mar 2026 15:07:52 -0700 Subject: [PATCH v22 6/6] Add pg_dump tests related to CREATE SUBSCRIPTION ... SERVER. Suggested-by: Ashutosh Bapat <[email protected]> Discussion: https://postgr.es/m/CAExHW5vV5znEvecX=ra2-v7UBj9-M6qvdDzuB78M-TxbYD1PEA@mail.gmail.com --- src/bin/pg_dump/t/002_pg_dump.pl | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 6d1d38128fc..11a944dd5f8 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -2846,6 +2846,40 @@ my %tests = ( like => { %full_runs, section_pre_data => 1, }, }, + 'CREATE FUNCTION public.test_fdw_connection(oid, oid, internal)' => { + create_order => 37, + create_sql => "CREATE FUNCTION public.test_fdw_connection(oid, oid, internal) RETURNS text AS '\$libdir/regress', 'test_fdw_connection' LANGUAGE C;", + regexp => qr/^ + \QCREATE FUNCTION public.test_fdw_connection(oid, oid, internal) \E + \QRETURNS text\E + \n\s+\QLANGUAGE c\E + \n\s+AS\ \'\$ + \Qlibdir\/regress', 'test_fdw_connection';\E + /xm, + like => { %full_runs, section_pre_data => 1, }, + }, + + 'CREATE FOREIGN DATA WRAPPER test_fdw CONNECTION public.test_fdw_connection' => { + create_order => 38, + create_sql => 'CREATE FOREIGN DATA WRAPPER test_fdw CONNECTION public.test_fdw_connection;', + regexp => qr/CREATE FOREIGN DATA WRAPPER test_fdw CONNECTION public.test_fdw_connection;/m, + like => { %full_runs, section_pre_data => 1, }, + }, + + 'CREATE SERVER s2 FOREIGN DATA WRAPPER test_fdw' => { + create_order => 39, + create_sql => 'CREATE SERVER s2 FOREIGN DATA WRAPPER test_fdw;', + regexp => qr/CREATE SERVER s2 FOREIGN DATA WRAPPER test_fdw;/m, + like => { %full_runs, section_pre_data => 1, }, + }, + + 'CREATE USER MAPPING FOR public SERVER s2' => { + create_order => 40, + create_sql => 'CREATE USER MAPPING FOR public SERVER s2;', + regexp => qr/CREATE USER MAPPING FOR public SERVER s2;/m, + like => { %full_runs, section_pre_data => 1, }, + }, + 'CREATE FOREIGN TABLE dump_test.foreign_table SERVER s1' => { create_order => 88, create_sql => @@ -3275,6 +3309,21 @@ my %tests = ( }, }, + 'CREATE SUBSCRIPTION sub4 SERVER s2' => { + create_order => 50, + create_sql => 'CREATE SUBSCRIPTION sub4 + SERVER s2 PUBLICATION pub1 + WITH (connect = false, slot_name = NONE, origin = any, streaming = on);', + regexp => qr/^ + \QCREATE SUBSCRIPTION sub4 SERVER s2 PUBLICATION pub1 WITH (connect = false, slot_name = NONE, streaming = on);\E + /xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { + no_subscriptions => 1, + no_subscriptions_restore => 1, + }, + }, + # Regardless of whether the table or schema is excluded, publications must # still be dumped, as excluded objects do not apply to publications. We -- 2.43.0 ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-05 03:51 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-05 08:52 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-06 16:19 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Ashutosh Bapat <[email protected]> 2026-03-14 22:55 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> @ 2026-03-16 05:38 ` Amit Kapila <[email protected]> 2026-03-17 05:59 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 1 sibling, 1 reply; 17+ messages in thread From: Amit Kapila @ 2026-03-16 05:38 UTC (permalink / raw) To: Jeff Davis <[email protected]>; +Cc: Ashutosh Bapat <[email protected]>; Masahiko Sawada <[email protected]>; Shlok Kyal <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Sun, Mar 15, 2026 at 4:25 AM Jeff Davis <[email protected]> wrote: > > Attached series including patches to address Andres's and Amit's > comments, too. > 0001 LGTM. 0003: @@ -5056,8 +5058,15 @@ maybe_reread_subscription(void) started_tx = true; } - /* Ensure allocations in permanent context. */ - oldctx = MemoryContextSwitchTo(ApplyContext); + newctx = AllocSetContextCreate(ApplyContext, + "Subscription Context", + ALLOCSET_SMALL_SIZES); + Won't it be sufficient if we just reset MySubscriptionCtx here or in callback subscription_change_cb()? -- With Regards, Amit Kapila. ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-05 03:51 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-05 08:52 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-06 16:19 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Ashutosh Bapat <[email protected]> 2026-03-14 22:55 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-16 05:38 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> @ 2026-03-17 05:59 ` Amit Kapila <[email protected]> 2026-03-17 16:56 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 0 siblings, 1 reply; 17+ messages in thread From: Amit Kapila @ 2026-03-17 05:59 UTC (permalink / raw) To: Jeff Davis <[email protected]>; +Cc: Ashutosh Bapat <[email protected]>; Masahiko Sawada <[email protected]>; Shlok Kyal <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Mon, Mar 16, 2026 at 9:56 PM Jeff Davis <[email protected]> wrote: > > On Mon, 2026-03-16 at 11:08 +0530, Amit Kapila wrote: > > Won't it be sufficient if we just reset MySubscriptionCtx here or in > > callback subscription_change_cb()? > > The old and new subscriptions are compared against eachother (to see > whether to restart the worker or not), so they both have to exist at > the same time. If we put them in the same context, then we can't reset > it. > > I suppose we could have just two contexts and switch back and forth > between them, resetting the last one. But that doesn't seem to be worth > the trouble. > Yeah, or the other possibility could be to let the newsub information get allocated in the current transaction context and reset the subscription context if we decide not to exit from the worker. Then copy/get the subscription info in subscription context but not sure if that is worth it. The minor oddity in the proposed approach is that the worker will exit in many cases after allocating the new context but that may be the best we can do here. -- With Regards, Amit Kapila. ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-05 03:51 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-05 08:52 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-06 16:19 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Ashutosh Bapat <[email protected]> 2026-03-14 22:55 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-16 05:38 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-17 05:59 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> @ 2026-03-17 16:56 ` Jeff Davis <[email protected]> 0 siblings, 0 replies; 17+ messages in thread From: Jeff Davis @ 2026-03-17 16:56 UTC (permalink / raw) To: Amit Kapila <[email protected]>; +Cc: Ashutosh Bapat <[email protected]>; Masahiko Sawada <[email protected]>; Shlok Kyal <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Tue, 2026-03-17 at 11:29 +0530, Amit Kapila wrote: > Yeah, or the other possibility could be to let the newsub information > get allocated in the current transaction context and reset the > subscription context if we decide not to exit from the worker. Then > copy/get the subscription info in subscription context but not sure > if > that is worth it. Then we have to invent a deep copy for the Subscription, and we've already seen that the FreeSubscrpition() method was not being maintained properly. > The minor oddity in the proposed approach is that > the worker will exit in many cases after allocating the new context > but that may be the best we can do here. Agreed. Regards, Jeff Davis ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-05 03:51 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-05 08:52 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-06 16:19 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Ashutosh Bapat <[email protected]> 2026-03-14 22:55 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> @ 2026-03-18 19:06 ` Jeff Davis <[email protected]> 1 sibling, 0 replies; 17+ messages in thread From: Jeff Davis @ 2026-03-18 19:06 UTC (permalink / raw) To: Ashutosh Bapat <[email protected]>; +Cc: Amit Kapila <[email protected]>; Masahiko Sawada <[email protected]>; Shlok Kyal <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Sat, 2026-03-14 at 15:55 -0700, Jeff Davis wrote: > Attached series including patches to address Andres's and Amit's > comments, too. Committed two patches. New patch 0004: fixes missing dependencies from the FDW to the connection function. There's a related pre-existing issue with the dependency from the FDW to the handler function, which I will post as a separate backportable bugfix. I'd still like to find a good way to add pg_dump tests. The only idea I have now is to build the test function into core postgres (without pg_proc entry), which might be worthwhile. Regards, Jeff Davis Attachments: [text/x-patch] v23-0001-Temp-context-for-maybe_reread_subscription.patch (6.9K, 2-v23-0001-Temp-context-for-maybe_reread_subscription.patch) download | inline diff: From 2939a576908866394795460ac509cea369280b2b Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Thu, 12 Mar 2026 18:04:35 -0700 Subject: [PATCH v23 1/4] Temp context for maybe_reread_subscription(). Move temp context from ForeignServerConnectionString() to maybe_reread_subscription(), so that it prevents more invalidation-related leaks. Remove PG_TRY()/PG_FINALLY() from ForeignServerConnectionString(). Suggested-by: Andres Freund <[email protected]> Discussion: https://postgr.es/m/xvdjrdqnpap3uq7owbaox3r7p5gf7sv62aaqf2ju3vb6yglatr%40kvvwhoudrlxq --- src/backend/catalog/pg_subscription.c | 14 ----- src/backend/foreign/foreign.c | 66 +++++++----------------- src/backend/replication/logical/worker.c | 30 +++++++++-- src/include/catalog/pg_subscription.h | 1 - 4 files changed, 43 insertions(+), 68 deletions(-) diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c index 3673d4f0bc1..ca053c152cf 100644 --- a/src/backend/catalog/pg_subscription.c +++ b/src/backend/catalog/pg_subscription.c @@ -216,20 +216,6 @@ CountDBSubscriptions(Oid dbid) return nsubs; } -/* - * Free memory allocated by subscription struct. - */ -void -FreeSubscription(Subscription *sub) -{ - pfree(sub->name); - pfree(sub->conninfo); - if (sub->slotname) - pfree(sub->slotname); - list_free_deep(sub->publications); - pfree(sub); -} - /* * Disable the given subscription. */ diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c index 2edfac68d9b..1b53ca306a0 100644 --- a/src/backend/foreign/foreign.c +++ b/src/backend/foreign/foreign.c @@ -220,62 +220,32 @@ GetForeignServerByName(const char *srvname, bool missing_ok) /* * Retrieve connection string from server's FDW. + * + * NB: leaks into CurrentMemoryContext. */ char * ForeignServerConnectionString(Oid userid, Oid serverid) { - MemoryContext tempContext; - MemoryContext oldcxt; - text *volatile connection_text = NULL; - char *result = NULL; - - /* - * GetForeignServer, GetForeignDataWrapper, and the connection function - * itself all leak memory into CurrentMemoryContext. Switch to a temporary - * context for easy cleanup. - */ - tempContext = AllocSetContextCreate(CurrentMemoryContext, - "FDWConnectionContext", - ALLOCSET_SMALL_SIZES); - - oldcxt = MemoryContextSwitchTo(tempContext); - - PG_TRY(); - { - ForeignServer *server; - ForeignDataWrapper *fdw; - Datum connection_datum; - - server = GetForeignServer(serverid); - fdw = GetForeignDataWrapper(server->fdwid); - - if (!OidIsValid(fdw->fdwconnection)) - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("foreign data wrapper \"%s\" does not support subscription connections", - fdw->fdwname), - errdetail("Foreign data wrapper must be defined with CONNECTION specified."))); - - - connection_datum = OidFunctionCall3(fdw->fdwconnection, - ObjectIdGetDatum(userid), - ObjectIdGetDatum(serverid), - PointerGetDatum(NULL)); + ForeignServer *server; + ForeignDataWrapper *fdw; + Datum connection_datum; - connection_text = DatumGetTextPP(connection_datum); - } - PG_FINALLY(); - { - MemoryContextSwitchTo(oldcxt); + server = GetForeignServer(serverid); + fdw = GetForeignDataWrapper(server->fdwid); - if (connection_text) - result = text_to_cstring((text *) connection_text); + if (!OidIsValid(fdw->fdwconnection)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("foreign data wrapper \"%s\" does not support subscription connections", + fdw->fdwname), + errdetail("Foreign data wrapper must be defined with CONNECTION specified."))); - MemoryContextDelete(tempContext); - } - PG_END_TRY(); + connection_datum = OidFunctionCall3(fdw->fdwconnection, + ObjectIdGetDatum(userid), + ObjectIdGetDatum(serverid), + PointerGetDatum(NULL)); - return result; + return text_to_cstring(DatumGetTextPP(connection_datum)); } diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index 2d7708805a6..a8256a54a97 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -481,6 +481,7 @@ static MemoryContext LogicalStreamingContext = NULL; WalReceiverConn *LogRepWorkerWalRcvConn = NULL; Subscription *MySubscription = NULL; +static MemoryContext MySubscriptionCtx = NULL; static bool MySubscriptionValid = false; static List *on_commit_wakeup_workers_subids = NIL; @@ -5044,6 +5045,7 @@ void maybe_reread_subscription(void) { MemoryContext oldctx; + MemoryContext newctx; Subscription *newsub; bool started_tx = false; @@ -5058,8 +5060,15 @@ maybe_reread_subscription(void) started_tx = true; } - /* Ensure allocations in permanent context. */ - oldctx = MemoryContextSwitchTo(ApplyContext); + newctx = AllocSetContextCreate(ApplyContext, + "Subscription Context", + ALLOCSET_SMALL_SIZES); + + /* + * GetSubscription() leaks a number of small allocations, so use a + * subcontext for each call. + */ + oldctx = MemoryContextSwitchTo(newctx); newsub = GetSubscription(MyLogicalRepWorker->subid, true, true); @@ -5151,7 +5160,8 @@ maybe_reread_subscription(void) } /* Clean old subscription info and switch to new one. */ - FreeSubscription(MySubscription); + MemoryContextDelete(MySubscriptionCtx); + MySubscriptionCtx = newctx; MySubscription = newsub; MemoryContextSwitchTo(oldctx); @@ -5796,12 +5806,19 @@ InitializeLogRepWorker(void) */ SetConfigOption("search_path", "", PGC_SUSET, PGC_S_OVERRIDE); - /* Load the subscription into persistent memory context. */ ApplyContext = AllocSetContextCreate(TopMemoryContext, "ApplyContext", ALLOCSET_DEFAULT_SIZES); + + /* + * GetSubscription() leaks a number of small allocations, so use a + * subcontext for each call. + */ + MySubscriptionCtx = AllocSetContextCreate(ApplyContext, + "Subscription Context", + ALLOCSET_SMALL_SIZES); + StartTransactionCommand(); - oldctx = MemoryContextSwitchTo(ApplyContext); /* * Lock the subscription to prevent it from being concurrently dropped, @@ -5810,7 +5827,10 @@ InitializeLogRepWorker(void) */ LockSharedObject(SubscriptionRelationId, MyLogicalRepWorker->subid, 0, AccessShareLock); + + oldctx = MemoryContextSwitchTo(MySubscriptionCtx); MySubscription = GetSubscription(MyLogicalRepWorker->subid, true, true); + if (!MySubscription) { ereport(LOG, diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h index 0058d9387d7..2f6f7b57698 100644 --- a/src/include/catalog/pg_subscription.h +++ b/src/include/catalog/pg_subscription.h @@ -212,7 +212,6 @@ typedef struct Subscription extern Subscription *GetSubscription(Oid subid, bool missing_ok, bool aclcheck); -extern void FreeSubscription(Subscription *sub); extern void DisableSubscription(Oid subid); extern int CountDBSubscriptions(Oid dbid); -- 2.43.0 [text/x-patch] v23-0002-Refactor-to-remove-ForeignServerName.patch (7.3K, 3-v23-0002-Refactor-to-remove-ForeignServerName.patch) download | inline diff: From ca756b2802b689b87323f017455bb179b691a6c6 Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Fri, 13 Mar 2026 19:37:21 -0700 Subject: [PATCH v23 2/4] Refactor to remove ForeignServerName(). Callers either have a ForeignServer object or can readily construct one. Also simplify ForeignServerConnectionString() by accepting a ForeignServer rather than its OID. Discussion: https://postgr.es/m/CAExHW5vV5znEvecX=ra2-v7UBj9-M6qvdDzuB78M-TxbYD1PEA@mail.gmail.com Suggested-by: Ashutosh Bapat <[email protected]> --- src/backend/catalog/pg_subscription.c | 7 ++++-- src/backend/commands/subscriptioncmds.c | 20 +++++++++------- src/backend/foreign/foreign.c | 31 ++----------------------- src/include/foreign/foreign.h | 4 ++-- 4 files changed, 20 insertions(+), 42 deletions(-) diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c index ca053c152cf..d9e220172e9 100644 --- a/src/backend/catalog/pg_subscription.c +++ b/src/backend/catalog/pg_subscription.c @@ -114,6 +114,9 @@ GetSubscription(Oid subid, bool missing_ok, bool aclcheck) if (OidIsValid(subform->subserver)) { AclResult aclresult; + ForeignServer *server; + + server = GetForeignServer(subform->subserver); /* recheck ACL if requested */ if (aclcheck) @@ -127,11 +130,11 @@ GetSubscription(Oid subid, bool missing_ok, bool aclcheck) (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", GetUserNameFromId(subform->subowner, false), - ForeignServerName(subform->subserver)))); + server->servername))); } sub->conninfo = ForeignServerConnectionString(subform->subowner, - subform->subserver); + server); } else { diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index 724637cff5b..7375e214cb4 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -753,7 +753,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, GetUserMapping(owner, server->serverid); serverid = server->serverid; - conninfo = ForeignServerConnectionString(owner, serverid); + conninfo = ForeignServerConnectionString(owner, server); } else { @@ -1841,13 +1841,13 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"", GetUserNameFromId(form->subowner, false), - ForeignServerName(new_server->serverid))); + new_server->servername)); /* make sure a user mapping exists */ GetUserMapping(form->subowner, new_server->serverid); conninfo = ForeignServerConnectionString(form->subowner, - new_server->serverid); + new_server); /* Load the library providing us libpq calls. */ load_file("libpqwalreceiver", false); @@ -2250,7 +2250,9 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) if (OidIsValid(form->subserver)) { AclResult aclresult; + ForeignServer *server; + server = GetForeignServer(form->subserver); aclresult = object_aclcheck(ForeignServerRelationId, form->subserver, form->subowner, ACL_USAGE); if (aclresult != ACLCHECK_OK) @@ -2263,12 +2265,12 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) */ err = psprintf(_("subscription owner \"%s\" does not have permission on foreign server \"%s\""), GetUserNameFromId(form->subowner, false), - ForeignServerName(form->subserver)); + server->servername); conninfo = NULL; } else conninfo = ForeignServerConnectionString(form->subowner, - form->subserver); + server); } else { @@ -2593,18 +2595,18 @@ AlterSubscriptionOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId) */ if (OidIsValid(form->subserver)) { - Oid serverid = form->subserver; + ForeignServer *server = GetForeignServer(form->subserver); - aclresult = object_aclcheck(ForeignServerRelationId, serverid, newOwnerId, ACL_USAGE); + aclresult = object_aclcheck(ForeignServerRelationId, server->serverid, newOwnerId, ACL_USAGE); if (aclresult != ACLCHECK_OK) ereport(ERROR, errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("new subscription owner \"%s\" does not have permission on foreign server \"%s\"", GetUserNameFromId(newOwnerId, false), - ForeignServerName(serverid))); + server->servername)); /* make sure a user mapping exists */ - GetUserMapping(newOwnerId, serverid); + GetUserMapping(newOwnerId, server->serverid); } form->subowner = newOwnerId; diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c index 1b53ca306a0..005282f17f6 100644 --- a/src/backend/foreign/foreign.c +++ b/src/backend/foreign/foreign.c @@ -178,31 +178,6 @@ GetForeignServerExtended(Oid serverid, bits16 flags) } -/* - * ForeignServerName - get name of foreign server. - */ -char * -ForeignServerName(Oid serverid) -{ - Form_pg_foreign_server serverform; - char *servername; - HeapTuple tp; - - tp = SearchSysCache1(FOREIGNSERVEROID, ObjectIdGetDatum(serverid)); - - if (!HeapTupleIsValid(tp)) - elog(ERROR, "cache lookup failed for foreign server %u", serverid); - - serverform = (Form_pg_foreign_server) GETSTRUCT(tp); - - servername = pstrdup(NameStr(serverform->srvname)); - - ReleaseSysCache(tp); - - return servername; -} - - /* * GetForeignServerByName - look up the foreign server definition by name. */ @@ -224,13 +199,11 @@ GetForeignServerByName(const char *srvname, bool missing_ok) * NB: leaks into CurrentMemoryContext. */ char * -ForeignServerConnectionString(Oid userid, Oid serverid) +ForeignServerConnectionString(Oid userid, ForeignServer *server) { - ForeignServer *server; ForeignDataWrapper *fdw; Datum connection_datum; - server = GetForeignServer(serverid); fdw = GetForeignDataWrapper(server->fdwid); if (!OidIsValid(fdw->fdwconnection)) @@ -242,7 +215,7 @@ ForeignServerConnectionString(Oid userid, Oid serverid) connection_datum = OidFunctionCall3(fdw->fdwconnection, ObjectIdGetDatum(userid), - ObjectIdGetDatum(serverid), + ObjectIdGetDatum(server->serverid), PointerGetDatum(NULL)); return text_to_cstring(DatumGetTextPP(connection_datum)); diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h index 65ed9a7f987..564c3cc1b7f 100644 --- a/src/include/foreign/foreign.h +++ b/src/include/foreign/foreign.h @@ -66,12 +66,12 @@ typedef struct ForeignTable extern ForeignServer *GetForeignServer(Oid serverid); -extern char *ForeignServerName(Oid serverid); extern ForeignServer *GetForeignServerExtended(Oid serverid, bits16 flags); extern ForeignServer *GetForeignServerByName(const char *srvname, bool missing_ok); -extern char *ForeignServerConnectionString(Oid userid, Oid serverid); +extern char *ForeignServerConnectionString(Oid userid, + ForeignServer *server); extern UserMapping *GetUserMapping(Oid userid, Oid serverid); extern ForeignDataWrapper *GetForeignDataWrapper(Oid fdwid); extern ForeignDataWrapper *GetForeignDataWrapperExtended(Oid fdwid, -- 2.43.0 [text/x-patch] v23-0003-Add-pg_dump-tests-related-to-CREATE-SUBSCRIPTION.patch (3.0K, 4-v23-0003-Add-pg_dump-tests-related-to-CREATE-SUBSCRIPTION.patch) download | inline diff: From 92ce5061780886ddf22398d829e047bb27b135a8 Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Sat, 14 Mar 2026 15:07:52 -0700 Subject: [PATCH v23 3/4] Add pg_dump tests related to CREATE SUBSCRIPTION ... SERVER. Suggested-by: Ashutosh Bapat <[email protected]> Discussion: https://postgr.es/m/CAExHW5vV5znEvecX=ra2-v7UBj9-M6qvdDzuB78M-TxbYD1PEA@mail.gmail.com --- src/bin/pg_dump/t/002_pg_dump.pl | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 051a3d8ea3d..c0cbdd4c65e 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -2846,6 +2846,40 @@ my %tests = ( like => { %full_runs, section_pre_data => 1, }, }, + 'CREATE FUNCTION public.test_fdw_connection(oid, oid, internal)' => { + create_order => 37, + create_sql => "CREATE FUNCTION public.test_fdw_connection(oid, oid, internal) RETURNS text AS '\$libdir/regress', 'test_fdw_connection' LANGUAGE C;", + regexp => qr/^ + \QCREATE FUNCTION public.test_fdw_connection(oid, oid, internal) \E + \QRETURNS text\E + \n\s+\QLANGUAGE c\E + \n\s+AS\ \'\$ + \Qlibdir\/regress', 'test_fdw_connection';\E + /xm, + like => { %full_runs, section_pre_data => 1, }, + }, + + 'CREATE FOREIGN DATA WRAPPER test_fdw CONNECTION public.test_fdw_connection' => { + create_order => 38, + create_sql => 'CREATE FOREIGN DATA WRAPPER test_fdw CONNECTION public.test_fdw_connection;', + regexp => qr/CREATE FOREIGN DATA WRAPPER test_fdw CONNECTION public.test_fdw_connection;/m, + like => { %full_runs, section_pre_data => 1, }, + }, + + 'CREATE SERVER s2 FOREIGN DATA WRAPPER test_fdw' => { + create_order => 39, + create_sql => 'CREATE SERVER s2 FOREIGN DATA WRAPPER test_fdw;', + regexp => qr/CREATE SERVER s2 FOREIGN DATA WRAPPER test_fdw;/m, + like => { %full_runs, section_pre_data => 1, }, + }, + + 'CREATE USER MAPPING FOR public SERVER s2' => { + create_order => 40, + create_sql => 'CREATE USER MAPPING FOR public SERVER s2;', + regexp => qr/CREATE USER MAPPING FOR public SERVER s2;/m, + like => { %full_runs, section_pre_data => 1, }, + }, + 'CREATE FOREIGN TABLE dump_test.foreign_table SERVER s1' => { create_order => 88, create_sql => @@ -3287,6 +3321,21 @@ my %tests = ( }, }, + 'CREATE SUBSCRIPTION sub4 SERVER s2' => { + create_order => 50, + create_sql => 'CREATE SUBSCRIPTION sub4 + SERVER s2 PUBLICATION pub1 + WITH (connect = false, slot_name = NONE, origin = any, streaming = on);', + regexp => qr/^ + \QCREATE SUBSCRIPTION sub4 SERVER s2 PUBLICATION pub1 WITH (connect = false, slot_name = NONE, streaming = on);\E + /xm, + like => { %full_runs, section_post_data => 1, }, + unlike => { + no_subscriptions => 1, + no_subscriptions_restore => 1, + }, + }, + # Regardless of whether the table or schema is excluded, publications must # still be dumped, as excluded objects do not apply to publications. We -- 2.43.0 [text/x-patch] v23-0004-Add-dependency-entry-for-FDW-connection-function.patch (5.1K, 5-v23-0004-Add-dependency-entry-for-FDW-connection-function.patch) download | inline diff: From 9540cc9f76178f71e2d4f7ca9621064281207131 Mon Sep 17 00:00:00 2001 From: Jeff Davis <[email protected]> Date: Wed, 18 Mar 2026 10:31:38 -0700 Subject: [PATCH v23 4/4] Add dependency entry for FDW connection function. Missed in commit 8185bb5347. Catalog version bump. --- src/backend/commands/foreigncmds.c | 40 +++++++++++++++++++++- src/include/catalog/catversion.h | 2 +- src/test/regress/expected/subscription.out | 9 +++++ src/test/regress/sql/subscription.sql | 8 +++++ 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c index 45681235782..61ee25b345d 100644 --- a/src/backend/commands/foreigncmds.c +++ b/src/backend/commands/foreigncmds.c @@ -707,6 +707,14 @@ CreateForeignDataWrapper(ParseState *pstate, CreateFdwStmt *stmt) recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } + if (OidIsValid(fdwconnection)) + { + referenced.classId = ProcedureRelationId; + referenced.objectId = fdwconnection; + referenced.objectSubId = 0; + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + } + recordDependencyOnOwner(ForeignDataWrapperRelationId, fdwId, ownerId); /* dependency on extension */ @@ -814,6 +822,28 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) { repl_val[Anum_pg_foreign_data_wrapper_fdwconnection - 1] = ObjectIdGetDatum(fdwconnection); repl_repl[Anum_pg_foreign_data_wrapper_fdwconnection - 1] = true; + + /* + * If the connection function is changed, behavior of dependent + * subscriptions can change. If NO CONNECTION, dependent + * subscriptions will fail. + */ + if (OidIsValid(fdwForm->fdwconnection)) + { + if (OidIsValid(fdwconnection)) + ereport(WARNING, + (errmsg("changing the foreign-data wrapper connection function can cause " + "the options for dependent objects to become invalid"))); + else + ereport(WARNING, + (errmsg("removing the foreign-data wrapper connection function will cause " + "dependent subscriptions to fail"))); + } + } + else + { + /* connection function unchanged */ + fdwconnection = fdwForm->fdwconnection; } /* @@ -854,7 +884,7 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) ObjectAddressSet(myself, ForeignDataWrapperRelationId, fdwId); /* Update function dependencies if we changed them */ - if (handler_given || validator_given) + if (handler_given || validator_given || connection_given) { ObjectAddress referenced; @@ -884,6 +914,14 @@ AlterForeignDataWrapper(ParseState *pstate, AlterFdwStmt *stmt) referenced.objectSubId = 0; recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); } + + if (OidIsValid(fdwconnection)) + { + referenced.classId = ProcedureRelationId; + referenced.objectId = fdwconnection; + referenced.objectSubId = 0; + recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL); + } } InvokeObjectPostAlterHook(ForeignDataWrapperRelationId, fdwId, 0); diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 55a8fbbd509..a4f0d02af9c 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202603171 +#define CATALOG_VERSION_NO 202603181 #endif diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out index f57f359127b..7e3cabdb93f 100644 --- a/src/test/regress/expected/subscription.out +++ b/src/test/regress/expected/subscription.out @@ -185,6 +185,15 @@ DROP SUBSCRIPTION regress_testsub6; SET SESSION AUTHORIZATION regress_subscription_user; REVOKE CREATE ON DATABASE REGRESSION FROM regress_subscription_user3; DROP SERVER test_server; +-- fail, FDW is dependent +DROP FUNCTION test_fdw_connection(oid, oid, internal); +ERROR: cannot drop function test_fdw_connection(oid,oid,internal) because other objects depend on it +DETAIL: foreign-data wrapper test_fdw depends on function test_fdw_connection(oid,oid,internal) +HINT: Use DROP ... CASCADE to drop the dependent objects too. +-- warn +ALTER FOREIGN DATA WRAPPER test_fdw NO CONNECTION; +WARNING: removing the foreign-data wrapper connection function will cause dependent subscriptions to fail +DROP FUNCTION test_fdw_connection(oid, oid, internal); DROP FOREIGN DATA WRAPPER test_fdw; -- fail - invalid connection string during ALTER ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar'; diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql index a642b368183..6c3d9632e8a 100644 --- a/src/test/regress/sql/subscription.sql +++ b/src/test/regress/sql/subscription.sql @@ -138,6 +138,14 @@ SET SESSION AUTHORIZATION regress_subscription_user; REVOKE CREATE ON DATABASE REGRESSION FROM regress_subscription_user3; DROP SERVER test_server; + +-- fail, FDW is dependent +DROP FUNCTION test_fdw_connection(oid, oid, internal); +-- warn +ALTER FOREIGN DATA WRAPPER test_fdw NO CONNECTION; + +DROP FUNCTION test_fdw_connection(oid, oid, internal); + DROP FOREIGN DATA WRAPPER test_fdw; -- fail - invalid connection string during ALTER -- 2.43.0 ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-05 03:51 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-05 08:52 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> @ 2026-03-07 07:01 ` Amit Kapila <[email protected]> 2026-03-09 06:23 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 1 sibling, 1 reply; 17+ messages in thread From: Amit Kapila @ 2026-03-07 07:01 UTC (permalink / raw) To: Jeff Davis <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Shlok Kyal <[email protected]>; Ashutosh Bapat <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Thu, Mar 5, 2026 at 2:23 PM Jeff Davis <[email protected]> wrote: > > Additionally, I ran into a problem that's worth highlighting: > > DROP SERVER ... CASCADE was broken, because the subscription is > dependent on it but that's in a global catalog, which is not handled by > doDeletion(). The subscription is conceptually a per-database object, > but it's in a shared catalog with a subdbid field. I solved that > problem by adding a guard to findDependentObjects() to check for the > referenced object belonging to a shared catalog, and if so it just > throws an error (so CASCADE is not supported for servers used in > subscriptions). That's a simple but not a very satisfying solution, so > let me know if you see a problem with that. > I also can't think of any straight-forward solution for it. I've not thought in detail but can a new type of dependency be required to solve this problem? I am not aware if we are doing something similar in any other CASCADE operation, so even if we want to go with giving ERROR for this case, it may be better to get somewhat wider acceptance for the same unless few other people respond here and consider this as an acceptable solution. Few other minor comments: ====================== 1. +# Replicate the changes without columns +$node_publisher->safe_psql('postgres', "CREATE TABLE tab_no_col()"); +$node_publisher->safe_psql('postgres', + "INSERT INTO tab_no_col default VALUES"); I don't see a subscriber-side table or verification code to verify the above test. 2. + Oid subserver BKI_LOOKUP_OPT(pg_foreign_server); /* If connection uses + * server */ + Isn't it better to keep this along with other oids in the beginning of the catalog, say after subowner? It will also avoid padding before subserver field. -- With Regards, Amit Kapila. ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-05 03:51 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-05 08:52 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-07 07:01 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> @ 2026-03-09 06:23 ` Amit Kapila <[email protected]> 2026-03-10 14:23 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 0 siblings, 1 reply; 17+ messages in thread From: Amit Kapila @ 2026-03-09 06:23 UTC (permalink / raw) To: Jeff Davis <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Shlok Kyal <[email protected]>; Ashutosh Bapat <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Sat, Mar 7, 2026 at 12:31 PM Amit Kapila <[email protected]> wrote: > > On Thu, Mar 5, 2026 at 2:23 PM Jeff Davis <[email protected]> wrote: > > > > Few other minor comments: > ====================== > 1. > +# Replicate the changes without columns > +$node_publisher->safe_psql('postgres', "CREATE TABLE tab_no_col()"); > +$node_publisher->safe_psql('postgres', > + "INSERT INTO tab_no_col default VALUES"); > > I don't see a subscriber-side table or verification code to verify the > above test. > I see that the committed version (8185bb5347) has this part of the test, isn't that test incomplete, if not, tell me what am I missing? It seems I have sent this message after you have committed the last version. > 2. > + Oid subserver BKI_LOOKUP_OPT(pg_foreign_server); /* If connection uses > + * server */ > + > > Isn't it better to keep this along with other oids in the beginning of > the catalog, say after subowner? It will also avoid padding before > subserver field. > We can probably consider this one as well though there is no correctness issue as such. -- With Regards, Amit Kapila. ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-05 03:51 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-05 08:52 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-07 07:01 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-09 06:23 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> @ 2026-03-10 14:23 ` Jeff Davis <[email protected]> 2026-03-14 09:44 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 0 siblings, 1 reply; 17+ messages in thread From: Jeff Davis @ 2026-03-10 14:23 UTC (permalink / raw) To: Amit Kapila <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Shlok Kyal <[email protected]>; Ashutosh Bapat <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Mon, 2026-03-09 at 11:53 +0530, Amit Kapila wrote: > > +# Replicate the changes without columns > > +$node_publisher->safe_psql('postgres', "CREATE TABLE > > tab_no_col()"); > > +$node_publisher->safe_psql('postgres', > > + "INSERT INTO tab_no_col default VALUES"); > > > > I don't see a subscriber-side table or verification code to verify > > the > > above test. > > > > I see that the committed version (8185bb5347) has this part of the > test, isn't that test incomplete, if not, tell me what am I missing? In 8185bb5347, contrib/postgres_fdw/t/010_subscription.pl has: ... # Setup structure on subscriber $node_subscriber->safe_psql('postgres', "CREATE EXTENSION postgres_fdw"); $node_subscriber->safe_psql('postgres', "CREATE TABLE tab_ins (a int, b int)"); ... $result = $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM (SELECT f.b = l.b as match FROM tab_ins l, f_tab_ins f WHERE l.a = f.a) WHERE match"); is($result, qq(1050), 'check that inserted data was copied to subscriber'); ... which creates the subscriber-side table and verifies the result. If I change 1050 -> 1051, then the test fails, so I think it's functioning. Perhaps I don't understand the question? > It seems I have sent this message after you have committed the last > version. Yes, thank you, I will address those shortly. Regards, Jeff Davis ^ permalink raw reply [nested|flat] 17+ messages in thread
* Re: [19] CREATE SUBSCRIPTION ... SERVER 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-02 21:34 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-05 03:51 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-05 08:52 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> 2026-03-07 07:01 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-09 06:23 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Amit Kapila <[email protected]> 2026-03-10 14:23 ` Re: [19] CREATE SUBSCRIPTION ... SERVER Jeff Davis <[email protected]> @ 2026-03-14 09:44 ` Amit Kapila <[email protected]> 0 siblings, 0 replies; 17+ messages in thread From: Amit Kapila @ 2026-03-14 09:44 UTC (permalink / raw) To: Jeff Davis <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Shlok Kyal <[email protected]>; Ashutosh Bapat <[email protected]>; Bharath Rupireddy <[email protected]>; Joe Conway <[email protected]>; pgsql-hackers On Tue, Mar 10, 2026 at 7:53 PM Jeff Davis <[email protected]> wrote: > > On Mon, 2026-03-09 at 11:53 +0530, Amit Kapila wrote: > > > +# Replicate the changes without columns > > > +$node_publisher->safe_psql('postgres', "CREATE TABLE > > > tab_no_col()"); > > > +$node_publisher->safe_psql('postgres', > > > + "INSERT INTO tab_no_col default VALUES"); > > > > > > I don't see a subscriber-side table or verification code to verify > > > the > > > above test. > > > > > > > I see that the committed version (8185bb5347) has this part of the > > test, isn't that test incomplete, if not, tell me what am I missing? > > In 8185bb5347, contrib/postgres_fdw/t/010_subscription.pl has: > > ... > # Setup structure on subscriber > $node_subscriber->safe_psql('postgres', "CREATE EXTENSION > postgres_fdw"); > $node_subscriber->safe_psql('postgres', "CREATE TABLE tab_ins (a int, > b int)"); > ... > $result = > $node_subscriber->safe_psql('postgres', "SELECT count(*) FROM (SELECT > f.b = l.b as match FROM tab_ins l, f_tab_ins f WHERE l.a = f.a) WHERE > match"); > is($result, qq(1050), 'check that inserted data was copied to > subscriber'); > ... > > which creates the subscriber-side table and verifies the result. > I am talking about a table with the name tab_no_col whereas you are talking about a table with the name tab_ins. The test doesn't create a table with the name tab_no_col on the subscriber-side which makes it redundant, am I missing something? -- With Regards, Amit Kapila. ^ permalink raw reply [nested|flat] 17+ messages in thread
end of thread, other threads:[~2026-03-18 19:06 UTC | newest] Thread overview: 17+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2026-02-04 04:53 Re: [19] CREATE SUBSCRIPTION ... SERVER Masahiko Sawada <[email protected]> 2026-02-26 19:12 ` Jeff Davis <[email protected]> 2026-03-02 21:34 ` Jeff Davis <[email protected]> 2026-03-03 18:19 ` Masahiko Sawada <[email protected]> 2026-03-05 03:51 ` Amit Kapila <[email protected]> 2026-03-05 08:52 ` Jeff Davis <[email protected]> 2026-03-06 16:19 ` Ashutosh Bapat <[email protected]> 2026-03-07 07:05 ` Amit Kapila <[email protected]> 2026-03-14 22:55 ` Jeff Davis <[email protected]> 2026-03-16 05:38 ` Amit Kapila <[email protected]> 2026-03-17 05:59 ` Amit Kapila <[email protected]> 2026-03-17 16:56 ` Jeff Davis <[email protected]> 2026-03-18 19:06 ` Jeff Davis <[email protected]> 2026-03-07 07:01 ` Amit Kapila <[email protected]> 2026-03-09 06:23 ` Amit Kapila <[email protected]> 2026-03-10 14:23 ` Jeff Davis <[email protected]> 2026-03-14 09:44 ` Amit Kapila <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox