Improve error message for replication of generated columns.
authorAmit Kapila <[email protected]>
Wed, 27 Nov 2024 03:39:20 +0000 (09:09 +0530)
committerAmit Kapila <[email protected]>
Wed, 27 Nov 2024 03:39:20 +0000 (09:09 +0530)
Currently, logical replication produces a generic error message when
targeting a subscriber-side table column that is either missing or
generated. The error message can be misleading for generated columns.

This  introduces a specific error message to clarify the issue when
generated columns are involved.

Author: Shubham Khanna
Reviewed-by: Peter Smith, Vignesh C, Amit Kapila
Discussion: https://postgr.es/m/CAHv8RjJBvYtqU7OAofBizOmQOK2Q8h+w9v2_cQWxT_gO7er3Aw@mail.gmail.com

src/backend/replication/logical/relation.c
src/test/subscription/t/011_generated.pl

index f5a0ef2bd9dde07d444a3148197fc1e980e913bd..b355f32f03c3e8d2059cfa4eaa1d2a55149db09d 100644 (file)
@@ -220,41 +220,63 @@ logicalrep_rel_att_by_name(LogicalRepRelation *remoterel, const char *attname)
 }
 
 /*
- * Report error with names of the missing local relation column(s), if any.
+ * Returns a comma-separated string of attribute names based on the provided
+ * relation and bitmap indicating which attributes to include.
  */
-static void
-logicalrep_report_missing_attrs(LogicalRepRelation *remoterel,
-                               Bitmapset *missingatts)
+static char *
+logicalrep_get_attrs_str(LogicalRepRelation *remoterel, Bitmapset *atts)
 {
-   if (!bms_is_empty(missingatts))
+   StringInfoData attsbuf;
+   int         attcnt = 0;
+   int         i = -1;
+
+   Assert(!bms_is_empty(atts));
+
+   initStringInfo(&attsbuf);
+
+   while ((i = bms_next_member(atts, i)) >= 0)
    {
-       StringInfoData missingattsbuf;
-       int         missingattcnt = 0;
-       int         i;
+       attcnt++;
+       if (attcnt > 1)
+           appendStringInfo(&attsbuf, _(", "));
 
-       initStringInfo(&missingattsbuf);
+       appendStringInfo(&attsbuf, _("\"%s\""), remoterel->attnames[i]);
+   }
 
-       i = -1;
-       while ((i = bms_next_member(missingatts, i)) >= 0)
-       {
-           missingattcnt++;
-           if (missingattcnt == 1)
-               appendStringInfo(&missingattsbuf, _("\"%s\""),
-                                remoterel->attnames[i]);
-           else
-               appendStringInfo(&missingattsbuf, _(", \"%s\""),
-                                remoterel->attnames[i]);
-       }
+   return attsbuf.data;
+}
 
+/*
+ * If attempting to replicate missing or generated columns, report an error.
+ * Prioritize 'missing' errors if both occur though the prioritization is
+ * arbitrary.
+ */
+static void
+logicalrep_report_missing_or_gen_attrs(LogicalRepRelation *remoterel,
+                                      Bitmapset *missingatts,
+                                      Bitmapset *generatedatts)
+{
+   if (!bms_is_empty(missingatts))
        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)));
-   }
+               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",
+                             bms_num_members(missingatts),
+                             remoterel->nspname,
+                             remoterel->relname,
+                             logicalrep_get_attrs_str(remoterel,
+                                                      missingatts)));
+
+   if (!bms_is_empty(generatedatts))
+       ereport(ERROR,
+               errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+               errmsg_plural("logical replication target relation \"%s.%s\" has incompatible generated column: %s",
+                             "logical replication target relation \"%s.%s\" has incompatible generated columns: %s",
+                             bms_num_members(generatedatts),
+                             remoterel->nspname,
+                             remoterel->relname,
+                             logicalrep_get_attrs_str(remoterel,
+                                                      generatedatts)));
 }
 
 /*
@@ -380,6 +402,7 @@ logicalrep_rel_open(LogicalRepRelId remoteid, LOCKMODE lockmode)
        MemoryContext oldctx;
        int         i;
        Bitmapset  *missingatts;
+       Bitmapset  *generatedattrs = NULL;
 
        /* Release the no-longer-useful attrmap, if any. */
        if (entry->attrmap)
@@ -421,7 +444,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 +455,20 @@ logicalrep_rel_open(LogicalRepRelId remoteid, LOCKMODE lockmode)
 
            entry->attrmap->attnums[i] = attnum;
            if (attnum >= 0)
+           {
+               /* Remember which subscriber columns are generated. */
+               if (attr->attgenerated)
+                   generatedattrs = bms_add_member(generatedattrs, attnum);
+
                missingatts = bms_del_member(missingatts, attnum);
+           }
        }
 
-       logicalrep_report_missing_attrs(remoterel, missingatts);
+       logicalrep_report_missing_or_gen_attrs(remoterel, missingatts,
+                                              generatedattrs);
 
        /* be tidy */
+       bms_free(generatedattrs);
        bms_free(missingatts);
 
        /*
index 211b54c316214508a87ee9aa707000ad328cceab..66e6d8da5ac776783bdd61875a04f7427fe9698f 100644 (file)
@@ -326,4 +326,46 @@ is( $result, qq(|2|
 $node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
 $node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
 
+# =============================================================================
+# The following test verifies the expected error when replicating to a
+# generated subscriber column. Test the following combinations:
+# - regular -> generated
+# - generated -> generated
+# =============================================================================
+
+# --------------------------------------------------
+# A "regular -> generated" or "generated -> generated" replication fails,
+# reporting an error that the generated column on the subscriber side cannot
+# be replicated.
+#
+# Test Case: regular -> 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]:)? logical replication target relation "public.t1" has incompatible generated columns: "c2", "c3"/,
+   $offset);
+
+# cleanup
+$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
+$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
+
 done_testing();