On Thu, Nov 14, 2024 at 2:09 PM Peter Smith <smithpb2...@gmail.com> wrote:
>
> Hi Shubham,
>
> +1 for the patch idea.
>
> Improving this error message for subscriber-side generated columns
> will help to remove some confusion.
>
> Here are my review comments for patch v1-0001.
>
> ======
> Commit message.
>
> 1.
> The error message was misleading, as it failed to clarify that the replication
> of regular column on the publisher to the corresponding generated column on
> the subscriber is not supported.
>
> This patch improves the error handling and reporting mechanism to make it 
> clear
> that the replication of regular column on the subscriber is not supported,
> resolving the misleading "missing column" error.
>
> ~
>
> It makes no difference whether the publishing table column is regular
> or generated, so you should not be implying that this has anything to
> do with the replication of just regular columns. AFAIK, the *only*
> thing that matters is that you cannot replicate into a subscriber-side
> generated column or a subscriber-side missing column.
>
> The current master reports replication into either a generated or a
> missing column as the same "missing replication column" error. IIUC,
> the errors were "correct", although clearly, for the generated column
> case the error was quite misleading.
>
> So, this patch is really *only* to improve the error wording when
> attempting to replicate into a subscriber-side generated column.
> That's what the commit message should be conveying.
>
> ======
> src/backend/replication/logical/relation.c
>
> logicalrep_rel_open:
>
> 2.
>   Bitmapset  *missingatts;
> + StringInfoData gencolsattsbuf;
> + int generatedatts = 0;
> +
> + initStringInfo(&gencolsattsbuf);
>
> The existing "missing columns" error is implemented by building a BMS
> and then passing it to the function 'logicalrep_report_missing_attrs'
> to report the error.
>
> IMO the generated column error is essentially the same, so should be
> implemented with almost identical logic -- i.e. you should build a
> 'generatedattrs' BMS of generated cols with matching names and (if
> that BMS is not empty) then pass that to a new function
> 'logicalrep_report_generated_attrs' (a sibling function to the
> existing one).
>
> ~~~
>
> 3.
> + /*
> + * Check if the subscription table generated column has
> + * same name as a non-generated column in the
> + * corresponding publication table.
> + */
>
> This (misplaced) comment talks about checking if the names are the
> same. But I don't see any name-checking logic here (???). Where is it?
>
> ~~~
>
> 4.
> + ereport(ERROR,
> + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
> + errmsg_plural("replicating to a target relation's generated column
> \"%s\" for \"%s.%s\" is not supported",
> +    "replicating to a target relation's generated column \"%s\" for
> \"%s.%s\" is not supported",
> +    generatedatts, gencolsattsbuf.data, remoterel->nspname,
> remoterel->relname)));
>
> There are no plural differences here. This smells like a cut/paste
> mistake from logicalrep_report_generated_attrs'.
>
> IMO this error should close match the existing "missing replication
> columns" error, and use the errmsg_plural correctly. In other words,
> it should look something more like this:
>
> ereport(ERROR,
>   (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
>   errmsg_plural("cannot replicate to target relation \"%s.%s\"
> generated column: %s",
>                 "cannot replicate to target relation \"%s.%s\"
> generated columns: %s",
> ...
>
> ======
> src/test/subscription/t/011_generated.pl
>
> 5.
> +# 
> =============================================================================
> +# Exercise logical replication of a regular column to a subscriber side
> +# generated column.
> +#
> +# A "normal --> generated" replication fails, reporting an error that the
> +# replication of a generated column on subscriber side is not supported.
> +# 
> =============================================================================
> +
> +# --------------------------------------------------
> +# Test Case: normal --> generated
> +# Publisher table has regular columns 'c2' and 'c3'.
> +# Subscriber table has generated columns 'c2' and 'c3'.
> +# --------------------------------------------------
> +
>
> As I have said in previous internal reviews, this test (and the
> comments) can be much more sophisticated. AFAICT by cleverly arranging
> different publication table column types and different subscriber-side
> table column ordering I think you should be able to test multiple
> things at once.
>
> Such as
> - regular -> generated is detected
> - generated -> generated is detected
> - that the error only reports the generated column problems where the
> column names are matching, not others
>
> ~~~~
>
> 6.
> Also, as previously mentioned in internal reviews, this patch should
> include a 2nd test case to do pretty much the same testing but
> expecting to get a "missing replication column".
>
> The reasons to include this 2nd test are:
> a) The missing column was never tested properly before.
> b) This current patch has overlapping logic so you need to be assured
> that adding this new error doesn't break the existing one.
> c) Only one of these errors wins. Adding both tests will define the
> expected order if both error scenarios exist at the same time.
>

I have fixed the given comments. The attached Patch contains the
required changes.

Thanks and regards,
Shubham Khanna.
From 2886da4ad94bd2a4c0b99deaa21d57127b210a3e Mon Sep 17 00:00:00 2001
From: Shubham Khanna <shubham.khanna@fujitsu.com>
Date: Thu, 7 Nov 2024 15:54:19 +0530
Subject: [PATCH v2] Error-message-improvement

Currently, if logical replication attempts to target a subscriber-side table
column that is either missing or generated, it produces the following
identical error message:
ERROR: logical replication target relation \"%s.%s\" is missing
replicated columns: %s

While the error itself is valid, the message wording can be misleading for
generated columns. This patch introduces a distinct error message specifically
for the generated column scenario.
---
 src/backend/replication/logical/relation.c | 71 +++++++++++++-------
 src/test/subscription/t/011_generated.pl   | 76 ++++++++++++++++++++++
 2 files changed, 124 insertions(+), 23 deletions(-)

diff --git a/src/backend/replication/logical/relation.c b/src/backend/replication/logical/relation.c
index f5a0ef2bd9..276bf210bb 100644
--- a/src/backend/replication/logical/relation.c
+++ b/src/backend/replication/logical/relation.c
@@ -220,40 +220,52 @@ logicalrep_rel_att_by_name(LogicalRepRelation *remoterel, const char *attname)
 }
 
 /*
- * Report error with names of the missing local relation column(s), if any.
+ * Report error with names of the missing and generated local relation column(s), if any.
  */
 static void
-logicalrep_report_missing_attrs(LogicalRepRelation *remoterel,
-								Bitmapset *missingatts)
+logicalrep_report_missing_and_gen_attrs(LogicalRepRelation *remoterel,
+										Bitmapset *atts,
+										bool ismissing)
 {
-	if (!bms_is_empty(missingatts))
+	if (!bms_is_empty(atts))
 	{
-		StringInfoData missingattsbuf;
-		int			missingattcnt = 0;
+		StringInfoData attsbuf;
+		int			attcnt = 0;
 		int			i;
 
-		initStringInfo(&missingattsbuf);
+		initStringInfo(&attsbuf);
 
 		i = -1;
-		while ((i = bms_next_member(missingatts, i)) >= 0)
+		while ((i = bms_next_member(atts, i)) >= 0)
 		{
-			missingattcnt++;
-			if (missingattcnt == 1)
-				appendStringInfo(&missingattsbuf, _("\"%s\""),
+			attcnt++;
+			if (attcnt == 1)
+				appendStringInfo(&attsbuf, _("\"%s\""),
 								 remoterel->attnames[i]);
 			else
-				appendStringInfo(&missingattsbuf, _(", \"%s\""),
+				appendStringInfo(&attsbuf, _(", \"%s\""),
 								 remoterel->attnames[i]);
 		}
 
-		ereport(ERROR,
-				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
-				 errmsg_plural("logical replication target relation \"%s.%s\" is missing replicated column: %s",
-							   "logical replication target relation \"%s.%s\" is missing replicated columns: %s",
-							   missingattcnt,
-							   remoterel->nspname,
-							   remoterel->relname,
-							   missingattsbuf.data)));
+		if (ismissing)
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					 errmsg_plural("logical replication target relation \"%s.%s\" is missing replicated column: %s",
+								   "logical replication target relation \"%s.%s\" is missing replicated columns: %s",
+								   attcnt,
+								   remoterel->nspname,
+								   remoterel->relname,
+								   attsbuf.data)));
+
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					 errmsg_plural("cannot replicate to target relation \"%s.%s\" generated column: %s",
+								   "cannot replicate to target relation \"%s.%s\" generated columns: %s",
+								   attcnt,
+								   remoterel->nspname,
+								   remoterel->relname,
+								   attsbuf.data)));
 	}
 }
 
@@ -379,7 +391,9 @@ logicalrep_rel_open(LogicalRepRelId remoteid, LOCKMODE lockmode)
 		TupleDesc	desc;
 		MemoryContext oldctx;
 		int			i;
-		Bitmapset  *missingatts;
+		Bitmapset  *missingatts;	/* Bitmapset for missing attributes. */
+		Bitmapset  *generatedattrs = NULL;	/* Bitmapset for generated
+											 * attributes. */
 
 		/* Release the no-longer-useful attrmap, if any. */
 		if (entry->attrmap)
@@ -421,7 +435,7 @@ logicalrep_rel_open(LogicalRepRelId remoteid, LOCKMODE lockmode)
 			int			attnum;
 			Form_pg_attribute attr = TupleDescAttr(desc, i);
 
-			if (attr->attisdropped || attr->attgenerated)
+			if (attr->attisdropped)
 			{
 				entry->attrmap->attnums[i] = -1;
 				continue;
@@ -432,12 +446,23 @@ logicalrep_rel_open(LogicalRepRelId remoteid, LOCKMODE lockmode)
 
 			entry->attrmap->attnums[i] = attnum;
 			if (attnum >= 0)
+			{
+				/*
+				 * Add to generatedattrs if names match but definitions
+				 * differ.
+				 */
+				if (attr->attgenerated)
+					generatedattrs = bms_add_member(generatedattrs, i);
+
 				missingatts = bms_del_member(missingatts, attnum);
+			}
 		}
 
-		logicalrep_report_missing_attrs(remoterel, missingatts);
+		logicalrep_report_missing_and_gen_attrs(remoterel, generatedattrs, false);
+		logicalrep_report_missing_and_gen_attrs(remoterel, missingatts, true);
 
 		/* be tidy */
+		bms_free(generatedattrs);
 		bms_free(missingatts);
 
 		/*
diff --git a/src/test/subscription/t/011_generated.pl b/src/test/subscription/t/011_generated.pl
index 211b54c316..2b1c7924ae 100644
--- a/src/test/subscription/t/011_generated.pl
+++ b/src/test/subscription/t/011_generated.pl
@@ -326,4 +326,80 @@ is( $result, qq(|2|
 $node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
 $node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
 
+# =============================================================================
+# The following test cases exercise logical replication for the combinations
+# where there is a generated column on one or both sides of pub/sub:
+# - normal -> generated and generated -> generated
+# - normal -> missing
+# =============================================================================
+
+# --------------------------------------------------
+# A "normal -> generated" and "generated -> generated" replication fails,
+# reporting an error that the generated column on the subscriber side
+# cannot be replicated.
+#
+# Test Case: normal -> generated and generated -> generated
+# Publisher table has regular column 'c2' and generated column 'c3'.
+# Subscriber table has generated columns 'c2' and 'c3'.
+# --------------------------------------------------
+
+# Create table and publication. Insert data into the table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE t1(c1 int, c2 int, c3 int GENERATED ALWAYS AS (c1 * 2) STORED);
+	CREATE PUBLICATION pub1 for table t1(c1, c2, c3);
+	INSERT INTO t1 VALUES (1);
+));
+
+# Create table and subscription.
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE t1(c1 int, c2 int GENERATED ALWAYS AS (c1 + 2) STORED, c3 int GENERATED ALWAYS AS (c1 + 2) STORED);
+	CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub1;
+));
+
+# Verify that an error occurs.
+my $offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+	qr/ERROR: ( [A-Z0-9]:)? cannot replicate to target relation "public.t1" generated columns: "c2", "c3"/,
+	$offset);
+
+# cleanup
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
+
+# --------------------------------------------------
+# A "normal -> missing" replication fails, reporting an error
+# that the subscriber side is missing replicated columns.
+#
+# Testcase: normal -> missing
+# Publisher table has normal columns 'c2' and 'c3'.
+# Subscriber table is missing columns 'c2' and 'c3'.
+# --------------------------------------------------
+
+# Create table and publication. Insert data into the table.
+$node_publisher->safe_psql(
+	'postgres', qq(
+	CREATE TABLE t2(c1 int, c2 int, c3 int);
+	CREATE PUBLICATION pub1 for table t2(c1, c2, c3);
+	INSERT INTO t2 VALUES (1);
+));
+
+# Create table and subscription.
+$node_subscriber->safe_psql(
+	'postgres', qq(
+	CREATE TABLE t2(c1 int);
+	CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub1;
+));
+
+# Verify that an error occurs.
+$offset = -s $node_subscriber->logfile;
+$node_subscriber->wait_for_log(
+	qr/ERROR: ( [A-Z0-9]:)? logical replication target relation "public.t2" is missing replicated columns: "c2", "c3"/,
+	$offset);
+
+# cleanup
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
+
 done_testing();
-- 
2.41.0.windows.3

Reply via email to