Hi Amit and Dilip,
Thanks for the fix! All six LOG callers pass the entry the search used.
Two things:
1. The new comment says relmapentry is NULL when no search was done.
But update_exists also passes NULL, and it did search.
2. Get_tuple_desc() still calls GetRelationIdentityOrPK(), so with
conflict_log_destination = 'all' the log and the table can disagree.
I hit it with the apply-update-before-open-indices injection point
and DROP INDEX CONCURRENTLY on the replica identity index on x. The
table row has {"x":"7"}, but the log for the same conflict says
replica identity (z)=(107).
z is the pk
Should we pass the same index down there too
and drop the XXX?
repro attached.
Thanks,
Shihao
# Copyright (c) 2026, PostgreSQL Global Development Group
# Reproducer: the conflict log table and the server log disagree on the key
# when DROP INDEX CONCURRENTLY demotes the replica identity index while the
# apply worker is in the middle of a change.
#
# Copy to src/test/subscription/t/ and run with meson test or prove. The
# last test fails on v87: the table gets {"x":"7"} from the index the search
# used, the log gets the primary key from GetRelationIdentityOrPK().
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
plan skip_all => 'Injection points not supported by this build'
unless $ENV{enable_injection_points} eq 'yes';
my $pub = PostgreSQL::Test::Cluster->new('publisher');
$pub->init(allows_streaming => 'logical');
$pub->start;
my $sub = PostgreSQL::Test::Cluster->new('subscriber');
$sub->init;
$sub->append_conf('postgresql.conf', "track_commit_timestamp = on\n");
$sub->start;
$sub->safe_psql('postgres', 'CREATE EXTENSION injection_points');
# Replica identity is the unique index on x, the primary key is on z.
$pub->safe_psql(
'postgres', q[
CREATE TABLE t (x int NOT NULL, y int, z int NOT NULL);
CREATE UNIQUE INDEX t_ri ON t (x);
ALTER TABLE t REPLICA IDENTITY USING INDEX t_ri;
INSERT INTO t SELECT i, i, i + 100 FROM generate_series(1, 10) i;
CREATE PUBLICATION pub FOR TABLE t;
]);
$sub->safe_psql(
'postgres', q[
CREATE TABLE t (x int NOT NULL, y int, z int PRIMARY KEY);
CREATE UNIQUE INDEX t_ri ON t (x);
ALTER TABLE t REPLICA IDENTITY USING INDEX t_ri;
]);
my $connstr = $pub->connstr . ' dbname=postgres';
$sub->safe_psql('postgres',
"CREATE SUBSCRIPTION sub CONNECTION '$connstr application_name=sub' "
. "PUBLICATION pub WITH (conflict_log_destination = 'all')");
$sub->wait_for_subscription_sync($pub, 'sub');
my $clt = $sub->safe_psql('postgres',
"SELECT 'pg_conflict.pg_conflict_log_' || oid FROM pg_subscription WHERE subname = 'sub'"
);
# Make the next remote update an update_origin_differs conflict.
$sub->safe_psql('postgres', "UPDATE t SET y = -7 WHERE x = 7");
# Park the worker before it opens the indexes, then commit the first phase
# of the drop, which clears indisreplident.
$sub->safe_psql('postgres',
"SELECT injection_points_attach('apply-update-before-open-indices', 'wait')"
);
my $log_offset = -s $sub->logfile;
$pub->safe_psql('postgres', "UPDATE t SET y = 77 WHERE x = 7");
$sub->wait_for_event('logical replication apply worker',
'apply-update-before-open-indices');
my $drop = $sub->background_psql('postgres');
$drop->query_until(
qr/starting_drop/, q[
\echo starting_drop
DROP INDEX CONCURRENTLY t_ri;
]);
$sub->poll_query_until('postgres',
"SELECT count(*) = 0 FROM pg_index WHERE indrelid = 't'::regclass AND indisreplident"
) or die "timed out waiting for indisreplident to be cleared";
$sub->safe_psql(
'postgres', q[
SELECT injection_points_detach('apply-update-before-open-indices');
SELECT injection_points_wakeup('apply-update-before-open-indices');
]);
$pub->wait_for_catchup('sub');
$sub->wait_for_log(qr/conflict=update_origin_differs/, $log_offset);
ok($drop->quit, 'DROP INDEX CONCURRENTLY completes');
is($sub->safe_psql('postgres', "SELECT y FROM t WHERE x = 7"),
'77', 'change is applied through the demoted index');
is( $sub->safe_psql(
'postgres',
"SELECT replica_identity::text FROM $clt WHERE conflict_type = 'update_origin_differs'"
),
'{"x":"7"}',
'conflict log table shows the key the search used');
like(
slurp_file($sub->logfile, $log_offset),
qr/replica identity \(x\)=\(7\)/,
'server log shows the same key');
done_testing();