Identify missing publications from publisher while create/alter subscription.
Hi,
Creating/altering subscription is successful when we specify a
publication which does not exist in the publisher. I felt we should
throw an error in this case, that will help the user to check if there
is any typo in the create subscription command or to create the
publication before the subscription is created.
If the above analysis looks correct, then please find a patch that
checks if the specified publications are present in the publisher and
throws an error if any of the publications is missing in the
publisher.
Thoughts?
Regards,
Vignesh
EnterpriseDB: http://www.enterprisedb.com
Attachments:
v1-0001-Identify-missing-publications-from-publisher-whil.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Identify-missing-publications-from-publisher-whil.patchDownload
From 2e7a6e41f789f7f1717058e9c78441ae8d5faf9e Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh.c@enterprisedb.com>
Date: Thu, 21 Jan 2021 18:38:54 +0530
Subject: [PATCH v1] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
src/backend/commands/subscriptioncmds.c | 87 +++++++++++++++++++++++++++++++++
src/test/subscription/t/100_bugs.pl | 46 ++++++++++++++++-
2 files changed, 132 insertions(+), 1 deletion(-)
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 082f785..16c89cb 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -46,6 +46,7 @@
#include "utils/syscache.h"
static List *fetch_table_list(WalReceiverConn *wrconn, List *publications);
+static void check_publications(WalReceiverConn *wrconn, List *publications);
/*
* Common option parsing function for CREATE and ALTER SUBSCRIPTION commands.
@@ -490,6 +491,7 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
PG_TRY();
{
+ check_publications(wrconn, publications);
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -576,6 +578,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
ereport(ERROR,
(errmsg("could not connect to the publisher: %s", err)));
+ check_publications(wrconn, sub->publications);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -1211,6 +1215,89 @@ AlterSubscriptionOwner_oid(Oid subid, Oid newOwnerId)
}
/*
+ * Verify that the specified publication(s) exists in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfoData cmd;
+ StringInfoData nonExistentPublications;
+ bool first;
+ TupleTableSlot *slot;
+ ListCell *lc;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ Assert(list_length(publications) > 0);
+ initStringInfo(&cmd);
+ appendStringInfoString(&cmd, "SELECT t.pubname\n"
+ " FROM pg_catalog.pg_publication t\n"
+ " WHERE t.pubname IN (");
+ first = true;
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(&cmd, ", ");
+
+ appendStringInfoString(&cmd, quote_literal_cstr(pubname));
+ }
+ appendStringInfoChar(&cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd.data, 1, tableRow);
+ pfree(cmd.data);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg("could not receive list of publications from the publisher: %s",
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publications. */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ walrcv_clear_result(res);
+ if (list_length(publicationsCopy))
+ {
+ first = true;
+
+ /* Convert the publications which does not exist into a string. */
+ initStringInfo(&nonExistentPublications);
+ foreach(lc, publicationsCopy)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(&nonExistentPublications, ", ");
+ appendStringInfoString(&nonExistentPublications, pubname);
+ }
+
+ ereport(ERROR,
+ (errmsg("publication(s) %s does not exist in the publisher",
+ nonExistentPublications.data)));
+ }
+}
+
+/*
* Get the list of tables which belong to specified publications on the
* publisher connection.
*/
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index d1e407a..54e082d 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -3,7 +3,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 5;
+use Test::More tests => 9;
# Bug #15114
@@ -153,3 +153,47 @@ is($node_twoways->safe_psql('d2', "SELECT count(f) FROM t"),
$rows * 2, "2x$rows rows in t");
is($node_twoways->safe_psql('d2', "SELECT count(f) FROM t2"),
$rows * 2, "2x$rows rows in t2");
+
+# Create subcription for a publication which does not exist.
+$node_publisher = get_new_node('testpublisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+$node_subscriber = get_new_node('testsubscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+$publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+
+$node_publisher->safe_psql('postgres',
+ "CREATE PUBLICATION testpub1 FOR ALL TABLES");
+
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION testsub1 CONNECTION '$publisher_connstr' PUBLICATION testpub1");
+
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql(
+ 'postgres', "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION pub_doesnt_exist");
+ok($stderr =~ /ERROR: publication\(s\) pub_doesnt_exist does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql(
+ 'postgres', "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION testpub1, pub_doesnt_exist");
+ok($stderr =~ /ERROR: publication\(s\) pub_doesnt_exist does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Multiple publications does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql(
+ 'postgres', "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION pub_doesnt_exist, pub_doesnt_exist1");
+ok($stderr =~ /ERROR: publication\(s\) pub_doesnt_exist, pub_doesnt_exist1 does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql(
+ 'postgres', "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist");
+ok($stderr =~ /ERROR: publication\(s\) pub_doesnt_exist does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+$node_publisher->stop('fast');
+$node_subscriber->stop('fast');
--
1.8.3.1
On Thu, Jan 21, 2021 at 6:56 PM vignesh C <vignesh21@gmail.com> wrote:
Hi,
Creating/altering subscription is successful when we specify a
publication which does not exist in the publisher. I felt we should
throw an error in this case, that will help the user to check if there
is any typo in the create subscription command or to create the
publication before the subscription is created.
If the above analysis looks correct, then please find a patch that
checks if the specified publications are present in the publisher and
throws an error if any of the publications is missing in the
publisher.
Thoughts?
I was having similar thoughts (while working on the logical
replication bug on alter publication...drop table behaviour) on why
create subscription succeeds without checking the publication
existence. I checked in documentation, to find if there's a strong
reason for that, but I couldn't. Maybe it's because of the principle
"first let users create subscriptions, later the publications can be
created on the publisher system", similar to this behaviour
"publications can be created without any tables attached to it
initially, later they can be added".
Others may have better thoughts.
If we do check publication existence for CREATE SUBSCRIPTION, I think
we should also do it for ALTER SUBSCRIPTION ... SET PUBLICATION.
I wonder, why isn't dropping a publication from a list of publications
that are with subscription is not allowed?
Some comments so far on the patch:
1) I see most of the code in the new function check_publications() and
existing fetch_table_list() is the same. Can we have a generic
function, with maybe a flag to separate out the logic specific for
checking publication and fetching table list from the publisher.
2) Can't we know whether the publications exist on the publisher with
the existing (or modifying it a bit if required) query in
fetch_table_list(), so that we can avoid making another connection to
the publisher system from the subscriber?
3) If multiple publications are specified in the CREATE SUBSCRIPTION
query, IIUC, with your patch, the query fails even if at least one of
the publications doesn't exist. Should we throw a warning in this case
and allow the subscription be created for other existing
publications?
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Fri, 22 Jan 2021 at 00:51, Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com> wrote:
On Thu, Jan 21, 2021 at 6:56 PM vignesh C <vignesh21@gmail.com> wrote:
Hi,
Creating/altering subscription is successful when we specify a
publication which does not exist in the publisher. I felt we should
throw an error in this case, that will help the user to check if there
is any typo in the create subscription command or to create the
publication before the subscription is created.
If the above analysis looks correct, then please find a patch that
checks if the specified publications are present in the publisher and
throws an error if any of the publications is missing in the
publisher.
Thoughts?I was having similar thoughts (while working on the logical
replication bug on alter publication...drop table behaviour) on why
create subscription succeeds without checking the publication
existence. I checked in documentation, to find if there's a strong
reason for that, but I couldn't. Maybe it's because of the principle
"first let users create subscriptions, later the publications can be
created on the publisher system", similar to this behaviour
"publications can be created without any tables attached to it
initially, later they can be added".Others may have better thoughts.
If we do check publication existence for CREATE SUBSCRIPTION, I think
we should also do it for ALTER SUBSCRIPTION ... SET PUBLICATION.
Agreed. Current patch do not check publication existence for
ALTER SUBSCRIPTION ... SET PUBLICATION ... WITH (refresh = false).
I wonder, why isn't dropping a publication from a list of publications
that are with subscription is not allowed?Some comments so far on the patch:
1) I see most of the code in the new function check_publications() and
existing fetch_table_list() is the same. Can we have a generic
function, with maybe a flag to separate out the logic specific for
checking publication and fetching table list from the publisher.
+1
2) Can't we know whether the publications exist on the publisher with
the existing (or modifying it a bit if required) query in
fetch_table_list(), so that we can avoid making another connection to
the publisher system from the subscriber?
IIUC, the patch does not make another connection, it just execute a new
query in already connection. If we want to check publication existence
for ALTER SUBSCRIPTION ... SET PUBLICATION ... WITH (refresh = false)
we should make another connection.
3) If multiple publications are specified in the CREATE SUBSCRIPTION
query, IIUC, with your patch, the query fails even if at least one of
the publications doesn't exist. Should we throw a warning in this case
and allow the subscription be created for other existing
publications?
+1. If all the publications do not exist, we should throw an error.
--
Regrads,
Japin Li.
ChengDu WenWu Information Technology Co.,Ltd.
On Fri, Jan 22, 2021 at 10:14 AM japin <japinli@hotmail.com> wrote:
2) Can't we know whether the publications exist on the publisher with
the existing (or modifying it a bit if required) query in
fetch_table_list(), so that we can avoid making another connection to
the publisher system from the subscriber?IIUC, the patch does not make another connection, it just execute a new
query in already connection. If we want to check publication existence
for ALTER SUBSCRIPTION ... SET PUBLICATION ... WITH (refresh = false)
we should make another connection.
Actually, I meant that we can avoid submitting another SQL query to
the publisher if we could manage to submit a single query that first
checks if a given publication exists in pg_publication and if yes
returns the tables associated with it from pg_publication_tables. Can
we modify the existing query in fetch_table_list that gets only the
table list from pg_publcation_tables to see if the given publication
exists in the pg_publication?
Yes you are right, if we were to check the existence of publications
provided with ALTER SUBSCRIPTION statements, we need to do
walrcv_connect, walrcv_exec. We could just call a common function from
there.
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Fri, Jan 22, 2021 at 12:14 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
On Fri, Jan 22, 2021 at 10:14 AM japin <japinli@hotmail.com> wrote:
2) Can't we know whether the publications exist on the publisher with
the existing (or modifying it a bit if required) query in
fetch_table_list(), so that we can avoid making another connection to
the publisher system from the subscriber?IIUC, the patch does not make another connection, it just execute a new
query in already connection. If we want to check publication existence
for ALTER SUBSCRIPTION ... SET PUBLICATION ... WITH (refresh = false)
we should make another connection.Actually, I meant that we can avoid submitting another SQL query to
the publisher if we could manage to submit a single query that first
checks if a given publication exists in pg_publication and if yes
returns the tables associated with it from pg_publication_tables. Can
we modify the existing query in fetch_table_list that gets only the
table list from pg_publcation_tables to see if the given publication
exists in the pg_publication?
When I was implementing this, I had given it a thought on this. To do
that we might need some function/procedure to do this. I felt this
approach is more simpler and chose this approach.
Thoughts?
Yes you are right, if we were to check the existence of publications
provided with ALTER SUBSCRIPTION statements, we need to do
walrcv_connect, walrcv_exec. We could just call a common function from
there.
Yes I agree this should be done in ALTER SUBSCRIPTION SET PUBLICATION
case also, currently we do if refresh is enabled, it should also be
done in ALTER SUBSCRIPTION mysub SET PUBLICATION mypub WITH (REFRESH =
FALSE) also. I will include this in my next version of the patch.
Regards,
Vignesh
EnterpriseDB: http://www.enterprisedb.com
On Fri, Jan 22, 2021 at 10:14 AM japin <japinli@hotmail.com> wrote:
On Fri, 22 Jan 2021 at 00:51, Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com> wrote:
On Thu, Jan 21, 2021 at 6:56 PM vignesh C <vignesh21@gmail.com> wrote:
Hi,
Creating/altering subscription is successful when we specify a
publication which does not exist in the publisher. I felt we should
throw an error in this case, that will help the user to check if there
is any typo in the create subscription command or to create the
publication before the subscription is created.
If the above analysis looks correct, then please find a patch that
checks if the specified publications are present in the publisher and
throws an error if any of the publications is missing in the
publisher.
Thoughts?I was having similar thoughts (while working on the logical
replication bug on alter publication...drop table behaviour) on why
create subscription succeeds without checking the publication
existence. I checked in documentation, to find if there's a strong
reason for that, but I couldn't. Maybe it's because of the principle
"first let users create subscriptions, later the publications can be
created on the publisher system", similar to this behaviour
"publications can be created without any tables attached to it
initially, later they can be added".Others may have better thoughts.
If we do check publication existence for CREATE SUBSCRIPTION, I think
we should also do it for ALTER SUBSCRIPTION ... SET PUBLICATION.Agreed. Current patch do not check publication existence for
ALTER SUBSCRIPTION ... SET PUBLICATION ... WITH (refresh = false).I wonder, why isn't dropping a publication from a list of publications
that are with subscription is not allowed?Some comments so far on the patch:
1) I see most of the code in the new function check_publications() and
existing fetch_table_list() is the same. Can we have a generic
function, with maybe a flag to separate out the logic specific for
checking publication and fetching table list from the publisher.+1
2) Can't we know whether the publications exist on the publisher with
the existing (or modifying it a bit if required) query in
fetch_table_list(), so that we can avoid making another connection to
the publisher system from the subscriber?IIUC, the patch does not make another connection, it just execute a new
query in already connection. If we want to check publication existence
for ALTER SUBSCRIPTION ... SET PUBLICATION ... WITH (refresh = false)
we should make another connection.3) If multiple publications are specified in the CREATE SUBSCRIPTION
query, IIUC, with your patch, the query fails even if at least one of
the publications doesn't exist. Should we throw a warning in this case
and allow the subscription be created for other existing
publications?+1. If all the publications do not exist, we should throw an error.
I also felt if any of the publications are not there, we should throw an error.
Regards,
Vignesh
EnterpriseDB: http://www.enterprisedb.com
On Thu, Jan 21, 2021 at 10:21 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
On Thu, Jan 21, 2021 at 6:56 PM vignesh C <vignesh21@gmail.com> wrote:
Hi,
Creating/altering subscription is successful when we specify a
publication which does not exist in the publisher. I felt we should
throw an error in this case, that will help the user to check if there
is any typo in the create subscription command or to create the
publication before the subscription is created.
If the above analysis looks correct, then please find a patch that
checks if the specified publications are present in the publisher and
throws an error if any of the publications is missing in the
publisher.
Thoughts?I was having similar thoughts (while working on the logical
replication bug on alter publication...drop table behaviour) on why
create subscription succeeds without checking the publication
existence. I checked in documentation, to find if there's a strong
reason for that, but I couldn't. Maybe it's because of the principle
"first let users create subscriptions, later the publications can be
created on the publisher system", similar to this behaviour
"publications can be created without any tables attached to it
initially, later they can be added".Others may have better thoughts.
If we do check publication existence for CREATE SUBSCRIPTION, I think
we should also do it for ALTER SUBSCRIPTION ... SET PUBLICATION.I wonder, why isn't dropping a publication from a list of publications
that are with subscription is not allowed?Some comments so far on the patch:
1) I see most of the code in the new function check_publications() and
existing fetch_table_list() is the same. Can we have a generic
function, with maybe a flag to separate out the logic specific for
checking publication and fetching table list from the publisher.
I have made the common code between the check_publications and
fetch_table_list into a common function
get_appended_publications_query. I felt the rest of the code is better
off kept as it is.
The Attached patch has the changes for the same and also the change to
check publication exists during alter subscription set publication.
Thoughts?
Regards,
Vignesh
EnterpriseDB: http://www.enterprisedb.com
Attachments:
v2-0001-Identify-missing-publications-from-publisher-whil.patchtext/x-patch; charset=US-ASCII; name=v2-0001-Identify-missing-publications-from-publisher-whil.patchDownload
From 6ef5a35d82065ec7e497ebe38e575afa45ad9469 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh.c@enterprisedb.com>
Date: Thu, 21 Jan 2021 18:38:54 +0530
Subject: [PATCH v2] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher. One of the Alter Subscritpion test from regression was
moved to tap test as the error message "/tmp/pg_regress-P6KQrj/.s.PGSQL.58080"
varies from exeuction to execution.
---
src/backend/commands/subscriptioncmds.c | 158 ++++++++++++++++++++++++-----
src/test/regress/expected/subscription.out | 33 +++---
src/test/regress/sql/subscription.sql | 1 -
src/test/subscription/t/100_bugs.pl | 78 +++++++++++++-
4 files changed, 224 insertions(+), 46 deletions(-)
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 082f785..59076b5 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -46,6 +46,7 @@
#include "utils/syscache.h"
static List *fetch_table_list(WalReceiverConn *wrconn, List *publications);
+static void check_publications(WalReceiverConn *wrconn, List *publications);
/*
* Common option parsing function for CREATE and ALTER SUBSCRIPTION commands.
@@ -490,6 +491,9 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
PG_TRY();
{
+ /* Verify specified publications exists in the publisher. */
+ check_publications(wrconn, publications);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -557,7 +561,7 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data, bool check_pub)
{
char *err;
List *pubrel_names;
@@ -576,6 +580,10 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
ereport(ERROR,
(errmsg("could not connect to the publisher: %s", err)));
+ /* Verify specified publications exists in the publisher. */
+ if (check_pub)
+ check_publications(wrconn, sub->publications);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -822,6 +830,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt)
{
bool copy_data;
bool refresh;
+ char *err;
+ WalReceiverConn *wrconn;
parse_subscription_options(stmt->options,
NULL, /* no "connect" */
@@ -839,6 +849,21 @@ AlterSubscription(AlterSubscriptionStmt *stmt)
update_tuple = true;
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ /* Verify specified publications exists in the publisher. */
+ check_publications(wrconn, stmt->publication);
+
+ /* We are done with the remote side, close connection. */
+ walrcv_disconnect(wrconn);
+
/* Refresh if user asked us to. */
if (refresh)
{
@@ -851,7 +876,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt)
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -877,7 +902,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt)
NULL, NULL, /* no "binary" */
NULL, NULL); /* no "streaming" */
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, true);
break;
}
@@ -1211,27 +1236,20 @@ AlterSubscriptionOwner_oid(Oid subid, Oid newOwnerId)
}
/*
- * Get the list of tables which belong to specified publications on the
- * publisher connection.
+ * Return a query by appending the publications to the input query.
*/
-static List *
-fetch_table_list(WalReceiverConn *wrconn, List *publications)
+static StringInfoData *
+get_appended_publications_query(List *publications, char *query)
{
- WalRcvExecResult *res;
- StringInfoData cmd;
- TupleTableSlot *slot;
- Oid tableRow[2] = {TEXTOID, TEXTOID};
+ bool first = true;
+ StringInfoData *cmd = makeStringInfo();
ListCell *lc;
- bool first;
- List *tablelist = NIL;
Assert(list_length(publications) > 0);
- initStringInfo(&cmd);
- appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
+ initStringInfo(cmd);
+ appendStringInfoString(cmd, query);
+
foreach(lc, publications)
{
char *pubname = strVal(lfirst(lc));
@@ -1239,14 +1257,108 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
if (first)
first = false;
else
- appendStringInfoString(&cmd, ", ");
+ appendStringInfoString(cmd, ", ");
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
+ appendStringInfoString(cmd, quote_literal_cstr(pubname));
}
- appendStringInfoChar(&cmd, ')');
- res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
- pfree(cmd.data);
+ appendStringInfoChar(cmd, ')');
+ return cmd;
+}
+
+/*
+ * Verify that the specified publication(s) exists in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfoData *cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ cmd = get_appended_publications_query(publications, "SELECT t.pubname\n"
+ " FROM pg_catalog.pg_publication t\n"
+ " WHERE t.pubname IN (");
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg("could not receive list of publications from the publisher: %s",
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publications. */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ StringInfoData nonExistentPublications;
+ bool first = true;
+ ListCell *lc;
+
+ /* Convert the publications which does not exist into a string. */
+ initStringInfo(&nonExistentPublications);
+ foreach(lc, publicationsCopy)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(&nonExistentPublications, ", ");
+ appendStringInfoString(&nonExistentPublications, "\"");
+ appendStringInfoString(&nonExistentPublications, pubname);
+ appendStringInfoString(&nonExistentPublications, "\"");
+ }
+
+ ereport(ERROR,
+ (errmsg("publication(s) %s does not exist in the publisher",
+ nonExistentPublications.data)));
+ }
+}
+
+/*
+ * Get the list of tables which belong to specified publications on the
+ * publisher connection.
+ */
+static List *
+fetch_table_list(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfoData *cmd;
+ TupleTableSlot *slot;
+ Oid tableRow[2] = {TEXTOID, TEXTOID};
+ List *tablelist = NIL;
+
+ cmd = get_appended_publications_query(publications,
+ "SELECT DISTINCT t.schemaname, t.tablename\n"
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ res = walrcv_exec(wrconn, cmd->data, 2, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
if (res->status != WALRCV_OK_TUPLES)
ereport(ERROR,
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index 2fa9bce..17792e0 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -82,7 +82,6 @@ ERROR: invalid connection string syntax: missing "=" after "foobar" in connecti
regress_testsub | regress_subscription_user | f | {testpub} | f | f | off | dbname=regress_doesnotexist
(1 row)
-ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
ALTER SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist2';
ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
-- fail
@@ -91,27 +90,27 @@ ERROR: subscription "regress_doesnotexist" does not exist
ALTER SUBSCRIPTION regress_testsub SET (create_slot = false);
ERROR: unrecognized subscription parameter: "create_slot"
\dRs+
- List of subscriptions
- Name | Owner | Enabled | Publication | Binary | Streaming | Synchronous commit | Conninfo
------------------+---------------------------+---------+---------------------+--------+-----------+--------------------+------------------------------
- regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | f | off | dbname=regress_doesnotexist2
+ List of subscriptions
+ Name | Owner | Enabled | Publication | Binary | Streaming | Synchronous commit | Conninfo
+-----------------+---------------------------+---------+-------------+--------+-----------+--------------------+------------------------------
+ regress_testsub | regress_subscription_user | f | {testpub} | f | f | off | dbname=regress_doesnotexist2
(1 row)
BEGIN;
ALTER SUBSCRIPTION regress_testsub ENABLE;
\dRs
- List of subscriptions
- Name | Owner | Enabled | Publication
------------------+---------------------------+---------+---------------------
- regress_testsub | regress_subscription_user | t | {testpub2,testpub3}
+ List of subscriptions
+ Name | Owner | Enabled | Publication
+-----------------+---------------------------+---------+-------------
+ regress_testsub | regress_subscription_user | t | {testpub}
(1 row)
ALTER SUBSCRIPTION regress_testsub DISABLE;
\dRs
- List of subscriptions
- Name | Owner | Enabled | Publication
------------------+---------------------------+---------+---------------------
- regress_testsub | regress_subscription_user | f | {testpub2,testpub3}
+ List of subscriptions
+ Name | Owner | Enabled | Publication
+-----------------+---------------------------+---------+-------------
+ regress_testsub | regress_subscription_user | f | {testpub}
(1 row)
COMMIT;
@@ -126,10 +125,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (synchronous_commit = foobar);
ERROR: invalid value for parameter "synchronous_commit": "foobar"
HINT: Available values: local, remote_write, remote_apply, on, off.
\dRs+
- List of subscriptions
- Name | Owner | Enabled | Publication | Binary | Streaming | Synchronous commit | Conninfo
----------------------+---------------------------+---------+---------------------+--------+-----------+--------------------+------------------------------
- regress_testsub_foo | regress_subscription_user | f | {testpub2,testpub3} | f | f | local | dbname=regress_doesnotexist2
+ List of subscriptions
+ Name | Owner | Enabled | Publication | Binary | Streaming | Synchronous commit | Conninfo
+---------------------+---------------------------+---------+-------------+--------+-----------+--------------------+------------------------------
+ regress_testsub_foo | regress_subscription_user | f | {testpub} | f | f | local | dbname=regress_doesnotexist2
(1 row)
-- rename back to keep the rest simple
diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql
index 14fa0b2..a8a7110 100644
--- a/src/test/regress/sql/subscription.sql
+++ b/src/test/regress/sql/subscription.sql
@@ -61,7 +61,6 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar';
\dRs+
-ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false);
ALTER SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist2';
ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname');
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index d1e407a..06dbae2 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -3,7 +3,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 5;
+use Test::More tests => 11;
# Bug #15114
@@ -134,10 +134,9 @@ $node_twoways->safe_psql(
INSERT INTO t SELECT * FROM generate_series(1, $rows);
INSERT INTO t2 SELECT * FROM generate_series(1, $rows);
});
-$node_twoways->safe_psql(
- 'd1', 'ALTER PUBLICATION testpub ADD TABLE t2');
-$node_twoways->safe_psql(
- 'd2', 'ALTER SUBSCRIPTION testsub REFRESH PUBLICATION');
+$node_twoways->safe_psql('d1', 'ALTER PUBLICATION testpub ADD TABLE t2');
+$node_twoways->safe_psql('d2',
+ 'ALTER SUBSCRIPTION testsub REFRESH PUBLICATION');
# We cannot rely solely on wait_for_catchup() here; it isn't sufficient
# when tablesync workers might still be running. So in addition to that,
@@ -153,3 +152,72 @@ is($node_twoways->safe_psql('d2', "SELECT count(f) FROM t"),
$rows * 2, "2x$rows rows in t");
is($node_twoways->safe_psql('d2', "SELECT count(f) FROM t2"),
$rows * 2, "2x$rows rows in t2");
+
+# Create subcription for a publication which does not exist.
+$node_publisher = get_new_node('testpublisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+$node_subscriber = get_new_node('testsubscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+$publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+
+$node_publisher->safe_psql('postgres',
+ "CREATE PUBLICATION testpub1 FOR ALL TABLES");
+
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION testsub1 CONNECTION '$publisher_connstr' PUBLICATION testpub1"
+);
+
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION pub_doesnt_exist"
+);
+ok( $stderr =~
+ /ERROR: publication\(s\) "pub_doesnt_exist" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION testpub1, pub_doesnt_exist"
+);
+ok( $stderr =~
+ /ERROR: publication\(s\) "pub_doesnt_exist" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Multiple publications does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION pub_doesnt_exist, pub_doesnt_exist1"
+);
+ok( $stderr =~
+ /ERROR: publication\(s\) "pub_doesnt_exist", "pub_doesnt_exist1" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist");
+ok( $stderr =~
+ /ERROR: publication\(s\) "pub_doesnt_exist" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist WITH (REFRESH = FALSE)"
+);
+ok( $stderr =~
+ /ERROR: publication\(s\) "pub_doesnt_exist" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist WITH (REFRESH = FALSE)"
+);
+ok( $stderr =~ /ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
+$node_publisher->stop('fast');
+$node_subscriber->stop('fast');
--
1.8.3.1
On Mon, Jan 25, 2021 at 1:10 PM vignesh C <vignesh21@gmail.com> wrote:
On Thu, Jan 21, 2021 at 10:21 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:On Thu, Jan 21, 2021 at 6:56 PM vignesh C <vignesh21@gmail.com> wrote:
Hi,
Creating/altering subscription is successful when we specify a
publication which does not exist in the publisher. I felt we should
throw an error in this case, that will help the user to check if there
is any typo in the create subscription command or to create the
publication before the subscription is created.
If the above analysis looks correct, then please find a patch that
checks if the specified publications are present in the publisher and
throws an error if any of the publications is missing in the
publisher.
Thoughts?I was having similar thoughts (while working on the logical
replication bug on alter publication...drop table behaviour) on why
create subscription succeeds without checking the publication
existence. I checked in documentation, to find if there's a strong
reason for that, but I couldn't. Maybe it's because of the principle
"first let users create subscriptions, later the publications can be
created on the publisher system", similar to this behaviour
"publications can be created without any tables attached to it
initially, later they can be added".Others may have better thoughts.
If we do check publication existence for CREATE SUBSCRIPTION, I think
we should also do it for ALTER SUBSCRIPTION ... SET PUBLICATION.I wonder, why isn't dropping a publication from a list of publications
that are with subscription is not allowed?Some comments so far on the patch:
1) I see most of the code in the new function check_publications() and
existing fetch_table_list() is the same. Can we have a generic
function, with maybe a flag to separate out the logic specific for
checking publication and fetching table list from the publisher.I have made the common code between the check_publications and
fetch_table_list into a common function
get_appended_publications_query. I felt the rest of the code is better
off kept as it is.
The Attached patch has the changes for the same and also the change to
check publication exists during alter subscription set publication.
Thoughts?
So basically, the create subscription will throw an error if the
publication does not exist. So will you throw an error if we try to
drop the publication which is subscribed by some subscription? I mean
basically, you are creating a dependency that if you are creating a
subscription then there must be a publication that is not completely
insane but then we will have to disallow dropping the publication as
well. Am I missing something?
--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com
On Mon, Jan 25, 2021 at 2:42 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Mon, Jan 25, 2021 at 1:10 PM vignesh C <vignesh21@gmail.com> wrote:
On Thu, Jan 21, 2021 at 10:21 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:On Thu, Jan 21, 2021 at 6:56 PM vignesh C <vignesh21@gmail.com> wrote:
Hi,
Creating/altering subscription is successful when we specify a
publication which does not exist in the publisher. I felt we should
throw an error in this case, that will help the user to check if there
is any typo in the create subscription command or to create the
publication before the subscription is created.
If the above analysis looks correct, then please find a patch that
checks if the specified publications are present in the publisher and
throws an error if any of the publications is missing in the
publisher.
Thoughts?I was having similar thoughts (while working on the logical
replication bug on alter publication...drop table behaviour) on why
create subscription succeeds without checking the publication
existence. I checked in documentation, to find if there's a strong
reason for that, but I couldn't. Maybe it's because of the principle
"first let users create subscriptions, later the publications can be
created on the publisher system", similar to this behaviour
"publications can be created without any tables attached to it
initially, later they can be added".Others may have better thoughts.
If we do check publication existence for CREATE SUBSCRIPTION, I think
we should also do it for ALTER SUBSCRIPTION ... SET PUBLICATION.I wonder, why isn't dropping a publication from a list of publications
that are with subscription is not allowed?Some comments so far on the patch:
1) I see most of the code in the new function check_publications() and
existing fetch_table_list() is the same. Can we have a generic
function, with maybe a flag to separate out the logic specific for
checking publication and fetching table list from the publisher.I have made the common code between the check_publications and
fetch_table_list into a common function
get_appended_publications_query. I felt the rest of the code is better
off kept as it is.
The Attached patch has the changes for the same and also the change to
check publication exists during alter subscription set publication.
Thoughts?So basically, the create subscription will throw an error if the
publication does not exist. So will you throw an error if we try to
drop the publication which is subscribed by some subscription? I mean
basically, you are creating a dependency that if you are creating a
subscription then there must be a publication that is not completely
insane but then we will have to disallow dropping the publication as
well. Am I missing something?
Do you mean DROP PUBLICATION non_existent_publication;?
Or
Do you mean when we drop publications from a subscription? If yes, do
we have a way to drop a publication from the subscription? See below
one of my earlier questions on this.
"I wonder, why isn't dropping a publication from a list of
publications that are with subscription is not allowed?"
At least, I see no ALTER SUBSCRIPTION ... DROP PUBLICATION mypub1 or
something similar?
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Mon, Jan 25, 2021 at 2:48 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
On Mon, Jan 25, 2021 at 2:42 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Mon, Jan 25, 2021 at 1:10 PM vignesh C <vignesh21@gmail.com> wrote:
On Thu, Jan 21, 2021 at 10:21 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:On Thu, Jan 21, 2021 at 6:56 PM vignesh C <vignesh21@gmail.com> wrote:
Hi,
Creating/altering subscription is successful when we specify a
publication which does not exist in the publisher. I felt we should
throw an error in this case, that will help the user to check if there
is any typo in the create subscription command or to create the
publication before the subscription is created.
If the above analysis looks correct, then please find a patch that
checks if the specified publications are present in the publisher and
throws an error if any of the publications is missing in the
publisher.
Thoughts?I was having similar thoughts (while working on the logical
replication bug on alter publication...drop table behaviour) on why
create subscription succeeds without checking the publication
existence. I checked in documentation, to find if there's a strong
reason for that, but I couldn't. Maybe it's because of the principle
"first let users create subscriptions, later the publications can be
created on the publisher system", similar to this behaviour
"publications can be created without any tables attached to it
initially, later they can be added".Others may have better thoughts.
If we do check publication existence for CREATE SUBSCRIPTION, I think
we should also do it for ALTER SUBSCRIPTION ... SET PUBLICATION.I wonder, why isn't dropping a publication from a list of publications
that are with subscription is not allowed?Some comments so far on the patch:
1) I see most of the code in the new function check_publications() and
existing fetch_table_list() is the same. Can we have a generic
function, with maybe a flag to separate out the logic specific for
checking publication and fetching table list from the publisher.I have made the common code between the check_publications and
fetch_table_list into a common function
get_appended_publications_query. I felt the rest of the code is better
off kept as it is.
The Attached patch has the changes for the same and also the change to
check publication exists during alter subscription set publication.
Thoughts?So basically, the create subscription will throw an error if the
publication does not exist. So will you throw an error if we try to
drop the publication which is subscribed by some subscription? I mean
basically, you are creating a dependency that if you are creating a
subscription then there must be a publication that is not completely
insane but then we will have to disallow dropping the publication as
well. Am I missing something?Do you mean DROP PUBLICATION non_existent_publication;?
Or
Do you mean when we drop publications from a subscription?
I mean it doesn’t seem right to disallow to create the subscription if
the publisher doesn't exist, and my reasoning was even though the
publisher exists while creating the subscription you might drop it
later right?. So basically, now also we can create the same scenario
that a subscription may exist for the publication which does not
exist.
--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com
On Mon, Jan 25, 2021 at 3:07 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
So basically, the create subscription will throw an error if the
publication does not exist. So will you throw an error if we try to
drop the publication which is subscribed by some subscription? I mean
basically, you are creating a dependency that if you are creating a
subscription then there must be a publication that is not completely
insane but then we will have to disallow dropping the publication as
well. Am I missing something?Do you mean DROP PUBLICATION non_existent_publication;?
Or
Do you mean when we drop publications from a subscription?
I mean it doesn’t seem right to disallow to create the subscription if
the publisher doesn't exist, and my reasoning was even though the
publisher exists while creating the subscription you might drop it
later right?. So basically, now also we can create the same scenario
that a subscription may exist for the publication which does not
exist.
Yes, the above scenario can be created even now. If a publication is
dropped in the publisher system, then it will not replicate/publish
the changes for that publication (publication_invalidation_cb,
rel_sync_cache_publication_cb, LoadPublications in
get_rel_sync_entry), so subscriber doesn't receive them. But the
subscription can still contain that dropped publication in it's list
of publications.
The patch proposed in this thread, just checks while creation/altering
of the subscription on the subscriber system whether or not the
publication exists on the publisher system. This is one way
dependency. But given the above scenario, there can exist another
dependency i.e. publisher dropping the publisher at any time. So to
make it a complete solution i.e. not allowing non-existent
publications from the list of publications in the subscription, we
need to detect when the publications are dropped in the publisher and
we should, may be on a next connection to the subscriber, also look at
the subscription for that dropped publication, if exists remove it.
But that's an overkill and impractical I feel. Thoughts?
I also feel the best way to remove the confusion is to document why we
allow creating subscriptions even when the specified publications
don't exist on the publisher system? Thoughts?
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Mon, Jan 25, 2021 at 3:38 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
On Mon, Jan 25, 2021 at 3:07 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
So basically, the create subscription will throw an error if the
publication does not exist. So will you throw an error if we try to
drop the publication which is subscribed by some subscription? I mean
basically, you are creating a dependency that if you are creating a
subscription then there must be a publication that is not completely
insane but then we will have to disallow dropping the publication as
well. Am I missing something?Do you mean DROP PUBLICATION non_existent_publication;?
Or
Do you mean when we drop publications from a subscription?
I mean it doesn’t seem right to disallow to create the subscription if
the publisher doesn't exist, and my reasoning was even though the
publisher exists while creating the subscription you might drop it
later right?. So basically, now also we can create the same scenario
that a subscription may exist for the publication which does not
exist.Yes, the above scenario can be created even now. If a publication is
dropped in the publisher system, then it will not replicate/publish
the changes for that publication (publication_invalidation_cb,
rel_sync_cache_publication_cb, LoadPublications in
get_rel_sync_entry), so subscriber doesn't receive them. But the
subscription can still contain that dropped publication in it's list
of publications.The patch proposed in this thread, just checks while creation/altering
of the subscription on the subscriber system whether or not the
publication exists on the publisher system. This is one way
dependency. But given the above scenario, there can exist another
dependency i.e. publisher dropping the publisher at any time. So to
make it a complete solution i.e. not allowing non-existent
publications from the list of publications in the subscription, we
need to detect when the publications are dropped in the publisher and
we should, may be on a next connection to the subscriber, also look at
the subscription for that dropped publication, if exists remove it.
But that's an overkill and impractical I feel. Thoughts?I also feel the best way to remove the confusion is to document why we
allow creating subscriptions even when the specified publications
don't exist on the publisher system? Thoughts?
Yes, that was my point that there is no point in solving it in some
cases and it can exist in other cases. So I am fine with documenting
the behavior.
--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com
On Mon, 25 Jan 2021 at 17:18, Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com> wrote:
On Mon, Jan 25, 2021 at 2:42 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Mon, Jan 25, 2021 at 1:10 PM vignesh C <vignesh21@gmail.com> wrote:
On Thu, Jan 21, 2021 at 10:21 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:On Thu, Jan 21, 2021 at 6:56 PM vignesh C <vignesh21@gmail.com> wrote:
Hi,
Creating/altering subscription is successful when we specify a
publication which does not exist in the publisher. I felt we should
throw an error in this case, that will help the user to check if there
is any typo in the create subscription command or to create the
publication before the subscription is created.
If the above analysis looks correct, then please find a patch that
checks if the specified publications are present in the publisher and
throws an error if any of the publications is missing in the
publisher.
Thoughts?I was having similar thoughts (while working on the logical
replication bug on alter publication...drop table behaviour) on why
create subscription succeeds without checking the publication
existence. I checked in documentation, to find if there's a strong
reason for that, but I couldn't. Maybe it's because of the principle
"first let users create subscriptions, later the publications can be
created on the publisher system", similar to this behaviour
"publications can be created without any tables attached to it
initially, later they can be added".Others may have better thoughts.
If we do check publication existence for CREATE SUBSCRIPTION, I think
we should also do it for ALTER SUBSCRIPTION ... SET PUBLICATION.I wonder, why isn't dropping a publication from a list of publications
that are with subscription is not allowed?Some comments so far on the patch:
1) I see most of the code in the new function check_publications() and
existing fetch_table_list() is the same. Can we have a generic
function, with maybe a flag to separate out the logic specific for
checking publication and fetching table list from the publisher.I have made the common code between the check_publications and
fetch_table_list into a common function
get_appended_publications_query. I felt the rest of the code is better
off kept as it is.
The Attached patch has the changes for the same and also the change to
check publication exists during alter subscription set publication.
Thoughts?So basically, the create subscription will throw an error if the
publication does not exist. So will you throw an error if we try to
drop the publication which is subscribed by some subscription? I mean
basically, you are creating a dependency that if you are creating a
subscription then there must be a publication that is not completely
insane but then we will have to disallow dropping the publication as
well. Am I missing something?Do you mean DROP PUBLICATION non_existent_publication;?
Or
Do you mean when we drop publications from a subscription? If yes, do
we have a way to drop a publication from the subscription? See below
one of my earlier questions on this.
"I wonder, why isn't dropping a publication from a list of
publications that are with subscription is not allowed?"
At least, I see no ALTER SUBSCRIPTION ... DROP PUBLICATION mypub1 or
something similar?
Why we do not support ALTER SUBSCRIPTION...ADD/DROP PUBLICATION? When we
have multiple publications in subscription, but I want to add/drop a single
publication, it is conveient. The ALTER SUBSCRIPTION...SET PUBLICATION...
should supply the completely publications.
Sorry, this question is unrelated with this subject.
--
Regrads,
Japin Li.
ChengDu WenWu Information Technology Co.,Ltd.
On Mon, Jan 25, 2021 at 5:18 PM japin <japinli@hotmail.com> wrote:
Do you mean when we drop publications from a subscription? If yes, do
we have a way to drop a publication from the subscription? See below
one of my earlier questions on this.
"I wonder, why isn't dropping a publication from a list of
publications that are with subscription is not allowed?"
At least, I see no ALTER SUBSCRIPTION ... DROP PUBLICATION mypub1 or
something similar?Why we do not support ALTER SUBSCRIPTION...ADD/DROP PUBLICATION? When we
have multiple publications in subscription, but I want to add/drop a single
publication, it is conveient. The ALTER SUBSCRIPTION...SET PUBLICATION...
should supply the completely publications.
Looks like the way to drop/add publication from the list of
publications in subscription requires users to specify all the list of
publications currently exists +/- the new publication that needs to be
added/dropped:
CREATE SUBSCRIPTION mysub1 CONNECTION 'host=localhost port=5432
dbname=postgres' PUBLICATION mypub1, mypub2, mypu3, mypub4, mypub5;
postgres=# select subpublications from pg_subscription;
subpublications
-------------------------------------
{mypub1,mypub2,mypu3,mypub4,mypub5}
(1 row)
Say, I want to drop mypub4:
ALTER SUBSCRIPTION mysub1 SET PUBLICATION mypub1, mypub2, mypu3, mypub5;
postgres=# select subpublications from pg_subscription;
subpublications
------------------------------
{mypub1,mypub2,mypu3,mypub5}
Say, I want toa dd mypub4 and mypub6:
ALTER SUBSCRIPTION mysub1 SET PUBLICATION mypub1, mypub2, mypu3,
mypub5, mypub4, mypub6;
postgres=# select subpublications from pg_subscription;
subpublications
--------------------------------------------
{mypub1,mypub2,mypu3,mypub5,mypub4,mypub6}
(1 row)
It will be good to have something like:
ALTER SUBSCRIPTION mysub1 ADD PUBLICATION mypub1, mypub3; which will
the publications to subscription if not added previously.
ALTER SUBSCRIPTION mysub1 DROP PUBLICATION mypub1, mypub3; which will
drop the publications from subscription if they exist in the
subscription's list of publications.
But I'm really not sure why the above syntax was not added earlier. We
may be missing something here.
Sorry, this question is unrelated with this subject.
Yes, IMO it can definitely be discussed in another thread. It will be
good to get a separate opinion for this.
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Mon, Jan 25, 2021 at 5:18 PM japin <japinli@hotmail.com> wrote:
On Mon, 25 Jan 2021 at 17:18, Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com> wrote:
On Mon, Jan 25, 2021 at 2:42 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Mon, Jan 25, 2021 at 1:10 PM vignesh C <vignesh21@gmail.com> wrote:
On Thu, Jan 21, 2021 at 10:21 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:On Thu, Jan 21, 2021 at 6:56 PM vignesh C <vignesh21@gmail.com> wrote:
Hi,
Creating/altering subscription is successful when we specify a
publication which does not exist in the publisher. I felt we should
throw an error in this case, that will help the user to check if there
is any typo in the create subscription command or to create the
publication before the subscription is created.
If the above analysis looks correct, then please find a patch that
checks if the specified publications are present in the publisher and
throws an error if any of the publications is missing in the
publisher.
Thoughts?I was having similar thoughts (while working on the logical
replication bug on alter publication...drop table behaviour) on why
create subscription succeeds without checking the publication
existence. I checked in documentation, to find if there's a strong
reason for that, but I couldn't. Maybe it's because of the principle
"first let users create subscriptions, later the publications can be
created on the publisher system", similar to this behaviour
"publications can be created without any tables attached to it
initially, later they can be added".Others may have better thoughts.
If we do check publication existence for CREATE SUBSCRIPTION, I think
we should also do it for ALTER SUBSCRIPTION ... SET PUBLICATION.I wonder, why isn't dropping a publication from a list of publications
that are with subscription is not allowed?Some comments so far on the patch:
1) I see most of the code in the new function check_publications() and
existing fetch_table_list() is the same. Can we have a generic
function, with maybe a flag to separate out the logic specific for
checking publication and fetching table list from the publisher.I have made the common code between the check_publications and
fetch_table_list into a common function
get_appended_publications_query. I felt the rest of the code is better
off kept as it is.
The Attached patch has the changes for the same and also the change to
check publication exists during alter subscription set publication.
Thoughts?So basically, the create subscription will throw an error if the
publication does not exist. So will you throw an error if we try to
drop the publication which is subscribed by some subscription? I mean
basically, you are creating a dependency that if you are creating a
subscription then there must be a publication that is not completely
insane but then we will have to disallow dropping the publication as
well. Am I missing something?Do you mean DROP PUBLICATION non_existent_publication;?
Or
Do you mean when we drop publications from a subscription? If yes, do
we have a way to drop a publication from the subscription? See below
one of my earlier questions on this.
"I wonder, why isn't dropping a publication from a list of
publications that are with subscription is not allowed?"
At least, I see no ALTER SUBSCRIPTION ... DROP PUBLICATION mypub1 or
something similar?Why we do not support ALTER SUBSCRIPTION...ADD/DROP PUBLICATION? When we
have multiple publications in subscription, but I want to add/drop a single
publication, it is conveient. The ALTER SUBSCRIPTION...SET PUBLICATION...
should supply the completely publications.Sorry, this question is unrelated with this subject.
Please start a new thread for this, let's discuss this separately.
Regards,
Vignesh
EnterpriseDB: http://www.enterprisedb.com
On Mon, Jan 25, 2021 at 3:07 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Mon, Jan 25, 2021 at 2:48 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:On Mon, Jan 25, 2021 at 2:42 PM Dilip Kumar <dilipbalaut@gmail.com>
wrote:
On Mon, Jan 25, 2021 at 1:10 PM vignesh C <vignesh21@gmail.com> wrote:
On Thu, Jan 21, 2021 at 10:21 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:On Thu, Jan 21, 2021 at 6:56 PM vignesh C <vignesh21@gmail.com>
wrote:
Hi,
Creating/altering subscription is successful when we specify a
publication which does not exist in the publisher. I felt we
should
throw an error in this case, that will help the user to check
if there
is any typo in the create subscription command or to create the
publication before the subscription is created.
If the above analysis looks correct, then please find a patch
that
checks if the specified publications are present in the
publisher and
throws an error if any of the publications is missing in the
publisher.
Thoughts?I was having similar thoughts (while working on the logical
replication bug on alter publication...drop table behaviour) on
why
create subscription succeeds without checking the publication
existence. I checked in documentation, to find if there's a strong
reason for that, but I couldn't. Maybe it's because of the
principle
"first let users create subscriptions, later the publications can
be
created on the publisher system", similar to this behaviour
"publications can be created without any tables attached to it
initially, later they can be added".Others may have better thoughts.
If we do check publication existence for CREATE SUBSCRIPTION, I
think
we should also do it for ALTER SUBSCRIPTION ... SET PUBLICATION.
I wonder, why isn't dropping a publication from a list of
publications
that are with subscription is not allowed?
Some comments so far on the patch:
1) I see most of the code in the new function
check_publications() and
existing fetch_table_list() is the same. Can we have a generic
function, with maybe a flag to separate out the logic specific for
checking publication and fetching table list from the publisher.I have made the common code between the check_publications and
fetch_table_list into a common function
get_appended_publications_query. I felt the rest of the code is
better
off kept as it is.
The Attached patch has the changes for the same and also the change
to
check publication exists during alter subscription set publication.
Thoughts?So basically, the create subscription will throw an error if the
publication does not exist. So will you throw an error if we try to
drop the publication which is subscribed by some subscription? I mean
basically, you are creating a dependency that if you are creating a
subscription then there must be a publication that is not completely
insane but then we will have to disallow dropping the publication as
well. Am I missing something?Do you mean DROP PUBLICATION non_existent_publication;?
Or
Do you mean when we drop publications from a subscription?
I mean it doesn’t seem right to disallow to create the subscription if
the publisher doesn't exist, and my reasoning was even though the
publisher exists while creating the subscription you might drop it
later right?. So basically, now also we can create the same scenario
that a subscription may exist for the publication which does not
exist.
I would like to defer on documentation for this.
I feel we should have the behavior similar to publication tables as given
below, then it will be consistent and easier for the users:
This is the behavior in case of table:
*Step 1:*PUBLISHER SIDE:
create table t1(c1 int);
create table t2(c1 int);
CREATE PUBLICATION mypub1 for table t1,t2;
-- All above commands succeeds
*Step 2:*SUBSCRIBER SIDE:
-- Create subscription without creating tables will result in error:
*CREATE SUBSCRIPTION mysub1 CONNECTION 'dbname=source_rep host=localhost
user=vignesh port=5432' PUBLICATION mypub1;ERROR: relation "public.t2"
does not exist*
create table t1(c1 int);
create table t2(c1 int);
CREATE SUBSCRIPTION mysub1 CONNECTION 'dbname=source_rep host=localhost
user=vignesh port=5432' PUBLICATION mypub1;
postgres=# select * from pg_subscription;
oid | subdbid | subname | subowner | subenabled | subbinary | substream
| subconninfo | subslotname |
subsynccommit | subpublications
-------+---------+---------+----------+------------+-----------+-----------+---------------------------------------------------------+-------------+---------------+-----------------
16392 | 13756 | mysub1 | 10 | t | f | f
| dbname=source_rep host=localhost user=vignesh port=5432 | mysub1 |
off | {mypub1}
(1 row)
*postgres=# select *,srrelid::oid::regclass from
pg_subscription_rel; srsubid | srrelid | srsubstate | srsublsn | srrelid
---------+---------+------------+-----------+--------- 16392 | 16389 |
r | 0/1608BD0 | t2 16392 | 16384 | r | 0/1608BD0 | t1*
(2 rows)
*Step 3:*PUBLISHER:
drop table t2;
create table t3;
CREATE PUBLICATION mypub2 for table t1,t3;
Step 4:
SUBSCRIBER:
postgres=# select *,srrelid::oid::regclass from pg_subscription_rel;
srsubid | srrelid | srsubstate | srsublsn | srrelid
---------+---------+------------+-----------+---------
16392 | 16389 | r | 0/1608BD0 | t2
16392 | 16384 | r | 0/1608BD0 | t1
(2 rows)
*postgres=# alter subscription mysub1 refresh publication ;ALTER
SUBSCRIPTION-- Subscription relation will be updated.postgres=# select
*,srrelid::oid::regclass from pg_subscription_rel; srsubid | srrelid |
srsubstate | srsublsn | srrelid
---------+---------+------------+-----------+--------- 16392 | 16384 |
r | 0/1608BD0 | t1(1 row)*
*-- Alter subscription fails while setting publication having a table that
does not existpostgres=# alter subscription mysub1 set publication
mysub2;ERROR: relation "public.t3" does not exist*
To maintain consistency, we should have similar behavior in case of
publication too.
If a publication which does not exist is specified during create
subscription, then we should throw an error similar to step 2 behavior.
Similarly if a publication which does not exist is specified during alter
subscription, then we should throw an error similar to step 4 behavior. If
publication is dropped after subscription is created, this should be
removed when an alter subscription subname refresh publication is performed
similar to step 4.
Thoughts?
Regards,
Vignesh
EnterpriseDB: http://www.enterprisedb.com
On Mon, 25 Jan 2021 at 21:55, Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com> wrote:
On Mon, Jan 25, 2021 at 5:18 PM japin <japinli@hotmail.com> wrote:
Do you mean when we drop publications from a subscription? If yes, do
we have a way to drop a publication from the subscription? See below
one of my earlier questions on this.
"I wonder, why isn't dropping a publication from a list of
publications that are with subscription is not allowed?"
At least, I see no ALTER SUBSCRIPTION ... DROP PUBLICATION mypub1 or
something similar?Why we do not support ALTER SUBSCRIPTION...ADD/DROP PUBLICATION? When we
have multiple publications in subscription, but I want to add/drop a single
publication, it is conveient. The ALTER SUBSCRIPTION...SET PUBLICATION...
should supply the completely publications.Looks like the way to drop/add publication from the list of
publications in subscription requires users to specify all the list of
publications currently exists +/- the new publication that needs to be
added/dropped:CREATE SUBSCRIPTION mysub1 CONNECTION 'host=localhost port=5432
dbname=postgres' PUBLICATION mypub1, mypub2, mypu3, mypub4, mypub5;
postgres=# select subpublications from pg_subscription;
subpublications
-------------------------------------
{mypub1,mypub2,mypu3,mypub4,mypub5}
(1 row)Say, I want to drop mypub4:
ALTER SUBSCRIPTION mysub1 SET PUBLICATION mypub1, mypub2, mypu3, mypub5;
postgres=# select subpublications from pg_subscription;
subpublications
------------------------------
{mypub1,mypub2,mypu3,mypub5}Say, I want toa dd mypub4 and mypub6:
ALTER SUBSCRIPTION mysub1 SET PUBLICATION mypub1, mypub2, mypu3,
mypub5, mypub4, mypub6;
postgres=# select subpublications from pg_subscription;
subpublications
--------------------------------------------
{mypub1,mypub2,mypu3,mypub5,mypub4,mypub6}
(1 row)It will be good to have something like:
ALTER SUBSCRIPTION mysub1 ADD PUBLICATION mypub1, mypub3; which will
the publications to subscription if not added previously.ALTER SUBSCRIPTION mysub1 DROP PUBLICATION mypub1, mypub3; which will
drop the publications from subscription if they exist in the
subscription's list of publications.But I'm really not sure why the above syntax was not added earlier. We
may be missing something here.Sorry, this question is unrelated with this subject.
Yes, IMO it can definitely be discussed in another thread. It will be
good to get a separate opinion for this.
I started a new thread [1]/messages/by-id/MEYP282MB166939D0D6C480B7FBE7EFFBB6BC0@MEYP282MB1669.AUSP282.PROD.OUTLOOK.COM for this, please have a look.
[1]: /messages/by-id/MEYP282MB166939D0D6C480B7FBE7EFFBB6BC0@MEYP282MB1669.AUSP282.PROD.OUTLOOK.COM
--
Regrads,
Japin Li.
ChengDu WenWu Information Technology Co.,Ltd.
On Mon, Jan 25, 2021 at 10:32 PM vignesh C <vignesh21@gmail.com> wrote:
I mean it doesn’t seem right to disallow to create the subscription if
the publisher doesn't exist, and my reasoning was even though the
publisher exists while creating the subscription you might drop it
later right?. So basically, now also we can create the same scenario
that a subscription may exist for the publication which does not
exist.I would like to defer on documentation for this.
I feel we should have the behavior similar to publication tables as given below, then it will be consistent and easier for the users:This is the behavior in case of table:
Step 1:
PUBLISHER SIDE:
create table t1(c1 int);
create table t2(c1 int);
CREATE PUBLICATION mypub1 for table t1,t2;
-- All above commands succeeds
Step 2:
SUBSCRIBER SIDE:
-- Create subscription without creating tables will result in error:
CREATE SUBSCRIPTION mysub1 CONNECTION 'dbname=source_rep host=localhost user=vignesh port=5432' PUBLICATION mypub1;
ERROR: relation "public.t2" does not exist
create table t1(c1 int);
create table t2(c1 int);CREATE SUBSCRIPTION mysub1 CONNECTION 'dbname=source_rep host=localhost user=vignesh port=5432' PUBLICATION mypub1;
postgres=# select * from pg_subscription;
oid | subdbid | subname | subowner | subenabled | subbinary | substream | subconninfo | subslotname | subsynccommit | subpublications
-------+---------+---------+----------+------------+-----------+-----------+---------------------------------------------------------+-------------+---------------+-----------------
16392 | 13756 | mysub1 | 10 | t | f | f | dbname=source_rep host=localhost user=vignesh port=5432 | mysub1 | off | {mypub1}
(1 row)postgres=# select *,srrelid::oid::regclass from pg_subscription_rel;
srsubid | srrelid | srsubstate | srsublsn | srrelid
---------+---------+------------+-----------+---------
16392 | 16389 | r | 0/1608BD0 | t2
16392 | 16384 | r | 0/1608BD0 | t1(2 rows)
Step 3:
PUBLISHER:
drop table t2;
create table t3;
CREATE PUBLICATION mypub2 for table t1,t3;Step 4:
SUBSCRIBER:
postgres=# select *,srrelid::oid::regclass from pg_subscription_rel;
srsubid | srrelid | srsubstate | srsublsn | srrelid
---------+---------+------------+-----------+---------
16392 | 16389 | r | 0/1608BD0 | t2
16392 | 16384 | r | 0/1608BD0 | t1(2 rows)
postgres=# alter subscription mysub1 refresh publication ;
ALTER SUBSCRIPTION-- Subscription relation will be updated.
postgres=# select *,srrelid::oid::regclass from pg_subscription_rel;
srsubid | srrelid | srsubstate | srsublsn | srrelid
---------+---------+------------+-----------+---------
16392 | 16384 | r | 0/1608BD0 | t1
(1 row)-- Alter subscription fails while setting publication having a table that does not exist
postgres=# alter subscription mysub1 set publication mysub2;
ERROR: relation "public.t3" does not existTo maintain consistency, we should have similar behavior in case of publication too.
If a publication which does not exist is specified during create subscription, then we should throw an error similar to step 2 behavior. Similarly if a publication which does not exist is specified during alter subscription, then we should throw an error similar to step 4 behavior. If publication is dropped after subscription is created, this should be removed when an alter subscription subname refresh publication is performed similar to step 4.
Thoughts?
IIUC, your idea is to check if the publications (that are associated
with a subscription) are present in the publisher or not during ALTER
SUBSCRIPTION ... REFRESH PUBLICATION;. If that's the case, then I have
scenario:
1) subscription is created with pub1, pub2 and assume both the
publications are present in the publisher
2) pub1 and pub2 tables data is replicated properly
3) pub2 is dropped on the publisher
4) run alter subscription .. refresh publication on the subscriber, so
that the pub2 tables will be removed from the subscriber
5) for some reason, user creates pub2 again on the publisher and want
to replicated some tables
6) run alter subscription .. refresh publication on the subscriber, so
that the pub2 tables will be added to the subscriber table list
Now, if we remove the dropped publication pub2 in step 4 from the
subscription list(as per your above analysis and suggestion), then
after step 5, users will need to add the publication pub2 to the
subscription again. I feel this is a change in the current behaviour.
The existing behaviour on master doesn't mandate this as the dropped
publications are not removed from the subscription list at all.
To not mandate any new behaviour, I would suggest to have a new option
for ALTER SUBSCRIPTION ... REFRESH PUBLICATION WITH
(remove_dropped_publications = false). The new option
remove_dropped_publications will have a default value false, when set
to true it will check if the publications that are present in the
subscription list are actually existing on the publisher or not, if
not remove them from the list. And also in the documentation we need
to clearly mention the consequence of this new option setting to true,
that is, the dropped publications if created again will need to be
added to the subscription list again.
Thoughts?
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Wed, Feb 3, 2021, at 2:13 AM, Bharath Rupireddy wrote:
On Mon, Jan 25, 2021 at 10:32 PM vignesh C <vignesh21@gmail.com> wrote:
If a publication which does not exist is specified during create subscription, then we should throw an error similar to step 2 behavior. Similarly if a publication which does not exist is specified during alter subscription, then we should throw an error similar to step 4 behavior. If publication is dropped after subscription is created, this should be removed when an alter subscription subname refresh publication is performed similar to step 4.
Thoughts?
Postgres implemented a replication mechanism that is decoupled which means that
both parties can perform "uncoordinated" actions. As you shown, the CREATE
SUBSCRIPTION informing a non-existent publication is one of them. I think that
being permissive in some situations can prevent you from painting yourself into
a corner. Even if you try to be strict on the subscriber side, the other side
(publisher) can impose you additional complexity.
You are arguing that in the initial phase, the CREATE SUBSCRIPTION has a strict
mechanism. It is a fair point. However, impose this strictness for the other
SUBSCRIPTION commands should be carefully forethought. If we go that route, I
suggest to have the current behavior as an option. The reasons are: (i) it is
backward-compatible, (ii) it allows us some known flexibility (non-existent
publication), and (iii) it would probably allow us to fix a scenario created by
the strict mode. This strict mode can be implemented via new parameter
validate_publication (ENOCAFFEINE to propose a better name) that checks if the
publication is available when you executed the CREATE SUBSCRIPTION. Similar
parameter can be used in ALTER SUBSCRIPTION ... SET PUBLICATION and ALTER
SUBSCRIPTION ... REFRESH PUBLICATION.
To not mandate any new behaviour, I would suggest to have a new option
for ALTER SUBSCRIPTION ... REFRESH PUBLICATION WITH
(remove_dropped_publications = false). The new option
remove_dropped_publications will have a default value false, when set
to true it will check if the publications that are present in the
subscription list are actually existing on the publisher or not, if
not remove them from the list. And also in the documentation we need
to clearly mention the consequence of this new option setting to true,
that is, the dropped publications if created again will need to be
added to the subscription list again.
REFRESH PUBLICATION is not the right command to remove publications. There is a
command for it: ALTER SUBSCRIPTION ... SET PUBLICATION.
The other alternative is to document that non-existent publication names can be
in the subscription catalog and it is ignored while executing SUBSCRIPTION
commands. You could possibly propose a NOTICE/WARNING that informs the user
that the SUBSCRIPTION command contains non-existent publication.
--
Euler Taveira
EDB https://www.enterprisedb.com/
On Wed, Mar 3, 2021 at 8:59 AM Euler Taveira <euler@eulerto.com> wrote:
On Wed, Feb 3, 2021, at 2:13 AM, Bharath Rupireddy wrote:
On Mon, Jan 25, 2021 at 10:32 PM vignesh C <vignesh21@gmail.com> wrote:
If a publication which does not exist is specified during create subscription, then we should throw an error similar to step 2 behavior. Similarly if a publication which does not exist is specified during alter subscription, then we should throw an error similar to step 4 behavior. If publication is dropped after subscription is created, this should be removed when an alter subscription subname refresh publication is performed similar to step 4.
Thoughts?Postgres implemented a replication mechanism that is decoupled which means that
both parties can perform "uncoordinated" actions. As you shown, the CREATE
SUBSCRIPTION informing a non-existent publication is one of them. I think that
being permissive in some situations can prevent you from painting yourself into
a corner. Even if you try to be strict on the subscriber side, the other side
(publisher) can impose you additional complexity.You are arguing that in the initial phase, the CREATE SUBSCRIPTION has a strict
mechanism. It is a fair point. However, impose this strictness for the other
SUBSCRIPTION commands should be carefully forethought. If we go that route, I
suggest to have the current behavior as an option. The reasons are: (i) it is
backward-compatible, (ii) it allows us some known flexibility (non-existent
publication), and (iii) it would probably allow us to fix a scenario created by
the strict mode. This strict mode can be implemented via new parameter
validate_publication (ENOCAFFEINE to propose a better name) that checks if the
publication is available when you executed the CREATE SUBSCRIPTION. Similar
parameter can be used in ALTER SUBSCRIPTION ... SET PUBLICATION and ALTER
SUBSCRIPTION ... REFRESH PUBLICATION.
IIUC the validate_publication is kind of the feature enable/disable
flag. With the default value being false, when set to true, it checks
whether the publications exists or not while CREATE/ALTER SUBSCRIPTION
and throw error. If I'm right, then the validate_publication option
looks good to me. So, whoever wants to impose strict restrictions to
CREATE/ALTER subscription can set it to true. All others will not see
any new behaviour or such.
To not mandate any new behaviour, I would suggest to have a new option
for ALTER SUBSCRIPTION ... REFRESH PUBLICATION WITH
(remove_dropped_publications = false). The new option
remove_dropped_publications will have a default value false, when set
to true it will check if the publications that are present in the
subscription list are actually existing on the publisher or not, if
not remove them from the list. And also in the documentation we need
to clearly mention the consequence of this new option setting to true,
that is, the dropped publications if created again will need to be
added to the subscription list again.REFRESH PUBLICATION is not the right command to remove publications. There is a
command for it: ALTER SUBSCRIPTION ... SET PUBLICATION.The other alternative is to document that non-existent publication names can be
in the subscription catalog and it is ignored while executing SUBSCRIPTION
commands. You could possibly propose a NOTICE/WARNING that informs the user
that the SUBSCRIPTION command contains non-existent publication.
I think, we can also have validate_publication option allowed for
ALTER SUBSCRIPTION SET PUBLICATION and REFRESH PUBLICATION commands
with the same behaviour i.e. error out when specified publications
don't exist in the publisher. Thoughts?
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Thu, Mar 4, 2021 at 1:04 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
On Wed, Mar 3, 2021 at 8:59 AM Euler Taveira <euler@eulerto.com> wrote:
On Wed, Feb 3, 2021, at 2:13 AM, Bharath Rupireddy wrote:
On Mon, Jan 25, 2021 at 10:32 PM vignesh C <vignesh21@gmail.com> wrote:
If a publication which does not exist is specified during create subscription, then we should throw an error similar to step 2 behavior. Similarly if a publication which does not exist is specified during alter subscription, then we should throw an error similar to step 4 behavior. If publication is dropped after subscription is created, this should be removed when an alter subscription subname refresh publication is performed similar to step 4.
Thoughts?Postgres implemented a replication mechanism that is decoupled which means that
both parties can perform "uncoordinated" actions. As you shown, the CREATE
SUBSCRIPTION informing a non-existent publication is one of them. I think that
being permissive in some situations can prevent you from painting yourself into
a corner. Even if you try to be strict on the subscriber side, the other side
(publisher) can impose you additional complexity.You are arguing that in the initial phase, the CREATE SUBSCRIPTION has a strict
mechanism. It is a fair point. However, impose this strictness for the other
SUBSCRIPTION commands should be carefully forethought. If we go that route, I
suggest to have the current behavior as an option. The reasons are: (i) it is
backward-compatible, (ii) it allows us some known flexibility (non-existent
publication), and (iii) it would probably allow us to fix a scenario created by
the strict mode. This strict mode can be implemented via new parameter
validate_publication (ENOCAFFEINE to propose a better name) that checks if the
publication is available when you executed the CREATE SUBSCRIPTION. Similar
parameter can be used in ALTER SUBSCRIPTION ... SET PUBLICATION and ALTER
SUBSCRIPTION ... REFRESH PUBLICATION.IIUC the validate_publication is kind of the feature enable/disable
flag. With the default value being false, when set to true, it checks
whether the publications exists or not while CREATE/ALTER SUBSCRIPTION
and throw error. If I'm right, then the validate_publication option
looks good to me. So, whoever wants to impose strict restrictions to
CREATE/ALTER subscription can set it to true. All others will not see
any new behaviour or such.To not mandate any new behaviour, I would suggest to have a new option
for ALTER SUBSCRIPTION ... REFRESH PUBLICATION WITH
(remove_dropped_publications = false). The new option
remove_dropped_publications will have a default value false, when set
to true it will check if the publications that are present in the
subscription list are actually existing on the publisher or not, if
not remove them from the list. And also in the documentation we need
to clearly mention the consequence of this new option setting to true,
that is, the dropped publications if created again will need to be
added to the subscription list again.REFRESH PUBLICATION is not the right command to remove publications. There is a
command for it: ALTER SUBSCRIPTION ... SET PUBLICATION.The other alternative is to document that non-existent publication names can be
in the subscription catalog and it is ignored while executing SUBSCRIPTION
commands. You could possibly propose a NOTICE/WARNING that informs the user
that the SUBSCRIPTION command contains non-existent publication.I think, we can also have validate_publication option allowed for
ALTER SUBSCRIPTION SET PUBLICATION and REFRESH PUBLICATION commands
with the same behaviour i.e. error out when specified publications
don't exist in the publisher. Thoughts?
Sorry for the delayed reply, I was working on a few other projects so
I was not able to reply quickly.
Since we are getting the opinion that if we make the check
publications by default it might affect the existing users, I'm fine
with having an option validate_option to check if the publication
exists in the publisher, that way there is no change for the existing
user. I have made a patch in similar lines, attached patch has the
changes for the same.
Thoughts?
Regards,
Vignesh
Attachments:
v3-0001-Identify-missing-publications-from-publisher-whil.patchtext/x-patch; charset=US-ASCII; name=v3-0001-Identify-missing-publications-from-publisher-whil.patchDownload
From 2bae27e37c6c1ace313f544709c0582e2b94b937 Mon Sep 17 00:00:00 2001
From: vignesh <vignesh21@gmail.com>
Date: Wed, 7 Apr 2021 22:05:53 +0530
Subject: [PATCH v3] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 12 ++
doc/src/sgml/ref/create_subscription.sgml | 12 ++
src/backend/commands/subscriptioncmds.c | 242 +++++++++++++++++++---
src/test/subscription/t/100_bugs.pl | 71 ++++++-
4 files changed, 306 insertions(+), 31 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 367ac814f4..85d3b8f17b 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -160,6 +160,18 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ Specifies whether the subscriber must verify if the specified
+ publications are present in the publisher. By default, the subscriber
+ will not check if the publications are present in the publisher.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index e812beee37..ee9e656c60 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -239,6 +239,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ Specifies whether the subscriber must verify if the specified
+ publications are present in the publisher. By default, the subscriber
+ will not check if the publications are present in the publisher.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 517c8edd3b..e595388472 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@ static List *fetch_table_list(WalReceiverConn *wrconn, List *publications);
static void check_duplicates_in_publist(List *publist, Datum *datums);
static List *merge_publications(List *oldpublist, List *newpublist, bool addpub, const char *subname);
static void ReportSlotConnectionError(List *rstates, Oid subid, char *slotname, char *err);
+static void check_publications(WalReceiverConn *wrconn, List *publications);
/*
@@ -69,7 +70,9 @@ parse_subscription_options(List *options,
char **synchronous_commit,
bool *refresh,
bool *binary_given, bool *binary,
- bool *streaming_given, bool *streaming)
+ bool *streaming_given, bool *streaming,
+ bool *validate_publication_given,
+ bool *validate_publication)
{
ListCell *lc;
bool connect_given = false;
@@ -111,6 +114,12 @@ parse_subscription_options(List *options,
*streaming = false;
}
+ if (validate_publication)
+ {
+ *validate_publication_given = false;
+ *validate_publication = false;
+ }
+
/* Parse options */
foreach(lc, options)
{
@@ -215,6 +224,16 @@ parse_subscription_options(List *options,
*streaming_given = true;
*streaming = defGetBoolean(defel);
}
+ else if (strcmp(defel->defname, "validate_publication") == 0 && validate_publication)
+ {
+ if (*validate_publication_given)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+
+ *validate_publication_given = true;
+ *validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -247,10 +266,18 @@ parse_subscription_options(List *options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (validate_publication && validate_publication_given &&
+ *validate_publication)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
*enabled = false;
*create_slot = false;
*copy_data = false;
+ *validate_publication = false;
}
/*
@@ -343,6 +370,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
bool slotname_given;
bool binary;
bool binary_given;
+ bool validate_publication;
+ bool validate_publication_given;
char originname[NAMEDATALEN];
bool create_slot;
List *publications;
@@ -361,7 +390,9 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ &validate_publication_given,
+ &validate_publication);
/*
* Since creating a replication slot is not transactional, rolling back
@@ -472,6 +503,12 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
PG_TRY();
{
+ if (validate_publication)
+ {
+ /* Verify specified publications exists in the publisher. */
+ check_publications(wrconn, publications);
+ }
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -539,7 +576,7 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data, bool check_pub)
{
char *err;
List *pubrel_names;
@@ -568,6 +605,10 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
ereport(ERROR,
(errmsg("could not connect to the publisher: %s", err)));
+ /* Verify specified publications exists in the publisher. */
+ if (check_pub)
+ check_publications(wrconn, sub->publications);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -814,7 +855,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ NULL, NULL);
if (slotname_given)
{
@@ -871,6 +913,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
+ NULL, NULL,
NULL, NULL); /* no streaming */
Assert(enabled_given);
@@ -906,6 +949,10 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
{
bool copy_data;
bool refresh;
+ bool validate_publication;
+ bool validate_publication_given;
+ char *err;
+ WalReceiverConn *wrconn;
parse_subscription_options(stmt->options,
NULL, /* no "connect" */
@@ -916,13 +963,33 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(stmt->publication);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (validate_publication)
+ {
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ /* Verify specified publications exists in the publisher. */
+ check_publications(wrconn, stmt->publication);
+
+ /* We are done with the remote side, close connection. */
+ walrcv_disconnect(wrconn);
+ }
+
/* Refresh if user asked us to. */
if (refresh)
{
@@ -937,7 +1004,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -950,6 +1017,10 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
bool copy_data;
bool refresh;
List *publist;
+ bool validate_publication;
+ bool validate_publication_given;
+ char *err;
+ WalReceiverConn *wrconn;
publist = merge_publications(sub->publications, stmt->publication, isadd, stmt->subname);
@@ -963,7 +1034,9 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(publist);
@@ -971,6 +1044,24 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
update_tuple = true;
+ if (validate_publication)
+ {
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ /* Verify specified publications exists in the publisher. */
+ check_publications(wrconn, stmt->publication);
+
+ /* We are done with the remote side, close connection. */
+ walrcv_disconnect(wrconn);
+ }
+
/* Refresh if user asked us to. */
if (refresh)
{
@@ -985,7 +1076,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Only refresh the added/dropped list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -994,6 +1085,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
case ALTER_SUBSCRIPTION_REFRESH:
{
bool copy_data;
+ bool validate_publication;
+ bool validate_publication_given;
if (!sub->enabled)
ereport(ERROR,
@@ -1009,11 +1102,13 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, validate_publication);
break;
}
@@ -1466,27 +1561,20 @@ AlterSubscriptionOwner_oid(Oid subid, Oid newOwnerId)
}
/*
- * Get the list of tables which belong to specified publications on the
- * publisher connection.
+ * Return a query by appending the publications to the input query.
*/
-static List *
-fetch_table_list(WalReceiverConn *wrconn, List *publications)
+static StringInfoData *
+get_appended_publications_query(char *query, List *publications)
{
- WalRcvExecResult *res;
- StringInfoData cmd;
- TupleTableSlot *slot;
- Oid tableRow[2] = {TEXTOID, TEXTOID};
+ bool first = true;
+ StringInfoData *cmd = makeStringInfo();
ListCell *lc;
- bool first;
- List *tablelist = NIL;
Assert(list_length(publications) > 0);
- initStringInfo(&cmd);
- appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
+ initStringInfo(cmd);
+ appendStringInfoString(cmd, query);
+
foreach(lc, publications)
{
char *pubname = strVal(lfirst(lc));
@@ -1494,14 +1582,108 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
if (first)
first = false;
else
- appendStringInfoString(&cmd, ", ");
+ appendStringInfoString(cmd, ", ");
+
+ appendStringInfoString(cmd, quote_literal_cstr(pubname));
+ }
+
+ appendStringInfoChar(cmd, ')');
+ return cmd;
+}
+
+/*
+ * Verify that the specified publication(s) exists in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfoData *cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ cmd = get_appended_publications_query("SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (", publications);
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg("could not receive list of publications from the publisher: %s",
+ res->err)));
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
+ publicationsCopy = list_copy(publications);
+
+ /* Process publications. */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
}
- appendStringInfoChar(&cmd, ')');
- res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
- pfree(cmd.data);
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ StringInfoData nonExistentPublications;
+ bool first = true;
+ ListCell *lc;
+
+ /* Convert the publications which does not exist into a string. */
+ initStringInfo(&nonExistentPublications);
+ foreach(lc, publicationsCopy)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(&nonExistentPublications, ", ");
+ appendStringInfoString(&nonExistentPublications, "\"");
+ appendStringInfoString(&nonExistentPublications, pubname);
+ appendStringInfoString(&nonExistentPublications, "\"");
+ }
+
+ ereport(ERROR,
+ (errmsg("publication(s) %s does not exist in the publisher",
+ nonExistentPublications.data)));
+ }
+}
+
+/*
+ * Get the list of tables which belong to specified publications on the
+ * publisher connection.
+ */
+static List *
+fetch_table_list(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfoData *cmd;
+ TupleTableSlot *slot;
+ Oid tableRow[2] = {TEXTOID, TEXTOID};
+ List *tablelist = NIL;
+
+ cmd = get_appended_publications_query(
+ "SELECT DISTINCT t.schemaname, t.tablename\n"
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (", publications);
+ res = walrcv_exec(wrconn, cmd->data, 2, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
if (res->status != WALRCV_OK_TUPLES)
ereport(ERROR,
diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl
index b8f46f08cc..6ad640600f 100644
--- a/src/test/subscription/t/100_bugs.pl
+++ b/src/test/subscription/t/100_bugs.pl
@@ -3,7 +3,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 5;
+use Test::More tests => 11;
# Bug #15114
@@ -154,6 +154,75 @@ is($node_twoways->safe_psql('d2', "SELECT count(f) FROM t"),
is($node_twoways->safe_psql('d2', "SELECT count(f) FROM t2"),
$rows * 2, "2x$rows rows in t2");
+# Create subcription for a publication which does not exist.
+$node_publisher = get_new_node('testpublisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+$node_subscriber = get_new_node('testsubscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+$publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+
+$node_publisher->safe_psql('postgres',
+ "CREATE PUBLICATION testpub1 FOR ALL TABLES");
+
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION testsub1 CONNECTION '$publisher_connstr' PUBLICATION testpub1"
+);
+
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION pub_doesnt_exist WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication\(s\) "pub_doesnt_exist" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION testpub1, pub_doesnt_exist WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication\(s\) "pub_doesnt_exist" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Multiple publications does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION pub_doesnt_exist, pub_doesnt_exist1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication\(s\) "pub_doesnt_exist", "pub_doesnt_exist1" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist WITH (VALIDATE_PUBLICATION = TRUE)");
+ok( $stderr =~
+ /ERROR: publication\(s\) "pub_doesnt_exist" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication\(s\) "pub_doesnt_exist" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ /ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
+$node_publisher->stop('fast');
+$node_subscriber->stop('fast');
+
# Verify table data is synced with cascaded replication setup. This is mainly
# to test whether the data written by tablesync worker gets replicated.
my $node_pub = get_new_node('testpublisher1');
--
2.25.1
On Wed, Apr 7, 2021 at 10:37 PM vignesh C <vignesh21@gmail.com> wrote:
I think, we can also have validate_publication option allowed for
ALTER SUBSCRIPTION SET PUBLICATION and REFRESH PUBLICATION commands
with the same behaviour i.e. error out when specified publications
don't exist in the publisher. Thoughts?Sorry for the delayed reply, I was working on a few other projects so
I was not able to reply quickly.
Since we are getting the opinion that if we make the check
publications by default it might affect the existing users, I'm fine
with having an option validate_option to check if the publication
exists in the publisher, that way there is no change for the existing
user. I have made a patch in similar lines, attached patch has the
changes for the same.
Thoughts?
Here are some comments on v3 patch:
1) Please mention what's the default value of the option
+ <varlistentry>
+ <term><literal>validate_publication</literal>
(<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ Specifies whether the subscriber must verify if the specified
+ publications are present in the publisher. By default, the subscriber
+ will not check if the publications are present in the publisher.
+ </para>
+ </listitem>
+ </varlistentry>
2) How about
+ Specifies whether the subscriber must verify the
publications that are
+ being subscribed to are present in the publisher. By default,
the subscriber
instead of
+ Specifies whether the subscriber must verify if the specified
+ publications are present in the publisher. By default, the subscriber
3) I think we can make below common code into a single function with
flags to differentiate processing for both, something like:
StringInfoData *get_publist_str(List *publicaitons, bool use_quotes,
bool is_fetch_table_list);
check_publications:
+ /* Convert the publications which does not exist into a string. */
+ initStringInfo(&nonExistentPublications);
+ foreach(lc, publicationsCopy)
+ {
and get_appended_publications_query:
foreach(lc, publications)
With the new function that only prepares comma separated list of
publications, you can get rid of get_appended_publications_query and
just append the returned list to the query.
fetch_table_list: get_publist_str(publications, true, true);
check_publications: for select query preparation
get_publist_str(publications, true, false); and for error string
preparation get_publist_str(publications, false, false);
And also let the new function get_publist_str allocate the string and
just mention as a note in the function comment that the callers should
pfree the returned string.
4) We could do following,
ereport(ERROR,
(errcode(ERRCODE_TOO_MANY_ARGUMENTS),
errmsg_plural("publication %s does not exist in the publisher",
"publications %s do not exist in the publisher",
list_length(publicationsCopy),
nonExistentPublications.data)));
instead of
+ ereport(ERROR,
+ (errmsg("publication(s) %s does not exist in the publisher",
+ nonExistentPublications.data)));
if (list_member(cte->ctecolnames,
makeString(cte->search_clause->search_seq_column)))
5) I think it's better with
+ * Check the specified publication(s) is(are) present in the publisher.
instead of
+ * Verify that the specified publication(s) exists in the publisher.
6) Instead of such a longer variable name "nonExistentPublications"
how about just "pubnames" and add a comment there saying "going to
error out with the list of non-existent publications" with that the
variable and that part of code's context is clear.
7) You can just do
publications = list_copy(publications);
instead of using another variable publicationsCopy
publicationsCopy = list_copy(publications);
8) If you have done StringInfoData *cmd = makeStringInfo();, then no
need of initStringInfo(cmd);
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Thu, Apr 8, 2021 at 12:13 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
On Wed, Apr 7, 2021 at 10:37 PM vignesh C <vignesh21@gmail.com> wrote:
I think, we can also have validate_publication option allowed for
ALTER SUBSCRIPTION SET PUBLICATION and REFRESH PUBLICATION commands
with the same behaviour i.e. error out when specified publications
don't exist in the publisher. Thoughts?Sorry for the delayed reply, I was working on a few other projects so
I was not able to reply quickly.
Since we are getting the opinion that if we make the check
publications by default it might affect the existing users, I'm fine
with having an option validate_option to check if the publication
exists in the publisher, that way there is no change for the existing
user. I have made a patch in similar lines, attached patch has the
changes for the same.
Thoughts?Here are some comments on v3 patch:
Thanks for the comments
1) Please mention what's the default value of the option + <varlistentry> + <term><literal>validate_publication</literal> (<type>boolean</type>)</term> + <listitem> + <para> + Specifies whether the subscriber must verify if the specified + publications are present in the publisher. By default, the subscriber + will not check if the publications are present in the publisher. + </para> + </listitem> + </varlistentry>
Modified.
2) How about + Specifies whether the subscriber must verify the publications that are + being subscribed to are present in the publisher. By default, the subscriber instead of + Specifies whether the subscriber must verify if the specified + publications are present in the publisher. By default, the subscriber
Slightly reworded and modified.
3) I think we can make below common code into a single function with flags to differentiate processing for both, something like: StringInfoData *get_publist_str(List *publicaitons, bool use_quotes, bool is_fetch_table_list); check_publications: + /* Convert the publications which does not exist into a string. */ + initStringInfo(&nonExistentPublications); + foreach(lc, publicationsCopy) + { and get_appended_publications_query: foreach(lc, publications)With the new function that only prepares comma separated list of
publications, you can get rid of get_appended_publications_query and
just append the returned list to the query.
fetch_table_list: get_publist_str(publications, true, true);
check_publications: for select query preparation
get_publist_str(publications, true, false); and for error string
preparation get_publist_str(publications, false, false);And also let the new function get_publist_str allocate the string and
just mention as a note in the function comment that the callers should
pfree the returned string.
I felt the existing code looks better, if we have a common function,
we will have to lot of if conditions as both the functions is not same
to same, they operate on different data types and do the preparation
appropriately. Like fetch_table_list get nspname & relname and
converts it to RangeVar and adds to the list other function prepares a
text and deletes the entries present from the list. So I did not fix
this. Thoughts?
4) We could do following, ereport(ERROR, (errcode(ERRCODE_TOO_MANY_ARGUMENTS), errmsg_plural("publication %s does not exist in the publisher", "publications %s do not exist in the publisher", list_length(publicationsCopy), nonExistentPublications.data))); instead of + ereport(ERROR, + (errmsg("publication(s) %s does not exist in the publisher", + nonExistentPublications.data)));if (list_member(cte->ctecolnames,
makeString(cte->search_clause->search_seq_column)))
Modified.
5) I think it's better with + * Check the specified publication(s) is(are) present in the publisher. instead of + * Verify that the specified publication(s) exists in the publisher.
Modified.
6) Instead of such a longer variable name "nonExistentPublications"
how about just "pubnames" and add a comment there saying "going to
error out with the list of non-existent publications" with that the
variable and that part of code's context is clear.
Modified.
7) You can just do
publications = list_copy(publications);
instead of using another variable publicationsCopy
publicationsCopy = list_copy(publications);
publications is an input list to this function, I did not want this
function to change this list. I felt existing is fine. Thoughts?
8) If you have done StringInfoData *cmd = makeStringInfo();, then no
need of initStringInfo(cmd);
Modified.
Attached v4 patch has the fixes for the comments.
Regards,
Vignesh
Attachments:
v4-0001-Identify-missing-publications-from-publisher-whil.patchapplication/x-patch; name=v4-0001-Identify-missing-publications-from-publisher-whil.patchDownload
From ffce0a164730bf872149d04dd39cdafcb7b2f7f8 Mon Sep 17 00:00:00 2001
From: vignesh <vignesh21@gmail.com>
Date: Wed, 7 Apr 2021 22:05:53 +0530
Subject: [PATCH v4] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 12 +
doc/src/sgml/ref/create_subscription.sgml | 12 +
src/backend/commands/subscriptioncmds.c | 245 +++++++++++++++---
.../t/021_validate_publications.pl | 74 ++++++
4 files changed, 313 insertions(+), 30 deletions(-)
create mode 100644 src/test/subscription/t/021_validate_publications.pl
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 367ac814f4..2e94135305 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -160,6 +160,18 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command will try to verify if the specified
+ publications that are subscribed is present in the publisher.
+ The default is <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index e812beee37..697342855e 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -239,6 +239,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command will try to verify if the specified
+ publications that are subscribed is present in the publisher.
+ The default is <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 517c8edd3b..d470bf5946 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -50,6 +50,7 @@ static List *fetch_table_list(WalReceiverConn *wrconn, List *publications);
static void check_duplicates_in_publist(List *publist, Datum *datums);
static List *merge_publications(List *oldpublist, List *newpublist, bool addpub, const char *subname);
static void ReportSlotConnectionError(List *rstates, Oid subid, char *slotname, char *err);
+static void check_publications(WalReceiverConn *wrconn, List *publications);
/*
@@ -69,7 +70,9 @@ parse_subscription_options(List *options,
char **synchronous_commit,
bool *refresh,
bool *binary_given, bool *binary,
- bool *streaming_given, bool *streaming)
+ bool *streaming_given, bool *streaming,
+ bool *validate_publication_given,
+ bool *validate_publication)
{
ListCell *lc;
bool connect_given = false;
@@ -111,6 +114,12 @@ parse_subscription_options(List *options,
*streaming = false;
}
+ if (validate_publication)
+ {
+ *validate_publication_given = false;
+ *validate_publication = false;
+ }
+
/* Parse options */
foreach(lc, options)
{
@@ -215,6 +224,16 @@ parse_subscription_options(List *options,
*streaming_given = true;
*streaming = defGetBoolean(defel);
}
+ else if (strcmp(defel->defname, "validate_publication") == 0 && validate_publication)
+ {
+ if (*validate_publication_given)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+
+ *validate_publication_given = true;
+ *validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -247,10 +266,18 @@ parse_subscription_options(List *options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (validate_publication && validate_publication_given &&
+ *validate_publication)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
*enabled = false;
*create_slot = false;
*copy_data = false;
+ *validate_publication = false;
}
/*
@@ -343,6 +370,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
bool slotname_given;
bool binary;
bool binary_given;
+ bool validate_publication;
+ bool validate_publication_given;
char originname[NAMEDATALEN];
bool create_slot;
List *publications;
@@ -361,7 +390,9 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ &validate_publication_given,
+ &validate_publication);
/*
* Since creating a replication slot is not transactional, rolling back
@@ -472,6 +503,12 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
PG_TRY();
{
+ if (validate_publication)
+ {
+ /* Verify specified publications exists in the publisher. */
+ check_publications(wrconn, publications);
+ }
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -539,7 +576,7 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data, bool check_pub)
{
char *err;
List *pubrel_names;
@@ -568,6 +605,10 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
ereport(ERROR,
(errmsg("could not connect to the publisher: %s", err)));
+ /* Verify specified publications exists in the publisher. */
+ if (check_pub)
+ check_publications(wrconn, sub->publications);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -814,7 +855,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ NULL, NULL);
if (slotname_given)
{
@@ -871,6 +913,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
+ NULL, NULL,
NULL, NULL); /* no streaming */
Assert(enabled_given);
@@ -906,6 +949,10 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
{
bool copy_data;
bool refresh;
+ bool validate_publication;
+ bool validate_publication_given;
+ char *err;
+ WalReceiverConn *wrconn;
parse_subscription_options(stmt->options,
NULL, /* no "connect" */
@@ -916,13 +963,33 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(stmt->publication);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (validate_publication)
+ {
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ /* Verify specified publications exists in the publisher. */
+ check_publications(wrconn, stmt->publication);
+
+ /* We are done with the remote side, close connection. */
+ walrcv_disconnect(wrconn);
+ }
+
/* Refresh if user asked us to. */
if (refresh)
{
@@ -937,7 +1004,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -950,6 +1017,10 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
bool copy_data;
bool refresh;
List *publist;
+ bool validate_publication;
+ bool validate_publication_given;
+ char *err;
+ WalReceiverConn *wrconn;
publist = merge_publications(sub->publications, stmt->publication, isadd, stmt->subname);
@@ -963,7 +1034,9 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(publist);
@@ -971,6 +1044,24 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
update_tuple = true;
+ if (validate_publication)
+ {
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ /* Verify specified publications exists in the publisher. */
+ check_publications(wrconn, stmt->publication);
+
+ /* We are done with the remote side, close connection. */
+ walrcv_disconnect(wrconn);
+ }
+
/* Refresh if user asked us to. */
if (refresh)
{
@@ -985,7 +1076,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Only refresh the added/dropped list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -994,6 +1085,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
case ALTER_SUBSCRIPTION_REFRESH:
{
bool copy_data;
+ bool validate_publication;
+ bool validate_publication_given;
if (!sub->enabled)
ereport(ERROR,
@@ -1009,11 +1102,13 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, validate_publication);
break;
}
@@ -1466,27 +1561,19 @@ AlterSubscriptionOwner_oid(Oid subid, Oid newOwnerId)
}
/*
- * Get the list of tables which belong to specified publications on the
- * publisher connection.
+ * Return a query by appending the publications to the input query, memory
+ * allocated for cmd should be freed by the caller.
*/
-static List *
-fetch_table_list(WalReceiverConn *wrconn, List *publications)
+static StringInfoData *
+get_appended_publications_query(char *query, List *publications)
{
- WalRcvExecResult *res;
- StringInfoData cmd;
- TupleTableSlot *slot;
- Oid tableRow[2] = {TEXTOID, TEXTOID};
+ bool first = true;
+ StringInfo cmd = makeStringInfo();
ListCell *lc;
- bool first;
- List *tablelist = NIL;
Assert(list_length(publications) > 0);
- initStringInfo(&cmd);
- appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
+ appendStringInfoString(cmd, query);
foreach(lc, publications)
{
char *pubname = strVal(lfirst(lc));
@@ -1494,14 +1581,112 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
if (first)
first = false;
else
- appendStringInfoString(&cmd, ", ");
+ appendStringInfoString(cmd, ", ");
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
+ appendStringInfoString(cmd, quote_literal_cstr(pubname));
}
- appendStringInfoChar(&cmd, ')');
- res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
- pfree(cmd.data);
+ appendStringInfoChar(cmd, ')');
+ return cmd;
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ cmd = get_appended_publications_query("SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (", publications);
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg("could not receive list of publications from the publisher: %s",
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publications. */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publications for error message. */
+ StringInfoData pubnames;
+ bool first = true;
+ ListCell *lc;
+
+ /* Convert the publications which does not exist into a string. */
+ initStringInfo(&pubnames);
+ foreach(lc, publicationsCopy)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(&pubnames, ", ");
+ appendStringInfoString(&pubnames, "\"");
+ appendStringInfoString(&pubnames, pubname);
+ appendStringInfoString(&pubnames, "\"");
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames.data)));
+ }
+}
+
+/*
+ * Get the list of tables which belong to specified publications on the
+ * publisher connection.
+ */
+static List *
+fetch_table_list(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ Oid tableRow[2] = {TEXTOID, TEXTOID};
+ List *tablelist = NIL;
+
+ cmd = get_appended_publications_query(
+ "SELECT DISTINCT t.schemaname, t.tablename\n"
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (", publications);
+ res = walrcv_exec(wrconn, cmd->data, 2, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
if (res->status != WALRCV_OK_TUPLES)
ereport(ERROR,
diff --git a/src/test/subscription/t/021_validate_publications.pl b/src/test/subscription/t/021_validate_publications.pl
new file mode 100644
index 0000000000..29291d87cd
--- /dev/null
+++ b/src/test/subscription/t/021_validate_publications.pl
@@ -0,0 +1,74 @@
+# Tests for various bugs found over time
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 6;
+
+# Create subcription for a publication which does not exist.
+my $node_publisher = get_new_node('testpublisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+my $node_subscriber = get_new_node('testsubscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+
+$node_publisher->safe_psql('postgres',
+ "CREATE PUBLICATION testpub1 FOR ALL TABLES");
+
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION testsub1 CONNECTION '$publisher_connstr' PUBLICATION testpub1"
+);
+
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION pub_doesnt_exist WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION testpub1, pub_doesnt_exist WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+# Multiple publications does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION pub_doesnt_exist, pub_doesnt_exist1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publications "pub_doesnt_exist", "pub_doesnt_exist1" do not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist WITH (VALIDATE_PUBLICATION = TRUE)");
+ok( $stderr =~
+ /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ /ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
+$node_publisher->stop('fast');
+$node_subscriber->stop('fast');
--
2.25.1
On Tue, Apr 13, 2021 at 6:22 PM vignesh C <vignesh21@gmail.com> wrote:
2) How about + Specifies whether the subscriber must verify the publications that are + being subscribed to are present in the publisher. By default, the subscriber instead of + Specifies whether the subscriber must verify if the specified + publications are present in the publisher. By default, the subscriberSlightly reworded and modified.
+ <para>
+ When true, the command will try to verify if the specified
+ publications that are subscribed is present in the publisher.
+ The default is <literal>false</literal>.
+ </para>
"publications that are subscribed" is not right as the subscriber is
not yet subscribed, it is "trying to subscribing", and it's not that
the command "will try to verify", it actually verifies. So you can
modify as follows:
+ <para>
+ When true, the command verifies if all the specified
publications that are being subscribed to are present in the publisher
and throws an error if any of the publication doesn't exist. The
default is <literal>false</literal>.
+ </para>
3) I think we can make below common code into a single function with flags to differentiate processing for both, something like: StringInfoData *get_publist_str(List *publicaitons, bool use_quotes, bool is_fetch_table_list); check_publications: + /* Convert the publications which does not exist into a string. */ + initStringInfo(&nonExistentPublications); + foreach(lc, publicationsCopy) + { and get_appended_publications_query: foreach(lc, publications)With the new function that only prepares comma separated list of
publications, you can get rid of get_appended_publications_query and
just append the returned list to the query.
fetch_table_list: get_publist_str(publications, true, true);
check_publications: for select query preparation
get_publist_str(publications, true, false); and for error string
preparation get_publist_str(publications, false, false);And also let the new function get_publist_str allocate the string and
just mention as a note in the function comment that the callers should
pfree the returned string.I felt the existing code looks better, if we have a common function,
we will have to lot of if conditions as both the functions is not same
to same, they operate on different data types and do the preparation
appropriately. Like fetch_table_list get nspname & relname and
converts it to RangeVar and adds to the list other function prepares a
text and deletes the entries present from the list. So I did not fix
this. Thoughts?
I was actually thinking we could move the following duplicate code
into a function:
foreach(lc, publicationsCopy)
{
char *pubname = strVal(lfirst(lc));
if (first)
first = false;
else
appendStringInfoString(&pubnames, ", ");
appendStringInfoString(&pubnames, "\"");
appendStringInfoString(&pubnames, pubname);
appendStringInfoString(&pubnames, "\"");
}
and
foreach(lc, publications)
{
char *pubname = strVal(lfirst(lc));
if (first)
first = false;
else
appendStringInfoString(cmd, ", ");
appendStringInfoString(cmd, quote_literal_cstr(pubname));
}
that function can be:
static void
get_publications_str(List *publications, StringInfo dest, bool quote_literal)
{
ListCell *lc;
bool first = true;
Assert(list_length(publications) > 0);
foreach(lc, publications)
{
char *pubname = strVal(lfirst(lc));
if (first)
first = false;
else
appendStringInfoString(dest, ", ");
if (quote_literal)
appendStringInfoString(pubnames, quote_literal_cstr(pubname));
else
{
appendStringInfoString(&dest, "\"");
appendStringInfoString(&dest, pubname);
appendStringInfoString(&dest, "\"");
}
}
}
This way, we can get rid of get_appended_publications_query and use
the above function to return the appended list of publications. We
need to just pass quote_literal as true while preparing the publist
string for publication query and append it to the query outside the
function. While preparing publist str for error, pass quote_literal as
false. Thoughts?
7) You can just do
publications = list_copy(publications);
instead of using another variable publicationsCopy
publicationsCopy = list_copy(publications);publications is an input list to this function, I did not want this
function to change this list. I felt existing is fine. Thoughts?
Okay.
Typo - it's not "subcription" +# Create subcription for a publication
which does not exist.
I think we can remove extra { } by moving the comment above if clause
much like you did in AlterSubscription_refresh. And it's not "exists",
it is "exist" change in both AlterSubscription_refresh and
CreateSubscription.
+ if (validate_publication)
+ {
+ /* Verify specified publications exists in the publisher. */
+ check_publications(wrconn, publications);
+ }
+
Move /*no streaming */ to above NULL, NULL line:
+ NULL, NULL,
NULL, NULL); /* no streaming */
Can we have a new function for below duplicate code? Something like:
void connect_and_check_pubs(Subscription *sub, List *publications);?
+ if (validate_publication)
+ {
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true,
sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the
publisher: %s", err)));
+
+ /* Verify specified publications exists in the
publisher. */
+ check_publications(wrconn, stmt->publication);
+
+ /* We are done with the remote side, close connection. */
+ walrcv_disconnect(wrconn);
+ }
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Tue, Apr 13, 2021 at 8:01 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
On Tue, Apr 13, 2021 at 6:22 PM vignesh C <vignesh21@gmail.com> wrote:
2) How about + Specifies whether the subscriber must verify the publications that are + being subscribed to are present in the publisher. By default, the subscriber instead of + Specifies whether the subscriber must verify if the specified + publications are present in the publisher. By default, the subscriberSlightly reworded and modified.
+ <para> + When true, the command will try to verify if the specified + publications that are subscribed is present in the publisher. + The default is <literal>false</literal>. + </para>"publications that are subscribed" is not right as the subscriber is
not yet subscribed, it is "trying to subscribing", and it's not that
the command "will try to verify", it actually verifies. So you can
modify as follows:+ <para> + When true, the command verifies if all the specified publications that are being subscribed to are present in the publisher and throws an error if any of the publication doesn't exist. The default is <literal>false</literal>. + </para>3) I think we can make below common code into a single function with flags to differentiate processing for both, something like: StringInfoData *get_publist_str(List *publicaitons, bool use_quotes, bool is_fetch_table_list); check_publications: + /* Convert the publications which does not exist into a string. */ + initStringInfo(&nonExistentPublications); + foreach(lc, publicationsCopy) + { and get_appended_publications_query: foreach(lc, publications)With the new function that only prepares comma separated list of
publications, you can get rid of get_appended_publications_query and
just append the returned list to the query.
fetch_table_list: get_publist_str(publications, true, true);
check_publications: for select query preparation
get_publist_str(publications, true, false); and for error string
preparation get_publist_str(publications, false, false);And also let the new function get_publist_str allocate the string and
just mention as a note in the function comment that the callers should
pfree the returned string.I felt the existing code looks better, if we have a common function,
we will have to lot of if conditions as both the functions is not same
to same, they operate on different data types and do the preparation
appropriately. Like fetch_table_list get nspname & relname and
converts it to RangeVar and adds to the list other function prepares a
text and deletes the entries present from the list. So I did not fix
this. Thoughts?I was actually thinking we could move the following duplicate code
into a function:
foreach(lc, publicationsCopy)
{
char *pubname = strVal(lfirst(lc));if (first)
first = false;
else
appendStringInfoString(&pubnames, ", ");
appendStringInfoString(&pubnames, "\"");
appendStringInfoString(&pubnames, pubname);
appendStringInfoString(&pubnames, "\"");
}
and
foreach(lc, publications)
{
char *pubname = strVal(lfirst(lc));if (first)
first = false;
else
appendStringInfoString(cmd, ", ");appendStringInfoString(cmd, quote_literal_cstr(pubname));
}
that function can be:
static void
get_publications_str(List *publications, StringInfo dest, bool quote_literal)
{
ListCell *lc;
bool first = true;Assert(list_length(publications) > 0);
foreach(lc, publications)
{
char *pubname = strVal(lfirst(lc));if (first)
first = false;
else
appendStringInfoString(dest, ", ");if (quote_literal)
appendStringInfoString(pubnames, quote_literal_cstr(pubname));
else
{
appendStringInfoString(&dest, "\"");
appendStringInfoString(&dest, pubname);
appendStringInfoString(&dest, "\"");
}
}
}This way, we can get rid of get_appended_publications_query and use
the above function to return the appended list of publications. We
need to just pass quote_literal as true while preparing the publist
string for publication query and append it to the query outside the
function. While preparing publist str for error, pass quote_literal as
false. Thoughts?
Modified.
7) You can just do
publications = list_copy(publications);
instead of using another variable publicationsCopy
publicationsCopy = list_copy(publications);publications is an input list to this function, I did not want this
function to change this list. I felt existing is fine. Thoughts?Okay.
Typo - it's not "subcription" +# Create subcription for a publication
which does not exist.
Modified
I think we can remove extra { } by moving the comment above if clause much like you did in AlterSubscription_refresh. And it's not "exists", it is "exist" change in both AlterSubscription_refresh and CreateSubscription. + if (validate_publication) + { + /* Verify specified publications exists in the publisher. */ + check_publications(wrconn, publications); + } +
Modified.
Move /*no streaming */ to above NULL, NULL line:
+ NULL, NULL,
NULL, NULL); /* no streaming */
Modified.
Can we have a new function for below duplicate code? Something like: void connect_and_check_pubs(Subscription *sub, List *publications);? + if (validate_publication) + { + /* Load the library providing us libpq calls. */ + load_file("libpqwalreceiver", false); + + /* Try to connect to the publisher. */ + wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err); + if (!wrconn) + ereport(ERROR, + (errmsg("could not connect to the publisher: %s", err))); + + /* Verify specified publications exists in the publisher. */ + check_publications(wrconn, stmt->publication); + + /* We are done with the remote side, close connection. */ + walrcv_disconnect(wrconn); + }
Modified.
Thanks for the comments, Attached patch has the fixes for the same.
Thoughts?
Regards,
Vignesh
Attachments:
v5-0001-Identify-missing-publications-from-publisher-whil.patchtext/x-patch; charset=US-ASCII; name=v5-0001-Identify-missing-publications-from-publisher-whil.patchDownload
From 12acbcdce9dbc3a867c4ab0305d430ef55c4fa39 Mon Sep 17 00:00:00 2001
From: vignesh <vignesh21@gmail.com>
Date: Wed, 7 Apr 2021 22:05:53 +0530
Subject: [PATCH v5] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 13 +
doc/src/sgml/ref/create_subscription.sgml | 13 +
src/backend/commands/subscriptioncmds.c | 227 +++++++++++++++---
src/bin/psql/tab-complete.c | 7 +-
.../t/021_validate_publications.pl | 82 +++++++
5 files changed, 309 insertions(+), 33 deletions(-)
create mode 100644 src/test/subscription/t/021_validate_publications.pl
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 367ac814f4..81e156437b 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -160,6 +160,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index e812beee37..2f1f541253 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -239,6 +239,19 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 517c8edd3b..c7e7abb16c 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -51,7 +51,6 @@ static void check_duplicates_in_publist(List *publist, Datum *datums);
static List *merge_publications(List *oldpublist, List *newpublist, bool addpub, const char *subname);
static void ReportSlotConnectionError(List *rstates, Oid subid, char *slotname, char *err);
-
/*
* Common option parsing function for CREATE and ALTER SUBSCRIPTION commands.
*
@@ -69,7 +68,9 @@ parse_subscription_options(List *options,
char **synchronous_commit,
bool *refresh,
bool *binary_given, bool *binary,
- bool *streaming_given, bool *streaming)
+ bool *streaming_given, bool *streaming,
+ bool *validate_publication_given,
+ bool *validate_publication)
{
ListCell *lc;
bool connect_given = false;
@@ -111,6 +112,12 @@ parse_subscription_options(List *options,
*streaming = false;
}
+ if (validate_publication)
+ {
+ *validate_publication_given = false;
+ *validate_publication = false;
+ }
+
/* Parse options */
foreach(lc, options)
{
@@ -215,6 +222,16 @@ parse_subscription_options(List *options,
*streaming_given = true;
*streaming = defGetBoolean(defel);
}
+ else if (strcmp(defel->defname, "validate_publication") == 0 && validate_publication)
+ {
+ if (*validate_publication_given)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+
+ *validate_publication_given = true;
+ *validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -247,10 +264,18 @@ parse_subscription_options(List *options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (validate_publication && validate_publication_given &&
+ *validate_publication)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
*enabled = false;
*create_slot = false;
*copy_data = false;
+ *validate_publication = false;
}
/*
@@ -287,6 +312,133 @@ parse_subscription_options(List *options,
}
}
+/*
+ * Append the list of publication to dest string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoString(dest, "\"");
+ appendStringInfoString(dest, pubname);
+ appendStringInfoString(dest, "\"");
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg("could not receive list of publications from the publisher: %s",
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publications. */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publications for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ (errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data)));
+ }
+}
+
+/*
+ * Connect to the publisher and check if the publications exist.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
+{
+ char *err;
+
+ if (validate_publication)
+ {
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ /* Verify specified publications exist in the publisher. */
+ PG_TRY();
+ check_publications(wrconn, publications);
+ PG_FINALLY();
+ {
+ /* We are done with the remote side, close connection. */
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+ }
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -343,6 +495,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
bool slotname_given;
bool binary;
bool binary_given;
+ bool validate_publication;
+ bool validate_publication_given;
char originname[NAMEDATALEN];
bool create_slot;
List *publications;
@@ -361,7 +515,9 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ &validate_publication_given,
+ &validate_publication);
/*
* Since creating a replication slot is not transactional, rolling back
@@ -472,6 +628,10 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
PG_TRY();
{
+ /* Verify specified publications exist in the publisher. */
+ if (validate_publication)
+ check_publications(wrconn, publications);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -539,7 +699,7 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data, bool check_pub)
{
char *err;
List *pubrel_names;
@@ -568,6 +728,10 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
ereport(ERROR,
(errmsg("could not connect to the publisher: %s", err)));
+ /* Verify specified publications exist in the publisher. */
+ if (check_pub)
+ check_publications(wrconn, sub->publications);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -814,7 +978,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ NULL, NULL);
if (slotname_given)
{
@@ -871,7 +1036,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no streaming */
+ NULL, NULL, /* no streaming */
+ NULL, NULL);
Assert(enabled_given);
if (!sub->slotname && enabled)
@@ -906,6 +1072,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
{
bool copy_data;
bool refresh;
+ bool validate_publication;
+ bool validate_publication_given;
parse_subscription_options(stmt->options,
NULL, /* no "connect" */
@@ -916,12 +1084,15 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(stmt->publication);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -937,7 +1108,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -950,6 +1121,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
bool copy_data;
bool refresh;
List *publist;
+ bool validate_publication;
+ bool validate_publication_given;
publist = merge_publications(sub->publications, stmt->publication, isadd, stmt->subname);
@@ -963,13 +1136,18 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ /* for drop, no "validate_publication" */
+ isadd ? &validate_publication_given : NULL,
+ isadd ? &validate_publication : NULL);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(publist);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd)
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -985,7 +1163,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Only refresh the added/dropped list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -994,6 +1172,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
case ALTER_SUBSCRIPTION_REFRESH:
{
bool copy_data;
+ bool validate_publication;
+ bool validate_publication_given;
if (!sub->enabled)
ereport(ERROR,
@@ -1009,11 +1189,13 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, validate_publication);
break;
}
@@ -1476,28 +1658,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 7c4933333b..5a0d0f3c8d 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1674,7 +1674,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1693,7 +1693,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> ADD|DROP|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny, "WITH", "("))
- COMPLETE_WITH("copy_data", "refresh");
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
/* ALTER SCHEMA <name> */
else if (Matches("ALTER", "SCHEMA", MatchAny))
@@ -2780,7 +2780,8 @@ psql_completion(const char *text, int start, int end)
/* Complete "CREATE SUBSCRIPTION <name> ... WITH ( <opt>" */
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("copy_data", "connect", "create_slot", "enabled",
- "slot_name", "synchronous_commit");
+ "slot_name", "synchronous_commit",
+ "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/021_validate_publications.pl b/src/test/subscription/t/021_validate_publications.pl
new file mode 100644
index 0000000000..95676148da
--- /dev/null
+++ b/src/test/subscription/t/021_validate_publications.pl
@@ -0,0 +1,82 @@
+# Tests for various bugs found over time
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 7;
+
+# Create subscription for a publication which does not exist.
+my $node_publisher = get_new_node('testpublisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+my $node_subscriber = get_new_node('testsubscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+
+$node_publisher->safe_psql('postgres',
+ "CREATE PUBLICATION testpub1 FOR ALL TABLES");
+
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION testsub1 CONNECTION '$publisher_connstr' PUBLICATION testpub1"
+);
+
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION pub_doesnt_exist WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION testpub1, pub_doesnt_exist WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+# Multiple publications does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION pub_doesnt_exist, pub_doesnt_exist1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publications "pub_doesnt_exist", "pub_doesnt_exist1" do not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Add non existent publication.
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 ADD PUBLICATION pub_doesnt_exist WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
+ "Alter subscription add non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist WITH (VALIDATE_PUBLICATION = TRUE)");
+ok( $stderr =~
+ /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ /ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
+$node_publisher->stop('fast');
+$node_subscriber->stop('fast');
--
2.25.1
On Sat, May 1, 2021 at 12:49 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, Attached patch has the fixes for the same.
Thoughts?
Few more comments on v5:
1) Deletion of below empty new line is spurious:
-
/*
* Common option parsing function for CREATE and ALTER SUBSCRIPTION commands.
*
2) I think we could just do as below to save indentation of the code
for validate_publication == true.
static void
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
+{
+ char *err;
+
+ if (validate_pulication == false )
+ return;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
3) To be consistent, either we pass in validate_publication to both
connect_and_check_pubs and check_publications, return immediately from
them if it is false or do the checks outside. I suggest to pass in the
bool parameter to check_publications like you did for
connect_and_check_pubs. Or remove validate_publication from
connect_and_check_pubs and do the check outside.
+ if (validate_publication)
+ check_publications(wrconn, publications);
+ if (check_pub)
+ check_publications(wrconn, sub->publications);
4) Below line of code is above 80-char limit:
+ else if (strcmp(defel->defname, "validate_publication") == 0
&& validate_publication)
5) Instead of adding a new file 021_validate_publications.pl for
tests, spawning a new test database which would make the overall
regression slower, can't we add with the existing database nodes in
0001_rep_changes.pl? I would suggest adding the tests in there even if
the number of tests are many, I don't mind.
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Sat, May 1, 2021 at 7:58 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
On Sat, May 1, 2021 at 12:49 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, Attached patch has the fixes for the same.
Thoughts?Few more comments on v5:
1) Deletion of below empty new line is spurious:
-
/*
* Common option parsing function for CREATE and ALTER SUBSCRIPTION commands.
*
Modified.
2) I think we could just do as below to save indentation of the code for validate_publication == true. static void +connect_and_check_pubs(Subscription *sub, List *publications, + bool validate_publication) +{ + char *err; + + if (validate_pulication == false ) + return; + + /* Load the library providing us libpq calls. */ + load_file("libpqwalreceiver", false);
Modified.
3) To be consistent, either we pass in validate_publication to both connect_and_check_pubs and check_publications, return immediately from them if it is false or do the checks outside. I suggest to pass in the bool parameter to check_publications like you did for connect_and_check_pubs. Or remove validate_publication from connect_and_check_pubs and do the check outside. + if (validate_publication) + check_publications(wrconn, publications); + if (check_pub) + check_publications(wrconn, sub->publications);
Modified.
4) Below line of code is above 80-char limit:
+ else if (strcmp(defel->defname, "validate_publication") == 0
&& validate_publication)
Modified
5) Instead of adding a new file 021_validate_publications.pl for
tests, spawning a new test database which would make the overall
regression slower, can't we add with the existing database nodes in
0001_rep_changes.pl? I would suggest adding the tests in there even if
the number of tests are many, I don't mind.
001_rep_changes.pl has the changes mainly for checking the replicated
data. I did not find an appropriate file in the current tap tests, I
preferred these tests to be in a separate file. Thoughts?
Thanks for the comments.
The Attached patch has the fixes for the same.
Regards,
Vignesh
Attachments:
v6-0001-Identify-missing-publications-from-publisher-whil.patchtext/x-patch; charset=US-ASCII; name=v6-0001-Identify-missing-publications-from-publisher-whil.patchDownload
From 89a1e47c8be015fac520d2f94d16f86bf78a481c Mon Sep 17 00:00:00 2001
From: vignesh <vignesh21@gmail.com>
Date: Wed, 7 Apr 2021 22:05:53 +0530
Subject: [PATCH v6] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 13 +
doc/src/sgml/ref/create_subscription.sgml | 13 +
src/backend/commands/subscriptioncmds.c | 232 +++++++++++++++---
src/bin/psql/tab-complete.c | 7 +-
.../t/021_validate_publications.pl | 82 +++++++
5 files changed, 315 insertions(+), 32 deletions(-)
create mode 100644 src/test/subscription/t/021_validate_publications.pl
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 367ac814f4..81e156437b 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -160,6 +160,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index e812beee37..2f1f541253 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -239,6 +239,19 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 517c8edd3b..d731ba3e14 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -69,7 +69,9 @@ parse_subscription_options(List *options,
char **synchronous_commit,
bool *refresh,
bool *binary_given, bool *binary,
- bool *streaming_given, bool *streaming)
+ bool *streaming_given, bool *streaming,
+ bool *validate_publication_given,
+ bool *validate_publication)
{
ListCell *lc;
bool connect_given = false;
@@ -111,6 +113,12 @@ parse_subscription_options(List *options,
*streaming = false;
}
+ if (validate_publication)
+ {
+ *validate_publication_given = false;
+ *validate_publication = false;
+ }
+
/* Parse options */
foreach(lc, options)
{
@@ -215,6 +223,17 @@ parse_subscription_options(List *options,
*streaming_given = true;
*streaming = defGetBoolean(defel);
}
+ else if (strcmp(defel->defname, "validate_publication") == 0 &&
+ validate_publication)
+ {
+ if (*validate_publication_given)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+
+ *validate_publication_given = true;
+ *validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -247,10 +266,18 @@ parse_subscription_options(List *options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (validate_publication && validate_publication_given &&
+ *validate_publication)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
*enabled = false;
*create_slot = false;
*copy_data = false;
+ *validate_publication = false;
}
/*
@@ -287,6 +314,139 @@ parse_subscription_options(List *options,
}
}
+/*
+ * Append the list of publication to dest string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoString(dest, "\"");
+ appendStringInfoString(dest, pubname);
+ appendStringInfoString(dest, "\"");
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications,
+ bool validate_publication)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ if (validate_publication == false)
+ return;
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg("could not receive list of publications from the publisher: %s",
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publications. */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publications for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ (errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data)));
+ }
+}
+
+/*
+ * Connect to the publisher and check if the publications exist.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
+{
+ char *err;
+
+ if (validate_publication == false)
+ return;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ /* Verify specified publications exist in the publisher. */
+ PG_TRY();
+ {
+ check_publications(wrconn, publications, true);
+ }
+ PG_FINALLY();
+ {
+ /* We are done with the remote side, close connection. */
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -343,6 +503,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
bool slotname_given;
bool binary;
bool binary_given;
+ bool validate_publication;
+ bool validate_publication_given;
char originname[NAMEDATALEN];
bool create_slot;
List *publications;
@@ -361,7 +523,9 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ &validate_publication_given,
+ &validate_publication);
/*
* Since creating a replication slot is not transactional, rolling back
@@ -472,6 +636,9 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
PG_TRY();
{
+ /* Verify specified publications exist in the publisher. */
+ check_publications(wrconn, publications, validate_publication);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -539,7 +706,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -568,6 +736,9 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
ereport(ERROR,
(errmsg("could not connect to the publisher: %s", err)));
+ /* Verify specified publications exist in the publisher. */
+ check_publications(wrconn, sub->publications, validate_publication);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -814,7 +985,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ NULL, NULL);
if (slotname_given)
{
@@ -871,7 +1043,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no streaming */
+ NULL, NULL, /* no streaming */
+ NULL, NULL);
Assert(enabled_given);
if (!sub->slotname && enabled)
@@ -906,6 +1079,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
{
bool copy_data;
bool refresh;
+ bool validate_publication;
+ bool validate_publication_given;
parse_subscription_options(stmt->options,
NULL, /* no "connect" */
@@ -916,12 +1091,15 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(stmt->publication);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -937,7 +1115,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -950,6 +1128,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
bool copy_data;
bool refresh;
List *publist;
+ bool validate_publication;
+ bool validate_publication_given;
publist = merge_publications(sub->publications, stmt->publication, isadd, stmt->subname);
@@ -963,13 +1143,18 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ /* for drop, no "validate_publication" */
+ isadd ? &validate_publication_given : NULL,
+ isadd ? &validate_publication : NULL);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(publist);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd)
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -985,7 +1170,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Only refresh the added/dropped list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -994,6 +1179,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
case ALTER_SUBSCRIPTION_REFRESH:
{
bool copy_data;
+ bool validate_publication;
+ bool validate_publication_given;
if (!sub->enabled)
ereport(ERROR,
@@ -1009,11 +1196,13 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, validate_publication);
break;
}
@@ -1476,28 +1665,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 7c4933333b..5a0d0f3c8d 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1674,7 +1674,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1693,7 +1693,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> ADD|DROP|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny, "WITH", "("))
- COMPLETE_WITH("copy_data", "refresh");
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
/* ALTER SCHEMA <name> */
else if (Matches("ALTER", "SCHEMA", MatchAny))
@@ -2780,7 +2780,8 @@ psql_completion(const char *text, int start, int end)
/* Complete "CREATE SUBSCRIPTION <name> ... WITH ( <opt>" */
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("copy_data", "connect", "create_slot", "enabled",
- "slot_name", "synchronous_commit");
+ "slot_name", "synchronous_commit",
+ "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/021_validate_publications.pl b/src/test/subscription/t/021_validate_publications.pl
new file mode 100644
index 0000000000..95676148da
--- /dev/null
+++ b/src/test/subscription/t/021_validate_publications.pl
@@ -0,0 +1,82 @@
+# Tests for various bugs found over time
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 7;
+
+# Create subscription for a publication which does not exist.
+my $node_publisher = get_new_node('testpublisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+my $node_subscriber = get_new_node('testsubscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+
+$node_publisher->safe_psql('postgres',
+ "CREATE PUBLICATION testpub1 FOR ALL TABLES");
+
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION testsub1 CONNECTION '$publisher_connstr' PUBLICATION testpub1"
+);
+
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION pub_doesnt_exist WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION testpub1, pub_doesnt_exist WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+# Multiple publications does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION pub_doesnt_exist, pub_doesnt_exist1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publications "pub_doesnt_exist", "pub_doesnt_exist1" do not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Add non existent publication.
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 ADD PUBLICATION pub_doesnt_exist WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
+ "Alter subscription add non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist WITH (VALIDATE_PUBLICATION = TRUE)");
+ok( $stderr =~
+ /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION testsub1 SET PUBLICATION pub_doesnt_exist WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ /ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
+$node_publisher->stop('fast');
+$node_subscriber->stop('fast');
--
2.25.1
On Sun, May 2, 2021 at 10:04 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments.
The Attached patch has the fixes for the same.
I was reviewing the documentation part, I think in the below paragraph
we should include validate_publication as well?
<varlistentry>
<term><literal>connect</literal> (<type>boolean</type>)</term>
<listitem>
<para>
Specifies whether the <command>CREATE SUBSCRIPTION</command>
should connect to the publisher at all. Setting this to
<literal>false</literal> will change default values of
<literal>enabled</literal>, <literal>create_slot</literal> and
<literal>copy_data</literal> to <literal>false</literal>.
</para>
I will review/test the other parts of the patch and let you know.
--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com
On Sun, May 2, 2021 at 10:04 PM vignesh C <vignesh21@gmail.com> wrote:
5) Instead of adding a new file 021_validate_publications.pl for
tests, spawning a new test database which would make the overall
regression slower, can't we add with the existing database nodes in
0001_rep_changes.pl? I would suggest adding the tests in there even if
the number of tests are many, I don't mind.001_rep_changes.pl has the changes mainly for checking the replicated
data. I did not find an appropriate file in the current tap tests, I
preferred these tests to be in a separate file. Thoughts?
If 001_rep_changes.pl is not the right place, how about adding them
into 007_ddl.pl? That file seems to be only for DDL changes, and since
the feature tests cases are for CREATE/ALTER SUBSCRIPTION, it's the
right place. I strongly feel that we don't need a new file for these
tests.
Comment on the tests:
1) Instead of "pub_doesnt_exist" name, how about "non_existent_pub" or
just pub_non_existent" or some other?
+ "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr'
PUBLICATION pub_doesnt_exist WITH (VALIDATE_PUBLICATION = TRUE)"
The error message with this name looks a bit odd to me.
+ /ERROR: publication "pub_doesnt_exist" does not exist in
the publisher/,
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Mon, May 3, 2021 at 10:48 AM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Sun, May 2, 2021 at 10:04 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments.
The Attached patch has the fixes for the same.I was reviewing the documentation part, I think in the below paragraph
we should include validate_publication as well?<varlistentry>
<term><literal>connect</literal> (<type>boolean</type>)</term>
<listitem>
<para>
Specifies whether the <command>CREATE SUBSCRIPTION</command>
should connect to the publisher at all. Setting this to
<literal>false</literal> will change default values of
<literal>enabled</literal>, <literal>create_slot</literal> and
<literal>copy_data</literal> to <literal>false</literal>.
</para>I will review/test the other parts of the patch and let you know.
I have reviewed it and it mostly looks good to me. I have some minor
suggestions though.
1.
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
vs
+
+/*
+ * Connect to the publisher and check if the publications exist.
+ */
I think the formatting of the comments are not uniform. Some places
we are using "publication(s) is(are)" whereas other places are just
"publications".
2. Add a error case for connect=false and VALIDATE_PUBLICATION = true
--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com
On Mon, May 3, 2021 at 1:46 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Mon, May 3, 2021 at 10:48 AM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Sun, May 2, 2021 at 10:04 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments.
The Attached patch has the fixes for the same.I was reviewing the documentation part, I think in the below paragraph
we should include validate_publication as well?<varlistentry>
<term><literal>connect</literal> (<type>boolean</type>)</term>
<listitem>
<para>
Specifies whether the <command>CREATE SUBSCRIPTION</command>
should connect to the publisher at all. Setting this to
<literal>false</literal> will change default values of
<literal>enabled</literal>, <literal>create_slot</literal> and
<literal>copy_data</literal> to <literal>false</literal>.
</para>
Modified.
I will review/test the other parts of the patch and let you know.
I have reviewed it and it mostly looks good to me. I have some minor
suggestions though.1. +/* + * Check the specified publication(s) is(are) present in the publisher. + */vs
+ +/* + * Connect to the publisher and check if the publications exist. + */I think the formatting of the comments are not uniform. Some places
we are using "publication(s) is(are)" whereas other places are just
"publications".
Modified.
2. Add a error case for connect=false and VALIDATE_PUBLICATION = true
Added.
Thanks for the comments, attached v7 patch has the fixes for the same.
Thoughts?
Regards,
Vignesh
Attachments:
v7-0001-Identify-missing-publications-from-publisher-whil.patchtext/x-patch; charset=US-ASCII; name=v7-0001-Identify-missing-publications-from-publisher-whil.patchDownload
From 09ae1cac60320bde08a6c380d3637eecdbb3d6ff Mon Sep 17 00:00:00 2001
From: vignesh <vignesh21@gmail.com>
Date: Wed, 7 Apr 2021 22:05:53 +0530
Subject: [PATCH v7] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 13 ++
doc/src/sgml/ref/create_subscription.sgml | 18 +-
src/backend/commands/subscriptioncmds.c | 234 +++++++++++++++++++---
src/bin/psql/tab-complete.c | 7 +-
src/test/subscription/t/007_ddl.pl | 68 ++++++-
5 files changed, 305 insertions(+), 35 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 367ac814f4..81e156437b 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -160,6 +160,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index e812beee37..cad9285c16 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -207,8 +207,9 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
Specifies whether the <command>CREATE SUBSCRIPTION</command>
should connect to the publisher at all. Setting this to
<literal>false</literal> will change default values of
- <literal>enabled</literal>, <literal>create_slot</literal> and
- <literal>copy_data</literal> to <literal>false</literal>.
+ <literal>enabled</literal>, <literal>create_slot</literal>,
+ <literal>copy_data</literal> and
+ <literal>validate_publication</literal> to <literal>false</literal>.
</para>
<para>
@@ -239,6 +240,19 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 517c8edd3b..3de4488e4d 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -69,7 +69,9 @@ parse_subscription_options(List *options,
char **synchronous_commit,
bool *refresh,
bool *binary_given, bool *binary,
- bool *streaming_given, bool *streaming)
+ bool *streaming_given, bool *streaming,
+ bool *validate_publication_given,
+ bool *validate_publication)
{
ListCell *lc;
bool connect_given = false;
@@ -111,6 +113,12 @@ parse_subscription_options(List *options,
*streaming = false;
}
+ if (validate_publication)
+ {
+ *validate_publication_given = false;
+ *validate_publication = false;
+ }
+
/* Parse options */
foreach(lc, options)
{
@@ -215,6 +223,17 @@ parse_subscription_options(List *options,
*streaming_given = true;
*streaming = defGetBoolean(defel);
}
+ else if (strcmp(defel->defname, "validate_publication") == 0 &&
+ validate_publication)
+ {
+ if (*validate_publication_given)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+
+ *validate_publication_given = true;
+ *validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -247,10 +266,18 @@ parse_subscription_options(List *options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (validate_publication && validate_publication_given &&
+ *validate_publication)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
*enabled = false;
*create_slot = false;
*copy_data = false;
+ *validate_publication = false;
}
/*
@@ -287,6 +314,141 @@ parse_subscription_options(List *options,
}
}
+/*
+ * Append the list of publication to dest string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoString(dest, "\"");
+ appendStringInfoString(dest, pubname);
+ appendStringInfoString(dest, "\"");
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications,
+ bool validate_publication)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ if (validate_publication == false)
+ return;
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ (errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data)));
+ }
+}
+
+/*
+ * Connect to the publisher and check if the publication(s) exist.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
+{
+ char *err;
+
+ if (validate_publication == false)
+ return;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ /* Verify specified publication(s) exist in the publisher. */
+ PG_TRY();
+ {
+ check_publications(wrconn, publications, true);
+ }
+ PG_FINALLY();
+ {
+ /* We are done with the remote side, close connection. */
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -343,6 +505,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
bool slotname_given;
bool binary;
bool binary_given;
+ bool validate_publication;
+ bool validate_publication_given;
char originname[NAMEDATALEN];
bool create_slot;
List *publications;
@@ -361,7 +525,9 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ &validate_publication_given,
+ &validate_publication);
/*
* Since creating a replication slot is not transactional, rolling back
@@ -472,6 +638,9 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
PG_TRY();
{
+ /* Verify specified publication(s) exist in the publisher. */
+ check_publications(wrconn, publications, validate_publication);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -539,7 +708,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -568,6 +738,9 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
ereport(ERROR,
(errmsg("could not connect to the publisher: %s", err)));
+ /* Verify specified publication(s) exist in the publisher. */
+ check_publications(wrconn, sub->publications, validate_publication);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -814,7 +987,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ NULL, NULL);
if (slotname_given)
{
@@ -871,7 +1045,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no streaming */
+ NULL, NULL, /* no streaming */
+ NULL, NULL);
Assert(enabled_given);
if (!sub->slotname && enabled)
@@ -906,6 +1081,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
{
bool copy_data;
bool refresh;
+ bool validate_publication;
+ bool validate_publication_given;
parse_subscription_options(stmt->options,
NULL, /* no "connect" */
@@ -916,12 +1093,15 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(stmt->publication);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -937,7 +1117,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -950,6 +1130,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
bool copy_data;
bool refresh;
List *publist;
+ bool validate_publication;
+ bool validate_publication_given;
publist = merge_publications(sub->publications, stmt->publication, isadd, stmt->subname);
@@ -963,13 +1145,18 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ /* for drop, no "validate_publication" */
+ isadd ? &validate_publication_given : NULL,
+ isadd ? &validate_publication : NULL);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(publist);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd)
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -985,7 +1172,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Only refresh the added/dropped list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -994,6 +1181,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
case ALTER_SUBSCRIPTION_REFRESH:
{
bool copy_data;
+ bool validate_publication;
+ bool validate_publication_given;
if (!sub->enabled)
ereport(ERROR,
@@ -1009,11 +1198,13 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, validate_publication);
break;
}
@@ -1476,28 +1667,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 7c4933333b..5a0d0f3c8d 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1674,7 +1674,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1693,7 +1693,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> ADD|DROP|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny, "WITH", "("))
- COMPLETE_WITH("copy_data", "refresh");
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
/* ALTER SCHEMA <name> */
else if (Matches("ALTER", "SCHEMA", MatchAny))
@@ -2780,7 +2780,8 @@ psql_completion(const char *text, int start, int end)
/* Complete "CREATE SUBSCRIPTION <name> ... WITH ( <opt>" */
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("copy_data", "connect", "create_slot", "enabled",
- "slot_name", "synchronous_commit");
+ "slot_name", "synchronous_commit",
+ "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index 7fe6cc6d63..61f64f4629 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -3,7 +3,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 1;
+use Test::More tests => 9;
my $node_publisher = get_new_node('publisher');
$node_publisher->init(allows_streaming => 'logical');
@@ -38,5 +38,71 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+# Multiple publications does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Create subscription with mutually exclusive options connect as false and validate_publication as true.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (CONNECT = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: connect = false and validate_publication = true are mutually exclusive options/,
+ "Create subscription with connect=false and validate_publication=true should fail");
+
+# Add non existent publication.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)");
+ok( $stderr =~
+ /ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ /ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ /ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.25.1
On Mon, May 3, 2021 at 11:11 AM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
On Sun, May 2, 2021 at 10:04 PM vignesh C <vignesh21@gmail.com> wrote:
5) Instead of adding a new file 021_validate_publications.pl for
tests, spawning a new test database which would make the overall
regression slower, can't we add with the existing database nodes in
0001_rep_changes.pl? I would suggest adding the tests in there even if
the number of tests are many, I don't mind.001_rep_changes.pl has the changes mainly for checking the replicated
data. I did not find an appropriate file in the current tap tests, I
preferred these tests to be in a separate file. Thoughts?If 001_rep_changes.pl is not the right place, how about adding them
into 007_ddl.pl? That file seems to be only for DDL changes, and since
the feature tests cases are for CREATE/ALTER SUBSCRIPTION, it's the
right place. I strongly feel that we don't need a new file for these
tests.
Modified.
Comment on the tests: 1) Instead of "pub_doesnt_exist" name, how about "non_existent_pub" or just pub_non_existent" or some other? + "CREATE SUBSCRIPTION testsub2 CONNECTION '$publisher_connstr' PUBLICATION pub_doesnt_exist WITH (VALIDATE_PUBLICATION = TRUE)" The error message with this name looks a bit odd to me. + /ERROR: publication "pub_doesnt_exist" does not exist in the publisher/,
Modified.
Thanks for the comments, these comments are handle in the v7 patch
posted in my earlier mail.
Regards,
Vignesh
On Mon, May 3, 2021 at 7:59 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, these comments are handle in the v7 patch
posted in my earlier mail.
Thanks. Some comments on v7 patch:
1) How about "Add publication names from the list to a string."
instead of
* Append the list of publication to dest string.
2) How about "Connect to the publisher and see if the given
publication(s) is(are) present."
instead of
* Connect to the publisher and check if the publication(s) exist.
3) Below comments are unnecessary as the functions/code following them
will tell what the code does.
/* Verify specified publication(s) exist in the publisher. */
/* We are done with the remote side, close connection. */
/* Verify specified publication(s) exist in the publisher. */
PG_TRY();
{
check_publications(wrconn, publications, true);
}
PG_FINALLY();
{
/* We are done with the remote side, close connection. */
walrcv_disconnect(wrconn);
}
4) And also the comment below that's there before check_publications
is unnecessary, as the function name and description would say it all.
/* Verify specified publication(s) exist in the publisher. */
5) A typo - it is "do not exist"
# Multiple publications does not exist.
6) Should we use "m" specified in all the test cases something like we
do for $stderr =~ m/threads are not supported on this platform/ or
m/replication slot "test_slot" was not created in this database/?
$stderr =~
/ERROR: publication "non_existent_pub" does not exist in the
publisher/,
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Tue, May 4, 2021 at 2:37 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
On Mon, May 3, 2021 at 7:59 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, these comments are handle in the v7 patch
posted in my earlier mail.Thanks. Some comments on v7 patch:
1) How about "Add publication names from the list to a string."
instead of
* Append the list of publication to dest string.
Modified.
2) How about "Connect to the publisher and see if the given
publication(s) is(are) present."
instead of
* Connect to the publisher and check if the publication(s) exist.
Modified.
3) Below comments are unnecessary as the functions/code following them
will tell what the code does.
/* Verify specified publication(s) exist in the publisher. */
/* We are done with the remote side, close connection. *//* Verify specified publication(s) exist in the publisher. */
PG_TRY();
{
check_publications(wrconn, publications, true);
}
PG_FINALLY();
{
/* We are done with the remote side, close connection. */
walrcv_disconnect(wrconn);
}
Modified.
4) And also the comment below that's there before check_publications
is unnecessary, as the function name and description would say it all.
/* Verify specified publication(s) exist in the publisher. */
Modified.
5) A typo - it is "do not exist"
# Multiple publications does not exist.
Modified.
6) Should we use "m" specified in all the test cases something like we
do for $stderr =~ m/threads are not supported on this platform/ or
m/replication slot "test_slot" was not created in this database/?
$stderr =~
/ERROR: publication "non_existent_pub" does not exist in the
publisher/,
Modified.
Thanks for the comments, Attached patch has the fixes for the same.
Regards,
Vignesh
Attachments:
v8-0001-Identify-missing-publications-from-publisher-whil.patchtext/x-patch; charset=US-ASCII; name=v8-0001-Identify-missing-publications-from-publisher-whil.patchDownload
From c16bb01580c789bd0c099badb23f45845f9449eb Mon Sep 17 00:00:00 2001
From: vignesh <vignesh21@gmail.com>
Date: Wed, 7 Apr 2021 22:05:53 +0530
Subject: [PATCH v8] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 13 ++
doc/src/sgml/ref/create_subscription.sgml | 18 +-
src/backend/commands/subscriptioncmds.c | 230 +++++++++++++++++++---
src/bin/psql/tab-complete.c | 7 +-
src/test/subscription/t/007_ddl.pl | 68 ++++++-
5 files changed, 301 insertions(+), 35 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 367ac814f4..81e156437b 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -160,6 +160,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index e812beee37..cad9285c16 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -207,8 +207,9 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
Specifies whether the <command>CREATE SUBSCRIPTION</command>
should connect to the publisher at all. Setting this to
<literal>false</literal> will change default values of
- <literal>enabled</literal>, <literal>create_slot</literal> and
- <literal>copy_data</literal> to <literal>false</literal>.
+ <literal>enabled</literal>, <literal>create_slot</literal>,
+ <literal>copy_data</literal> and
+ <literal>validate_publication</literal> to <literal>false</literal>.
</para>
<para>
@@ -239,6 +240,19 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 517c8edd3b..2e64e042d9 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -69,7 +69,9 @@ parse_subscription_options(List *options,
char **synchronous_commit,
bool *refresh,
bool *binary_given, bool *binary,
- bool *streaming_given, bool *streaming)
+ bool *streaming_given, bool *streaming,
+ bool *validate_publication_given,
+ bool *validate_publication)
{
ListCell *lc;
bool connect_given = false;
@@ -111,6 +113,12 @@ parse_subscription_options(List *options,
*streaming = false;
}
+ if (validate_publication)
+ {
+ *validate_publication_given = false;
+ *validate_publication = false;
+ }
+
/* Parse options */
foreach(lc, options)
{
@@ -215,6 +223,17 @@ parse_subscription_options(List *options,
*streaming_given = true;
*streaming = defGetBoolean(defel);
}
+ else if (strcmp(defel->defname, "validate_publication") == 0 &&
+ validate_publication)
+ {
+ if (*validate_publication_given)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+
+ *validate_publication_given = true;
+ *validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -247,10 +266,18 @@ parse_subscription_options(List *options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (validate_publication && validate_publication_given &&
+ *validate_publication)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
*enabled = false;
*create_slot = false;
*copy_data = false;
+ *validate_publication = false;
}
/*
@@ -287,6 +314,139 @@ parse_subscription_options(List *options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoString(dest, "\"");
+ appendStringInfoString(dest, pubname);
+ appendStringInfoString(dest, "\"");
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications,
+ bool validate_publication)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ if (validate_publication == false)
+ return;
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ (errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data)));
+ }
+}
+
+/*
+ * Connect to the publisher and see if the given publication(s) is(are) present.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
+{
+ char *err;
+
+ if (validate_publication == false)
+ return;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ PG_TRY();
+ {
+ check_publications(wrconn, publications, true);
+ }
+ PG_FINALLY();
+ {
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -343,6 +503,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
bool slotname_given;
bool binary;
bool binary_given;
+ bool validate_publication;
+ bool validate_publication_given;
char originname[NAMEDATALEN];
bool create_slot;
List *publications;
@@ -361,7 +523,9 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ &validate_publication_given,
+ &validate_publication);
/*
* Since creating a replication slot is not transactional, rolling back
@@ -472,6 +636,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
PG_TRY();
{
+ check_publications(wrconn, publications, validate_publication);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -539,7 +705,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -568,6 +735,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
ereport(ERROR,
(errmsg("could not connect to the publisher: %s", err)));
+ check_publications(wrconn, sub->publications, validate_publication);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -814,7 +983,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ NULL, NULL);
if (slotname_given)
{
@@ -871,7 +1041,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no streaming */
+ NULL, NULL, /* no streaming */
+ NULL, NULL);
Assert(enabled_given);
if (!sub->slotname && enabled)
@@ -906,6 +1077,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
{
bool copy_data;
bool refresh;
+ bool validate_publication;
+ bool validate_publication_given;
parse_subscription_options(stmt->options,
NULL, /* no "connect" */
@@ -916,12 +1089,15 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(stmt->publication);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -937,7 +1113,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -950,6 +1126,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
bool copy_data;
bool refresh;
List *publist;
+ bool validate_publication;
+ bool validate_publication_given;
publist = merge_publications(sub->publications, stmt->publication, isadd, stmt->subname);
@@ -963,13 +1141,18 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ /* for drop, no "validate_publication" */
+ isadd ? &validate_publication_given : NULL,
+ isadd ? &validate_publication : NULL);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(publist);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd)
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -985,7 +1168,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Only refresh the added/dropped list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -994,6 +1177,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
case ALTER_SUBSCRIPTION_REFRESH:
{
bool copy_data;
+ bool validate_publication;
+ bool validate_publication_given;
if (!sub->enabled)
ereport(ERROR,
@@ -1009,11 +1194,13 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, validate_publication);
break;
}
@@ -1476,28 +1663,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 7c4933333b..5a0d0f3c8d 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1674,7 +1674,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1693,7 +1693,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> ADD|DROP|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny, "WITH", "("))
- COMPLETE_WITH("copy_data", "refresh");
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
/* ALTER SCHEMA <name> */
else if (Matches("ALTER", "SCHEMA", MatchAny))
@@ -2780,7 +2780,8 @@ psql_completion(const char *text, int start, int end)
/* Complete "CREATE SUBSCRIPTION <name> ... WITH ( <opt>" */
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("copy_data", "connect", "create_slot", "enabled",
- "slot_name", "synchronous_commit");
+ "slot_name", "synchronous_commit",
+ "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index 7fe6cc6d63..3292e38744 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -3,7 +3,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 1;
+use Test::More tests => 9;
my $node_publisher = get_new_node('publisher');
$node_publisher->init(allows_streaming => 'logical');
@@ -38,5 +38,71 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+# Multiple publications do not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Create subscription with mutually exclusive options connect as false and validate_publication as true.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (CONNECT = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: connect = false and validate_publication = true are mutually exclusive options/,
+ "Create subscription with connect=false and validate_publication=true should fail");
+
+# Add non existent publication.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)");
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ m/ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.25.1
On Tue, May 4, 2021 at 6:50 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, Attached patch has the fixes for the same.
Thanks! I took a final look over the v8 patch, it looks good to me and
regression tests were passed with it. I have no further comments at
this moment. I will make it "ready for committer" if others have no
comments.
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Tue, 04 May 2021 at 21:20, vignesh C <vignesh21@gmail.com> wrote:
On Tue, May 4, 2021 at 2:37 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:On Mon, May 3, 2021 at 7:59 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, these comments are handle in the v7 patch
posted in my earlier mail.Thanks. Some comments on v7 patch:
1) How about "Add publication names from the list to a string."
instead of
* Append the list of publication to dest string.Modified.
2) How about "Connect to the publisher and see if the given
publication(s) is(are) present."
instead of
* Connect to the publisher and check if the publication(s) exist.Modified.
3) Below comments are unnecessary as the functions/code following them
will tell what the code does.
/* Verify specified publication(s) exist in the publisher. */
/* We are done with the remote side, close connection. *//* Verify specified publication(s) exist in the publisher. */
PG_TRY();
{
check_publications(wrconn, publications, true);
}
PG_FINALLY();
{
/* We are done with the remote side, close connection. */
walrcv_disconnect(wrconn);
}Modified.
4) And also the comment below that's there before check_publications
is unnecessary, as the function name and description would say it all.
/* Verify specified publication(s) exist in the publisher. */Modified.
5) A typo - it is "do not exist"
# Multiple publications does not exist.Modified.
6) Should we use "m" specified in all the test cases something like we
do for $stderr =~ m/threads are not supported on this platform/ or
m/replication slot "test_slot" was not created in this database/?
$stderr =~
/ERROR: publication "non_existent_pub" does not exist in the
publisher/,Modified.
Thanks for the comments, Attached patch has the fixes for the same.
Thanks for updating the patch. Some comments on v8 patch.
1) How about use appendStringInfoChar() to replace the first and last one,
since it more faster.
+ appendStringInfoString(dest, "\"");
+ appendStringInfoString(dest, pubname);
+ appendStringInfoString(dest, "\"");
2) How about use if (!validate_publication) to keep the code style consistent?
+ if (validate_publication == false)
+ return;
3) Should we free the memory when finish the check_publications()?
+ publicationsCopy = list_copy(publications);
4) It is better wrap the word "streaming" with quote. Also, should we add
'no "validate_publication"' comment for validate_publication parameters?
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no streaming */
+ NULL, NULL, /* no streaming */
+ NULL, NULL);
--
Regrads,
Japin Li.
ChengDu WenWu Information Technology Co.,Ltd.
On Thu, May 6, 2021 at 9:08 AM Japin Li <japinli@hotmail.com> wrote:
On Tue, 04 May 2021 at 21:20, vignesh C <vignesh21@gmail.com> wrote:
On Tue, May 4, 2021 at 2:37 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:On Mon, May 3, 2021 at 7:59 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, these comments are handle in the v7 patch
posted in my earlier mail.Thanks. Some comments on v7 patch:
1) How about "Add publication names from the list to a string."
instead of
* Append the list of publication to dest string.Modified.
2) How about "Connect to the publisher and see if the given
publication(s) is(are) present."
instead of
* Connect to the publisher and check if the publication(s) exist.Modified.
3) Below comments are unnecessary as the functions/code following them
will tell what the code does.
/* Verify specified publication(s) exist in the publisher. */
/* We are done with the remote side, close connection. *//* Verify specified publication(s) exist in the publisher. */
PG_TRY();
{
check_publications(wrconn, publications, true);
}
PG_FINALLY();
{
/* We are done with the remote side, close connection. */
walrcv_disconnect(wrconn);
}Modified.
4) And also the comment below that's there before check_publications
is unnecessary, as the function name and description would say it all.
/* Verify specified publication(s) exist in the publisher. */Modified.
5) A typo - it is "do not exist"
# Multiple publications does not exist.Modified.
6) Should we use "m" specified in all the test cases something like we
do for $stderr =~ m/threads are not supported on this platform/ or
m/replication slot "test_slot" was not created in this database/?
$stderr =~
/ERROR: publication "non_existent_pub" does not exist in the
publisher/,Modified.
Thanks for the comments, Attached patch has the fixes for the same.
Thanks for updating the patch. Some comments on v8 patch.
1) How about use appendStringInfoChar() to replace the first and last one, since it more faster. + appendStringInfoString(dest, "\""); + appendStringInfoString(dest, pubname); + appendStringInfoString(dest, "\"");
Modified.
2) How about use if (!validate_publication) to keep the code style consistent? + if (validate_publication == false) + return;
Modified.
3) Should we free the memory when finish the check_publications()?
+ publicationsCopy = list_copy(publications);
I felt this list entries will be deleted in the success case, in error
case I felt no need to delete it as we will be exiting. Thoughts?
4) It is better wrap the word "streaming" with quote. Also, should we add 'no "validate_publication"' comment for validate_publication parameters? NULL, NULL, /* no "binary" */ - NULL, NULL); /* no streaming */ + NULL, NULL, /* no streaming */ + NULL, NULL);
Modified.
Thanks for the comments, attached v9 patch has the fixes for the same.
Regards,
Vignesh
Attachments:
v9-0001-Identify-missing-publications-from-publisher-whil.patchtext/x-patch; charset=US-ASCII; name=v9-0001-Identify-missing-publications-from-publisher-whil.patchDownload
From e34353210e22f1c61c13f43c24b263cd88d9fc3e Mon Sep 17 00:00:00 2001
From: vignesh <vignesh21@gmail.com>
Date: Wed, 7 Apr 2021 22:05:53 +0530
Subject: [PATCH v9] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 13 ++
doc/src/sgml/ref/create_subscription.sgml | 18 +-
src/backend/commands/subscriptioncmds.c | 230 +++++++++++++++++++---
src/bin/psql/tab-complete.c | 7 +-
src/test/subscription/t/007_ddl.pl | 68 ++++++-
5 files changed, 301 insertions(+), 35 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 367ac814f4..81e156437b 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -160,6 +160,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index e812beee37..cad9285c16 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -207,8 +207,9 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
Specifies whether the <command>CREATE SUBSCRIPTION</command>
should connect to the publisher at all. Setting this to
<literal>false</literal> will change default values of
- <literal>enabled</literal>, <literal>create_slot</literal> and
- <literal>copy_data</literal> to <literal>false</literal>.
+ <literal>enabled</literal>, <literal>create_slot</literal>,
+ <literal>copy_data</literal> and
+ <literal>validate_publication</literal> to <literal>false</literal>.
</para>
<para>
@@ -239,6 +240,19 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 517c8edd3b..fd46f43798 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -69,7 +69,9 @@ parse_subscription_options(List *options,
char **synchronous_commit,
bool *refresh,
bool *binary_given, bool *binary,
- bool *streaming_given, bool *streaming)
+ bool *streaming_given, bool *streaming,
+ bool *validate_publication_given,
+ bool *validate_publication)
{
ListCell *lc;
bool connect_given = false;
@@ -111,6 +113,12 @@ parse_subscription_options(List *options,
*streaming = false;
}
+ if (validate_publication)
+ {
+ *validate_publication_given = false;
+ *validate_publication = false;
+ }
+
/* Parse options */
foreach(lc, options)
{
@@ -215,6 +223,17 @@ parse_subscription_options(List *options,
*streaming_given = true;
*streaming = defGetBoolean(defel);
}
+ else if (strcmp(defel->defname, "validate_publication") == 0 &&
+ validate_publication)
+ {
+ if (*validate_publication_given)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+
+ *validate_publication_given = true;
+ *validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -247,10 +266,18 @@ parse_subscription_options(List *options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (validate_publication && validate_publication_given &&
+ *validate_publication)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
*enabled = false;
*create_slot = false;
*copy_data = false;
+ *validate_publication = false;
}
/*
@@ -287,6 +314,139 @@ parse_subscription_options(List *options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications,
+ bool validate_publication)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ if (!validate_publication)
+ return;
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ (errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data)));
+ }
+}
+
+/*
+ * Connect to the publisher and see if the given publication(s) is(are) present.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
+{
+ char *err;
+
+ if (!validate_publication)
+ return;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ PG_TRY();
+ {
+ check_publications(wrconn, publications, true);
+ }
+ PG_FINALLY();
+ {
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -343,6 +503,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
bool slotname_given;
bool binary;
bool binary_given;
+ bool validate_publication;
+ bool validate_publication_given;
char originname[NAMEDATALEN];
bool create_slot;
List *publications;
@@ -361,7 +523,9 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ &validate_publication_given,
+ &validate_publication);
/*
* Since creating a replication slot is not transactional, rolling back
@@ -472,6 +636,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
PG_TRY();
{
+ check_publications(wrconn, publications, validate_publication);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -539,7 +705,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -568,6 +735,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
ereport(ERROR,
(errmsg("could not connect to the publisher: %s", err)));
+ check_publications(wrconn, sub->publications, validate_publication);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -814,7 +983,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ NULL, NULL); /* no "validate_publication" */
if (slotname_given)
{
@@ -871,7 +1041,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no streaming */
+ NULL, NULL, /* no "streaming" */
+ NULL, NULL); /* no "validate_publication" */
Assert(enabled_given);
if (!sub->slotname && enabled)
@@ -906,6 +1077,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
{
bool copy_data;
bool refresh;
+ bool validate_publication;
+ bool validate_publication_given;
parse_subscription_options(stmt->options,
NULL, /* no "connect" */
@@ -916,12 +1089,15 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(stmt->publication);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -937,7 +1113,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -950,6 +1126,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
bool copy_data;
bool refresh;
List *publist;
+ bool validate_publication;
+ bool validate_publication_given;
publist = merge_publications(sub->publications, stmt->publication, isadd, stmt->subname);
@@ -963,13 +1141,18 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ /* for drop, no "validate_publication" */
+ isadd ? &validate_publication_given : NULL,
+ isadd ? &validate_publication : NULL);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(publist);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd)
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -985,7 +1168,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Only refresh the added/dropped list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -994,6 +1177,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
case ALTER_SUBSCRIPTION_REFRESH:
{
bool copy_data;
+ bool validate_publication;
+ bool validate_publication_given;
if (!sub->enabled)
ereport(ERROR,
@@ -1009,11 +1194,13 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, validate_publication);
break;
}
@@ -1476,28 +1663,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 7c4933333b..5a0d0f3c8d 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1674,7 +1674,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1693,7 +1693,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> ADD|DROP|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny, "WITH", "("))
- COMPLETE_WITH("copy_data", "refresh");
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
/* ALTER SCHEMA <name> */
else if (Matches("ALTER", "SCHEMA", MatchAny))
@@ -2780,7 +2780,8 @@ psql_completion(const char *text, int start, int end)
/* Complete "CREATE SUBSCRIPTION <name> ... WITH ( <opt>" */
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("copy_data", "connect", "create_slot", "enabled",
- "slot_name", "synchronous_commit");
+ "slot_name", "synchronous_commit",
+ "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index 7fe6cc6d63..3292e38744 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -3,7 +3,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 1;
+use Test::More tests => 9;
my $node_publisher = get_new_node('publisher');
$node_publisher->init(allows_streaming => 'logical');
@@ -38,5 +38,71 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+# Multiple publications do not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Create subscription with mutually exclusive options connect as false and validate_publication as true.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (CONNECT = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: connect = false and validate_publication = true are mutually exclusive options/,
+ "Create subscription with connect=false and validate_publication=true should fail");
+
+# Add non existent publication.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)");
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ m/ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.25.1
On Thu, 06 May 2021 at 21:52, vignesh C <vignesh21@gmail.com> wrote:
On Thu, May 6, 2021 at 9:08 AM Japin Li <japinli@hotmail.com> wrote:
3) Should we free the memory when finish the check_publications()?
+ publicationsCopy = list_copy(publications);I felt this list entries will be deleted in the success case, in error
case I felt no need to delete it as we will be exiting. Thoughts?
Sorry for the noise! You are right. The v9 patch set looks good to me.
--
Regrads,
Japin Li.
ChengDu WenWu Information Technology Co.,Ltd.
On Thu, May 6, 2021 at 7:22 PM vignesh C <vignesh21@gmail.com> wrote:
Some comments:
1.
I don't see any change in pg_dump.c, don't we need to dump this option?
2.
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
Instead of using global wrconn, I think you should use a local variable?
--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com
On Fri, May 7, 2021 at 11:50 AM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Thu, May 6, 2021 at 7:22 PM vignesh C <vignesh21@gmail.com> wrote:
Some comments:
1.
I don't see any change in pg_dump.c, don't we need to dump this option?
I don't think it is necessary there as the default value of the
validate_publication is false, so even if the pg_dump has no mention
of the option, then it is assumed to be false while restoring. Note
that the validate_publication option is transient (like with other
options such as create_slot, copy_data) which means it can't be stored
in pg_subscritpion catalogue. Therefore, user specified value can't be
fetched once the CREATE/ALTER subscription command is finished. If we
were to dump the option, we should be storing it in the catalogue,
which I don't think is necessary. Thoughts?
2. + /* Try to connect to the publisher. */ + wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err); + if (!wrconn) + ereport(ERROR, + (errmsg("could not connect to the publisher: %s", err)));Instead of using global wrconn, I think you should use a local variable?
Yeah, we should be using local wrconn, otherwise there can be
consequences, see the patches at [1]/messages/by-id/CAHut+PuSwWmmeK+fe6E2duep8588Jk82XXH73nE4dUxwDNkNUg@mail.gmail.com. Thanks for pointing out this.
[1]: /messages/by-id/CAHut+PuSwWmmeK+fe6E2duep8588Jk82XXH73nE4dUxwDNkNUg@mail.gmail.com
With Regards,
Bharath Rupireddy.
EnterpriseDB: http://www.enterprisedb.com
On Fri, May 7, 2021 at 5:38 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
On Fri, May 7, 2021 at 11:50 AM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Thu, May 6, 2021 at 7:22 PM vignesh C <vignesh21@gmail.com> wrote:
Some comments:
1.
I don't see any change in pg_dump.c, don't we need to dump this option?I don't think it is necessary there as the default value of the
validate_publication is false, so even if the pg_dump has no mention
of the option, then it is assumed to be false while restoring. Note
that the validate_publication option is transient (like with other
options such as create_slot, copy_data) which means it can't be stored
in pg_subscritpion catalogue. Therefore, user specified value can't be
fetched once the CREATE/ALTER subscription command is finished. If we
were to dump the option, we should be storing it in the catalogue,
which I don't think is necessary. Thoughts?
If we are not storing it in the catalog then it does not need to be dumped.
2. + /* Try to connect to the publisher. */ + wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err); + if (!wrconn) + ereport(ERROR, + (errmsg("could not connect to the publisher: %s", err)));Instead of using global wrconn, I think you should use a local variable?
Yeah, we should be using local wrconn, otherwise there can be
consequences, see the patches at [1]. Thanks for pointing out this.
Right.
--
Regards,
Dilip Kumar
EnterpriseDB: http://www.enterprisedb.com
On Fri, May 7, 2021 at 5:44 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Fri, May 7, 2021 at 5:38 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:On Fri, May 7, 2021 at 11:50 AM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Thu, May 6, 2021 at 7:22 PM vignesh C <vignesh21@gmail.com> wrote:
Some comments:
1.
I don't see any change in pg_dump.c, don't we need to dump this option?I don't think it is necessary there as the default value of the
validate_publication is false, so even if the pg_dump has no mention
of the option, then it is assumed to be false while restoring. Note
that the validate_publication option is transient (like with other
options such as create_slot, copy_data) which means it can't be stored
in pg_subscritpion catalogue. Therefore, user specified value can't be
fetched once the CREATE/ALTER subscription command is finished. If we
were to dump the option, we should be storing it in the catalogue,
which I don't think is necessary. Thoughts?If we are not storing it in the catalog then it does not need to be dumped.
I intentionally did not store this value, I felt we need not persist
this option's value. This value will be false while dumping similar to
other non stored parameters.
2. + /* Try to connect to the publisher. */ + wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err); + if (!wrconn) + ereport(ERROR, + (errmsg("could not connect to the publisher: %s", err)));Instead of using global wrconn, I think you should use a local variable?
Yeah, we should be using local wrconn, otherwise there can be
consequences, see the patches at [1]. Thanks for pointing out this.
Modified.
Thanks for the comments, the attached patch has the fix for the same.
Regards,
Vignesh
Attachments:
v10-0001-Identify-missing-publications-from-publisher-whi.patchapplication/x-patch; name=v10-0001-Identify-missing-publications-from-publisher-whi.patchDownload
From c6e4df57824309644eb486296e69741404186b9e Mon Sep 17 00:00:00 2001
From: vignesh <vignesh21@gmail.com>
Date: Wed, 7 Apr 2021 22:05:53 +0530
Subject: [PATCH v10] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 13 ++
doc/src/sgml/ref/create_subscription.sgml | 18 +-
src/backend/commands/subscriptioncmds.c | 231 +++++++++++++++++++---
src/bin/psql/tab-complete.c | 7 +-
src/test/subscription/t/007_ddl.pl | 68 ++++++-
5 files changed, 302 insertions(+), 35 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 367ac814f4..81e156437b 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -160,6 +160,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index e812beee37..cad9285c16 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -207,8 +207,9 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
Specifies whether the <command>CREATE SUBSCRIPTION</command>
should connect to the publisher at all. Setting this to
<literal>false</literal> will change default values of
- <literal>enabled</literal>, <literal>create_slot</literal> and
- <literal>copy_data</literal> to <literal>false</literal>.
+ <literal>enabled</literal>, <literal>create_slot</literal>,
+ <literal>copy_data</literal> and
+ <literal>validate_publication</literal> to <literal>false</literal>.
</para>
<para>
@@ -239,6 +240,19 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 517c8edd3b..2352f7e71f 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -69,7 +69,9 @@ parse_subscription_options(List *options,
char **synchronous_commit,
bool *refresh,
bool *binary_given, bool *binary,
- bool *streaming_given, bool *streaming)
+ bool *streaming_given, bool *streaming,
+ bool *validate_publication_given,
+ bool *validate_publication)
{
ListCell *lc;
bool connect_given = false;
@@ -111,6 +113,12 @@ parse_subscription_options(List *options,
*streaming = false;
}
+ if (validate_publication)
+ {
+ *validate_publication_given = false;
+ *validate_publication = false;
+ }
+
/* Parse options */
foreach(lc, options)
{
@@ -215,6 +223,17 @@ parse_subscription_options(List *options,
*streaming_given = true;
*streaming = defGetBoolean(defel);
}
+ else if (strcmp(defel->defname, "validate_publication") == 0 &&
+ validate_publication)
+ {
+ if (*validate_publication_given)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+
+ *validate_publication_given = true;
+ *validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -247,10 +266,18 @@ parse_subscription_options(List *options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (validate_publication && validate_publication_given &&
+ *validate_publication)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
*enabled = false;
*create_slot = false;
*copy_data = false;
+ *validate_publication = false;
}
/*
@@ -287,6 +314,140 @@ parse_subscription_options(List *options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications,
+ bool validate_publication)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ if (!validate_publication)
+ return;
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ (errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data)));
+ }
+}
+
+/*
+ * Connect to the publisher and see if the given publication(s) is(are) present.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
+{
+ char *err;
+ WalReceiverConn *wrconn;
+
+ if (!validate_publication)
+ return;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ PG_TRY();
+ {
+ check_publications(wrconn, publications, true);
+ }
+ PG_FINALLY();
+ {
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -343,6 +504,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
bool slotname_given;
bool binary;
bool binary_given;
+ bool validate_publication;
+ bool validate_publication_given;
char originname[NAMEDATALEN];
bool create_slot;
List *publications;
@@ -361,7 +524,9 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ &validate_publication_given,
+ &validate_publication);
/*
* Since creating a replication slot is not transactional, rolling back
@@ -472,6 +637,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
PG_TRY();
{
+ check_publications(wrconn, publications, validate_publication);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -539,7 +706,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -568,6 +736,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
ereport(ERROR,
(errmsg("could not connect to the publisher: %s", err)));
+ check_publications(wrconn, sub->publications, validate_publication);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -814,7 +984,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ NULL, NULL); /* no "validate_publication" */
if (slotname_given)
{
@@ -871,7 +1042,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no streaming */
+ NULL, NULL, /* no "streaming" */
+ NULL, NULL); /* no "validate_publication" */
Assert(enabled_given);
if (!sub->slotname && enabled)
@@ -906,6 +1078,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
{
bool copy_data;
bool refresh;
+ bool validate_publication;
+ bool validate_publication_given;
parse_subscription_options(stmt->options,
NULL, /* no "connect" */
@@ -916,12 +1090,15 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(stmt->publication);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -937,7 +1114,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -950,6 +1127,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
bool copy_data;
bool refresh;
List *publist;
+ bool validate_publication;
+ bool validate_publication_given;
publist = merge_publications(sub->publications, stmt->publication, isadd, stmt->subname);
@@ -963,13 +1142,18 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ /* for drop, no "validate_publication" */
+ isadd ? &validate_publication_given : NULL,
+ isadd ? &validate_publication : NULL);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(publist);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd)
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -985,7 +1169,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Only refresh the added/dropped list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -994,6 +1178,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
case ALTER_SUBSCRIPTION_REFRESH:
{
bool copy_data;
+ bool validate_publication;
+ bool validate_publication_given;
if (!sub->enabled)
ereport(ERROR,
@@ -1009,11 +1195,13 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, validate_publication);
break;
}
@@ -1476,28 +1664,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d917987fd5..21c82e3f50 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1659,7 +1659,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1678,7 +1678,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> ADD|DROP|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny, "WITH", "("))
- COMPLETE_WITH("copy_data", "refresh");
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
/* ALTER SCHEMA <name> */
else if (Matches("ALTER", "SCHEMA", MatchAny))
@@ -2759,7 +2759,8 @@ psql_completion(const char *text, int start, int end)
/* Complete "CREATE SUBSCRIPTION <name> ... WITH ( <opt>" */
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("copy_data", "connect", "create_slot", "enabled",
- "slot_name", "synchronous_commit");
+ "slot_name", "synchronous_commit",
+ "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index 7fe6cc6d63..3292e38744 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -3,7 +3,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 1;
+use Test::More tests => 9;
my $node_publisher = get_new_node('publisher');
$node_publisher->init(allows_streaming => 'logical');
@@ -38,5 +38,71 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+# Multiple publications do not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Create subscription with mutually exclusive options connect as false and validate_publication as true.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (CONNECT = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: connect = false and validate_publication = true are mutually exclusive options/,
+ "Create subscription with connect=false and validate_publication=true should fail");
+
+# Add non existent publication.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)");
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ m/ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.25.1
On Fri, May 7, 2021 at 6:44 PM vignesh C <vignesh21@gmail.com> wrote:
On Fri, May 7, 2021 at 5:44 PM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Fri, May 7, 2021 at 5:38 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:On Fri, May 7, 2021 at 11:50 AM Dilip Kumar <dilipbalaut@gmail.com> wrote:
On Thu, May 6, 2021 at 7:22 PM vignesh C <vignesh21@gmail.com> wrote:
Some comments:
1.
I don't see any change in pg_dump.c, don't we need to dump this option?I don't think it is necessary there as the default value of the
validate_publication is false, so even if the pg_dump has no mention
of the option, then it is assumed to be false while restoring. Note
that the validate_publication option is transient (like with other
options such as create_slot, copy_data) which means it can't be stored
in pg_subscritpion catalogue. Therefore, user specified value can't be
fetched once the CREATE/ALTER subscription command is finished. If we
were to dump the option, we should be storing it in the catalogue,
which I don't think is necessary. Thoughts?If we are not storing it in the catalog then it does not need to be dumped.
I intentionally did not store this value, I felt we need not persist
this option's value. This value will be false while dumping similar to
other non stored parameters.2. + /* Try to connect to the publisher. */ + wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err); + if (!wrconn) + ereport(ERROR, + (errmsg("could not connect to the publisher: %s", err)));Instead of using global wrconn, I think you should use a local variable?
Yeah, we should be using local wrconn, otherwise there can be
consequences, see the patches at [1]. Thanks for pointing out this.Modified.
Thanks for the comments, the attached patch has the fix for the same.
The patch was not applying on the head, attached patch which is rebased on HEAD.
Regards,
Vignesh
Attachments:
v11-0001-Identify-missing-publications-from-publisher-whi.patchtext/x-patch; charset=US-ASCII; name=v11-0001-Identify-missing-publications-from-publisher-whi.patchDownload
From 71d4e99d0ec678d82f6e38483cef303b20a12de0 Mon Sep 17 00:00:00 2001
From: vignesh <vignesh21@gmail.com>
Date: Sun, 6 Jun 2021 11:50:38 +0530
Subject: [PATCH v11] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 13 ++
doc/src/sgml/ref/create_subscription.sgml | 18 +-
src/backend/commands/subscriptioncmds.c | 231 +++++++++++++++++++---
src/bin/psql/tab-complete.c | 7 +-
src/test/subscription/t/007_ddl.pl | 68 ++++++-
5 files changed, 302 insertions(+), 35 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 367ac814f4..81e156437b 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -160,6 +160,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index e812beee37..cad9285c16 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -207,8 +207,9 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
Specifies whether the <command>CREATE SUBSCRIPTION</command>
should connect to the publisher at all. Setting this to
<literal>false</literal> will change default values of
- <literal>enabled</literal>, <literal>create_slot</literal> and
- <literal>copy_data</literal> to <literal>false</literal>.
+ <literal>enabled</literal>, <literal>create_slot</literal>,
+ <literal>copy_data</literal> and
+ <literal>validate_publication</literal> to <literal>false</literal>.
</para>
<para>
@@ -239,6 +240,19 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 8aa6de1785..918e8f482a 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -69,7 +69,9 @@ parse_subscription_options(List *options,
char **synchronous_commit,
bool *refresh,
bool *binary_given, bool *binary,
- bool *streaming_given, bool *streaming)
+ bool *streaming_given, bool *streaming,
+ bool *validate_publication_given,
+ bool *validate_publication)
{
ListCell *lc;
bool connect_given = false;
@@ -111,6 +113,12 @@ parse_subscription_options(List *options,
*streaming = false;
}
+ if (validate_publication)
+ {
+ *validate_publication_given = false;
+ *validate_publication = false;
+ }
+
/* Parse options */
foreach(lc, options)
{
@@ -215,6 +223,17 @@ parse_subscription_options(List *options,
*streaming_given = true;
*streaming = defGetBoolean(defel);
}
+ else if (strcmp(defel->defname, "validate_publication") == 0 &&
+ validate_publication)
+ {
+ if (*validate_publication_given)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+
+ *validate_publication_given = true;
+ *validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -247,10 +266,18 @@ parse_subscription_options(List *options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (validate_publication && validate_publication_given &&
+ *validate_publication)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
*enabled = false;
*create_slot = false;
*copy_data = false;
+ *validate_publication = false;
}
/*
@@ -287,6 +314,140 @@ parse_subscription_options(List *options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications,
+ bool validate_publication)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ if (!validate_publication)
+ return;
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ (errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data)));
+ }
+}
+
+/*
+ * Connect to the publisher and see if the given publication(s) is(are) present.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
+{
+ char *err;
+ WalReceiverConn *wrconn;
+
+ if (!validate_publication)
+ return;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ PG_TRY();
+ {
+ check_publications(wrconn, publications, true);
+ }
+ PG_FINALLY();
+ {
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -343,6 +504,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
bool slotname_given;
bool binary;
bool binary_given;
+ bool validate_publication;
+ bool validate_publication_given;
char originname[NAMEDATALEN];
bool create_slot;
List *publications;
@@ -361,7 +524,9 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ &validate_publication_given,
+ &validate_publication);
/*
* Since creating a replication slot is not transactional, rolling back
@@ -472,6 +637,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
PG_TRY();
{
+ check_publications(wrconn, publications, validate_publication);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -539,7 +706,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -569,6 +737,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
PG_TRY();
{
+ check_publications(wrconn, sub->publications, validate_publication);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -814,7 +984,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ NULL, NULL); /* no "validate_publication" */
if (slotname_given)
{
@@ -871,7 +1042,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no streaming */
+ NULL, NULL, /* no "streaming" */
+ NULL, NULL); /* no "validate_publication" */
Assert(enabled_given);
if (!sub->slotname && enabled)
@@ -906,6 +1078,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
{
bool copy_data;
bool refresh;
+ bool validate_publication;
+ bool validate_publication_given;
parse_subscription_options(stmt->options,
NULL, /* no "connect" */
@@ -916,12 +1090,15 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(stmt->publication);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -937,7 +1114,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -950,6 +1127,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
bool copy_data;
bool refresh;
List *publist;
+ bool validate_publication;
+ bool validate_publication_given;
publist = merge_publications(sub->publications, stmt->publication, isadd, stmt->subname);
@@ -963,13 +1142,18 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ /* for drop, no "validate_publication" */
+ isadd ? &validate_publication_given : NULL,
+ isadd ? &validate_publication : NULL);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(publist);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd)
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -985,7 +1169,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Only refresh the added/dropped list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -994,6 +1178,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
case ALTER_SUBSCRIPTION_REFRESH:
{
bool copy_data;
+ bool validate_publication;
+ bool validate_publication_given;
if (!sub->enabled)
ereport(ERROR,
@@ -1009,11 +1195,13 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, validate_publication);
break;
}
@@ -1476,28 +1664,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 109b22acb6..bd339bf4cc 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1659,7 +1659,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1678,7 +1678,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> ADD|DROP|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny, "WITH", "("))
- COMPLETE_WITH("copy_data", "refresh");
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
/* ALTER SCHEMA <name> */
else if (Matches("ALTER", "SCHEMA", MatchAny))
@@ -2759,7 +2759,8 @@ psql_completion(const char *text, int start, int end)
/* Complete "CREATE SUBSCRIPTION <name> ... WITH ( <opt>" */
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("copy_data", "connect", "create_slot", "enabled",
- "slot_name", "synchronous_commit");
+ "slot_name", "synchronous_commit",
+ "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index dd10d5cffa..f94d67532b 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -6,7 +6,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 1;
+use Test::More tests => 9;
my $node_publisher = get_new_node('publisher');
$node_publisher->init(allows_streaming => 'logical');
@@ -41,5 +41,71 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+# Multiple publications do not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Create subscription with mutually exclusive options connect as false and validate_publication as true.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (CONNECT = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: connect = false and validate_publication = true are mutually exclusive options/,
+ "Create subscription with connect=false and validate_publication=true should fail");
+
+# Add non existent publication.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)");
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ m/ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.25.1
On Sun, Jun 6, 2021 at 11:55 AM vignesh C <vignesh21@gmail.com> wrote:
On Fri, May 7, 2021 at 6:44 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, the attached patch has the fix for the same.
The patch was not applying on the head, attached patch which is rebased on HEAD.
The patch was not applying on the head, attached patch which is rebased on HEAD.
Regards,
Vignesh
Attachments:
v11-0001-Identify-missing-publications-from-publisher-whi.patchapplication/x-patch; name=v11-0001-Identify-missing-publications-from-publisher-whi.patchDownload
From 65b2eb5c380f09faec0e72edff0b8e84e9b9267c Mon Sep 17 00:00:00 2001
From: vignesh <vignesh21@gmail.com>
Date: Wed, 30 Jun 2021 20:02:08 +0530
Subject: [PATCH v11] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 13 ++
doc/src/sgml/ref/create_subscription.sgml | 18 +-
src/backend/commands/subscriptioncmds.c | 231 +++++++++++++++++++---
src/bin/psql/tab-complete.c | 6 +-
src/test/subscription/t/007_ddl.pl | 68 ++++++-
5 files changed, 301 insertions(+), 35 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index b3d173179f..205f82d0d0 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -161,6 +161,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index e812beee37..cad9285c16 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -207,8 +207,9 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
Specifies whether the <command>CREATE SUBSCRIPTION</command>
should connect to the publisher at all. Setting this to
<literal>false</literal> will change default values of
- <literal>enabled</literal>, <literal>create_slot</literal> and
- <literal>copy_data</literal> to <literal>false</literal>.
+ <literal>enabled</literal>, <literal>create_slot</literal>,
+ <literal>copy_data</literal> and
+ <literal>validate_publication</literal> to <literal>false</literal>.
</para>
<para>
@@ -239,6 +240,19 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index b862e59f1d..5eeded5e5e 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -69,7 +69,9 @@ parse_subscription_options(List *options,
char **synchronous_commit,
bool *refresh,
bool *binary_given, bool *binary,
- bool *streaming_given, bool *streaming)
+ bool *streaming_given, bool *streaming,
+ bool *validate_publication_given,
+ bool *validate_publication)
{
ListCell *lc;
bool connect_given = false;
@@ -111,6 +113,12 @@ parse_subscription_options(List *options,
*streaming = false;
}
+ if (validate_publication)
+ {
+ *validate_publication_given = false;
+ *validate_publication = false;
+ }
+
/* Parse options */
foreach(lc, options)
{
@@ -215,6 +223,17 @@ parse_subscription_options(List *options,
*streaming_given = true;
*streaming = defGetBoolean(defel);
}
+ else if (strcmp(defel->defname, "validate_publication") == 0 &&
+ validate_publication)
+ {
+ if (*validate_publication_given)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+
+ *validate_publication_given = true;
+ *validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -247,10 +266,18 @@ parse_subscription_options(List *options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (validate_publication && validate_publication_given &&
+ *validate_publication)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
*enabled = false;
*create_slot = false;
*copy_data = false;
+ *validate_publication = false;
}
/*
@@ -287,6 +314,140 @@ parse_subscription_options(List *options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications,
+ bool validate_publication)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ if (!validate_publication)
+ return;
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ (errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data)));
+ }
+}
+
+/*
+ * Connect to the publisher and see if the given publication(s) is(are) present.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
+{
+ char *err;
+ WalReceiverConn *wrconn;
+
+ if (!validate_publication)
+ return;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ PG_TRY();
+ {
+ check_publications(wrconn, publications, true);
+ }
+ PG_FINALLY();
+ {
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -343,6 +504,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
bool slotname_given;
bool binary;
bool binary_given;
+ bool validate_publication;
+ bool validate_publication_given;
char originname[NAMEDATALEN];
bool create_slot;
List *publications;
@@ -361,7 +524,9 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ &validate_publication_given,
+ &validate_publication);
/*
* Since creating a replication slot is not transactional, rolling back
@@ -473,6 +638,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
PG_TRY();
{
+ check_publications(wrconn, publications, validate_publication);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -540,7 +707,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -571,6 +739,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
PG_TRY();
{
+ check_publications(wrconn, sub->publications, validate_publication);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -816,7 +986,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
&synchronous_commit,
NULL, /* no "refresh" */
&binary_given, &binary,
- &streaming_given, &streaming);
+ &streaming_given, &streaming,
+ NULL, NULL); /* no "validate_publication" */
if (slotname_given)
{
@@ -873,7 +1044,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no streaming */
+ NULL, NULL, /* no "streaming" */
+ NULL, NULL); /* no "validate_publication" */
Assert(enabled_given);
if (!sub->slotname && enabled)
@@ -908,6 +1080,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
{
bool copy_data;
bool refresh;
+ bool validate_publication;
+ bool validate_publication_given;
parse_subscription_options(stmt->options,
NULL, /* no "connect" */
@@ -918,12 +1092,15 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
values[Anum_pg_subscription_subpublications - 1] =
publicationListToArray(stmt->publication);
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -939,7 +1116,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -952,6 +1129,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
bool copy_data = false;
bool refresh;
List *publist;
+ bool validate_publication;
+ bool validate_publication_given;
parse_subscription_options(stmt->options,
NULL, /* no "connect" */
@@ -963,7 +1142,10 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
&refresh,
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ /* for drop, no "validate_publication" */
+ isadd ? &validate_publication_given : NULL,
+ isadd ? &validate_publication : NULL);
publist = merge_publications(sub->publications, stmt->publication, isadd, stmt->subname);
@@ -972,6 +1154,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd)
+ connect_and_check_pubs(sub, stmt->publication, validate_publication);
/* Refresh if user asked us to. */
if (refresh)
@@ -987,7 +1171,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Only refresh the added/dropped list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, false);
}
break;
@@ -996,6 +1180,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
case ALTER_SUBSCRIPTION_REFRESH:
{
bool copy_data;
+ bool validate_publication;
+ bool validate_publication_given;
if (!sub->enabled)
ereport(ERROR,
@@ -1011,11 +1197,13 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
NULL, /* no "synchronous_commit" */
NULL, /* no "refresh" */
NULL, NULL, /* no "binary" */
- NULL, NULL); /* no "streaming" */
+ NULL, NULL, /* no "streaming" */
+ &validate_publication_given,
+ &validate_publication);
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, copy_data);
+ AlterSubscription_refresh(sub, copy_data, validate_publication);
break;
}
@@ -1479,28 +1667,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 0ebd5aa41a..c5e5e5839c 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1659,7 +1659,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1678,7 +1678,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> ADD|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|SET", "PUBLICATION", MatchAny, "WITH", "("))
- COMPLETE_WITH("copy_data", "refresh");
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
/* ALTER SUBSCRIPTION <name> DROP PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("DROP", "PUBLICATION", MatchAny, "WITH", "("))
@@ -2764,7 +2764,7 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
"enabled", "slot_name", "streaming",
- "synchronous_commit");
+ "synchronous_commit", "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index dd10d5cffa..f94d67532b 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -6,7 +6,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 1;
+use Test::More tests => 9;
my $node_publisher = get_new_node('publisher');
$node_publisher->init(allows_streaming => 'logical');
@@ -41,5 +41,71 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+# Multiple publications do not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Create subscription with mutually exclusive options connect as false and validate_publication as true.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (CONNECT = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: connect = false and validate_publication = true are mutually exclusive options/,
+ "Create subscription with connect=false and validate_publication=true should fail");
+
+# Add non existent publication.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)");
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ m/ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.25.1
On Wed, Jun 30, 2021 at 8:23 PM vignesh C <vignesh21@gmail.com> wrote:
On Sun, Jun 6, 2021 at 11:55 AM vignesh C <vignesh21@gmail.com> wrote:
On Fri, May 7, 2021 at 6:44 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, the attached patch has the fix for the same.
The patch was not applying on the head, attached patch which is rebased on HEAD.
The patch was not applying on the head, attached patch which is rebased on HEAD.
The patch was not applying on the head, attached patch which is rebased on HEAD.
Regards,
Vignesh
Attachments:
v11-0001-Identify-missing-publications-from-publisher-whi.patchtext/x-patch; charset=US-ASCII; name=v11-0001-Identify-missing-publications-from-publisher-whi.patchDownload
From 059130a6d393c8c020234747865368596a3c4702 Mon Sep 17 00:00:00 2001
From: vignesh <vignesh21@gmail.com>
Date: Tue, 6 Jul 2021 19:54:11 +0530
Subject: [PATCH v11] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 13 ++
doc/src/sgml/ref/create_subscription.sgml | 18 +-
src/backend/commands/subscriptioncmds.c | 208 +++++++++++++++++++---
src/bin/psql/tab-complete.c | 6 +-
src/test/subscription/t/007_ddl.pl | 68 ++++++-
5 files changed, 281 insertions(+), 32 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index b3d173179f..205f82d0d0 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -161,6 +161,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index e812beee37..cad9285c16 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -207,8 +207,9 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
Specifies whether the <command>CREATE SUBSCRIPTION</command>
should connect to the publisher at all. Setting this to
<literal>false</literal> will change default values of
- <literal>enabled</literal>, <literal>create_slot</literal> and
- <literal>copy_data</literal> to <literal>false</literal>.
+ <literal>enabled</literal>, <literal>create_slot</literal>,
+ <literal>copy_data</literal> and
+ <literal>validate_publication</literal> to <literal>false</literal>.
</para>
<para>
@@ -239,6 +240,19 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index eb88d877a5..e3f8438e5f 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -59,6 +59,7 @@
#define SUBOPT_REFRESH 0x00000040
#define SUBOPT_BINARY 0x00000080
#define SUBOPT_STREAMING 0x00000100
+#define SUBOPT_VALIDATE_PUB 0x00000200
/* check if the 'val' has 'bits' set */
#define IsSet(val, bits) (((val) & (bits)) == (bits))
@@ -79,6 +80,7 @@ typedef struct SubOpts
bool refresh;
bool binary;
bool streaming;
+ bool validate_publication;
} SubOpts;
static List *fetch_table_list(WalReceiverConn *wrconn, List *publications);
@@ -123,6 +125,8 @@ parse_subscription_options(List *stmt_options, bits32 supported_opts, SubOpts *o
opts->binary = false;
if (IsSet(supported_opts, SUBOPT_STREAMING))
opts->streaming = false;
+ if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB))
+ opts->validate_publication = false;
/* Parse options */
foreach(lc, stmt_options)
@@ -237,6 +241,17 @@ parse_subscription_options(List *stmt_options, bits32 supported_opts, SubOpts *o
opts->specified_opts |= SUBOPT_STREAMING;
opts->streaming = defGetBoolean(defel);
}
+ else if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ strcmp(defel->defname, "validate_publication") == 0)
+ {
+ if (IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("conflicting or redundant options")));
+
+ opts->specified_opts |= SUBOPT_VALIDATE_PUB;
+ opts->validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -275,10 +290,19 @@ parse_subscription_options(List *stmt_options, bits32 supported_opts, SubOpts *o
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (opts->validate_publication &&
+ IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
opts->enabled = false;
opts->create_slot = false;
opts->copy_data = false;
+ opts->validate_publication = false;
}
/*
@@ -327,6 +351,140 @@ parse_subscription_options(List *stmt_options, bits32 supported_opts, SubOpts *o
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications,
+ bool validate_publication)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ if (!validate_publication)
+ return;
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ (errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data)));
+ }
+}
+
+/*
+ * Connect to the publisher and see if the given publication(s) is(are) present.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
+{
+ char *err;
+ WalReceiverConn *wrconn;
+
+ if (!validate_publication)
+ return;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ PG_TRY();
+ {
+ check_publications(wrconn, publications, true);
+ }
+ PG_FINALLY();
+ {
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -385,7 +543,7 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
supported_opts = (SUBOPT_CONNECT | SUBOPT_ENABLED | SUBOPT_CREATE_SLOT |
SUBOPT_SLOT_NAME | SUBOPT_COPY_DATA |
SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
- SUBOPT_STREAMING);
+ SUBOPT_STREAMING | SUBOPT_VALIDATE_PUB);
parse_subscription_options(stmt->options, supported_opts, &opts);
/*
@@ -499,6 +657,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
PG_TRY();
{
+ check_publications(wrconn, publications, opts.validate_publication);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -566,7 +726,8 @@ CreateSubscription(CreateSubscriptionStmt *stmt, bool isTopLevel)
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -597,6 +758,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
PG_TRY();
{
+ check_publications(wrconn, sub->publications, validate_publication);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -909,7 +1072,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
case ALTER_SUBSCRIPTION_SET_PUBLICATION:
{
- supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH;
+ supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH | SUBOPT_VALIDATE_PUB;
parse_subscription_options(stmt->options, supported_opts, &opts);
values[Anum_pg_subscription_subpublications - 1] =
@@ -917,6 +1080,8 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ connect_and_check_pubs(sub, stmt->publication,
+ opts.validate_publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -932,7 +1097,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -946,7 +1111,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
supported_opts = SUBOPT_REFRESH;
if (isadd)
- supported_opts |= SUBOPT_COPY_DATA;
+ supported_opts |= SUBOPT_COPY_DATA | SUBOPT_VALIDATE_PUB;
parse_subscription_options(stmt->options, supported_opts, &opts);
@@ -956,6 +1121,9 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd)
+ connect_and_check_pubs(sub, stmt->publication,
+ opts.validate_publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -971,7 +1139,7 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
/* Only refresh the added/dropped list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -984,11 +1152,14 @@ AlterSubscription(AlterSubscriptionStmt *stmt, bool isTopLevel)
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("ALTER SUBSCRIPTION ... REFRESH is not allowed for disabled subscriptions")));
- parse_subscription_options(stmt->options, SUBOPT_COPY_DATA, &opts);
+ parse_subscription_options(stmt->options,
+ SUBOPT_COPY_DATA | SUBOPT_VALIDATE_PUB,
+ &opts);
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data,
+ opts.validate_publication);
break;
}
@@ -1452,28 +1623,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 0ebd5aa41a..c5e5e5839c 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1659,7 +1659,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1678,7 +1678,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> ADD|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|SET", "PUBLICATION", MatchAny, "WITH", "("))
- COMPLETE_WITH("copy_data", "refresh");
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
/* ALTER SUBSCRIPTION <name> DROP PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("DROP", "PUBLICATION", MatchAny, "WITH", "("))
@@ -2764,7 +2764,7 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
"enabled", "slot_name", "streaming",
- "synchronous_commit");
+ "synchronous_commit", "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index dd10d5cffa..f94d67532b 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -6,7 +6,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 1;
+use Test::More tests => 9;
my $node_publisher = get_new_node('publisher');
$node_publisher->init(allows_streaming => 'logical');
@@ -41,5 +41,71 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+# Multiple publications do not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Create subscription with mutually exclusive options connect as false and validate_publication as true.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (CONNECT = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: connect = false and validate_publication = true are mutually exclusive options/,
+ "Create subscription with connect=false and validate_publication=true should fail");
+
+# Add non existent publication.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)");
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ m/ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.25.1
On Tue, Jul 6, 2021 at 8:09 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, Jun 30, 2021 at 8:23 PM vignesh C <vignesh21@gmail.com> wrote:
On Sun, Jun 6, 2021 at 11:55 AM vignesh C <vignesh21@gmail.com> wrote:
On Fri, May 7, 2021 at 6:44 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, the attached patch has the fix for the same.
The patch was not applying on the head, attached patch which is rebased on HEAD.
The patch was not applying on the head, attached patch which is rebased on HEAD.
The patch was not applying on the head, attached patch which is rebased on HEAD.
The patch was not applying on the head, attached patch which is rebased on HEAD.
Regards,
Vignesh
Attachments:
v11-0001-Identify-missing-publications-from-publisher-whi.patchtext/x-patch; charset=US-ASCII; name=v11-0001-Identify-missing-publications-from-publisher-whi.patchDownload
From 09bd61d8ebcc4b5119048aeb7274bff2bdcae794 Mon Sep 17 00:00:00 2001
From: vignesh <vignesh21@gmail.com>
Date: Thu, 15 Jul 2021 17:39:52 +0530
Subject: [PATCH v11] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 13 ++
doc/src/sgml/ref/create_subscription.sgml | 18 +-
src/backend/commands/subscriptioncmds.c | 206 +++++++++++++++++++---
src/bin/psql/tab-complete.c | 7 +-
src/test/subscription/t/007_ddl.pl | 68 ++++++-
5 files changed, 280 insertions(+), 32 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index a6f994450d..e6b20542f9 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -166,6 +166,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index 143390593d..194f0b977a 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -207,8 +207,9 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
Specifies whether the <command>CREATE SUBSCRIPTION</command>
should connect to the publisher at all. Setting this to
<literal>false</literal> will change default values of
- <literal>enabled</literal>, <literal>create_slot</literal> and
- <literal>copy_data</literal> to <literal>false</literal>.
+ <literal>enabled</literal>, <literal>create_slot</literal>,
+ <literal>copy_data</literal> and
+ <literal>validate_publication</literal> to <literal>false</literal>.
</para>
<para>
@@ -276,6 +277,19 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 239d263f83..474fb92bbe 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -60,6 +60,7 @@
#define SUBOPT_BINARY 0x00000080
#define SUBOPT_STREAMING 0x00000100
#define SUBOPT_TWOPHASE_COMMIT 0x00000200
+#define SUBOPT_VALIDATE_PUB 0x00000400
/* check if the 'val' has 'bits' set */
#define IsSet(val, bits) (((val) & (bits)) == (bits))
@@ -81,6 +82,7 @@ typedef struct SubOpts
bool binary;
bool streaming;
bool twophase;
+ bool validate_publication;
} SubOpts;
static List *fetch_table_list(WalReceiverConn *wrconn, List *publications);
@@ -128,6 +130,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->streaming = false;
if (IsSet(supported_opts, SUBOPT_TWOPHASE_COMMIT))
opts->twophase = false;
+ if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB))
+ opts->validate_publication = false;
/* Parse options */
foreach(lc, stmt_options)
@@ -245,6 +249,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->specified_opts |= SUBOPT_TWOPHASE_COMMIT;
opts->twophase = defGetBoolean(defel);
}
+ else if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ strcmp(defel->defname, "validate_publication") == 0)
+ {
+ if (IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ errorConflictingDefElem(defel, pstate);
+
+ opts->specified_opts |= SUBOPT_VALIDATE_PUB;
+ opts->validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -283,10 +296,19 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (opts->validate_publication &&
+ IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
opts->enabled = false;
opts->create_slot = false;
opts->copy_data = false;
+ opts->validate_publication = false;
}
/*
@@ -354,6 +376,140 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications,
+ bool validate_publication)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ if (!validate_publication)
+ return;
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ (errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data)));
+ }
+}
+
+/*
+ * Connect to the publisher and see if the given publication(s) is(are) present.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
+{
+ char *err;
+ WalReceiverConn *wrconn;
+
+ if (!validate_publication)
+ return;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ PG_TRY();
+ {
+ check_publications(wrconn, publications, true);
+ }
+ PG_FINALLY();
+ {
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -413,7 +569,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
supported_opts = (SUBOPT_CONNECT | SUBOPT_ENABLED | SUBOPT_CREATE_SLOT |
SUBOPT_SLOT_NAME | SUBOPT_COPY_DATA |
SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
- SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT);
+ SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
+ SUBOPT_VALIDATE_PUB);
parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
/*
@@ -531,6 +688,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
PG_TRY();
{
+ check_publications(wrconn, publications, opts.validate_publication);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -623,7 +782,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -654,6 +814,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
PG_TRY();
{
+ check_publications(wrconn, sub->publications, validate_publication);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -975,7 +1137,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
case ALTER_SUBSCRIPTION_SET_PUBLICATION:
{
- supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH;
+ supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH | SUBOPT_VALIDATE_PUB;
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -984,6 +1146,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ connect_and_check_pubs(sub, stmt->publication,
+ opts.validate_publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -1010,7 +1174,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -1024,7 +1188,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
supported_opts = SUBOPT_REFRESH;
if (isadd)
- supported_opts |= SUBOPT_COPY_DATA;
+ supported_opts |= SUBOPT_COPY_DATA | SUBOPT_VALIDATE_PUB;
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -1035,6 +1199,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd)
+ connect_and_check_pubs(sub, stmt->publication,
+ opts.validate_publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -1061,7 +1228,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Only refresh the added/dropped list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -1075,7 +1242,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
errmsg("ALTER SUBSCRIPTION ... REFRESH is not allowed for disabled subscriptions")));
parse_subscription_options(pstate, stmt->options,
- SUBOPT_COPY_DATA, &opts);
+ SUBOPT_COPY_DATA | SUBOPT_VALIDATE_PUB,
+ &opts);
/*
* The subscription option "two_phase" requires that
@@ -1103,7 +1271,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data,
+ opts.validate_publication);
break;
}
@@ -1567,28 +1736,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d6bf725971..5c41c379dd 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1659,7 +1659,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1678,7 +1678,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> ADD|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|SET", "PUBLICATION", MatchAny, "WITH", "("))
- COMPLETE_WITH("copy_data", "refresh");
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
/* ALTER SUBSCRIPTION <name> DROP PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("DROP", "PUBLICATION", MatchAny, "WITH", "("))
@@ -2764,7 +2764,8 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
"enabled", "slot_name", "streaming",
- "synchronous_commit", "two_phase");
+ "synchronous_commit", "two_phase",
+ "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index dd10d5cffa..f94d67532b 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -6,7 +6,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 1;
+use Test::More tests => 9;
my $node_publisher = get_new_node('publisher');
$node_publisher->init(allows_streaming => 'logical');
@@ -41,5 +41,71 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+# Multiple publications do not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Create subscription with mutually exclusive options connect as false and validate_publication as true.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (CONNECT = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: connect = false and validate_publication = true are mutually exclusive options/,
+ "Create subscription with connect=false and validate_publication=true should fail");
+
+# Add non existent publication.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)");
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ m/ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.25.1
On Thu, Jul 15, 2021 at 5:57 PM vignesh C <vignesh21@gmail.com> wrote:
On Tue, Jul 6, 2021 at 8:09 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, Jun 30, 2021 at 8:23 PM vignesh C <vignesh21@gmail.com> wrote:
On Sun, Jun 6, 2021 at 11:55 AM vignesh C <vignesh21@gmail.com> wrote:
On Fri, May 7, 2021 at 6:44 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, the attached patch has the fix for the same.
The patch was not applying on the head, attached patch which is rebased on HEAD.
Regards,
Vignesh
Attachments:
v11-0001-Identify-missing-publications-from-publisher-whi.patchtext/x-patch; charset=US-ASCII; name=v11-0001-Identify-missing-publications-from-publisher-whi.patchDownload
From 36e522ba31829c76e6c4c2069f2806c653aecf62 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Thu, 26 Aug 2021 16:32:56 +0530
Subject: [PATCH v11] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 13 ++
doc/src/sgml/ref/create_subscription.sgml | 18 +-
src/backend/commands/subscriptioncmds.c | 207 +++++++++++++++++++---
src/bin/psql/tab-complete.c | 14 +-
src/test/subscription/t/007_ddl.pl | 68 ++++++-
5 files changed, 287 insertions(+), 33 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 835be0d2a4..d5b28e9afa 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -163,6 +163,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index 702934eba1..a39c85b2bb 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -207,8 +207,9 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
Specifies whether the <command>CREATE SUBSCRIPTION</command>
should connect to the publisher at all. Setting this to
<literal>false</literal> will change default values of
- <literal>enabled</literal>, <literal>create_slot</literal> and
- <literal>copy_data</literal> to <literal>false</literal>.
+ <literal>enabled</literal>, <literal>create_slot</literal>,
+ <literal>copy_data</literal> and
+ <literal>validate_publication</literal> to <literal>false</literal>.
</para>
<para>
@@ -266,6 +267,19 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c47ba26369..8d2aaf72c6 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -60,6 +60,7 @@
#define SUBOPT_BINARY 0x00000080
#define SUBOPT_STREAMING 0x00000100
#define SUBOPT_TWOPHASE_COMMIT 0x00000200
+#define SUBOPT_VALIDATE_PUB 0x00000400
/* check if the 'val' has 'bits' set */
#define IsSet(val, bits) (((val) & (bits)) == (bits))
@@ -81,6 +82,7 @@ typedef struct SubOpts
bool binary;
bool streaming;
bool twophase;
+ bool validate_publication;
} SubOpts;
static List *fetch_table_list(WalReceiverConn *wrconn, List *publications);
@@ -128,6 +130,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->streaming = false;
if (IsSet(supported_opts, SUBOPT_TWOPHASE_COMMIT))
opts->twophase = false;
+ if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB))
+ opts->validate_publication = false;
/* Parse options */
foreach(lc, stmt_options)
@@ -247,6 +251,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->specified_opts |= SUBOPT_TWOPHASE_COMMIT;
opts->twophase = defGetBoolean(defel);
}
+ else if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ strcmp(defel->defname, "validate_publication") == 0)
+ {
+ if (IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ errorConflictingDefElem(defel, pstate);
+
+ opts->specified_opts |= SUBOPT_VALIDATE_PUB;
+ opts->validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -285,10 +298,19 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (opts->validate_publication &&
+ IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
opts->enabled = false;
opts->create_slot = false;
opts->copy_data = false;
+ opts->validate_publication = false;
}
/*
@@ -337,6 +359,140 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications,
+ bool validate_publication)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ if (!validate_publication)
+ return;
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ (errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data)));
+ }
+}
+
+/*
+ * Connect to the publisher and see if the given publication(s) is(are) present.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
+{
+ char *err;
+ WalReceiverConn *wrconn;
+
+ if (!validate_publication)
+ return;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ PG_TRY();
+ {
+ check_publications(wrconn, publications, true);
+ }
+ PG_FINALLY();
+ {
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -396,7 +552,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
supported_opts = (SUBOPT_CONNECT | SUBOPT_ENABLED | SUBOPT_CREATE_SLOT |
SUBOPT_SLOT_NAME | SUBOPT_COPY_DATA |
SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
- SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT);
+ SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
+ SUBOPT_VALIDATE_PUB);
parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
/*
@@ -514,6 +671,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
PG_TRY();
{
+ check_publications(wrconn, publications, opts.validate_publication);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -606,7 +765,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -637,6 +797,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
PG_TRY();
{
+ check_publications(wrconn, sub->publications, validate_publication);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -959,7 +1121,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
case ALTER_SUBSCRIPTION_SET_PUBLICATION:
{
- supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH;
+ supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH | SUBOPT_VALIDATE_PUB;
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -968,6 +1130,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ connect_and_check_pubs(sub, stmt->publication,
+ opts.validate_publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -994,7 +1158,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -1007,6 +1171,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
bool isadd = stmt->kind == ALTER_SUBSCRIPTION_ADD_PUBLICATION;
supported_opts = SUBOPT_REFRESH | SUBOPT_COPY_DATA;
+ if (isadd)
+ supported_opts |= SUBOPT_VALIDATE_PUB;
+
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -1016,6 +1183,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd)
+ connect_and_check_pubs(sub, stmt->publication,
+ opts.validate_publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -1042,7 +1212,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Refresh the new list of publications. */
sub->publications = publist;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -1056,7 +1226,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
errmsg("ALTER SUBSCRIPTION ... REFRESH is not allowed for disabled subscriptions")));
parse_subscription_options(pstate, stmt->options,
- SUBOPT_COPY_DATA, &opts);
+ SUBOPT_COPY_DATA | SUBOPT_VALIDATE_PUB,
+ &opts);
/*
* The subscription option "two_phase" requires that
@@ -1084,7 +1255,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data,
+ opts.validate_publication);
break;
}
@@ -1548,28 +1720,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 7cdfc7c637..16e409be72 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1659,7 +1659,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1675,11 +1675,14 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny))
COMPLETE_WITH("WITH (");
- /* ALTER SUBSCRIPTION <name> ADD|DROP|SET PUBLICATION <name> WITH ( */
+ /* ALTER SUBSCRIPTION <name> ADD|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
- TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny, "WITH", "("))
+ TailMatches("ADD|SET", "PUBLICATION", MatchAny, "WITH", "("))
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
+ /* ALTER SUBSCRIPTION <name> DROP PUBLICATION <name> WITH ( */
+ else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
+ TailMatches("DROP", "PUBLICATION", MatchAny, "WITH", "("))
COMPLETE_WITH("copy_data", "refresh");
-
/* ALTER SCHEMA <name> */
else if (Matches("ALTER", "SCHEMA", MatchAny))
COMPLETE_WITH("OWNER TO", "RENAME TO");
@@ -2767,7 +2770,8 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
"enabled", "slot_name", "streaming",
- "synchronous_commit", "two_phase");
+ "synchronous_commit", "two_phase",
+ "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index 1a3a1dcf14..05503adbc2 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -6,7 +6,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 1;
+use Test::More tests => 9;
my $node_publisher = PostgresNode->new('publisher');
$node_publisher->init(allows_streaming => 'logical');
@@ -41,5 +41,71 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+# Multiple publications do not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Create subscription with mutually exclusive options connect as false and validate_publication as true.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (CONNECT = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: connect = false and validate_publication = true are mutually exclusive options/,
+ "Create subscription with connect=false and validate_publication=true should fail");
+
+# Add non existent publication.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)");
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ m/ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.30.2
On Thu, Aug 26, 2021 at 07:49:49PM +0530, vignesh C wrote:
On Thu, Jul 15, 2021 at 5:57 PM vignesh C <vignesh21@gmail.com> wrote:
On Tue, Jul 6, 2021 at 8:09 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, Jun 30, 2021 at 8:23 PM vignesh C <vignesh21@gmail.com> wrote:
On Sun, Jun 6, 2021 at 11:55 AM vignesh C <vignesh21@gmail.com> wrote:
On Fri, May 7, 2021 at 6:44 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, the attached patch has the fix for the same.
The patch was not applying on the head, attached patch which is rebased on HEAD.
Hi,
I'm testing this patch now. It doesn't apply cleanly but is the
documentation part, so while a rebase would be good it doesn't avoid me
to test.
A couple of questions:
+check_publications(WalReceiverConn *wrconn, List *publications,
+ bool validate_publication)
[...]
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
I wonder why validate_publication is passed as an argument just to
return if it's false, why not just test it before calling those
functions? Maybe is just a matter of style.
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
what's the purpose of the quote_literal argument?
--
Jaime Casanova
Director de Servicios Profesionales
SystemGuards - Consultores de PostgreSQL
On Tue, Sep 28, 2021 at 7:49 AM Jaime Casanova
<jcasanov@systemguards.com.ec> wrote:
On Thu, Aug 26, 2021 at 07:49:49PM +0530, vignesh C wrote:
On Thu, Jul 15, 2021 at 5:57 PM vignesh C <vignesh21@gmail.com> wrote:
On Tue, Jul 6, 2021 at 8:09 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, Jun 30, 2021 at 8:23 PM vignesh C <vignesh21@gmail.com> wrote:
On Sun, Jun 6, 2021 at 11:55 AM vignesh C <vignesh21@gmail.com> wrote:
On Fri, May 7, 2021 at 6:44 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, the attached patch has the fix for the same.
The patch was not applying on the head, attached patch which is rebased on HEAD.
Hi,
I'm testing this patch now. It doesn't apply cleanly but is the
documentation part, so while a rebase would be good it doesn't avoid me
to test.
I have rebased the patch on top of Head.
A couple of questions:
+check_publications(WalReceiverConn *wrconn, List *publications, + bool validate_publication) [...] +connect_and_check_pubs(Subscription *sub, List *publications, + bool validate_publication)I wonder why validate_publication is passed as an argument just to
return if it's false, why not just test it before calling those
functions? Maybe is just a matter of style.
I felt it will be better to have the check inside function so that it
need not be checked at the multiple caller function.
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
what's the purpose of the quote_literal argument?
In case of error the publication that is not present will be displayed
within double quotes like below:
ERROR: publications "pub3", "pub4" do not exist in the publisher
Whereas in case of the query we use single quotes, so quote_literal is
used to differentiate and handle accordingly.
Attached v12 version is rebased on top of Head.
Regards,
Vignesh
Attachments:
v12-0001-Identify-missing-publications-from-publisher-whi.patchtext/x-patch; charset=US-ASCII; name=v12-0001-Identify-missing-publications-from-publisher-whi.patchDownload
From 935841351cb264db6b8d8ddb7c83adcea08e0c76 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Tue, 9 Nov 2021 20:30:47 +0530
Subject: [PATCH v12] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 13 ++
doc/src/sgml/ref/create_subscription.sgml | 20 ++-
src/backend/commands/subscriptioncmds.c | 207 +++++++++++++++++++---
src/bin/psql/tab-complete.c | 14 +-
src/test/subscription/t/007_ddl.pl | 68 ++++++-
5 files changed, 288 insertions(+), 34 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 0b027cc346..95b6b6a0e5 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -168,6 +168,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index 990a41f1a1..8ec028ad7d 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -110,12 +110,14 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
command should connect to the publisher at all. The default
is <literal>true</literal>. Setting this to
<literal>false</literal> will force the values of
- <literal>create_slot</literal>, <literal>enabled</literal> and
- <literal>copy_data</literal> to <literal>false</literal>.
+ <literal>create_slot</literal>, <literal>enabled</literal>,
+ <literal>copy_data</literal> and
+ <literal>validate_publication</literal> to <literal>false</literal>.
(You cannot combine setting <literal>connect</literal>
to <literal>false</literal> with
setting <literal>create_slot</literal>, <literal>enabled</literal>,
- or <literal>copy_data</literal> to <literal>true</literal>.)
+ <literal>copy_data</literal> or
+ <literal>validate_publication</literal> to <literal>true</literal>.)
</para>
<para>
@@ -170,6 +172,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</para>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c47ba26369..8d2aaf72c6 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -60,6 +60,7 @@
#define SUBOPT_BINARY 0x00000080
#define SUBOPT_STREAMING 0x00000100
#define SUBOPT_TWOPHASE_COMMIT 0x00000200
+#define SUBOPT_VALIDATE_PUB 0x00000400
/* check if the 'val' has 'bits' set */
#define IsSet(val, bits) (((val) & (bits)) == (bits))
@@ -81,6 +82,7 @@ typedef struct SubOpts
bool binary;
bool streaming;
bool twophase;
+ bool validate_publication;
} SubOpts;
static List *fetch_table_list(WalReceiverConn *wrconn, List *publications);
@@ -128,6 +130,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->streaming = false;
if (IsSet(supported_opts, SUBOPT_TWOPHASE_COMMIT))
opts->twophase = false;
+ if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB))
+ opts->validate_publication = false;
/* Parse options */
foreach(lc, stmt_options)
@@ -247,6 +251,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->specified_opts |= SUBOPT_TWOPHASE_COMMIT;
opts->twophase = defGetBoolean(defel);
}
+ else if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ strcmp(defel->defname, "validate_publication") == 0)
+ {
+ if (IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ errorConflictingDefElem(defel, pstate);
+
+ opts->specified_opts |= SUBOPT_VALIDATE_PUB;
+ opts->validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -285,10 +298,19 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (opts->validate_publication &&
+ IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
opts->enabled = false;
opts->create_slot = false;
opts->copy_data = false;
+ opts->validate_publication = false;
}
/*
@@ -337,6 +359,140 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications,
+ bool validate_publication)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ if (!validate_publication)
+ return;
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ (errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data)));
+ }
+}
+
+/*
+ * Connect to the publisher and see if the given publication(s) is(are) present.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
+{
+ char *err;
+ WalReceiverConn *wrconn;
+
+ if (!validate_publication)
+ return;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ PG_TRY();
+ {
+ check_publications(wrconn, publications, true);
+ }
+ PG_FINALLY();
+ {
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -396,7 +552,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
supported_opts = (SUBOPT_CONNECT | SUBOPT_ENABLED | SUBOPT_CREATE_SLOT |
SUBOPT_SLOT_NAME | SUBOPT_COPY_DATA |
SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
- SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT);
+ SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
+ SUBOPT_VALIDATE_PUB);
parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
/*
@@ -514,6 +671,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
PG_TRY();
{
+ check_publications(wrconn, publications, opts.validate_publication);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -606,7 +765,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -637,6 +797,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
PG_TRY();
{
+ check_publications(wrconn, sub->publications, validate_publication);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -959,7 +1121,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
case ALTER_SUBSCRIPTION_SET_PUBLICATION:
{
- supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH;
+ supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH | SUBOPT_VALIDATE_PUB;
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -968,6 +1130,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ connect_and_check_pubs(sub, stmt->publication,
+ opts.validate_publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -994,7 +1158,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -1007,6 +1171,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
bool isadd = stmt->kind == ALTER_SUBSCRIPTION_ADD_PUBLICATION;
supported_opts = SUBOPT_REFRESH | SUBOPT_COPY_DATA;
+ if (isadd)
+ supported_opts |= SUBOPT_VALIDATE_PUB;
+
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -1016,6 +1183,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd)
+ connect_and_check_pubs(sub, stmt->publication,
+ opts.validate_publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -1042,7 +1212,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Refresh the new list of publications. */
sub->publications = publist;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -1056,7 +1226,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
errmsg("ALTER SUBSCRIPTION ... REFRESH is not allowed for disabled subscriptions")));
parse_subscription_options(pstate, stmt->options,
- SUBOPT_COPY_DATA, &opts);
+ SUBOPT_COPY_DATA | SUBOPT_VALIDATE_PUB,
+ &opts);
/*
* The subscription option "two_phase" requires that
@@ -1084,7 +1255,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data,
+ opts.validate_publication);
break;
}
@@ -1548,28 +1720,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 4f724e4428..b8af071e4f 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1675,7 +1675,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1691,11 +1691,14 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny))
COMPLETE_WITH("WITH (");
- /* ALTER SUBSCRIPTION <name> ADD|DROP|SET PUBLICATION <name> WITH ( */
+ /* ALTER SUBSCRIPTION <name> ADD|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
- TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny, "WITH", "("))
+ TailMatches("ADD|SET", "PUBLICATION", MatchAny, "WITH", "("))
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
+ /* ALTER SUBSCRIPTION <name> DROP PUBLICATION <name> WITH ( */
+ else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
+ TailMatches("DROP", "PUBLICATION", MatchAny, "WITH", "("))
COMPLETE_WITH("copy_data", "refresh");
-
/* ALTER SCHEMA <name> */
else if (Matches("ALTER", "SCHEMA", MatchAny))
COMPLETE_WITH("OWNER TO", "RENAME TO");
@@ -2898,7 +2901,8 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
"enabled", "slot_name", "streaming",
- "synchronous_commit", "two_phase");
+ "synchronous_commit", "two_phase",
+ "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index 8c869c5c15..0f3b968fbb 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -6,7 +6,7 @@ use strict;
use warnings;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
-use Test::More tests => 1;
+use Test::More tests => 9;
my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
$node_publisher->init(allows_streaming => 'logical');
@@ -41,5 +41,71 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# Specified publication does not exist.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# One of the specified publication exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+# Multiple publications do not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription for non existent publication fails");
+
+# Create subscription with mutually exclusive options connect as false and validate_publication as true.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (CONNECT = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: connect = false and validate_publication = true are mutually exclusive options/,
+ "Create subscription with connect=false and validate_publication=true should fail");
+
+# Add non existent publication.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add non existent publication fails");
+
+# Specified publication does not exist.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)");
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription for non existent publication fails");
+
+# Set publication on non existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ m/ERROR: could not connect to the publisher/,
+ "Alter subscription for non existent publication fails");
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.30.2
On Tue, Nov 9, 2021 at 9:27 PM vignesh C <vignesh21@gmail.com> wrote:
Attached v12 version is rebased on top of Head.
Thanks for the patch. Here are some comments on v12:
1) I think ERRCODE_TOO_MANY_ARGUMENTS isn't the right error code, the
ERRCODE_UNDEFINED_OBJECT is more meaningful. Please change.
+ ereport(ERROR,
+ (errcode(ERRCODE_TOO_MANY_ARGUMENTS),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
The existing code using
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("subscription \"%s\" does not exist", subname)));
2) Typo: It is "One of the specified publications exists."
+# One of the specified publication exist.
3) I think we can remove the test case "+# Specified publication does
not exist." because the "+# One of the specified publication exist."
covers the code.
4) Do we need the below test case? Even with refresh = false, it does
call connect_and_check_pubs() right? Please remove it.
+# Specified publication does not exist with refresh = false.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub
WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in
the publisher/,
+ "Alter subscription for non existent publication fails");
+
5) Change the test case names to different ones instead of the same.
Have something like:
"Create subscription fails with single non-existent publication");
"Create subscription fails with multiple non-existent publications");
"Create subscription fails with mutually exclusive options");
"Alter subscription add publication fails with non-existent publication");
"Alter subscription set publication fails with non-existent publication");
"Alter subscription set publication fails with connection to a
non-existent database");
Unnecessary test cases would add up to the "make check-world" times,
please remove them.
Regards,
Bharath Rupireddy.
On Wed, Nov 10, 2021 at 11:16 AM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
On Tue, Nov 9, 2021 at 9:27 PM vignesh C <vignesh21@gmail.com> wrote:
Attached v12 version is rebased on top of Head.
Thanks for the patch. Here are some comments on v12:
1) I think ERRCODE_TOO_MANY_ARGUMENTS isn't the right error code, the ERRCODE_UNDEFINED_OBJECT is more meaningful. Please change. + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ARGUMENTS), + errmsg_plural("publication %s does not exist in the publisher", + "publications %s do not exist in the publisher",The existing code using
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("subscription \"%s\" does not exist", subname)));
Modified
2) Typo: It is "One of the specified publications exists."
+# One of the specified publication exist.
Modified
3) I think we can remove the test case "+# Specified publication does
not exist." because the "+# One of the specified publication exist."
covers the code.
Modified
4) Do we need the below test case? Even with refresh = false, it does call connect_and_check_pubs() right? Please remove it. +# Specified publication does not exist with refresh = false. +($ret, $stdout, $stderr) = $node_subscriber->psql('postgres', + "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)" +); +ok( $stderr =~ + m/ERROR: publication "non_existent_pub" does not exist in the publisher/, + "Alter subscription for non existent publication fails"); +
Modified
5) Change the test case names to different ones instead of the same.
Have something like:
"Create subscription fails with single non-existent publication");
"Create subscription fails with multiple non-existent publications");
"Create subscription fails with mutually exclusive options");
"Alter subscription add publication fails with non-existent publication");
"Alter subscription set publication fails with non-existent publication");
"Alter subscription set publication fails with connection to a
non-existent database");
Modified
Thanks for the comments, the attached v13 patch has the fixes for the same.
Regards,
Vignesh
Attachments:
v13-0001-Identify-missing-publications-from-publisher-whi.patchapplication/x-patch; name=v13-0001-Identify-missing-publications-from-publisher-whi.patchDownload
From f85203bf95359dac6fb2eea3b973be7ceaadf4e1 Mon Sep 17 00:00:00 2001
From: Vignesh C <vignesh21@gmail.com>
Date: Tue, 9 Nov 2021 20:30:47 +0530
Subject: [PATCH v13] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 13 ++
doc/src/sgml/ref/create_subscription.sgml | 20 ++-
src/backend/commands/subscriptioncmds.c | 207 +++++++++++++++++++---
src/bin/psql/tab-complete.c | 14 +-
src/test/subscription/t/007_ddl.pl | 56 +++++-
5 files changed, 276 insertions(+), 34 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 0b027cc346..95b6b6a0e5 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -168,6 +168,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index 990a41f1a1..8ec028ad7d 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -110,12 +110,14 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
command should connect to the publisher at all. The default
is <literal>true</literal>. Setting this to
<literal>false</literal> will force the values of
- <literal>create_slot</literal>, <literal>enabled</literal> and
- <literal>copy_data</literal> to <literal>false</literal>.
+ <literal>create_slot</literal>, <literal>enabled</literal>,
+ <literal>copy_data</literal> and
+ <literal>validate_publication</literal> to <literal>false</literal>.
(You cannot combine setting <literal>connect</literal>
to <literal>false</literal> with
setting <literal>create_slot</literal>, <literal>enabled</literal>,
- or <literal>copy_data</literal> to <literal>true</literal>.)
+ <literal>copy_data</literal> or
+ <literal>validate_publication</literal> to <literal>true</literal>.)
</para>
<para>
@@ -170,6 +172,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</para>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index c47ba26369..05efd734e7 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -60,6 +60,7 @@
#define SUBOPT_BINARY 0x00000080
#define SUBOPT_STREAMING 0x00000100
#define SUBOPT_TWOPHASE_COMMIT 0x00000200
+#define SUBOPT_VALIDATE_PUB 0x00000400
/* check if the 'val' has 'bits' set */
#define IsSet(val, bits) (((val) & (bits)) == (bits))
@@ -81,6 +82,7 @@ typedef struct SubOpts
bool binary;
bool streaming;
bool twophase;
+ bool validate_publication;
} SubOpts;
static List *fetch_table_list(WalReceiverConn *wrconn, List *publications);
@@ -128,6 +130,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->streaming = false;
if (IsSet(supported_opts, SUBOPT_TWOPHASE_COMMIT))
opts->twophase = false;
+ if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB))
+ opts->validate_publication = false;
/* Parse options */
foreach(lc, stmt_options)
@@ -247,6 +251,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->specified_opts |= SUBOPT_TWOPHASE_COMMIT;
opts->twophase = defGetBoolean(defel);
}
+ else if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ strcmp(defel->defname, "validate_publication") == 0)
+ {
+ if (IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ errorConflictingDefElem(defel, pstate);
+
+ opts->specified_opts |= SUBOPT_VALIDATE_PUB;
+ opts->validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -285,10 +298,19 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (opts->validate_publication &&
+ IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
opts->enabled = false;
opts->create_slot = false;
opts->copy_data = false;
+ opts->validate_publication = false;
}
/*
@@ -337,6 +359,140 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications,
+ bool validate_publication)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ if (!validate_publication)
+ return;
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err)));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data)));
+ }
+}
+
+/*
+ * Connect to the publisher and see if the given publication(s) is(are) present.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications,
+ bool validate_publication)
+{
+ char *err;
+ WalReceiverConn *wrconn;
+
+ if (!validate_publication)
+ return;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
+
+ PG_TRY();
+ {
+ check_publications(wrconn, publications, true);
+ }
+ PG_FINALLY();
+ {
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -396,7 +552,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
supported_opts = (SUBOPT_CONNECT | SUBOPT_ENABLED | SUBOPT_CREATE_SLOT |
SUBOPT_SLOT_NAME | SUBOPT_COPY_DATA |
SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
- SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT);
+ SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
+ SUBOPT_VALIDATE_PUB);
parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
/*
@@ -514,6 +671,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
PG_TRY();
{
+ check_publications(wrconn, publications, opts.validate_publication);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -606,7 +765,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -637,6 +797,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
PG_TRY();
{
+ check_publications(wrconn, sub->publications, validate_publication);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -959,7 +1121,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
case ALTER_SUBSCRIPTION_SET_PUBLICATION:
{
- supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH;
+ supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH | SUBOPT_VALIDATE_PUB;
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -968,6 +1130,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ connect_and_check_pubs(sub, stmt->publication,
+ opts.validate_publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -994,7 +1158,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -1007,6 +1171,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
bool isadd = stmt->kind == ALTER_SUBSCRIPTION_ADD_PUBLICATION;
supported_opts = SUBOPT_REFRESH | SUBOPT_COPY_DATA;
+ if (isadd)
+ supported_opts |= SUBOPT_VALIDATE_PUB;
+
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -1016,6 +1183,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd)
+ connect_and_check_pubs(sub, stmt->publication,
+ opts.validate_publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -1042,7 +1212,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Refresh the new list of publications. */
sub->publications = publist;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -1056,7 +1226,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
errmsg("ALTER SUBSCRIPTION ... REFRESH is not allowed for disabled subscriptions")));
parse_subscription_options(pstate, stmt->options,
- SUBOPT_COPY_DATA, &opts);
+ SUBOPT_COPY_DATA | SUBOPT_VALIDATE_PUB,
+ &opts);
/*
* The subscription option "two_phase" requires that
@@ -1084,7 +1255,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data,
+ opts.validate_publication);
break;
}
@@ -1548,28 +1720,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 4f724e4428..b8af071e4f 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1675,7 +1675,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1691,11 +1691,14 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny))
COMPLETE_WITH("WITH (");
- /* ALTER SUBSCRIPTION <name> ADD|DROP|SET PUBLICATION <name> WITH ( */
+ /* ALTER SUBSCRIPTION <name> ADD|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
- TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny, "WITH", "("))
+ TailMatches("ADD|SET", "PUBLICATION", MatchAny, "WITH", "("))
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
+ /* ALTER SUBSCRIPTION <name> DROP PUBLICATION <name> WITH ( */
+ else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
+ TailMatches("DROP", "PUBLICATION", MatchAny, "WITH", "("))
COMPLETE_WITH("copy_data", "refresh");
-
/* ALTER SCHEMA <name> */
else if (Matches("ALTER", "SCHEMA", MatchAny))
COMPLETE_WITH("OWNER TO", "RENAME TO");
@@ -2898,7 +2901,8 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
"enabled", "slot_name", "streaming",
- "synchronous_commit", "two_phase");
+ "synchronous_commit", "two_phase",
+ "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index 8c869c5c15..9dc7ff60a6 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -6,7 +6,7 @@ use strict;
use warnings;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
-use Test::More tests => 1;
+use Test::More tests => 7;
my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
$node_publisher->init(allows_streaming => 'logical');
@@ -41,5 +41,59 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# One of the specified publications exists.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription fails with single non-existent publication");
+
+# Specifying multiple non-existent publications.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription fails with multiple non-existent publications");
+
+# Specifying mutually exclusive options.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (CONNECT = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: connect = false and validate_publication = true are mutually exclusive options/,
+ "Create subscription fails with mutually exclusive options");
+
+# Specifying non-existent publication along with add publication.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql(
+ 'postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add publication fails with non-existent publication");
+
+# Specifying non-existent publication along with set publication.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription set publication fails with non-existent publication");
+
+# Specifying non-existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ m/ERROR: could not connect to the publisher/,
+ "Alter subscription set publication fails with connection to a non-existent database"
+);
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.30.2
On Sat, Nov 13, 2021 at 12:50 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, the attached v13 patch has the fixes for the same.
Thanks for the updated v13 patch. I have no further comments, it looks
good to me.
Regards,
Bharath Rupireddy.
Just wondering if we should also be detecting the incorrect conninfo
set with ALTER SUBSCRIPTION command as well. See below:
-- try creating a subscription with incorrect conninfo. the command fails.
postgres=# create subscription sub1 connection 'host=localhost
port=5490 dbname=postgres' publication pub1;
ERROR: could not connect to the publisher: connection to server at
"localhost" (::1), port 5490 failed: Connection refused
Is the server running on that host and accepting TCP/IP connections?
connection to server at "localhost" (127.0.0.1), port 5490 failed:
Connection refused
Is the server running on that host and accepting TCP/IP connections?
postgres=#
postgres=#
-- this time the conninfo is correct and the command succeeded.
postgres=# create subscription sub1 connection 'host=localhost
port=5432 dbname=postgres' publication pub1;
NOTICE: created replication slot "sub1" on publisher
CREATE SUBSCRIPTION
postgres=#
postgres=#
-- reset the connninfo in the subscription to some wrong value. the
command succeeds.
postgres=# alter subscription sub1 connection 'host=localhost
port=5490 dbname=postgres';
ALTER SUBSCRIPTION
postgres=#
postgres=# drop subscription sub1;
ERROR: could not connect to publisher when attempting to drop
replication slot "sub1": connection to server at "localhost" (::1),
port 5490 failed: Connection refused
Is the server running on that host and accepting TCP/IP connections?
connection to server at "localhost" (127.0.0.1), port 5490 failed:
Connection refused
Is the server running on that host and accepting TCP/IP connections?
HINT: Use ALTER SUBSCRIPTION ... SET (slot_name = NONE) to
disassociate the subscription from the slot.
==
When creating a subscription we do connect to the publisher node hence
the incorrect connection info gets detected. But that's not the case
with alter subscription.
--
With Regards,
Ashutosh Sharma.
On Sat, Nov 13, 2021 at 6:27 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
Show quoted text
On Sat, Nov 13, 2021 at 12:50 PM vignesh C <vignesh21@gmail.com> wrote:
Thanks for the comments, the attached v13 patch has the fixes for the same.
Thanks for the updated v13 patch. I have no further comments, it looks
good to me.Regards,
Bharath Rupireddy.
On Wed, Feb 9, 2022, at 12:06 PM, Ashutosh Sharma wrote:
Just wondering if we should also be detecting the incorrect conninfo
set with ALTER SUBSCRIPTION command as well. See below:-- try creating a subscription with incorrect conninfo. the command fails.
postgres=# create subscription sub1 connection 'host=localhost
port=5490 dbname=postgres' publication pub1;
ERROR: could not connect to the publisher: connection to server at
"localhost" (::1), port 5490 failed: Connection refused
Is the server running on that host and accepting TCP/IP connections?
connection to server at "localhost" (127.0.0.1), port 5490 failed:
Connection refused
Is the server running on that host and accepting TCP/IP connections?
That's because by default 'connect' parameter is true.
The important routine for all SUBSCRIPTION commands that handle connection
string is to validate the connection string e.g. check if all parameters are
correct. See walrcv_check_conninfo that calls PQconninfoParse.
The connection string is syntactically correct. Hence, no error. It could be
the case that the service is temporarily down. It is a useful and common
scenario that I wouldn't want to be forbid.
-- reset the connninfo in the subscription to some wrong value. the
command succeeds.
postgres=# alter subscription sub1 connection 'host=localhost
port=5490 dbname=postgres';
ALTER SUBSCRIPTION
postgres=#postgres=# drop subscription sub1;
ERROR: could not connect to publisher when attempting to drop
replication slot "sub1": connection to server at "localhost" (::1),
port 5490 failed: Connection refused
Is the server running on that host and accepting TCP/IP connections?
connection to server at "localhost" (127.0.0.1), port 5490 failed:
Connection refused
Is the server running on that host and accepting TCP/IP connections?
HINT: Use ALTER SUBSCRIPTION ... SET (slot_name = NONE) to
disassociate the subscription from the slot.
Again, dropping a subscription that is associated with a replication slot
requires a connection to remove the replication slot. If the publisher is gone
(and so the replication slot), follow the HINT advice.
--
Euler Taveira
EDB https://www.enterprisedb.com/
On Wed, Feb 9, 2022 at 11:53 PM Euler Taveira <euler@eulerto.com> wrote:
On Wed, Feb 9, 2022, at 12:06 PM, Ashutosh Sharma wrote:
Just wondering if we should also be detecting the incorrect conninfo
set with ALTER SUBSCRIPTION command as well. See below:-- try creating a subscription with incorrect conninfo. the command fails.
postgres=# create subscription sub1 connection 'host=localhost
port=5490 dbname=postgres' publication pub1;
ERROR: could not connect to the publisher: connection to server at
"localhost" (::1), port 5490 failed: Connection refused
Is the server running on that host and accepting TCP/IP connections?
connection to server at "localhost" (127.0.0.1), port 5490 failed:
Connection refused
Is the server running on that host and accepting TCP/IP connections?That's because by default 'connect' parameter is true.
So can we use this option with the ALTER SUBSCRIPTION command. I think
we can't, which means if the user sets wrong conninfo using ALTER
SUBSCRIPTION command then we don't have the option to detect it like
we have in case of CREATE SUBSCRIPTION command. Since this thread is
trying to add the ability to identify the wrong/missing publication
name specified with the ALTER SUBSCRIPTION command, can't we do the
same for the wrong conninfo?
--
With Regards,
Ashutosh Sharma.
I have spent little time looking at the latest patch. The patch looks
to be in good shape as it has already been reviewed by many people
here, although I did get some comments. Please take a look and let me
know your thoughts.
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ (errmsg("could not connect to the publisher: %s", err)));
I think it would be good to also include the errcode
(ERRCODE_CONNECTION_FAILURE) here?
--
@@ -514,6 +671,8 @@ CreateSubscription(ParseState *pstate,
CreateSubscriptionStmt *stmt,
PG_TRY();
{
+ check_publications(wrconn, publications, opts.validate_publication);
+
Instead of passing the opts.validate_publication argument to
check_publication function, why can't we first check if this option is
set or not and accordingly call check_publication function? For other
options I see that it has been done in the similar way for e.g. check
for opts.connect or opts.refresh or opts.enabled etc.
--
Above comment also applies for:
@@ -968,6 +1130,8 @@ AlterSubscription(ParseState *pstate,
AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ connect_and_check_pubs(sub, stmt->publication,
+ opts.validate_publication);
--
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publication doesn't exist. The default is
+ <literal>false</literal>.
publication -> publications (in the 4th line : throw an error if any
of the publication doesn't exist)
This applies for both CREATE and ALTER subscription commands.
--
With Regards,
Ashutosh Sharma.
Show quoted text
On Sat, Nov 13, 2021 at 12:50 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, Nov 10, 2021 at 11:16 AM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:On Tue, Nov 9, 2021 at 9:27 PM vignesh C <vignesh21@gmail.com> wrote:
Attached v12 version is rebased on top of Head.
Thanks for the patch. Here are some comments on v12:
1) I think ERRCODE_TOO_MANY_ARGUMENTS isn't the right error code, the ERRCODE_UNDEFINED_OBJECT is more meaningful. Please change. + ereport(ERROR, + (errcode(ERRCODE_TOO_MANY_ARGUMENTS), + errmsg_plural("publication %s does not exist in the publisher", + "publications %s do not exist in the publisher",The existing code using
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("subscription \"%s\" does not exist", subname)));Modified
2) Typo: It is "One of the specified publications exists."
+# One of the specified publication exist.Modified
3) I think we can remove the test case "+# Specified publication does
not exist." because the "+# One of the specified publication exist."
covers the code.Modified
4) Do we need the below test case? Even with refresh = false, it does call connect_and_check_pubs() right? Please remove it. +# Specified publication does not exist with refresh = false. +($ret, $stdout, $stderr) = $node_subscriber->psql('postgres', + "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)" +); +ok( $stderr =~ + m/ERROR: publication "non_existent_pub" does not exist in the publisher/, + "Alter subscription for non existent publication fails"); +Modified
5) Change the test case names to different ones instead of the same.
Have something like:
"Create subscription fails with single non-existent publication");
"Create subscription fails with multiple non-existent publications");
"Create subscription fails with mutually exclusive options");
"Alter subscription add publication fails with non-existent publication");
"Alter subscription set publication fails with non-existent publication");
"Alter subscription set publication fails with connection to a
non-existent database");Modified
Thanks for the comments, the attached v13 patch has the fixes for the same.
Regards,
Vignesh
On Thu, Feb 10, 2022 at 3:15 PM Ashutosh Sharma <ashu.coek88@gmail.com> wrote:
On Wed, Feb 9, 2022 at 11:53 PM Euler Taveira <euler@eulerto.com> wrote:
On Wed, Feb 9, 2022, at 12:06 PM, Ashutosh Sharma wrote:
Just wondering if we should also be detecting the incorrect conninfo
set with ALTER SUBSCRIPTION command as well. See below:-- try creating a subscription with incorrect conninfo. the command fails.
postgres=# create subscription sub1 connection 'host=localhost
port=5490 dbname=postgres' publication pub1;
ERROR: could not connect to the publisher: connection to server at
"localhost" (::1), port 5490 failed: Connection refused
Is the server running on that host and accepting TCP/IP connections?
connection to server at "localhost" (127.0.0.1), port 5490 failed:
Connection refused
Is the server running on that host and accepting TCP/IP connections?That's because by default 'connect' parameter is true.
So can we use this option with the ALTER SUBSCRIPTION command. I think
we can't, which means if the user sets wrong conninfo using ALTER
SUBSCRIPTION command then we don't have the option to detect it like
we have in case of CREATE SUBSCRIPTION command. Since this thread is
trying to add the ability to identify the wrong/missing publication
name specified with the ALTER SUBSCRIPTION command, can't we do the
same for the wrong conninfo?
I felt this can be extended once this feature is committed. Thoughts?
Regards,
Vignesh
On Fri, Feb 11, 2022 at 7:14 PM Ashutosh Sharma <ashu.coek88@gmail.com> wrote:
I have spent little time looking at the latest patch. The patch looks
to be in good shape as it has already been reviewed by many people
here, although I did get some comments. Please take a look and let me
know your thoughts.+ /* Try to connect to the publisher. */ + wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err); + if (!wrconn) + ereport(ERROR, + (errmsg("could not connect to the publisher: %s", err)));I think it would be good to also include the errcode
(ERRCODE_CONNECTION_FAILURE) here?
Modified
--
@@ -514,6 +671,8 @@ CreateSubscription(ParseState *pstate,
CreateSubscriptionStmt *stmt,PG_TRY(); { + check_publications(wrconn, publications, opts.validate_publication); +Instead of passing the opts.validate_publication argument to
check_publication function, why can't we first check if this option is
set or not and accordingly call check_publication function? For other
options I see that it has been done in the similar way for e.g. check
for opts.connect or opts.refresh or opts.enabled etc.
Modified
--
Above comment also applies for:
@@ -968,6 +1130,8 @@ AlterSubscription(ParseState *pstate,
AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;update_tuple = true; + connect_and_check_pubs(sub, stmt->publication, + opts.validate_publication);
Modified
--
+ <para> + When true, the command verifies if all the specified publications + that are being subscribed to are present in the publisher and throws + an error if any of the publication doesn't exist. The default is + <literal>false</literal>.publication -> publications (in the 4th line : throw an error if any
of the publication doesn't exist)This applies for both CREATE and ALTER subscription commands.
Modified
Thanks for the comments, the attached v14 patch has the changes for the same.
Regard,s
Vignesh
Attachments:
v14-0001-Identify-missing-publications-from-publisher-whi.patchtext/x-patch; charset=US-ASCII; name=v14-0001-Identify-missing-publications-from-publisher-whi.patchDownload
From a753ffcc3d9ab2aa87d76c52adff10f9192e3ff3 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sun, 13 Feb 2022 17:48:26 +0530
Subject: [PATCH v14] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
---
doc/src/sgml/ref/alter_subscription.sgml | 13 ++
doc/src/sgml/ref/create_subscription.sgml | 20 ++-
src/backend/commands/subscriptioncmds.c | 201 +++++++++++++++++++---
src/bin/psql/tab-complete.c | 14 +-
src/test/subscription/t/007_ddl.pl | 54 ++++++
5 files changed, 269 insertions(+), 33 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 0b027cc346..604bf66b65 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -168,6 +168,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publications doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index 990a41f1a1..cd87daeae7 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -110,12 +110,14 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
command should connect to the publisher at all. The default
is <literal>true</literal>. Setting this to
<literal>false</literal> will force the values of
- <literal>create_slot</literal>, <literal>enabled</literal> and
- <literal>copy_data</literal> to <literal>false</literal>.
+ <literal>create_slot</literal>, <literal>enabled</literal>,
+ <literal>copy_data</literal> and
+ <literal>validate_publication</literal> to <literal>false</literal>.
(You cannot combine setting <literal>connect</literal>
to <literal>false</literal> with
setting <literal>create_slot</literal>, <literal>enabled</literal>,
- or <literal>copy_data</literal> to <literal>true</literal>.)
+ <literal>copy_data</literal> or
+ <literal>validate_publication</literal> to <literal>true</literal>.)
</para>
<para>
@@ -170,6 +172,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publications doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</para>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 3ef6607d24..42309735f4 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -61,6 +61,7 @@
#define SUBOPT_BINARY 0x00000080
#define SUBOPT_STREAMING 0x00000100
#define SUBOPT_TWOPHASE_COMMIT 0x00000200
+#define SUBOPT_VALIDATE_PUB 0x00000400
/* check if the 'val' has 'bits' set */
#define IsSet(val, bits) (((val) & (bits)) == (bits))
@@ -82,6 +83,7 @@ typedef struct SubOpts
bool binary;
bool streaming;
bool twophase;
+ bool validate_publication;
} SubOpts;
static List *fetch_table_list(WalReceiverConn *wrconn, List *publications);
@@ -130,6 +132,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->streaming = false;
if (IsSet(supported_opts, SUBOPT_TWOPHASE_COMMIT))
opts->twophase = false;
+ if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB))
+ opts->validate_publication = false;
/* Parse options */
foreach(lc, stmt_options)
@@ -249,6 +253,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->specified_opts |= SUBOPT_TWOPHASE_COMMIT;
opts->twophase = defGetBoolean(defel);
}
+ else if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ strcmp(defel->defname, "validate_publication") == 0)
+ {
+ if (IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ errorConflictingDefElem(defel, pstate);
+
+ opts->specified_opts |= SUBOPT_VALIDATE_PUB;
+ opts->validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -284,10 +297,19 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (opts->validate_publication &&
+ IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
opts->enabled = false;
opts->create_slot = false;
opts->copy_data = false;
+ opts->validate_publication = false;
}
/*
@@ -331,6 +353,133 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data));
+ }
+}
+
+/*
+ * Connect to the publisher and see if the given publication(s) is(are) present.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications)
+{
+ char *err;
+ WalReceiverConn *wrconn;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ errcode(ERRCODE_CONNECTION_FAILURE),
+ errmsg("could not connect to the publisher: %s", err));
+
+ PG_TRY();
+ {
+ check_publications(wrconn, publications);
+ }
+ PG_FINALLY();
+ {
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -390,7 +539,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
supported_opts = (SUBOPT_CONNECT | SUBOPT_ENABLED | SUBOPT_CREATE_SLOT |
SUBOPT_SLOT_NAME | SUBOPT_COPY_DATA |
SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
- SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT);
+ SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
+ SUBOPT_VALIDATE_PUB);
parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
/*
@@ -508,6 +658,9 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
PG_TRY();
{
+ if (opts.validate_publication)
+ check_publications(wrconn, publications);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -600,7 +753,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -631,6 +785,9 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
PG_TRY();
{
+ if (validate_publication)
+ check_publications(wrconn, sub->publications);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -953,7 +1110,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
case ALTER_SUBSCRIPTION_SET_PUBLICATION:
{
- supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH;
+ supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH | SUBOPT_VALIDATE_PUB;
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -962,6 +1119,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (opts.validate_publication)
+ connect_and_check_pubs(sub, stmt->publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -988,7 +1147,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -1001,6 +1160,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
bool isadd = stmt->kind == ALTER_SUBSCRIPTION_ADD_PUBLICATION;
supported_opts = SUBOPT_REFRESH | SUBOPT_COPY_DATA;
+ if (isadd)
+ supported_opts |= SUBOPT_VALIDATE_PUB;
+
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -1010,6 +1172,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd && opts.validate_publication)
+ connect_and_check_pubs(sub, stmt->publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -1036,7 +1200,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Refresh the new list of publications. */
sub->publications = publist;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -1050,7 +1214,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
errmsg("ALTER SUBSCRIPTION ... REFRESH is not allowed for disabled subscriptions")));
parse_subscription_options(pstate, stmt->options,
- SUBOPT_COPY_DATA, &opts);
+ SUBOPT_COPY_DATA | SUBOPT_VALIDATE_PUB,
+ &opts);
/*
* The subscription option "two_phase" requires that
@@ -1078,7 +1243,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data,
+ opts.validate_publication);
break;
}
@@ -1557,28 +1723,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 9888227213..155648ceaf 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1814,7 +1814,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1830,11 +1830,14 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny))
COMPLETE_WITH("WITH (");
- /* ALTER SUBSCRIPTION <name> ADD|DROP|SET PUBLICATION <name> WITH ( */
+ /* ALTER SUBSCRIPTION <name> ADD|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
- TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny, "WITH", "("))
+ TailMatches("ADD|SET", "PUBLICATION", MatchAny, "WITH", "("))
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
+ /* ALTER SUBSCRIPTION <name> DROP PUBLICATION <name> WITH ( */
+ else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
+ TailMatches("DROP", "PUBLICATION", MatchAny, "WITH", "("))
COMPLETE_WITH("copy_data", "refresh");
-
/* ALTER SCHEMA <name> */
else if (Matches("ALTER", "SCHEMA", MatchAny))
COMPLETE_WITH("OWNER TO", "RENAME TO");
@@ -3079,7 +3082,8 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
"enabled", "slot_name", "streaming",
- "synchronous_commit", "two_phase");
+ "synchronous_commit", "two_phase",
+ "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index 1144b005f6..2936b12fda 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -41,6 +41,60 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# One of the specified publications exists.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription fails with single non-existent publication");
+
+# Specifying multiple non-existent publications.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription fails with multiple non-existent publications");
+
+# Specifying mutually exclusive options.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (CONNECT = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: connect = false and validate_publication = true are mutually exclusive options/,
+ "Create subscription fails with mutually exclusive options");
+
+# Specifying non-existent publication along with add publication.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql(
+ 'postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add publication fails with non-existent publication");
+
+# Specifying non-existent publication along with set publication.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription set publication fails with non-existent publication");
+
+# Specifying non-existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ m/ERROR: could not connect to the publisher/,
+ "Alter subscription set publication fails with connection to a non-existent database"
+);
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.30.2
On Sun, Feb 13, 2022 at 7:32 PM vignesh C <vignesh21@gmail.com> wrote:
On Thu, Feb 10, 2022 at 3:15 PM Ashutosh Sharma <ashu.coek88@gmail.com> wrote:
On Wed, Feb 9, 2022 at 11:53 PM Euler Taveira <euler@eulerto.com> wrote:
On Wed, Feb 9, 2022, at 12:06 PM, Ashutosh Sharma wrote:
Just wondering if we should also be detecting the incorrect conninfo
set with ALTER SUBSCRIPTION command as well. See below:-- try creating a subscription with incorrect conninfo. the command fails.
postgres=# create subscription sub1 connection 'host=localhost
port=5490 dbname=postgres' publication pub1;
ERROR: could not connect to the publisher: connection to server at
"localhost" (::1), port 5490 failed: Connection refused
Is the server running on that host and accepting TCP/IP connections?
connection to server at "localhost" (127.0.0.1), port 5490 failed:
Connection refused
Is the server running on that host and accepting TCP/IP connections?That's because by default 'connect' parameter is true.
So can we use this option with the ALTER SUBSCRIPTION command. I think
we can't, which means if the user sets wrong conninfo using ALTER
SUBSCRIPTION command then we don't have the option to detect it like
we have in case of CREATE SUBSCRIPTION command. Since this thread is
trying to add the ability to identify the wrong/missing publication
name specified with the ALTER SUBSCRIPTION command, can't we do the
same for the wrong conninfo?I felt this can be extended once this feature is committed. Thoughts?
I think that should be okay. I just wanted to share with you people to
know if it can be taken care of in this patch itself but it's ok if we
see it later.
--
With Regards,
Ashutosh Sharma.
Thanks for working on my review comments. I'll take a look at the new
changes and let you know my comments, if any. I didn't get a chance to
check it out today as I was busy reviewing some other patches, but
I'll definitely take a look at the new patch in a day or so and let
you know my feedback.
--
With Regards,
Ashutosh Sharma.
Show quoted text
On Sun, Feb 13, 2022 at 7:34 PM vignesh C <vignesh21@gmail.com> wrote:
On Fri, Feb 11, 2022 at 7:14 PM Ashutosh Sharma <ashu.coek88@gmail.com> wrote:
I have spent little time looking at the latest patch. The patch looks
to be in good shape as it has already been reviewed by many people
here, although I did get some comments. Please take a look and let me
know your thoughts.+ /* Try to connect to the publisher. */ + wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err); + if (!wrconn) + ereport(ERROR, + (errmsg("could not connect to the publisher: %s", err)));I think it would be good to also include the errcode
(ERRCODE_CONNECTION_FAILURE) here?Modified
--
@@ -514,6 +671,8 @@ CreateSubscription(ParseState *pstate,
CreateSubscriptionStmt *stmt,PG_TRY(); { + check_publications(wrconn, publications, opts.validate_publication); +Instead of passing the opts.validate_publication argument to
check_publication function, why can't we first check if this option is
set or not and accordingly call check_publication function? For other
options I see that it has been done in the similar way for e.g. check
for opts.connect or opts.refresh or opts.enabled etc.Modified
--
Above comment also applies for:
@@ -968,6 +1130,8 @@ AlterSubscription(ParseState *pstate,
AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;update_tuple = true; + connect_and_check_pubs(sub, stmt->publication, + opts.validate_publication);Modified
--
+ <para> + When true, the command verifies if all the specified publications + that are being subscribed to are present in the publisher and throws + an error if any of the publication doesn't exist. The default is + <literal>false</literal>.publication -> publications (in the 4th line : throw an error if any
of the publication doesn't exist)This applies for both CREATE and ALTER subscription commands.
Modified
Thanks for the comments, the attached v14 patch has the changes for the same.
Regard,s
Vignesh
Thanks for working on the review comments. The changes in the new
patch look good to me. I am marking it as ready to commit.
--
With Regards,
Ashutosh Sharma.
Show quoted text
On Sun, Feb 13, 2022 at 7:34 PM vignesh C <vignesh21@gmail.com> wrote:
On Fri, Feb 11, 2022 at 7:14 PM Ashutosh Sharma <ashu.coek88@gmail.com> wrote:
I have spent little time looking at the latest patch. The patch looks
to be in good shape as it has already been reviewed by many people
here, although I did get some comments. Please take a look and let me
know your thoughts.+ /* Try to connect to the publisher. */ + wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err); + if (!wrconn) + ereport(ERROR, + (errmsg("could not connect to the publisher: %s", err)));I think it would be good to also include the errcode
(ERRCODE_CONNECTION_FAILURE) here?Modified
--
@@ -514,6 +671,8 @@ CreateSubscription(ParseState *pstate,
CreateSubscriptionStmt *stmt,PG_TRY(); { + check_publications(wrconn, publications, opts.validate_publication); +Instead of passing the opts.validate_publication argument to
check_publication function, why can't we first check if this option is
set or not and accordingly call check_publication function? For other
options I see that it has been done in the similar way for e.g. check
for opts.connect or opts.refresh or opts.enabled etc.Modified
--
Above comment also applies for:
@@ -968,6 +1130,8 @@ AlterSubscription(ParseState *pstate,
AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;update_tuple = true; + connect_and_check_pubs(sub, stmt->publication, + opts.validate_publication);Modified
--
+ <para> + When true, the command verifies if all the specified publications + that are being subscribed to are present in the publisher and throws + an error if any of the publication doesn't exist. The default is + <literal>false</literal>.publication -> publications (in the 4th line : throw an error if any
of the publication doesn't exist)This applies for both CREATE and ALTER subscription commands.
Modified
Thanks for the comments, the attached v14 patch has the changes for the same.
Regard,s
Vignesh
On 2022-02-13 19:34:05 +0530, vignesh C wrote:
Thanks for the comments, the attached v14 patch has the changes for the same.
The patch needs a rebase, it currently fails to apply:
http://cfbot.cputube.org/patch_37_2957.log
On Tue, Mar 22, 2022 at 5:29 AM Andres Freund <andres@anarazel.de> wrote:
On 2022-02-13 19:34:05 +0530, vignesh C wrote:
Thanks for the comments, the attached v14 patch has the changes for the same.
The patch needs a rebase, it currently fails to apply:
http://cfbot.cputube.org/patch_37_2957.log
The attached v15 patch is rebased on top of HEAD.
Regards,
Vignesh
Attachments:
v15-0001-Identify-missing-publications-from-publisher-whi.patchtext/x-patch; charset=US-ASCII; name=v15-0001-Identify-missing-publications-from-publisher-whi.patchDownload
From fa175c7c823dc9fbcca7676ddec944430da81022 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Tue, 22 Mar 2022 15:09:13 +0530
Subject: [PATCH v15] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
Author: Vignesh C
Reviewed-by: Bharath Rupireddy, Japin Li, Dilip Kumar, Euler Taveira, Ashutosh Sharma
Discussion: https://www.postgresql.org/message-id/flat/20220321235957.i4jtjn4wyjucex6b%40alap3.anarazel.de#b846aaaafd4ef657cfaa8c9890f044e4
---
doc/src/sgml/ref/alter_subscription.sgml | 13 ++
doc/src/sgml/ref/create_subscription.sgml | 20 ++-
src/backend/commands/subscriptioncmds.c | 201 +++++++++++++++++++---
src/bin/psql/tab-complete.c | 15 +-
src/test/subscription/t/007_ddl.pl | 54 ++++++
5 files changed, 270 insertions(+), 33 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index ac2db249cb..995c1f270d 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -172,6 +172,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publications doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index b701752fc9..f2e7e8744d 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -110,12 +110,14 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
command should connect to the publisher at all. The default
is <literal>true</literal>. Setting this to
<literal>false</literal> will force the values of
- <literal>create_slot</literal>, <literal>enabled</literal> and
- <literal>copy_data</literal> to <literal>false</literal>.
+ <literal>create_slot</literal>, <literal>enabled</literal>,
+ <literal>copy_data</literal> and
+ <literal>validate_publication</literal> to <literal>false</literal>.
(You cannot combine setting <literal>connect</literal>
to <literal>false</literal> with
setting <literal>create_slot</literal>, <literal>enabled</literal>,
- or <literal>copy_data</literal> to <literal>true</literal>.)
+ <literal>copy_data</literal> or
+ <literal>validate_publication</literal> to <literal>true</literal>.)
</para>
<para>
@@ -170,6 +172,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publications doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</para>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index e16f04626d..6c066a1dfc 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -64,6 +64,7 @@
#define SUBOPT_TWOPHASE_COMMIT 0x00000200
#define SUBOPT_DISABLE_ON_ERR 0x00000400
#define SUBOPT_LSN 0x00000800
+#define SUBOPT_VALIDATE_PUB 0x00001000
/* check if the 'val' has 'bits' set */
#define IsSet(val, bits) (((val) & (bits)) == (bits))
@@ -86,6 +87,7 @@ typedef struct SubOpts
bool streaming;
bool twophase;
bool disableonerr;
+ bool validate_publication;
XLogRecPtr lsn;
} SubOpts;
@@ -137,6 +139,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->twophase = false;
if (IsSet(supported_opts, SUBOPT_DISABLE_ON_ERR))
opts->disableonerr = false;
+ if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB))
+ opts->validate_publication = false;
/* Parse options */
foreach(lc, stmt_options)
@@ -292,6 +296,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->specified_opts |= SUBOPT_LSN;
opts->lsn = lsn;
}
+ else if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ strcmp(defel->defname, "validate_publication") == 0)
+ {
+ if (IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ errorConflictingDefElem(defel, pstate);
+
+ opts->specified_opts |= SUBOPT_VALIDATE_PUB;
+ opts->validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -327,10 +340,19 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (opts->validate_publication &&
+ IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
opts->enabled = false;
opts->create_slot = false;
opts->copy_data = false;
+ opts->validate_publication = false;
}
/*
@@ -374,6 +396,133 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data));
+ }
+}
+
+/*
+ * Connect to the publisher and see if the given publication(s) is(are) present.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications)
+{
+ char *err;
+ WalReceiverConn *wrconn;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ errcode(ERRCODE_CONNECTION_FAILURE),
+ errmsg("could not connect to the publisher: %s", err));
+
+ PG_TRY();
+ {
+ check_publications(wrconn, publications);
+ }
+ PG_FINALLY();
+ {
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -434,7 +583,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
SUBOPT_SLOT_NAME | SUBOPT_COPY_DATA |
SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
- SUBOPT_DISABLE_ON_ERR);
+ SUBOPT_DISABLE_ON_ERR | SUBOPT_VALIDATE_PUB);
+
parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
/*
@@ -554,6 +704,9 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
PG_TRY();
{
+ if (opts.validate_publication)
+ check_publications(wrconn, publications);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -646,7 +799,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -677,6 +831,9 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
PG_TRY();
{
+ if (validate_publication)
+ check_publications(wrconn, sub->publications);
+
/* Get the table list from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
@@ -1007,7 +1164,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
case ALTER_SUBSCRIPTION_SET_PUBLICATION:
{
- supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH;
+ supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH | SUBOPT_VALIDATE_PUB;
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -1016,6 +1173,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (opts.validate_publication)
+ connect_and_check_pubs(sub, stmt->publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -1042,7 +1201,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -1055,6 +1214,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
bool isadd = stmt->kind == ALTER_SUBSCRIPTION_ADD_PUBLICATION;
supported_opts = SUBOPT_REFRESH | SUBOPT_COPY_DATA;
+ if (isadd)
+ supported_opts |= SUBOPT_VALIDATE_PUB;
+
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -1064,6 +1226,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd && opts.validate_publication)
+ connect_and_check_pubs(sub, stmt->publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -1090,7 +1254,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Refresh the new list of publications. */
sub->publications = publist;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -1104,7 +1268,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
errmsg("ALTER SUBSCRIPTION ... REFRESH is not allowed for disabled subscriptions")));
parse_subscription_options(pstate, stmt->options,
- SUBOPT_COPY_DATA, &opts);
+ SUBOPT_COPY_DATA | SUBOPT_VALIDATE_PUB,
+ &opts);
/*
* The subscription option "two_phase" requires that
@@ -1132,7 +1297,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data,
+ opts.validate_publication);
break;
}
@@ -1653,28 +1819,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 5c064595a9..fa304b68ea 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1861,7 +1861,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1880,11 +1880,15 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny))
COMPLETE_WITH("WITH (");
- /* ALTER SUBSCRIPTION <name> ADD|DROP|SET PUBLICATION <name> WITH ( */
+ /* ALTER SUBSCRIPTION <name> ADD|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
- TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny, "WITH", "("))
- COMPLETE_WITH("copy_data", "refresh");
+ TailMatches("ADD|SET", "PUBLICATION", MatchAny, "WITH", "("))
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
+ /* ALTER SUBSCRIPTION <name> DROP PUBLICATION <name> WITH ( */
+ else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
+ TailMatches("DROP", "PUBLICATION", MatchAny, "WITH", "("))
+ COMPLETE_WITH("copy_data", "refresh");
/* ALTER SCHEMA <name> */
else if (Matches("ALTER", "SCHEMA", MatchAny))
COMPLETE_WITH("OWNER TO", "RENAME TO");
@@ -3145,7 +3149,8 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
"enabled", "slot_name", "streaming",
- "synchronous_commit", "two_phase", "disable_on_error");
+ "synchronous_commit", "two_phase", "disable_on_error",
+ "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index 1144b005f6..2936b12fda 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -41,6 +41,60 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# One of the specified publications exists.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription fails with single non-existent publication");
+
+# Specifying multiple non-existent publications.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription fails with multiple non-existent publications");
+
+# Specifying mutually exclusive options.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (CONNECT = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: connect = false and validate_publication = true are mutually exclusive options/,
+ "Create subscription fails with mutually exclusive options");
+
+# Specifying non-existent publication along with add publication.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql(
+ 'postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add publication fails with non-existent publication");
+
+# Specifying non-existent publication along with set publication.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription set publication fails with non-existent publication");
+
+# Specifying non-existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ m/ERROR: could not connect to the publisher/,
+ "Alter subscription set publication fails with connection to a non-existent database"
+);
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.32.0
On Tue, Mar 22, 2022 at 3:23 PM vignesh C <vignesh21@gmail.com> wrote:
On Tue, Mar 22, 2022 at 5:29 AM Andres Freund <andres@anarazel.de> wrote:
On 2022-02-13 19:34:05 +0530, vignesh C wrote:
Thanks for the comments, the attached v14 patch has the changes for the same.
The patch needs a rebase, it currently fails to apply:
http://cfbot.cputube.org/patch_37_2957.log
The patch was not applying on HEAD, attached patch which is rebased on
top of HEAD.
Regards,
Vignesh
Attachments:
v15-0001-Identify-missing-publications-from-publisher-whi.patchtext/x-patch; charset=US-ASCII; name=v15-0001-Identify-missing-publications-from-publisher-whi.patchDownload
From 1c4096fc1c2b3cfa54d225c185a9e85bab114d8a Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 26 Mar 2022 19:43:27 +0530
Subject: [PATCH v15] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful when we specify a publication which
does not exist in the publisher. This patch checks if the specified publications
are present in the publisher and throws an error if any of the publication is
missing in the publisher.
Author: Vignesh C
Reviewed-by: Bharath Rupireddy, Japin Li, Dilip Kumar, Euler Taveira, Ashutosh Sharma
Discussion: https://www.postgresql.org/message-id/flat/20220321235957.i4jtjn4wyjucex6b%40alap3.anarazel.de#b846aaaafd4ef657cfaa8c9890f044e4
---
doc/src/sgml/ref/alter_subscription.sgml | 13 ++
doc/src/sgml/ref/create_subscription.sgml | 20 ++-
src/backend/commands/subscriptioncmds.c | 201 +++++++++++++++++++---
src/bin/psql/tab-complete.c | 15 +-
src/test/subscription/t/007_ddl.pl | 54 ++++++
5 files changed, 270 insertions(+), 33 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 3e46bbdb04..9f6a029600 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -172,6 +172,19 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publications doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist></para>
</listitem>
</varlistentry>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index b701752fc9..f2e7e8744d 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -110,12 +110,14 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
command should connect to the publisher at all. The default
is <literal>true</literal>. Setting this to
<literal>false</literal> will force the values of
- <literal>create_slot</literal>, <literal>enabled</literal> and
- <literal>copy_data</literal> to <literal>false</literal>.
+ <literal>create_slot</literal>, <literal>enabled</literal>,
+ <literal>copy_data</literal> and
+ <literal>validate_publication</literal> to <literal>false</literal>.
(You cannot combine setting <literal>connect</literal>
to <literal>false</literal> with
setting <literal>create_slot</literal>, <literal>enabled</literal>,
- or <literal>copy_data</literal> to <literal>true</literal>.)
+ <literal>copy_data</literal> or
+ <literal>validate_publication</literal> to <literal>true</literal>.)
</para>
<para>
@@ -170,6 +172,18 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
</para>
</listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><literal>validate_publication</literal> (<type>boolean</type>)</term>
+ <listitem>
+ <para>
+ When true, the command verifies if all the specified publications
+ that are being subscribed to are present in the publisher and throws
+ an error if any of the publications doesn't exist. The default is
+ <literal>false</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</para>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index abebffdf3b..ba6c2693d4 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -64,6 +64,7 @@
#define SUBOPT_TWOPHASE_COMMIT 0x00000200
#define SUBOPT_DISABLE_ON_ERR 0x00000400
#define SUBOPT_LSN 0x00000800
+#define SUBOPT_VALIDATE_PUB 0x00001000
/* check if the 'val' has 'bits' set */
#define IsSet(val, bits) (((val) & (bits)) == (bits))
@@ -86,6 +87,7 @@ typedef struct SubOpts
bool streaming;
bool twophase;
bool disableonerr;
+ bool validate_publication;
XLogRecPtr lsn;
} SubOpts;
@@ -138,6 +140,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->twophase = false;
if (IsSet(supported_opts, SUBOPT_DISABLE_ON_ERR))
opts->disableonerr = false;
+ if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB))
+ opts->validate_publication = false;
/* Parse options */
foreach(lc, stmt_options)
@@ -293,6 +297,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->specified_opts |= SUBOPT_LSN;
opts->lsn = lsn;
}
+ else if (IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ strcmp(defel->defname, "validate_publication") == 0)
+ {
+ if (IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ errorConflictingDefElem(defel, pstate);
+
+ opts->specified_opts |= SUBOPT_VALIDATE_PUB;
+ opts->validate_publication = defGetBoolean(defel);
+ }
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -328,10 +341,19 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
errmsg("%s and %s are mutually exclusive options",
"connect = false", "copy_data = true")));
+ if (opts->validate_publication &&
+ IsSet(supported_opts, SUBOPT_VALIDATE_PUB) &&
+ IsSet(opts->specified_opts, SUBOPT_VALIDATE_PUB))
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("%s and %s are mutually exclusive options",
+ "connect = false", "validate_publication = true")));
+
/* Change the defaults of other options. */
opts->enabled = false;
opts->create_slot = false;
opts->copy_data = false;
+ opts->validate_publication = false;
}
/*
@@ -375,6 +397,133 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data));
+ }
+}
+
+/*
+ * Connect to the publisher and see if the given publication(s) is(are) present.
+ */
+static void
+connect_and_check_pubs(Subscription *sub, List *publications)
+{
+ char *err;
+ WalReceiverConn *wrconn;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ errcode(ERRCODE_CONNECTION_FAILURE),
+ errmsg("could not connect to the publisher: %s", err));
+
+ PG_TRY();
+ {
+ check_publications(wrconn, publications);
+ }
+ PG_FINALLY();
+ {
+ walrcv_disconnect(wrconn);
+ }
+ PG_END_TRY();
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -435,7 +584,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
SUBOPT_SLOT_NAME | SUBOPT_COPY_DATA |
SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
- SUBOPT_DISABLE_ON_ERR);
+ SUBOPT_DISABLE_ON_ERR | SUBOPT_VALIDATE_PUB);
+
parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
/*
@@ -555,6 +705,9 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
PG_TRY();
{
+ if (opts.validate_publication)
+ check_publications(wrconn, publications);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -650,7 +803,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -681,6 +835,9 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
PG_TRY();
{
+ if (validate_publication)
+ check_publications(wrconn, sub->publications);
+
/* Get the list of relations from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
pubrel_names = list_concat(pubrel_names,
@@ -1013,7 +1170,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
case ALTER_SUBSCRIPTION_SET_PUBLICATION:
{
- supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH;
+ supported_opts = SUBOPT_COPY_DATA | SUBOPT_REFRESH | SUBOPT_VALIDATE_PUB;
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -1022,6 +1179,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (opts.validate_publication)
+ connect_and_check_pubs(sub, stmt->publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -1048,7 +1207,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -1061,6 +1220,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
bool isadd = stmt->kind == ALTER_SUBSCRIPTION_ADD_PUBLICATION;
supported_opts = SUBOPT_REFRESH | SUBOPT_COPY_DATA;
+ if (isadd)
+ supported_opts |= SUBOPT_VALIDATE_PUB;
+
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -1070,6 +1232,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subpublications - 1] = true;
update_tuple = true;
+ if (isadd && opts.validate_publication)
+ connect_and_check_pubs(sub, stmt->publication);
/* Refresh if user asked us to. */
if (opts.refresh)
@@ -1096,7 +1260,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Refresh the new list of publications. */
sub->publications = publist;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
}
break;
@@ -1110,7 +1274,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
errmsg("ALTER SUBSCRIPTION ... REFRESH is not allowed for disabled subscriptions")));
parse_subscription_options(pstate, stmt->options,
- SUBOPT_COPY_DATA, &opts);
+ SUBOPT_COPY_DATA | SUBOPT_VALIDATE_PUB,
+ &opts);
/*
* The subscription option "two_phase" requires that
@@ -1138,7 +1303,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data,
+ opts.validate_publication);
break;
}
@@ -1659,28 +1825,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 63bfdf11c6..4331e02862 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -1865,7 +1865,7 @@ psql_completion(const char *text, int start, int end)
/* ALTER SUBSCRIPTION <name> REFRESH PUBLICATION WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("REFRESH", "PUBLICATION", "WITH", "("))
- COMPLETE_WITH("copy_data");
+ COMPLETE_WITH("copy_data", "validate_publication");
/* ALTER SUBSCRIPTION <name> SET */
else if (Matches("ALTER", "SUBSCRIPTION", MatchAny, "SET"))
COMPLETE_WITH("(", "PUBLICATION");
@@ -1884,11 +1884,15 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny))
COMPLETE_WITH("WITH (");
- /* ALTER SUBSCRIPTION <name> ADD|DROP|SET PUBLICATION <name> WITH ( */
+ /* ALTER SUBSCRIPTION <name> ADD|SET PUBLICATION <name> WITH ( */
else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
- TailMatches("ADD|DROP|SET", "PUBLICATION", MatchAny, "WITH", "("))
- COMPLETE_WITH("copy_data", "refresh");
+ TailMatches("ADD|SET", "PUBLICATION", MatchAny, "WITH", "("))
+ COMPLETE_WITH("copy_data", "refresh", "validate_publication");
+ /* ALTER SUBSCRIPTION <name> DROP PUBLICATION <name> WITH ( */
+ else if (HeadMatches("ALTER", "SUBSCRIPTION", MatchAny) &&
+ TailMatches("DROP", "PUBLICATION", MatchAny, "WITH", "("))
+ COMPLETE_WITH("copy_data", "refresh");
/* ALTER SCHEMA <name> */
else if (Matches("ALTER", "SCHEMA", MatchAny))
COMPLETE_WITH("OWNER TO", "RENAME TO");
@@ -3155,7 +3159,8 @@ psql_completion(const char *text, int start, int end)
else if (HeadMatches("CREATE", "SUBSCRIPTION") && TailMatches("WITH", "("))
COMPLETE_WITH("binary", "connect", "copy_data", "create_slot",
"enabled", "slot_name", "streaming",
- "synchronous_commit", "two_phase", "disable_on_error");
+ "synchronous_commit", "two_phase", "disable_on_error",
+ "validate_publication");
/* CREATE TRIGGER --- is allowed inside CREATE SCHEMA, so use TailMatches */
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index 1144b005f6..2936b12fda 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -41,6 +41,60 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# One of the specified publications exists.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription fails with single non-existent publication");
+
+# Specifying multiple non-existent publications.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription fails with multiple non-existent publications");
+
+# Specifying mutually exclusive options.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1 WITH (CONNECT = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: connect = false and validate_publication = true are mutually exclusive options/,
+ "Create subscription fails with mutually exclusive options");
+
+# Specifying non-existent publication along with add publication.
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+($ret, $stdout, $stderr) = ($ret, $stdout, $stderr) = $node_subscriber->psql(
+ 'postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add publication fails with non-existent publication");
+
+# Specifying non-existent publication along with set publication.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~
+ m/ERROR: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription set publication fails with non-existent publication");
+
+# Specifying non-existent database.
+$node_subscriber->safe_psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 CONNECTION 'dbname=regress_doesnotexist2'");
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub WITH (REFRESH = FALSE, VALIDATE_PUBLICATION = TRUE)"
+);
+ok( $stderr =~ m/ERROR: could not connect to the publisher/,
+ "Alter subscription set publication fails with connection to a non-existent database"
+);
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.32.0
On Sat, Mar 26, 2022 at 7:53 PM vignesh C <vignesh21@gmail.com> wrote:
The patch was not applying on HEAD, attached patch which is rebased on
top of HEAD.
IIUC, this patch provides an option that allows us to give an error if
while creating/altering subcsiction, user gives non-existant
publications. I am not sure how useful it is to add such behavior via
an option especially when we know that it can occur in some other ways
like after creating the subscription, users can independently drop
publication from publisher. I think it could be useful to provide
additional information here but it would be better if we can follow
Euler's suggestion [1]/messages/by-id/a2f2fba6-40dd-44cc-b40e-58196bb77f1c@www.fastmail.com in the thread where he suggested issuing a
WARNING if the publications don't exist and document that the
subscription catalog can have non-existent publications.
I think we should avoid adding new options unless they are really
required and useful.
[1]: /messages/by-id/a2f2fba6-40dd-44cc-b40e-58196bb77f1c@www.fastmail.com
--
With Regards,
Amit Kapila.
On Tue, Mar 29, 2022 at 11:01 AM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Sat, Mar 26, 2022 at 7:53 PM vignesh C <vignesh21@gmail.com> wrote:
The patch was not applying on HEAD, attached patch which is rebased on
top of HEAD.IIUC, this patch provides an option that allows us to give an error if
while creating/altering subcsiction, user gives non-existant
publications. I am not sure how useful it is to add such behavior via
an option especially when we know that it can occur in some other ways
like after creating the subscription, users can independently drop
publication from publisher. I think it could be useful to provide
additional information here but it would be better if we can follow
Euler's suggestion [1] in the thread where he suggested issuing a
WARNING if the publications don't exist and document that the
subscription catalog can have non-existent publications.I think we should avoid adding new options unless they are really
required and useful.
*
+connect_and_check_pubs(Subscription *sub, List *publications)
+{
+ char *err;
+ WalReceiverConn *wrconn;
+
+ /* Load the library providing us libpq calls. */
+ load_file("libpqwalreceiver", false);
+
+ /* Try to connect to the publisher. */
+ wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err);
+ if (!wrconn)
+ ereport(ERROR,
+ errcode(ERRCODE_CONNECTION_FAILURE),
+ errmsg("could not connect to the publisher: %s", err));
I think it won't be a good idea to add new failure modes in existing
commands especially if we decide to make it non-optional. I think we
can do this check only in case we are already connecting to the
publisher.
--
With Regards,
Amit Kapila.
On Tue, Mar 29, 2022 at 11:02 AM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Sat, Mar 26, 2022 at 7:53 PM vignesh C <vignesh21@gmail.com> wrote:
The patch was not applying on HEAD, attached patch which is rebased on
top of HEAD.IIUC, this patch provides an option that allows us to give an error if
while creating/altering subcsiction, user gives non-existant
publications. I am not sure how useful it is to add such behavior via
an option especially when we know that it can occur in some other ways
like after creating the subscription, users can independently drop
publication from publisher. I think it could be useful to provide
additional information here but it would be better if we can follow
Euler's suggestion [1] in the thread where he suggested issuing a
WARNING if the publications don't exist and document that the
subscription catalog can have non-existent publications.I think we should avoid adding new options unless they are really
required and useful.[1] - /messages/by-id/a2f2fba6-40dd-44cc-b40e-58196bb77f1c@www.fastmail.com
Thanks for the suggestion, I have changed the patch as suggested.
Attached v16 patch has the changes for the same.
Regards,
Vignesh
Attachments:
v16-0001-Identify-missing-publications-from-publisher-whi.patchtext/x-patch; charset=US-ASCII; name=v16-0001-Identify-missing-publications-from-publisher-whi.patchDownload
From 4232956ff31192e903736e921b92aac06243c171 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 26 Mar 2022 19:43:27 +0530
Subject: [PATCH v16] Identify missing publications from publisher while
create/alter subscription.
Creating/altering subscription is successful without throwing any warning when
we specify a publication which does not exist in the publisher. This patch
checks if the specified publications are present in the publisher and throws
a warning if any of the publication is missing in the publisher.
Author: Vignesh C
Reviewed-by: Amit Kapila, Bharath Rupireddy, Japin Li, Dilip Kumar, Euler Taveira, Ashutosh Sharma
Discussion: https://www.postgresql.org/message-id/flat/20220321235957.i4jtjn4wyjucex6b%40alap3.anarazel.de#b846aaaafd4ef657cfaa8c9890f044e4
---
doc/src/sgml/ref/create_subscription.sgml | 7 ++
src/backend/commands/subscriptioncmds.c | 132 ++++++++++++++++++----
src/test/subscription/t/007_ddl.pl | 61 ++++++++++
3 files changed, 178 insertions(+), 22 deletions(-)
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index b701752fc9..761d34116c 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -356,6 +356,13 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
copied data that would be incompatible with subsequent filtering.
</para>
+ <para>
+ <link linkend="catalog-pg-subscription"><structname>pg_subscription</structname></link>
+ can have non-existent publications if non-existent publication was
+ specified during <command>CREATE SUBSCRIPTION</command> or if an existing
+ publication was removed.
+ </para>
+
</refsect1>
<refsect1>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index abebffdf3b..6db88f8443 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -375,6 +375,103 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(WARNING,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data));
+ }
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -555,6 +652,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
PG_TRY();
{
+ check_publications(wrconn, publications);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -650,7 +749,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ bool validate_publication)
{
char *err;
List *pubrel_names;
@@ -681,6 +781,9 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
PG_TRY();
{
+ if (validate_publication)
+ check_publications(wrconn, sub->publications);
+
/* Get the list of relations from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
pubrel_names = list_concat(pubrel_names,
@@ -1048,7 +1151,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, true);
}
break;
@@ -1096,7 +1199,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Refresh the new list of publications. */
sub->publications = publist;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, isadd);
}
break;
@@ -1138,7 +1241,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, false);
break;
}
@@ -1659,28 +1762,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
- " FROM pg_catalog.pg_publication_tables t\n"
- " WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ " FROM pg_catalog.pg_publication_tables t\n"
+ " WHERE t.pubname IN (");
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index 1144b005f6..c77ac5623b 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -41,6 +41,67 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# One of the specified publications exists.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub"
+);
+ok( $stderr =~
+ m/WARNING: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription throws warning for non-existent publication");
+
+$node_publisher->wait_for_catchup('mysub1');
+
+# Also wait for initial table sync to finish.
+my $synced_query =
+ "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+ or die "Timed out while waiting for subscriber to synchronize data";
+
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION mysub1;");
+
+# Specifying multiple non-existent publications.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1"
+);
+ok( $stderr =~
+ m/WARNING: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription throws warning for multiple non-existent publications");
+
+$node_publisher->wait_for_catchup('mysub1');
+
+# Also wait for initial table sync to finish.
+$node_subscriber->poll_query_until('postgres', $synced_query)
+ or die "Timed out while waiting for subscriber to synchronize data";
+
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION mysub1;");
+
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+
+$node_publisher->wait_for_catchup('mysub1');
+
+# Also wait for initial table sync to finish.
+$node_subscriber->poll_query_until('postgres', $synced_query)
+ or die "Timed out while waiting for subscriber to synchronize data";
+
+# Specifying non-existent publication along with add publication.
+($ret, $stdout, $stderr) = $node_subscriber->psql(
+ 'postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub"
+);
+ok( $stderr =~
+ m/WARNING: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add publication throws warning for non-existent publication");
+
+# Specifying non-existent publication along with set publication.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub"
+);
+ok( $stderr =~
+ m/WARNING: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription set publication throws warning for non-existent publication");
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.32.0
On Tue, Mar 29, 2022 at 4:12 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Mar 29, 2022 at 11:01 AM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Sat, Mar 26, 2022 at 7:53 PM vignesh C <vignesh21@gmail.com> wrote:
The patch was not applying on HEAD, attached patch which is rebased on
top of HEAD.IIUC, this patch provides an option that allows us to give an error if
while creating/altering subcsiction, user gives non-existant
publications. I am not sure how useful it is to add such behavior via
an option especially when we know that it can occur in some other ways
like after creating the subscription, users can independently drop
publication from publisher. I think it could be useful to provide
additional information here but it would be better if we can follow
Euler's suggestion [1] in the thread where he suggested issuing a
WARNING if the publications don't exist and document that the
subscription catalog can have non-existent publications.I think we should avoid adding new options unless they are really
required and useful.* +connect_and_check_pubs(Subscription *sub, List *publications) +{ + char *err; + WalReceiverConn *wrconn; + + /* Load the library providing us libpq calls. */ + load_file("libpqwalreceiver", false); + + /* Try to connect to the publisher. */ + wrconn = walrcv_connect(sub->conninfo, true, sub->name, &err); + if (!wrconn) + ereport(ERROR, + errcode(ERRCODE_CONNECTION_FAILURE), + errmsg("could not connect to the publisher: %s", err));I think it won't be a good idea to add new failure modes in existing
commands especially if we decide to make it non-optional. I think we
can do this check only in case we are already connecting to the
publisher.
I have modified it to check only in create subscription/alter
subscription .. add publication and alter subscription.. set
publication cases where we are connecting to the publisher.
The changes for the same are present at v16 patch attached at [1]/messages/by-id/CALDaNm2zHd9FAn+MAQ3x-2+Rnu8=Ru+BeQXokfNBKo6sNAVb3A@mail.gmail.com.
[1]: /messages/by-id/CALDaNm2zHd9FAn+MAQ3x-2+Rnu8=Ru+BeQXokfNBKo6sNAVb3A@mail.gmail.com
Regards,
Vignesh
On Tue, Mar 29, 2022 at 8:11 PM vignesh C <vignesh21@gmail.com> wrote:
On Tue, Mar 29, 2022 at 11:02 AM Amit Kapila <amit.kapila16@gmail.com> wrote:
Thanks for the suggestion, I have changed the patch as suggested.
Attached v16 patch has the changes for the same.
Thanks, I have one more comment.
postgres=# Alter subscription sub1 add publication pub4;
WARNING: publications "pub2", "pub4" do not exist in the publisher
ALTER SUBSCRIPTION
This gives additional publication in WARNING message which was not
part of current command but is present from the earlier time.
postgres=# Alter Subscription sub1 set publication pub5;
WARNING: publication "pub5" does not exist in the publisher
ALTER SUBSCRIPTION
SET variant doesn't give such a problem.
I feel we should be consistent here.
--
With Regards,
Amit Kapila.
On Wed, Mar 30, 2022 at 11:22 AM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Tue, Mar 29, 2022 at 8:11 PM vignesh C <vignesh21@gmail.com> wrote:
On Tue, Mar 29, 2022 at 11:02 AM Amit Kapila <amit.kapila16@gmail.com> wrote:
Thanks for the suggestion, I have changed the patch as suggested.
Attached v16 patch has the changes for the same.Thanks, I have one more comment.
postgres=# Alter subscription sub1 add publication pub4;
WARNING: publications "pub2", "pub4" do not exist in the publisher
ALTER SUBSCRIPTIONThis gives additional publication in WARNING message which was not
part of current command but is present from the earlier time.postgres=# Alter Subscription sub1 set publication pub5;
WARNING: publication "pub5" does not exist in the publisher
ALTER SUBSCRIPTIONSET variant doesn't give such a problem.
I feel we should be consistent here.
I have made the changes for this, attached v17 patch has the changes
for the same.
Regards,
Vignesh
Attachments:
v17-0001-Raise-WARNING-for-missing-publications.patchtext/x-patch; charset=US-ASCII; name=v17-0001-Raise-WARNING-for-missing-publications.patchDownload
From ded1d899a8edd4f970a4aec3bab54f067c0f3c58 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 26 Mar 2022 19:43:27 +0530
Subject: [PATCH v17] Raise WARNING for missing publications.
When we create or alter a subscription to add publications raise a warning
for non-existent publications. We don't want to give an error here because
it is possible that users can later create the missing publications.
Author: Vignesh C
Reviewed-by: Amit Kapila, Bharath Rupireddy, Japin Li, Dilip Kumar, Euler Taveira, Ashutosh Sharma
Discussion: https://www.postgresql.org/message-id/flat/20220321235957.i4jtjn4wyjucex6b%40alap3.anarazel.de#b846aaaafd4ef657cfaa8c9890f044e4
---
doc/src/sgml/ref/create_subscription.sgml | 7 ++
src/backend/commands/subscriptioncmds.c | 139 ++++++++++++++++++----
src/test/subscription/t/007_ddl.pl | 61 ++++++++++
3 files changed, 187 insertions(+), 20 deletions(-)
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index b701752fc9..761d34116c 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -356,6 +356,13 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
copied data that would be incompatible with subsequent filtering.
</para>
+ <para>
+ <link linkend="catalog-pg-subscription"><structname>pg_subscription</structname></link>
+ can have non-existent publications if non-existent publication was
+ specified during <command>CREATE SUBSCRIPTION</command> or if an existing
+ publication was removed.
+ </para>
+
</refsect1>
<refsect1>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index abebffdf3b..d0853db835 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -375,6 +375,103 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(WARNING,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data));
+ }
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -555,6 +652,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
PG_TRY();
{
+ check_publications(wrconn, publications);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -650,7 +749,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ List *validate_publications)
{
char *err;
List *pubrel_names;
@@ -681,6 +781,9 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
PG_TRY();
{
+ if (validate_publications)
+ check_publications(wrconn, validate_publications);
+
/* Get the list of relations from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
pubrel_names = list_concat(pubrel_names,
@@ -1048,7 +1151,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data,
+ stmt->publication);
}
break;
@@ -1074,6 +1178,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Refresh if user asked us to. */
if (opts.refresh)
{
+ List *validate_publications = (isadd) ? stmt->publication : NULL;
+
if (!sub->enabled)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
@@ -1096,7 +1202,15 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Refresh the new list of publications. */
sub->publications = publist;
- AlterSubscription_refresh(sub, opts.copy_data);
+ /*
+ * Since sub->publications will have the merged list of
+ * existing publications and user specified publication,
+ * validate_publications having only the publications
+ * specified by user should be sent for validation in this
+ * case.
+ */
+ AlterSubscription_refresh(sub, opts.copy_data,
+ validate_publications);
}
break;
@@ -1138,7 +1252,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, NULL);
break;
}
@@ -1659,28 +1773,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
" FROM pg_catalog.pg_publication_tables t\n"
" WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index 1144b005f6..c77ac5623b 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -41,6 +41,67 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# One of the specified publications exists.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub"
+);
+ok( $stderr =~
+ m/WARNING: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription throws warning for non-existent publication");
+
+$node_publisher->wait_for_catchup('mysub1');
+
+# Also wait for initial table sync to finish.
+my $synced_query =
+ "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+ or die "Timed out while waiting for subscriber to synchronize data";
+
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION mysub1;");
+
+# Specifying multiple non-existent publications.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1"
+);
+ok( $stderr =~
+ m/WARNING: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription throws warning for multiple non-existent publications");
+
+$node_publisher->wait_for_catchup('mysub1');
+
+# Also wait for initial table sync to finish.
+$node_subscriber->poll_query_until('postgres', $synced_query)
+ or die "Timed out while waiting for subscriber to synchronize data";
+
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION mysub1;");
+
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+
+$node_publisher->wait_for_catchup('mysub1');
+
+# Also wait for initial table sync to finish.
+$node_subscriber->poll_query_until('postgres', $synced_query)
+ or die "Timed out while waiting for subscriber to synchronize data";
+
+# Specifying non-existent publication along with add publication.
+($ret, $stdout, $stderr) = $node_subscriber->psql(
+ 'postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub"
+);
+ok( $stderr =~
+ m/WARNING: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add publication throws warning for non-existent publication");
+
+# Specifying non-existent publication along with set publication.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub"
+);
+ok( $stderr =~
+ m/WARNING: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription set publication throws warning for non-existent publication");
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.32.0
On Wed, Mar 30, 2022 at 12:22 PM vignesh C <vignesh21@gmail.com> wrote:
I have made the changes for this, attached v17 patch has the changes
for the same.
The patch looks good to me. I have made minor edits in the comments
and docs. See the attached and let me know what you think? I intend to
commit this tomorrow unless there are more comments or suggestions.
--
With Regards,
Amit Kapila.
Attachments:
v18-0001-Raise-WARNING-for-missing-publications.patchapplication/octet-stream; name=v18-0001-Raise-WARNING-for-missing-publications.patchDownload
From 978d637a0b612b605f8522992d8232987af5de82 Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 26 Mar 2022 19:43:27 +0530
Subject: [PATCH v18] Raise WARNING for missing publications.
When we create or alter a subscription to add publications raise a warning
for non-existent publications. We don't want to give an error here because
it is possible that users can later create the missing publications.
Author: Vignesh C
Reviewed-by: Amit Kapila, Bharath Rupireddy, Japin Li, Dilip Kumar, Euler Taveira, Ashutosh Sharma
Discussion: https://www.postgresql.org/message-id/flat/20220321235957.i4jtjn4wyjucex6b%40alap3.anarazel.de#b846aaaafd4ef657cfaa8c9890f044e4
---
doc/src/sgml/ref/alter_subscription.sgml | 4 +-
doc/src/sgml/ref/create_subscription.sgml | 7 ++
src/backend/commands/subscriptioncmds.c | 133 ++++++++++++++++++----
src/test/subscription/t/007_ddl.pl | 61 ++++++++++
4 files changed, 184 insertions(+), 21 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 3e46bbdb04..fe13ab9a2d 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -114,7 +114,9 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
replaces the entire list of publications with a new list,
<literal>ADD</literal> adds additional publications to the list of
publications, and <literal>DROP</literal> removes the publications from
- the list of publications. See <xref linkend="sql-createsubscription"/>
+ the list of publications. We allow non-existent publications to be
+ specified in <literal>ADD</literal> and <literal>SET</literal> variants
+ so that users can add those later. See <xref linkend="sql-createsubscription"/>
for more information. By default, this command will also act like
<literal>REFRESH PUBLICATION</literal>.
</para>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index b701752fc9..ebf7db57c5 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -356,6 +356,13 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
copied data that would be incompatible with subsequent filtering.
</para>
+ <para>
+ We allow non-existent publications to be specified so that users can add
+ those later. This means
+ <link linkend="catalog-pg-subscription"><structname>pg_subscription</structname></link>
+ can have non-existent publications.
+ </para>
+
</refsect1>
<refsect1>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index abebffdf3b..85dacbe93d 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -375,6 +375,103 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(WARNING,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data));
+ }
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -555,6 +652,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
PG_TRY();
{
+ check_publications(wrconn, publications);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -650,7 +749,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ List *validate_publications)
{
char *err;
List *pubrel_names;
@@ -681,6 +781,9 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
PG_TRY();
{
+ if (validate_publications)
+ check_publications(wrconn, validate_publications);
+
/* Get the list of relations from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
pubrel_names = list_concat(pubrel_names,
@@ -1048,7 +1151,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data,
+ stmt->publication);
}
break;
@@ -1074,6 +1178,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Refresh if user asked us to. */
if (opts.refresh)
{
+ /* We only need to validate user specified publications. */
+ List *validate_publications = (isadd) ? stmt->publication : NULL;
+
if (!sub->enabled)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
@@ -1096,7 +1203,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Refresh the new list of publications. */
sub->publications = publist;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data,
+ validate_publications);
}
break;
@@ -1138,7 +1246,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, NULL);
break;
}
@@ -1659,28 +1767,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
" FROM pg_catalog.pg_publication_tables t\n"
" WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index 1144b005f6..c77ac5623b 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -41,6 +41,67 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# One of the specified publications exists.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub"
+);
+ok( $stderr =~
+ m/WARNING: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription throws warning for non-existent publication");
+
+$node_publisher->wait_for_catchup('mysub1');
+
+# Also wait for initial table sync to finish.
+my $synced_query =
+ "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+ or die "Timed out while waiting for subscriber to synchronize data";
+
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION mysub1;");
+
+# Specifying multiple non-existent publications.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub, non_existent_pub1"
+);
+ok( $stderr =~
+ m/WARNING: publications "non_existent_pub", "non_existent_pub1" do not exist in the publisher/,
+ "Create subscription throws warning for multiple non-existent publications");
+
+$node_publisher->wait_for_catchup('mysub1');
+
+# Also wait for initial table sync to finish.
+$node_subscriber->poll_query_until('postgres', $synced_query)
+ or die "Timed out while waiting for subscriber to synchronize data";
+
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION mysub1;");
+
+$node_subscriber->safe_psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;"
+);
+
+$node_publisher->wait_for_catchup('mysub1');
+
+# Also wait for initial table sync to finish.
+$node_subscriber->poll_query_until('postgres', $synced_query)
+ or die "Timed out while waiting for subscriber to synchronize data";
+
+# Specifying non-existent publication along with add publication.
+($ret, $stdout, $stderr) = $node_subscriber->psql(
+ 'postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub"
+);
+ok( $stderr =~
+ m/WARNING: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription add publication throws warning for non-existent publication");
+
+# Specifying non-existent publication along with set publication.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub"
+);
+ok( $stderr =~
+ m/WARNING: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription set publication throws warning for non-existent publication");
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.28.0.windows.1
On Wed, Mar 30, 2022 at 4:29 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Mar 30, 2022 at 12:22 PM vignesh C <vignesh21@gmail.com> wrote:
I have made the changes for this, attached v17 patch has the changes
for the same.The patch looks good to me. I have made minor edits in the comments
and docs. See the attached and let me know what you think? I intend to
commit this tomorrow unless there are more comments or suggestions.
I have one minor comment:
+ "Create subscription throws warning for multiple non-existent publications");
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION mysub1;");
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr'
PUBLICATION mypub;"
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub"
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub"
Why should we drop the subscription mysub1 and create it for ALTER ..
ADD and ALTER .. SET tests? Can't we just do below which saves
unnecessary subscription creation, drop, wait_for_catchup and
poll_query_until?
+ "Create subscription throws warning for multiple non-existent publications");
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub2"
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub3"
Regards,
Bharath Rupireddy.
On Wed, Mar 30, 2022 at 5:37 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
On Wed, Mar 30, 2022 at 4:29 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Mar 30, 2022 at 12:22 PM vignesh C <vignesh21@gmail.com> wrote:
I have made the changes for this, attached v17 patch has the changes
for the same.The patch looks good to me. I have made minor edits in the comments
and docs. See the attached and let me know what you think? I intend to
commit this tomorrow unless there are more comments or suggestions.I have one minor comment:
+ "Create subscription throws warning for multiple non-existent publications"); +$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION mysub1;"); + "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;" + "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub" + "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub"Why should we drop the subscription mysub1 and create it for ALTER ..
ADD and ALTER .. SET tests? Can't we just do below which saves
unnecessary subscription creation, drop, wait_for_catchup and
poll_query_until?+ "Create subscription throws warning for multiple non-existent publications"); + "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub2" + "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub3"
Or I would even simplify the entire tests as follows:
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr'
PUBLICATION mypub, non_existent_pub1"
+ "Create subscription throws warning for non-existent publication");
no drop of mysub1 >>
+ "CREATE SUBSCRIPTION mysub2 CONNECTION '$publisher_connstr'
PUBLICATION non_existent_pub1, non_existent_pub2"
+ "Create subscription throws warning for multiple non-existent publications");
no drop of mysub2 >>
+ "ALTER SUBSCRIPTION mysub2 ADD PUBLICATION non_existent_pub3"
+ "Alter subscription add publication throws warning for non-existent
publication");
+ "ALTER SUBSCRIPTION mysub2 SET PUBLICATION non_existent_pub4"
+ "Alter subscription set publication throws warning for non-existent
publication");
$node_subscriber->stop;
$node_publisher->stop;
Regards,
Bharath Rupireddy.
On Wed, Mar 30, 2022 at 5:42 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:
On Wed, Mar 30, 2022 at 5:37 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:On Wed, Mar 30, 2022 at 4:29 PM Amit Kapila <amit.kapila16@gmail.com> wrote:
On Wed, Mar 30, 2022 at 12:22 PM vignesh C <vignesh21@gmail.com> wrote:
I have made the changes for this, attached v17 patch has the changes
for the same.The patch looks good to me. I have made minor edits in the comments
and docs. See the attached and let me know what you think? I intend to
commit this tomorrow unless there are more comments or suggestions.I have one minor comment:
+ "Create subscription throws warning for multiple non-existent publications"); +$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION mysub1;"); + "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub;" + "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub" + "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub"Why should we drop the subscription mysub1 and create it for ALTER ..
ADD and ALTER .. SET tests? Can't we just do below which saves
unnecessary subscription creation, drop, wait_for_catchup and
poll_query_until?+ "Create subscription throws warning for multiple non-existent publications"); + "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub2" + "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub3"Or I would even simplify the entire tests as follows:
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub1" + "Create subscription throws warning for non-existent publication");no drop of mysub1 >>
+ "CREATE SUBSCRIPTION mysub2 CONNECTION '$publisher_connstr' PUBLICATION non_existent_pub1, non_existent_pub2" + "Create subscription throws warning for multiple non-existent publications");no drop of mysub2 >>
+ "ALTER SUBSCRIPTION mysub2 ADD PUBLICATION non_existent_pub3" + "Alter subscription add publication throws warning for non-existent publication"); + "ALTER SUBSCRIPTION mysub2 SET PUBLICATION non_existent_pub4" + "Alter subscription set publication throws warning for non-existent publication"); $node_subscriber->stop; $node_publisher->stop;
Your suggestion looks valid, I have modified it as suggested.
Additionally I have removed Create subscription with multiple
non-existent publications and changed add publication with sing
non-existent publication to add publication with multiple non-existent
publications to cover the multiple non-existent publications path.
Attached v19 patch has the changes for the same.
Regards,
Vignesh
Attachments:
v19-0001-Raise-WARNING-for-missing-publications.patchtext/x-patch; charset=US-ASCII; name=v19-0001-Raise-WARNING-for-missing-publications.patchDownload
From fc175350780ca046e977c9b60d1f79873d19ae7e Mon Sep 17 00:00:00 2001
From: Vigneshwaran C <vignesh21@gmail.com>
Date: Sat, 26 Mar 2022 19:43:27 +0530
Subject: [PATCH v19] Raise WARNING for missing publications.
When we create or alter a subscription to add publications raise a warning
for non-existent publications. We don't want to give an error here because
it is possible that users can later create the missing publications.
Author: Vignesh C
Reviewed-by: Amit Kapila, Bharath Rupireddy, Japin Li, Dilip Kumar, Euler Taveira, Ashutosh Sharma
Discussion: https://www.postgresql.org/message-id/flat/20220321235957.i4jtjn4wyjucex6b%40alap3.anarazel.de#b846aaaafd4ef657cfaa8c9890f044e4
---
doc/src/sgml/ref/alter_subscription.sgml | 4 +-
doc/src/sgml/ref/create_subscription.sgml | 7 ++
src/backend/commands/subscriptioncmds.c | 133 ++++++++++++++++++----
src/test/subscription/t/007_ddl.pl | 37 ++++++
4 files changed, 160 insertions(+), 21 deletions(-)
diff --git a/doc/src/sgml/ref/alter_subscription.sgml b/doc/src/sgml/ref/alter_subscription.sgml
index 3e46bbdb04..fe13ab9a2d 100644
--- a/doc/src/sgml/ref/alter_subscription.sgml
+++ b/doc/src/sgml/ref/alter_subscription.sgml
@@ -114,7 +114,9 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
replaces the entire list of publications with a new list,
<literal>ADD</literal> adds additional publications to the list of
publications, and <literal>DROP</literal> removes the publications from
- the list of publications. See <xref linkend="sql-createsubscription"/>
+ the list of publications. We allow non-existent publications to be
+ specified in <literal>ADD</literal> and <literal>SET</literal> variants
+ so that users can add those later. See <xref linkend="sql-createsubscription"/>
for more information. By default, this command will also act like
<literal>REFRESH PUBLICATION</literal>.
</para>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index b701752fc9..ebf7db57c5 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -356,6 +356,13 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
copied data that would be incompatible with subsequent filtering.
</para>
+ <para>
+ We allow non-existent publications to be specified so that users can add
+ those later. This means
+ <link linkend="catalog-pg-subscription"><structname>pg_subscription</structname></link>
+ can have non-existent publications.
+ </para>
+
</refsect1>
<refsect1>
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index abebffdf3b..85dacbe93d 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -375,6 +375,103 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
}
}
+/*
+ * Add publication names from the list to a string.
+ */
+static void
+get_publications_str(List *publications, StringInfo dest, bool quote_literal)
+{
+ ListCell *lc;
+ bool first = true;
+
+ Assert(list_length(publications) > 0);
+
+ foreach(lc, publications)
+ {
+ char *pubname = strVal(lfirst(lc));
+
+ if (first)
+ first = false;
+ else
+ appendStringInfoString(dest, ", ");
+
+ if (quote_literal)
+ appendStringInfoString(dest, quote_literal_cstr(pubname));
+ else
+ {
+ appendStringInfoChar(dest, '"');
+ appendStringInfoString(dest, pubname);
+ appendStringInfoChar(dest, '"');
+ }
+ }
+}
+
+/*
+ * Check the specified publication(s) is(are) present in the publisher.
+ */
+static void
+check_publications(WalReceiverConn *wrconn, List *publications)
+{
+ WalRcvExecResult *res;
+ StringInfo cmd;
+ TupleTableSlot *slot;
+ List *publicationsCopy = NIL;
+ Oid tableRow[1] = {TEXTOID};
+
+ cmd = makeStringInfo();
+ appendStringInfoString(cmd, "SELECT t.pubname FROM\n"
+ " pg_catalog.pg_publication t WHERE\n"
+ " t.pubname IN (");
+ get_publications_str(publications, cmd, true);
+ appendStringInfoChar(cmd, ')');
+
+ res = walrcv_exec(wrconn, cmd->data, 1, tableRow);
+ pfree(cmd->data);
+ pfree(cmd);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ errmsg_plural("could not receive publication from the publisher: %s",
+ "could not receive list of publications from the publisher: %s",
+ list_length(publications),
+ res->err));
+
+ publicationsCopy = list_copy(publications);
+
+ /* Process publication(s). */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ /* Delete the publication present in publisher from the list. */
+ publicationsCopy = list_delete(publicationsCopy, makeString(pubname));
+ ExecClearTuple(slot);
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+
+ if (list_length(publicationsCopy))
+ {
+ /* Prepare the list of non-existent publication(s) for error message. */
+ StringInfo pubnames = makeStringInfo();
+
+ get_publications_str(publicationsCopy, pubnames, false);
+ ereport(WARNING,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg_plural("publication %s does not exist in the publisher",
+ "publications %s do not exist in the publisher",
+ list_length(publicationsCopy),
+ pubnames->data));
+ }
+}
+
/*
* Auxiliary function to build a text array out of a list of String nodes.
*/
@@ -555,6 +652,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
PG_TRY();
{
+ check_publications(wrconn, publications);
+
/*
* Set sync state based on if we were asked to do data copy or
* not.
@@ -650,7 +749,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
}
static void
-AlterSubscription_refresh(Subscription *sub, bool copy_data)
+AlterSubscription_refresh(Subscription *sub, bool copy_data,
+ List *validate_publications)
{
char *err;
List *pubrel_names;
@@ -681,6 +781,9 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data)
PG_TRY();
{
+ if (validate_publications)
+ check_publications(wrconn, validate_publications);
+
/* Get the list of relations from publisher. */
pubrel_names = fetch_table_list(wrconn, sub->publications);
pubrel_names = list_concat(pubrel_names,
@@ -1048,7 +1151,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Make sure refresh sees the new list of publications. */
sub->publications = stmt->publication;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data,
+ stmt->publication);
}
break;
@@ -1074,6 +1178,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Refresh if user asked us to. */
if (opts.refresh)
{
+ /* We only need to validate user specified publications. */
+ List *validate_publications = (isadd) ? stmt->publication : NULL;
+
if (!sub->enabled)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
@@ -1096,7 +1203,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Refresh the new list of publications. */
sub->publications = publist;
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data,
+ validate_publications);
}
break;
@@ -1138,7 +1246,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
PreventInTransactionBlock(isTopLevel, "ALTER SUBSCRIPTION ... REFRESH");
- AlterSubscription_refresh(sub, opts.copy_data);
+ AlterSubscription_refresh(sub, opts.copy_data, NULL);
break;
}
@@ -1659,28 +1767,13 @@ fetch_table_list(WalReceiverConn *wrconn, List *publications)
StringInfoData cmd;
TupleTableSlot *slot;
Oid tableRow[2] = {TEXTOID, TEXTOID};
- ListCell *lc;
- bool first;
List *tablelist = NIL;
- Assert(list_length(publications) > 0);
-
initStringInfo(&cmd);
appendStringInfoString(&cmd, "SELECT DISTINCT t.schemaname, t.tablename\n"
" FROM pg_catalog.pg_publication_tables t\n"
" WHERE t.pubname IN (");
- first = true;
- foreach(lc, publications)
- {
- char *pubname = strVal(lfirst(lc));
-
- if (first)
- first = false;
- else
- appendStringInfoString(&cmd, ", ");
-
- appendStringInfoString(&cmd, quote_literal_cstr(pubname));
- }
+ get_publications_str(publications, &cmd, true);
appendStringInfoChar(&cmd, ')');
res = walrcv_exec(wrconn, cmd.data, 2, tableRow);
diff --git a/src/test/subscription/t/007_ddl.pl b/src/test/subscription/t/007_ddl.pl
index 1144b005f6..39c32eda44 100644
--- a/src/test/subscription/t/007_ddl.pl
+++ b/src/test/subscription/t/007_ddl.pl
@@ -41,6 +41,43 @@ COMMIT;
pass "subscription disable and drop in same transaction did not hang";
+# One of the specified publications exists.
+my ($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "CREATE SUBSCRIPTION mysub1 CONNECTION '$publisher_connstr' PUBLICATION mypub, non_existent_pub"
+);
+ok( $stderr =~
+ m/WARNING: publication "non_existent_pub" does not exist in the publisher/,
+ "Create subscription throws warning for non-existent publication");
+
+$node_publisher->wait_for_catchup('mysub1');
+
+# Also wait for initial table sync to finish.
+my $synced_query =
+ "SELECT count(1) = 0 FROM pg_subscription_rel WHERE srsubstate NOT IN ('r', 's');";
+$node_subscriber->poll_query_until('postgres', $synced_query)
+ or die "Timed out while waiting for subscriber to synchronize data";
+
+# Also wait for initial table sync to finish.
+$node_subscriber->poll_query_until('postgres', $synced_query)
+ or die "Timed out while waiting for subscriber to synchronize data";
+
+# Specifying non-existent publication along with add publication.
+($ret, $stdout, $stderr) = $node_subscriber->psql(
+ 'postgres',
+ "ALTER SUBSCRIPTION mysub1 ADD PUBLICATION non_existent_pub1, non_existent_pub2"
+);
+ok( $stderr =~
+ m/WARNING: publications "non_existent_pub1", "non_existent_pub2" do not exist in the publisher/,
+ "Alter subscription add publication throws warning for non-existent publications");
+
+# Specifying non-existent publication along with set publication.
+($ret, $stdout, $stderr) = $node_subscriber->psql('postgres',
+ "ALTER SUBSCRIPTION mysub1 SET PUBLICATION non_existent_pub"
+);
+ok( $stderr =~
+ m/WARNING: publication "non_existent_pub" does not exist in the publisher/,
+ "Alter subscription set publication throws warning for non-existent publication");
+
$node_subscriber->stop;
$node_publisher->stop;
--
2.32.0
On Wed, Mar 30, 2022 at 9:54 PM vignesh C <vignesh21@gmail.com> wrote:
On Wed, Mar 30, 2022 at 5:42 PM Bharath Rupireddy
<bharath.rupireddyforpostgres@gmail.com> wrote:Your suggestion looks valid, I have modified it as suggested.
Additionally I have removed Create subscription with multiple
non-existent publications and changed add publication with sing
non-existent publication to add publication with multiple non-existent
publications to cover the multiple non-existent publications path.
Attached v19 patch has the changes for the same.
Pushed.
--
With Regards,
Amit Kapila.