From c551c7f424ab713a7a4275eec9980110977e41b9 Mon Sep 17 00:00:00 2001
From: Roberto Mello <roberto.mello@gmail.com>
Date: Fri, 27 Mar 2026 14:29:15 -0600
Subject: [PATCH v2] Emit WARNING when publication row filter columns miss
 replica identity

When a publication WHERE clause references columns not covered by the
table replica identity, UPDATE and DELETE fail at DML time with an
ERROR from CheckCmdReplicaIdentity().  The DBA only discovers this
misconfiguration when production writes start failing.

Add DDL-time WARNINGs so the problem is reported immediately.  The
warnings fire at:

  - CREATE PUBLICATION / ALTER PUBLICATION ... ADD TABLE / SET TABLE
    when the WHERE clause references non-identity columns

  - ALTER PUBLICATION SET (publish = ...) when widening the publish
    set to include UPDATE or DELETE while existing row filters
    reference non-identity columns

  - ALTER TABLE ... REPLICA IDENTITY when the new identity no longer
    covers columns used in a publication WHERE clause

The existing DML-time ERROR is preserved as a safety net.

A CommandCounterIncrement() is needed at each call site so that catalog
rows inserted by PublicationAddTables() or relation_mark_replica_identity()
are visible to the syscache/relcache when the check runs.  For the ALTER
TABLE path, a fresh table_open() after CCI ensures the relcache reflects
the updated replica identity.
---
 doc/src/sgml/logical-replication.sgml     |  15 +++
 src/backend/commands/publicationcmds.c    | 130 ++++++++++++++++++++++
 src/backend/commands/tablecmds.c          |  89 ++++++++++++++-
 src/test/regress/expected/publication.out |  63 +++++++++++
 src/test/regress/sql/publication.sql      |  35 ++++++
 5 files changed, 330 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 23b268273b9..cc0dd5f57e1 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -881,6 +881,21 @@ HINT:  To initiate replication, you must manually create the replication slot, e
     <literal>WHERE</literal> clause can use any column.
    </para>
 
+   <warning>
+    <para>
+     If a row filter references columns not covered by the replica identity,
+     <command>UPDATE</command> and <command>DELETE</command> operations on the
+     table will fail at run time with an error. A <literal>WARNING</literal> is
+     emitted at <command>CREATE PUBLICATION</command>, <command>ALTER
+     PUBLICATION</command>, or <command>ALTER TABLE ... REPLICA IDENTITY</command>
+     time to alert the user to this misconfiguration. To resolve it, either
+     adjust the row filter to reference only replica identity columns, set the
+     replica identity to <literal>FULL</literal>, or configure the publication
+     to not publish <command>UPDATE</command> or <command>DELETE</command>
+     operations.
+    </para>
+   </warning>
+
   </sect2>
 
   <sect2 id="logical-replication-row-filter-transformations">
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 440adb356ad..8769e342a54 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -22,6 +22,7 @@
 #include "catalog/namespace.h"
 #include "catalog/objectaccess.h"
 #include "catalog/objectaddress.h"
+#include "catalog/partition.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/pg_namespace.h"
@@ -346,6 +347,55 @@ pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors,
 	return result;
 }
 
+/*
+ * Warn at DDL time if a publication's row filter references columns that are
+ * not part of the table's replica identity.  This catches a configuration
+ * mistake that would otherwise only surface as a hard ERROR on the first
+ * UPDATE or DELETE against the table (in CheckCmdReplicaIdentity).
+ *
+ * We only warn (not error) so that the existing DML-time safety check
+ * remains the authoritative gate.  The warning gives the DBA immediate
+ * feedback when creating or altering the publication.
+ */
+static void
+check_pub_rf_columns_at_ddl(Oid pubid, List *rels,
+							bool pubupdate, bool pubdelete,
+							bool pubviaroot)
+{
+	ListCell   *lc;
+
+	/* No point checking if the publication doesn't replicate UPD/DEL. */
+	if (!pubupdate && !pubdelete)
+		return;
+
+	foreach(lc, rels)
+	{
+		PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc);
+		Relation	rel = pub_rel->relation;
+		List	   *ancestors = NIL;
+
+		/* Only tables with a WHERE clause need the check. */
+		if (pub_rel->whereClause == NULL)
+			continue;
+
+		if (rel->rd_rel->relispartition)
+			ancestors = get_partition_ancestors(RelationGetRelid(rel));
+
+		if (pub_rf_contains_invalid_column(pubid, rel, ancestors,
+										   pubviaroot))
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					 errmsg("column used in the publication WHERE expression is not part of the replica identity for table \"%s\"",
+							RelationGetRelationName(rel)),
+					 errhint("UPDATE and DELETE on this table will fail until "
+							 "the replica identity covers the filtered columns, "
+							 "or the publication is changed to not publish those "
+							 "operations.")));
+
+		list_free(ancestors);
+	}
+}
+
 /*
  * Check for invalid columns in the publication table definition.
  *
@@ -978,6 +1028,15 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
 									   publish_via_partition_root);
 
 			PublicationAddTables(puboid, rels, true, NULL);
+
+			/* Make publication_rel rows visible for the check. */
+			CommandCounterIncrement();
+
+			check_pub_rf_columns_at_ddl(puboid, rels,
+										pubactions.pubupdate,
+										pubactions.pubdelete,
+										publish_via_partition_root);
+
 			CloseTableList(rels);
 		}
 
@@ -1028,6 +1087,8 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 	char		publish_generated_columns;
 	ObjectAddress obj;
 	Form_pg_publication pubform;
+	bool		old_pubupdate;
+	bool		old_pubdelete;
 	List	   *root_relids = NIL;
 	ListCell   *lc;
 
@@ -1128,6 +1189,10 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 		}
 	}
 
+	/* Remember current publish flags so we can detect widening. */
+	old_pubupdate = pubform->pubupdate;
+	old_pubdelete = pubform->pubdelete;
+
 	/* Everything ok, form a new tuple. */
 	memset(values, 0, sizeof(values));
 	memset(nulls, false, sizeof(nulls));
@@ -1170,6 +1235,55 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
 
 	pubform = (Form_pg_publication) GETSTRUCT(tup);
 
+	/*
+	 * If the publish set was widened to include UPDATE or DELETE, check
+	 * whether any existing row filters reference columns not covered by the
+	 * replica identity.  Skip FOR ALL TABLES publications, which cannot have
+	 * per-table WHERE clauses.
+	 */
+	if (publish_given &&
+		((pubactions.pubupdate && !old_pubupdate) ||
+		 (pubactions.pubdelete && !old_pubdelete)) &&
+		!pubform->puballtables)
+	{
+		List	   *relids;
+		ListCell   *rlc;
+
+		relids = GetIncludedPublicationRelations(pubform->oid,
+												 PUBLICATION_PART_ALL);
+
+		foreach(rlc, relids)
+		{
+			Oid			relid = lfirst_oid(rlc);
+			Relation	pubrel;
+			List	   *ancestors = NIL;
+
+			pubrel = table_open(relid, AccessShareLock);
+
+			if (pubrel->rd_rel->relispartition)
+				ancestors = get_partition_ancestors(relid);
+
+			if (pub_rf_contains_invalid_column(pubform->oid, pubrel,
+											   ancestors,
+											   pubform->pubviaroot))
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+						 errmsg("column used in the row filter of publication \"%s\" "
+								"is not part of the replica identity for table \"%s\"",
+								stmt->pubname,
+								RelationGetRelationName(pubrel)),
+						 errhint("UPDATE and DELETE on this table will fail until "
+								 "the replica identity covers the filtered columns, "
+								 "or the publication is changed to not publish those "
+								 "operations.")));
+
+			list_free(ancestors);
+			table_close(pubrel, AccessShareLock);
+		}
+
+		list_free(relids);
+	}
+
 	/* Invalidate the relcache. */
 	if (pubform->puballtables)
 	{
@@ -1267,6 +1381,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 								   pubform->pubviaroot);
 
 		PublicationAddTables(pubid, rels, false, stmt);
+
+		/* Make publication_rel rows visible for the check. */
+		CommandCounterIncrement();
+
+		check_pub_rf_columns_at_ddl(pubid, rels,
+									pubform->pubupdate,
+									pubform->pubdelete,
+									pubform->pubviaroot);
 	}
 	else if (stmt->action == AP_DropObjects)
 		PublicationDropTables(pubid, rels, false);
@@ -1412,6 +1534,14 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 		 */
 		PublicationAddTables(pubid, rels, true, stmt);
 
+		/* Make publication_rel rows visible for the check. */
+		CommandCounterIncrement();
+
+		check_pub_rf_columns_at_ddl(pubid, rels,
+									pubform->pubupdate,
+									pubform->pubdelete,
+									pubform->pubviaroot);
+
 		CloseTableList(delrels);
 	}
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 8b4ebc6f226..80391b578fd 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -48,6 +48,7 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_policy.h"
 #include "catalog/pg_proc.h"
+#include "catalog/pg_publication.h"
 #include "catalog/pg_publication_rel.h"
 #include "catalog/pg_rewrite.h"
 #include "catalog/pg_statistic_ext.h"
@@ -61,6 +62,7 @@
 #include "commands/comment.h"
 #include "commands/defrem.h"
 #include "commands/event_trigger.h"
+#include "commands/publicationcmds.h"
 #include "commands/sequence.h"
 #include "commands/tablecmds.h"
 #include "commands/tablespace.h"
@@ -18720,6 +18722,70 @@ relation_mark_replica_identity(Relation rel, char ri_type, Oid indexOid,
 	table_close(pg_index, RowExclusiveLock);
 }
 
+/*
+ * Check whether any publication row filter on this relation references
+ * columns that are not part of the (possibly just-changed) replica identity.
+ * If so, emit a WARNING so the DBA knows that UPDATE/DELETE will fail.
+ */
+static void
+check_relation_publications_rf(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	List	   *puboids;
+	ListCell   *lc;
+	List	   *ancestors = NIL;
+
+	puboids = GetRelationIncludedPublications(relid);
+	if (puboids == NIL)
+		return;
+
+	if (rel->rd_rel->relispartition)
+		ancestors = get_partition_ancestors(relid);
+
+	foreach(lc, puboids)
+	{
+		Oid			pubid = lfirst_oid(lc);
+		HeapTuple	pubtup;
+		Form_pg_publication pubform;
+
+		pubtup = SearchSysCache1(PUBLICATIONOID, ObjectIdGetDatum(pubid));
+		if (!HeapTupleIsValid(pubtup))
+			continue;
+
+		pubform = (Form_pg_publication) GETSTRUCT(pubtup);
+
+		/*
+		 * Only check publications that replicate UPDATE or DELETE;
+		 * INSERT-only publications do not need old tuple values.
+		 */
+		if ((pubform->pubupdate || pubform->pubdelete) &&
+			!pubform->puballtables &&
+			pub_rf_contains_invalid_column(pubid, rel, ancestors,
+										   pubform->pubviaroot))
+		{
+			char	   *pubname = pstrdup(NameStr(pubform->pubname));
+
+			ReleaseSysCache(pubtup);
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+					 errmsg("column used in the row filter of publication \"%s\" "
+							"is not part of the replica identity for table \"%s\"",
+							pubname, RelationGetRelationName(rel)),
+					 errhint("UPDATE and DELETE on this table will fail until "
+							 "the replica identity covers the filtered columns, "
+							 "or the publication is changed to not publish those "
+							 "operations.")));
+			pfree(pubname);
+			continue;
+		}
+
+		ReleaseSysCache(pubtup);
+	}
+
+	list_free(ancestors);
+	list_free(puboids);
+}
+
 /*
  * ALTER TABLE <name> REPLICA IDENTITY ...
  */
@@ -18733,17 +18799,18 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode
 	if (stmt->identity_type == REPLICA_IDENTITY_DEFAULT)
 	{
 		relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true);
-		return;
+		goto check_publications;
 	}
 	else if (stmt->identity_type == REPLICA_IDENTITY_FULL)
 	{
 		relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true);
+		/* FULL covers all columns, so no warning is possible. */
 		return;
 	}
 	else if (stmt->identity_type == REPLICA_IDENTITY_NOTHING)
 	{
 		relation_mark_replica_identity(rel, stmt->identity_type, InvalidOid, true);
-		return;
+		goto check_publications;
 	}
 	else if (stmt->identity_type == REPLICA_IDENTITY_INDEX)
 	{
@@ -18832,6 +18899,24 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode
 	relation_mark_replica_identity(rel, stmt->identity_type, indexOid, true);
 
 	index_close(indexRel, NoLock);
+
+check_publications:
+
+	/*
+	 * The catalog changes from relation_mark_replica_identity are not yet
+	 * visible to the syscache/relcache.  Make them visible so that
+	 * check_relation_publications_rf can see the new replica identity when
+	 * evaluating publication row filters.
+	 */
+	CommandCounterIncrement();
+	{
+		Oid			relid = RelationGetRelid(rel);
+		Relation	freshrel;
+
+		freshrel = table_open(relid, NoLock);
+		check_relation_publications_rf(freshrel);
+		table_close(freshrel, NoLock);
+	}
 }
 
 /*
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index d2aa9d45e4a..0d291c98a0a 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -883,17 +883,23 @@ ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk WHERE (b > 99);
 -- ok - "b" is a PK col
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk WHERE (c > 99);
+WARNING:  column used in the publication WHERE expression is not part of the replica identity for table "rf_tbl_abcd_pk"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
 -- fail - "c" is not part of the PK
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_pk"
 DETAIL:  Column used in the publication WHERE expression is not part of the replica identity.
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk WHERE (d > 99);
+WARNING:  column used in the publication WHERE expression is not part of the replica identity for table "rf_tbl_abcd_pk"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
 -- fail - "d" is not part of the PK
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_pk"
 DETAIL:  Column used in the publication WHERE expression is not part of the replica identity.
 -- 1b. REPLICA IDENTITY is DEFAULT and table has no PK
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk WHERE (a > 99);
+WARNING:  column used in the publication WHERE expression is not part of the replica identity for table "rf_tbl_abcd_nopk"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
 -- fail - "a" is not part of REPLICA IDENTITY
 UPDATE rf_tbl_abcd_nopk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_nopk"
@@ -910,17 +916,25 @@ UPDATE rf_tbl_abcd_nopk SET a = 1;
 -- Case 3. REPLICA IDENTITY NOTHING
 ALTER TABLE rf_tbl_abcd_pk REPLICA IDENTITY NOTHING;
 ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY NOTHING;
+WARNING:  column used in the row filter of publication "testpub6" is not part of the replica identity for table "rf_tbl_abcd_nopk"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk WHERE (a > 99);
+WARNING:  column used in the publication WHERE expression is not part of the replica identity for table "rf_tbl_abcd_pk"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
 -- fail - "a" is in PK but it is not part of REPLICA IDENTITY NOTHING
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_pk"
 DETAIL:  Column used in the publication WHERE expression is not part of the replica identity.
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk WHERE (c > 99);
+WARNING:  column used in the publication WHERE expression is not part of the replica identity for table "rf_tbl_abcd_pk"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
 -- fail - "c" is not in PK and not in REPLICA IDENTITY NOTHING
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_pk"
 DETAIL:  Column used in the publication WHERE expression is not part of the replica identity.
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk WHERE (a > 99);
+WARNING:  column used in the publication WHERE expression is not part of the replica identity for table "rf_tbl_abcd_nopk"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
 -- fail - "a" is not in REPLICA IDENTITY NOTHING
 UPDATE rf_tbl_abcd_nopk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_nopk"
@@ -932,7 +946,11 @@ ALTER TABLE rf_tbl_abcd_pk REPLICA IDENTITY USING INDEX idx_abcd_pk_c;
 ALTER TABLE rf_tbl_abcd_nopk ALTER COLUMN c SET NOT NULL;
 CREATE UNIQUE INDEX idx_abcd_nopk_c ON rf_tbl_abcd_nopk(c);
 ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY USING INDEX idx_abcd_nopk_c;
+WARNING:  column used in the row filter of publication "testpub6" is not part of the replica identity for table "rf_tbl_abcd_nopk"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk WHERE (a > 99);
+WARNING:  column used in the publication WHERE expression is not part of the replica identity for table "rf_tbl_abcd_pk"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
 -- fail - "a" is in PK but it is not part of REPLICA IDENTITY INDEX
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_pk"
@@ -941,6 +959,8 @@ ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk WHERE (c > 99);
 -- ok - "c" is not in PK but it is part of REPLICA IDENTITY INDEX
 UPDATE rf_tbl_abcd_pk SET a = 1;
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk WHERE (a > 99);
+WARNING:  column used in the publication WHERE expression is not part of the replica identity for table "rf_tbl_abcd_nopk"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
 -- fail - "a" is not in REPLICA IDENTITY INDEX
 UPDATE rf_tbl_abcd_nopk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_nopk"
@@ -979,6 +999,8 @@ ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=0);
 -- Now change the root filter to use a column "b"
 -- (which is not in the replica identity)
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk_1 WHERE (b > 99);
+WARNING:  column used in the publication WHERE expression is not part of the replica identity for table "rf_tbl_abcd_part_pk_1"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
 -- ok - we don't have row filter for partitioned table.
 ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=0);
 -- fail - "b" is not in REPLICA IDENTITY INDEX
@@ -990,10 +1012,51 @@ DETAIL:  Column used in the publication WHERE expression is not part of the repl
 ALTER PUBLICATION testpub6 SET (PUBLISH_VIA_PARTITION_ROOT=1);
 -- ok - can use row filter for partitioned table
 ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk WHERE (b > 99);
+WARNING:  column used in the publication WHERE expression is not part of the replica identity for table "rf_tbl_abcd_part_pk"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
 -- fail - "b" is not in REPLICA IDENTITY INDEX
 UPDATE rf_tbl_abcd_part_pk SET a = 1;
 ERROR:  cannot update table "rf_tbl_abcd_part_pk_1"
 DETAIL:  Column used in the publication WHERE expression is not part of the replica identity.
+-- Tests for DDL-time WARNING when row filter columns are not in replica
+-- identity.  The DML-time ERROR still fires as a safety net.
+-- Reset tables to known state
+ALTER TABLE rf_tbl_abcd_pk REPLICA IDENTITY DEFAULT;
+ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY FULL;
+-- DDL-time warning: "c" is not part of PK (DEFAULT replica identity)
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk WHERE (c > 99);
+WARNING:  column used in the publication WHERE expression is not part of the replica identity for table "rf_tbl_abcd_pk"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
+-- DML-time error still present
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_pk"
+DETAIL:  Column used in the publication WHERE expression is not part of the replica identity.
+-- No warning: publication only publishes INSERT
+ALTER PUBLICATION testpub6 SET (publish = 'insert');
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk WHERE (c > 99);
+-- DDL-time warning when widening publish set to include UPDATE/DELETE
+-- while an existing row filter references a non-identity column.
+ALTER PUBLICATION testpub6 SET (publish = 'insert, update, delete');
+WARNING:  column used in the row filter of publication "testpub6" is not part of the replica identity for table "rf_tbl_abcd_pk"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
+-- DML-time error still present after widening
+UPDATE rf_tbl_abcd_pk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_pk"
+DETAIL:  Column used in the publication WHERE expression is not part of the replica identity.
+-- No warning: REPLICA IDENTITY FULL covers all columns
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk WHERE (a > 99);
+-- No warning: widening publish set when REPLICA IDENTITY FULL
+ALTER PUBLICATION testpub6 SET (publish = 'insert');
+ALTER PUBLICATION testpub6 SET (publish = 'insert, update, delete');
+-- DDL-time warning from ALTER TABLE REPLICA IDENTITY: narrowing identity
+-- while a publication filter references a now-uncovered column.
+ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY NOTHING;
+WARNING:  column used in the row filter of publication "testpub6" is not part of the replica identity for table "rf_tbl_abcd_nopk"
+HINT:  UPDATE and DELETE on this table will fail until the replica identity covers the filtered columns, or the publication is changed to not publish those operations.
+-- DML-time error still present
+UPDATE rf_tbl_abcd_nopk SET a = 1;
+ERROR:  cannot update table "rf_tbl_abcd_nopk"
+DETAIL:  Column used in the publication WHERE expression is not part of the replica identity.
 DROP PUBLICATION testpub6;
 DROP TABLE rf_tbl_abcd_pk;
 DROP TABLE rf_tbl_abcd_nopk;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 6bafad27571..b35430da612 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -548,6 +548,41 @@ ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_part_pk WHERE (b > 99);
 -- fail - "b" is not in REPLICA IDENTITY INDEX
 UPDATE rf_tbl_abcd_part_pk SET a = 1;
 
+-- Tests for DDL-time WARNING when row filter columns are not in replica
+-- identity.  The DML-time ERROR still fires as a safety net.
+
+-- Reset tables to known state
+ALTER TABLE rf_tbl_abcd_pk REPLICA IDENTITY DEFAULT;
+ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY FULL;
+
+-- DDL-time warning: "c" is not part of PK (DEFAULT replica identity)
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk WHERE (c > 99);
+-- DML-time error still present
+UPDATE rf_tbl_abcd_pk SET a = 1;
+
+-- No warning: publication only publishes INSERT
+ALTER PUBLICATION testpub6 SET (publish = 'insert');
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_pk WHERE (c > 99);
+
+-- DDL-time warning when widening publish set to include UPDATE/DELETE
+-- while an existing row filter references a non-identity column.
+ALTER PUBLICATION testpub6 SET (publish = 'insert, update, delete');
+-- DML-time error still present after widening
+UPDATE rf_tbl_abcd_pk SET a = 1;
+
+-- No warning: REPLICA IDENTITY FULL covers all columns
+ALTER PUBLICATION testpub6 SET TABLE rf_tbl_abcd_nopk WHERE (a > 99);
+
+-- No warning: widening publish set when REPLICA IDENTITY FULL
+ALTER PUBLICATION testpub6 SET (publish = 'insert');
+ALTER PUBLICATION testpub6 SET (publish = 'insert, update, delete');
+
+-- DDL-time warning from ALTER TABLE REPLICA IDENTITY: narrowing identity
+-- while a publication filter references a now-uncovered column.
+ALTER TABLE rf_tbl_abcd_nopk REPLICA IDENTITY NOTHING;
+-- DML-time error still present
+UPDATE rf_tbl_abcd_nopk SET a = 1;
+
 DROP PUBLICATION testpub6;
 DROP TABLE rf_tbl_abcd_pk;
 DROP TABLE rf_tbl_abcd_nopk;
-- 
2.50.1 (Apple Git-155)

