[PATCH] Fix incorrect logical replication updates with a shared leaf partition

From: Mingjie Gao <mingjie(dot)gmj(at)gmail(dot)com>
To: pgsql-hackers(at)lists(dot)postgresql(dot)org
Subject: [PATCH] Fix incorrect logical replication updates with a shared leaf partition
Date: 2026-10-31 14:02:11
Message-ID: CAL5_ZS3ccGfO_HPVPcNibC1dKew0WWzKCv3-1HodyKs8NKRaSg@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi,

I found a logical replication issue where updates can silently write
values into the wrong columns when two published tables map to
different ancestors of the same subscriber-side leaf partition.

I reproduced this on master (20devel) at:
61a372b795ddec08c15518096fe1f3e9e9973096

The publisher has two independent tables with different column orders:

tab6(a, b)
tab6_1(b, a)

On the subscriber, tab6_1 is a partitioned partition of tab6, with a
single leaf below it. Changes from both published tables therefore reach
the same leaf. The source rows have distinct primary key values.

After inserting through both tables, an UPDATE through tab6 builds the
leaf's mapping. A subsequent UPDATE through tab6_1 incorrectly reuses
that mapping. In the example below, the subscriber contains (a,b)=(222,2)
instead of (2,222), even after replication has caught up.

LogicalRepPartMap is keyed only by the local partition OID.
logicalrep_partition_open() can return a cached entry without checking
whether it belongs to the current remote relation. The new tuple is then
interpreted using the previous remote relation's attribute mapping.

The attached patch resets the cached entry when the remote relation ID
changes. The existing initialization path then rebuilds the remote
metadata, attribute mapping, updatability information and index choice.
It keeps the existing hash key and fast path for repeated operations
from the same remote relation; alternating sources rebuild the mapping.

The patch includes regression coverage in 013_partition.pl for text and
binary transfer, including switching A -> B -> A within a transaction.
On the baseline with only the test changes applied, six assertions fail.
With the fix, all 84 assertions pass. These tests used an optimized build
with assertions disabled and TAP tests enabled. I have not yet checked
older major versions or run the full PostgreSQL test suite.

For reference, the small SQL example below uses two fresh instances and
a publication containing only these two tables. The publisher must have
logical replication enabled. Replace <publisher connection string> with
the connection details for the publisher.

-- Publisher:
CREATE TABLE tab6 (a int PRIMARY KEY, b int);
CREATE TABLE tab6_1 (b int, a int PRIMARY KEY);
CREATE PUBLICATION map_pub FOR TABLE tab6, tab6_1;

-- Subscriber:
CREATE TABLE tab6 (a int PRIMARY KEY, b int) PARTITION BY RANGE (a);
CREATE TABLE tab6_1 (b int, a int PRIMARY KEY) PARTITION BY RANGE (a);
ALTER TABLE tab6 ATTACH PARTITION tab6_1 DEFAULT;
CREATE TABLE tab6_leaf PARTITION OF tab6_1 DEFAULT;
CREATE SUBSCRIPTION map_sub
CONNECTION '<publisher connection string>'
PUBLICATION map_pub WITH (copy_data = false);

-- Publisher, after the subscription is ready:
INSERT INTO tab6 (a, b) VALUES (1, 11);
INSERT INTO tab6_1 (a, b) VALUES (2, 22);
-- Wait for replication to catch up before continuing.

UPDATE tab6 SET b = 111 WHERE a = 1;
-- Wait for replication to catch up before continuing.

UPDATE tab6_1 SET b = 222 WHERE a = 2;
-- Wait for replication to catch up before reading the subscriber.

-- Subscriber:
SELECT a, b FROM tab6 ORDER BY a;

Expected:
a | b
---+-----
1 | 111
2 | 222

Actual without the fix:
a | b
-----+-----
1 | 111
222 | 2

For each catch-up wait, obtain a target LSN on the publisher after the
preceding statements have committed:

SELECT pg_current_wal_insert_lsn();

Then poll the following query on the publisher until it returns true,
substituting that LSN for <target LSN>:

SELECT replay_lsn >= '<target LSN>'::pg_lsn
FROM pg_stat_replication
WHERE application_name = 'map_sub';

The inserts deliver both RELATION messages before the first UPDATE
builds the leaf mapping. The automated regression test handles cluster
setup and catch-up waits itself. To reproduce the failure on a clean
checkout of the baseline, apply only the test portion of the attachment:

git apply --include=src/test/subscription/t/013_partition.pl \
/path/to/v1-0001-Fix-logical-replication-partition-mapping-cache.patch

Then, from a configured and built tree with TAP tests enabled, run:

make -j8 -C src/test/subscription check PROVE_TESTS=t/013_partition.pl

For an out-of-tree build, run the make command from the build directory.
Applying the remaining relation.c change and rebuilding makes the test
pass.

Comments are welcome.

Best regards,
mingjiegao

Attachment Content-Type Size
v1-0001-Fix-logical-replication-partition-mapping-cache.patch text/x-patch 5.4 KB

Browse pgsql-hackers by date

  From Date Subject
Next Message Kirill Reshke 2026-10-31 14:08:14 Speeding up GIST index creation for hstore
Previous Message Greg Burd 2026-10-31 13:34:16 Re: [PATCH] Corruption Issue: Fix missing tts_tid in ExecForceStoreHeapTuple