Skip to content

Commit 11efe3b

Browse files
jeff-davisCommitfest Bot
authored and
Commitfest Bot
committed
CREATE SUSBCRIPTION ... SERVER.
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. Discussion: https://postgr.es/m/[email protected] Reviewed-by: Ashutosh Bapat
1 parent 5ee4762 commit 11efe3b

File tree

23 files changed

+587
-35
lines changed

23 files changed

+587
-35
lines changed

contrib/postgres_fdw/Makefile

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ DATA = postgres_fdw--1.0.sql postgres_fdw--1.0--1.1.sql postgres_fdw--1.1--1.2.s
1919
REGRESS = postgres_fdw query_cancel
2020
TAP_TESTS = 1
2121

22+
TAP_TESTS = 1
23+
2224
ifdef USE_PGXS
2325
PG_CONFIG = pg_config
2426
PGXS := $(shell $(PG_CONFIG) --pgxs)

contrib/postgres_fdw/connection.c

+73
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ PG_FUNCTION_INFO_V1(postgres_fdw_get_connections);
131131
PG_FUNCTION_INFO_V1(postgres_fdw_get_connections_1_2);
132132
PG_FUNCTION_INFO_V1(postgres_fdw_disconnect);
133133
PG_FUNCTION_INFO_V1(postgres_fdw_disconnect_all);
134+
PG_FUNCTION_INFO_V1(postgres_fdw_connection);
134135

135136
/* prototypes of private functions */
136137
static void make_new_connection(ConnCacheEntry *entry, UserMapping *user);
@@ -2305,6 +2306,78 @@ postgres_fdw_get_connections_internal(FunctionCallInfo fcinfo,
23052306
}
23062307
}
23072308

2309+
/*
2310+
* Values in connection strings must be enclosed in single quotes. Single
2311+
* quotes and backslashes must be escaped with backslash. NB: these rules are
2312+
* different from the rules for escaping a SQL literal.
2313+
*/
2314+
static void
2315+
appendEscapedValue(StringInfo str, const char *val)
2316+
{
2317+
appendStringInfoChar(str, '\'');
2318+
for (int i = 0; val[i] != '\0'; i++)
2319+
{
2320+
if (val[i] == '\\' || val[i] == '\'')
2321+
appendStringInfoChar(str, '\\');
2322+
appendStringInfoChar(str, val[i]);
2323+
}
2324+
appendStringInfoChar(str, '\'');
2325+
}
2326+
2327+
Datum
2328+
postgres_fdw_connection(PG_FUNCTION_ARGS)
2329+
{
2330+
Oid userid = PG_GETARG_OID(0);
2331+
Oid serverid = PG_GETARG_OID(1);
2332+
ForeignServer *server = GetForeignServer(serverid);
2333+
UserMapping *user = GetUserMapping(userid, serverid);
2334+
StringInfoData str;
2335+
const char **keywords;
2336+
const char **values;
2337+
int n;
2338+
2339+
/*
2340+
* Construct connection params from generic options of ForeignServer and
2341+
* UserMapping. (Some of them might not be libpq options, in which case
2342+
* we'll just waste a few array slots.) Add 4 extra slots for
2343+
* application_name, fallback_application_name, client_encoding, end
2344+
* marker.
2345+
*/
2346+
n = list_length(server->options) + list_length(user->options) + 4;
2347+
keywords = (const char **) palloc(n * sizeof(char *));
2348+
values = (const char **) palloc(n * sizeof(char *));
2349+
2350+
n = 0;
2351+
n += ExtractConnectionOptions(server->options,
2352+
keywords + n, values + n);
2353+
n += ExtractConnectionOptions(user->options,
2354+
keywords + n, values + n);
2355+
2356+
/* Set client_encoding so that libpq can convert encoding properly. */
2357+
keywords[n] = "client_encoding";
2358+
values[n] = GetDatabaseEncodingName();
2359+
n++;
2360+
2361+
keywords[n] = values[n] = NULL;
2362+
2363+
/* verify the set of connection parameters */
2364+
check_conn_params(keywords, values, user);
2365+
2366+
initStringInfo(&str);
2367+
for (int i = 0; i < n; i++)
2368+
{
2369+
char *sep = "";
2370+
2371+
appendStringInfo(&str, "%s%s = ", sep, keywords[i]);
2372+
appendEscapedValue(&str, values[i]);
2373+
sep = " ";
2374+
}
2375+
2376+
pfree(keywords);
2377+
pfree(values);
2378+
PG_RETURN_TEXT_P(cstring_to_text(str.data));
2379+
}
2380+
23082381
/*
23092382
* List active foreign server connections.
23102383
*

contrib/postgres_fdw/expected/postgres_fdw.out

+8
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,14 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again
256256
ANALYZE ft1;
257257
ALTER FOREIGN TABLE ft2 OPTIONS (use_remote_estimate 'true');
258258
-- ===================================================================
259+
-- test subscription
260+
-- ===================================================================
261+
CREATE SUBSCRIPTION regress_pgfdw_subscription SERVER testserver1
262+
PUBLICATION pub1 WITH (slot_name = NONE, connect = false);
263+
WARNING: subscription was created, but is not connected
264+
HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and refresh the subscription.
265+
DROP SUBSCRIPTION regress_pgfdw_subscription;
266+
-- ===================================================================
259267
-- test error case for create publication on foreign table
260268
-- ===================================================================
261269
CREATE PUBLICATION testpub_ftbl FOR TABLE ft1; -- should fail

contrib/postgres_fdw/meson.build

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ tests += {
4444
'tap': {
4545
'tests': [
4646
't/001_auth_scram.pl',
47+
't/010_subscription.pl',
4748
],
4849
},
4950
}

contrib/postgres_fdw/postgres_fdw--1.1--1.2.sql

+8
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,11 @@ CREATE FUNCTION postgres_fdw_get_connections (
1616
RETURNS SETOF record
1717
AS 'MODULE_PATHNAME', 'postgres_fdw_get_connections_1_2'
1818
LANGUAGE C STRICT PARALLEL RESTRICTED;
19+
20+
-- takes internal parameter to prevent calling from SQL
21+
CREATE FUNCTION postgres_fdw_connection(oid, oid, internal)
22+
RETURNS text
23+
AS 'MODULE_PATHNAME'
24+
LANGUAGE C STRICT;
25+
26+
ALTER FOREIGN DATA WRAPPER postgres_fdw CONNECTION postgres_fdw_connection;

contrib/postgres_fdw/sql/postgres_fdw.sql

+7
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,13 @@ SELECT c3, c4 FROM ft1 ORDER BY c3, c1 LIMIT 1; -- should work again
248248
ANALYZE ft1;
249249
ALTER FOREIGN TABLE ft2 OPTIONS (use_remote_estimate 'true');
250250

251+
-- ===================================================================
252+
-- test subscription
253+
-- ===================================================================
254+
CREATE SUBSCRIPTION regress_pgfdw_subscription SERVER testserver1
255+
PUBLICATION pub1 WITH (slot_name = NONE, connect = false);
256+
DROP SUBSCRIPTION regress_pgfdw_subscription;
257+
251258
-- ===================================================================
252259
-- test error case for create publication on foreign table
253260
-- ===================================================================
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
2+
# Copyright (c) 2021-2024, PostgreSQL Global Development Group
3+
4+
# Basic logical replication test
5+
use strict;
6+
use warnings FATAL => 'all';
7+
use PostgreSQL::Test::Cluster;
8+
use PostgreSQL::Test::Utils;
9+
use Test::More;
10+
11+
# Initialize publisher node
12+
my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
13+
$node_publisher->init(allows_streaming => 'logical');
14+
$node_publisher->start;
15+
16+
# Create subscriber node
17+
my $node_subscriber = PostgreSQL::Test::Cluster->new('subscriber');
18+
$node_subscriber->init;
19+
$node_subscriber->start;
20+
21+
# Create some preexisting content on publisher
22+
$node_publisher->safe_psql('postgres',
23+
"CREATE TABLE tab_ins AS SELECT a, a + 1 as b FROM generate_series(1,1002) AS a");
24+
25+
# Replicate the changes without columns
26+
$node_publisher->safe_psql('postgres', "CREATE TABLE tab_no_col()");
27+
$node_publisher->safe_psql('postgres',
28+
"INSERT INTO tab_no_col default VALUES");
29+
30+
# Setup structure on subscriber
31+
$node_subscriber->safe_psql('postgres', "CREATE EXTENSION postgres_fdw");
32+
$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_ins (a int, b int)");
33+
34+
# Setup logical replication
35+
my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
36+
$node_publisher->safe_psql('postgres', "CREATE PUBLICATION tap_pub FOR TABLE tab_ins");
37+
38+
my $publisher_host = $node_publisher->host;
39+
my $publisher_port = $node_publisher->port;
40+
$node_subscriber->safe_psql('postgres',
41+
"CREATE SERVER tap_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '$publisher_host', port '$publisher_port', dbname 'postgres')"
42+
);
43+
44+
$node_subscriber->safe_psql('postgres',
45+
"CREATE USER MAPPING FOR PUBLIC SERVER tap_server"
46+
);
47+
48+
$node_subscriber->safe_psql('postgres',
49+
"CREATE FOREIGN TABLE f_tab_ins (a int, b int) SERVER tap_server OPTIONS(table_name 'tab_ins')"
50+
);
51+
$node_subscriber->safe_psql('postgres',
52+
"CREATE SUBSCRIPTION tap_sub SERVER tap_server PUBLICATION tap_pub WITH (password_required=false)"
53+
);
54+
55+
# Wait for initial table sync to finish
56+
$node_subscriber->wait_for_subscription_sync($node_publisher, 'tap_sub');
57+
58+
my $result =
59+
$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");
60+
is($result, qq(1002), 'check initial data was copied to subscriber');
61+
62+
$node_publisher->safe_psql('postgres',
63+
"INSERT INTO tab_ins SELECT a, a + 1 FROM generate_series(1003,1050) a");
64+
65+
$node_publisher->wait_for_catchup('tap_sub');
66+
67+
$result =
68+
$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");
69+
is($result, qq(1050), 'check initial data was copied to subscriber');
70+
71+
done_testing();

doc/src/sgml/ref/alter_subscription.sgml

+15-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ PostgreSQL documentation
2121

2222
<refsynopsisdiv>
2323
<synopsis>
24+
ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> SERVER <replaceable>servername</replaceable>
2425
ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> CONNECTION '<replaceable>conninfo</replaceable>'
2526
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>] [, ... ] ) ]
2627
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>] [, ... ] ) ]
@@ -101,13 +102,24 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
101102
</listitem>
102103
</varlistentry>
103104

105+
<varlistentry id="sql-altersubscription-params-server">
106+
<term><literal>SERVER <replaceable class="parameter">servername</replaceable></literal></term>
107+
<listitem>
108+
<para>
109+
This clause replaces the foreign server or connection string originally
110+
set by <xref linkend="sql-createsubscription"/> with the foreign server
111+
<replaceable>servername</replaceable>.
112+
</para>
113+
</listitem>
114+
</varlistentry>
115+
104116
<varlistentry id="sql-altersubscription-params-connection">
105117
<term><literal>CONNECTION '<replaceable class="parameter">conninfo</replaceable>'</literal></term>
106118
<listitem>
107119
<para>
108-
This clause replaces the connection string originally set by
109-
<xref linkend="sql-createsubscription"/>. See there for more
110-
information.
120+
This clause replaces the foreign server or connection string originally
121+
set by <xref linkend="sql-createsubscription"/> with the connection
122+
string <replaceable>conninfo</replaceable>.
111123
</para>
112124
</listitem>
113125
</varlistentry>

doc/src/sgml/ref/create_subscription.sgml

+10-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ PostgreSQL documentation
2222
<refsynopsisdiv>
2323
<synopsis>
2424
CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceable>
25-
CONNECTION '<replaceable class="parameter">conninfo</replaceable>'
25+
{ SERVER <replaceable class="parameter">servername</replaceable> | CONNECTION '<replaceable class="parameter">conninfo</replaceable>' }
2626
PUBLICATION <replaceable class="parameter">publication_name</replaceable> [, ...]
2727
[ WITH ( <replaceable class="parameter">subscription_parameter</replaceable> [= <replaceable class="parameter">value</replaceable>] [, ... ] ) ]
2828
</synopsis>
@@ -77,6 +77,15 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
7777
</listitem>
7878
</varlistentry>
7979

80+
<varlistentry id="sql-createsubscription-params-server">
81+
<term><literal>SERVER <replaceable class="parameter">servername</replaceable></literal></term>
82+
<listitem>
83+
<para>
84+
A foreign server to use for the connection.
85+
</para>
86+
</listitem>
87+
</varlistentry>
88+
8089
<varlistentry id="sql-createsubscription-params-connection">
8190
<term><literal>CONNECTION '<replaceable class="parameter">conninfo</replaceable>'</literal></term>
8291
<listitem>

src/backend/catalog/pg_subscription.c

+33-5
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@
1919
#include "access/htup_details.h"
2020
#include "access/tableam.h"
2121
#include "catalog/indexing.h"
22+
#include "catalog/pg_foreign_server.h"
2223
#include "catalog/pg_subscription.h"
2324
#include "catalog/pg_subscription_rel.h"
2425
#include "catalog/pg_type.h"
26+
#include "foreign/foreign.h"
2527
#include "miscadmin.h"
2628
#include "storage/lmgr.h"
29+
#include "utils/acl.h"
2730
#include "utils/array.h"
2831
#include "utils/builtins.h"
2932
#include "utils/fmgroids.h"
@@ -69,7 +72,7 @@ GetPublicationsStr(List *publications, StringInfo dest, bool quote_literal)
6972
* Fetch the subscription from the syscache.
7073
*/
7174
Subscription *
72-
GetSubscription(Oid subid, bool missing_ok)
75+
GetSubscription(Oid subid, bool missing_ok, bool aclcheck)
7376
{
7477
HeapTuple tup;
7578
Subscription *sub;
@@ -105,10 +108,35 @@ GetSubscription(Oid subid, bool missing_ok)
105108
sub->failover = subform->subfailover;
106109

107110
/* Get conninfo */
108-
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
109-
tup,
110-
Anum_pg_subscription_subconninfo);
111-
sub->conninfo = TextDatumGetCString(datum);
111+
if (OidIsValid(subform->subserver))
112+
{
113+
AclResult aclresult;
114+
115+
/* recheck ACL if requested */
116+
if (aclcheck)
117+
{
118+
aclresult = object_aclcheck(ForeignServerRelationId,
119+
subform->subserver,
120+
subform->subowner, ACL_USAGE);
121+
122+
if (aclresult != ACLCHECK_OK)
123+
ereport(ERROR,
124+
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
125+
errmsg("subscription owner \"%s\" does not have permission on foreign server \"%s\"",
126+
GetUserNameFromId(subform->subowner, false),
127+
ForeignServerName(subform->subserver))));
128+
}
129+
130+
sub->conninfo = ForeignServerConnectionString(subform->subowner,
131+
subform->subserver);
132+
}
133+
else
134+
{
135+
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,
136+
tup,
137+
Anum_pg_subscription_subconninfo);
138+
sub->conninfo = TextDatumGetCString(datum);
139+
}
112140

113141
/* Get slotname */
114142
datum = SysCacheGetAttr(SUBSCRIPTIONOID,

0 commit comments

Comments
 (0)