From 3047c7473df8f3be43859c1bec74c99fba80ecf0 Mon Sep 17 00:00:00 2001
From: Zhijie Hou <houzj.fnst@fujitsu.com>
Date: Fri, 3 Apr 2026 13:44:40 +0800
Subject: [PATCH v1] write tablesync changes with the subscription origin ID

During initial table synchronization, tablesync workers were writing tuples with
the per-table tablesync origin identity. Later, when the leader apply worker
processed UPDATE/DELETE for those tuples, conflict checks could see an origin
mismatch and report benign update_origin_differs or delete_origin_differs noise
for changes from the same subscription.

Fix this by keeping the tablesync origin for per-table sync progress tracking
and resume but stamp tuple writes with the subscription-level apply origin
identity. This ensures conflict detection sees tablesync and apply changes as
coming from the same subscription.
---
 src/backend/replication/logical/tablesync.c | 27 ++++++++++++++++++---
 src/test/subscription/t/029_on_error.pl     |  9 +++++++
 2 files changed, 33 insertions(+), 3 deletions(-)

diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f49a4852ecb..4aa39341e8e 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -1226,6 +1226,7 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	AclResult	aclresult;
 	WalRcvExecResult *res;
 	char		originname[NAMEDATALEN];
+	char		applyoriginname[NAMEDATALEN];
 	ReplOriginId originid;
 	UserContext ucxt;
 	bool		must_use_password;
@@ -1285,12 +1286,17 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 		   MyLogicalRepWorker->relstate == SUBREL_STATE_DATASYNC ||
 		   MyLogicalRepWorker->relstate == SUBREL_STATE_FINISHEDCOPY);
 
-	/* Assign the origin tracking record name. */
+	/* Assign the origin tracking record names. */
 	ReplicationOriginNameForLogicalRep(MySubscription->oid,
 									   MyLogicalRepWorker->relid,
 									   originname,
 									   sizeof(originname));
 
+	ReplicationOriginNameForLogicalRep(MySubscription->oid,
+									   InvalidOid,
+									   applyoriginname,
+									   sizeof(applyoriginname));
+
 	if (MyLogicalRepWorker->relstate == SUBREL_STATE_DATASYNC)
 	{
 		/*
@@ -1320,7 +1326,15 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 		 */
 		originid = replorigin_by_name(originname, false);
 		replorigin_session_setup(originid, 0);
-		replorigin_xact_state.origin = originid;
+
+		/*
+		 * Tablesync keeps its own origin for replication progress, but writes
+		 * must be tagged with the subscription-level apply origin so conflict
+		 * detection sees tablesync and apply changes as coming from the same
+		 * subscription.
+		 */
+		replorigin_xact_state.origin = replorigin_by_name(applyoriginname, false);
+
 		*origin_startpos = replorigin_session_get_progress(false);
 
 		CommitTransactionCommand();
@@ -1407,7 +1421,14 @@ LogicalRepSyncTableStart(XLogRecPtr *origin_startpos)
 	UnlockRelationOid(ReplicationOriginRelationId, RowExclusiveLock);
 
 	replorigin_session_setup(originid, 0);
-	replorigin_xact_state.origin = originid;
+
+	/*
+	 * Tablesync keeps its own origin for replication progress, but writes
+	 * must be tagged with the subscription-level apply origin so conflict
+	 * detection sees tablesync and apply changes as coming from the same
+	 * subscription.
+	 */
+	replorigin_xact_state.origin = replorigin_by_name(applyoriginname, false);
 
 	/*
 	 * If the user did not opt to run as the owner of the subscription
diff --git a/src/test/subscription/t/029_on_error.pl b/src/test/subscription/t/029_on_error.pl
index 7d68759b6cd..85d3478f44f 100644
--- a/src/test/subscription/t/029_on_error.pl
+++ b/src/test/subscription/t/029_on_error.pl
@@ -146,6 +146,8 @@ COMMIT;
 test_skip_lsn($node_publisher, $node_subscriber,
 	"(2, NULL)", "2", "test skipping transaction");
 
+my $log_location = -s $node_subscriber->logfile;
+
 # Test for PREPARE and COMMIT PREPARED. Update the data and PREPARE the
 # transaction, raising an error on the subscriber due to violation of the
 # unique constraint on tbl. Then skip the transaction.
@@ -160,6 +162,13 @@ COMMIT PREPARED 'gtx';
 test_skip_lsn($node_publisher, $node_subscriber,
 	"(3, NULL)", "3", "test skipping prepare and commit prepared ");
 
+# Check that no update_origin_differs conflicts are raised
+my $logfile = slurp_file($node_subscriber->logfile(), $log_location);
+unlike(
+	$logfile,
+	qr/conflict detected on relation "public.tbl": conflict=update_origin_differs.*/,
+	'modifying the row copied by tablesync should not cause update_origin_differs conflict');
+
 # Test for STREAM COMMIT. Insert enough rows to tbl to exceed the 64kB
 # limit, also raising an error on the subscriber during applying spooled
 # changes for the same reason. Then skip the transaction.
-- 
2.53.0.windows.2

