From 8db6f660e310db0f664adc8b342c7b79fb79608b Mon Sep 17 00:00:00 2001 From: Mikhail Nikalayeu Date: Thu, 3 Sep 2026 11:20:56 +0200 Subject: [PATCH v3] Don't re-derive in the scan what the apply worker knew about its index FindLogicalRepLocalIndex() picks the index to search the local relation by, either as the relation's replica identity or primary key, or as one usable for a REPLICA IDENTITY FULL remote relation. Which of the two it is decides whether the first index match is the tuple or every match has to be compared against the search slot. Only the OID is kept, though, so RelationFindReplTupleByIndex() works that out a second time on its own, by testing the index it is handed against GetRelationIdentityOrPK(). The second answer need not match the first. Apply holds only RowExclusiveLock, which conflicts with neither REINDEX CONCURRENTLY swapping the index for its rebuilt copy nor DROP INDEX CONCURRENTLY marking it invalid, so either can commit in between, and ExecOpenIndices() is where the invalidation lands. The scan then compares whole rows against a search slot that carries one only under REPLICA IDENTITY FULL, nothing matches, and the change is silently dropped as an update_missing conflict. Assertion builds trip over the identity check just above instead. Record which of the two it was in the relation map entry, as idxisreplident, and hand the entry itself down to FindReplTupleInLocalRel(), settling the question once. The new field goes into the padding after updatable, so that the back branches keep sizeof(LogicalRepRelMapEntry) and every member offset unchanged. There RelationFindReplTupleByIndex() likewise keeps its signature, since out-of-tree apply code (spock, for one) calls it, and the new argument goes on a RelationFindReplTupleByIndexExt() that it wraps. HEAD keeps the single entry point. check_relation_updatable() asked the catalogs the same question to pick its error message, and could likewise get an answer that no longer matched the entry; it goes by the entry now too. The conflict report has the same problem describing the search slot, but fixing that means reaching through the executor, so it only gets an XXX comment here. abidiff still reports the member insertion, so the back branches need an entry in .abi-compliance-history; per that file's policy it is to be added once the abi-compliance-check buildfarm animals report it. Oversight in 89e46da5e51. Author: Mikhail Nikalayeu Reviewed-by: Amit Kapila Reviewed-by: vignesh C Reviewed-by: Zhijie Hou Discussion: https://postgr.es/m/CADzfLwUJovFcnknCC9wjZKECX9xecgnGzC2r2TMV8h4QDD_jwQ@mail.gmail.com Backpatch-through: 16, where it was introduced --- src/backend/executor/execReplication.c | 38 +++++++++-- src/backend/replication/logical/relation.c | 19 ++++-- src/backend/replication/logical/worker.c | 78 +++++++++++++--------- src/include/executor/executor.h | 5 ++ src/include/replication/logicalrelation.h | 7 ++ 5 files changed, 103 insertions(+), 44 deletions(-) diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c index cb1202e4506..2d7ed0f66bf 100644 --- a/src/backend/executor/execReplication.c +++ b/src/backend/executor/execReplication.c @@ -171,12 +171,20 @@ build_replindex_scan_key(ScanKey skey, Relation rel, Relation idxrel, * * If a matching tuple is found, lock it with lockmode, fill the slot with its * contents, and return true. Return false otherwise. + * + * 'isIdxSafeToSkipDuplicates' says how the scan's matches are to be treated: + * if true, the index determines the tuple all by itself and the first match + * is taken; if false, every match is compared against 'searchslot', which must + * then carry a complete row -- the remote relation has REPLICA IDENTITY FULL. + * The caller decides; deriving it here would let concurrent DDL flip it after + * the index was chosen. */ bool -RelationFindReplTupleByIndex(Relation rel, Oid idxoid, - LockTupleMode lockmode, - TupleTableSlot *searchslot, - TupleTableSlot *outslot) +RelationFindReplTupleByIndexExt(Relation rel, Oid idxoid, + bool isIdxSafeToSkipDuplicates, + LockTupleMode lockmode, + TupleTableSlot *searchslot, + TupleTableSlot *outslot) { ScanKeyData skey[INDEX_MAX_KEYS]; int skey_attoff; @@ -186,13 +194,10 @@ RelationFindReplTupleByIndex(Relation rel, Oid idxoid, Relation idxrel; bool found; TypeCacheEntry **eq = NULL; - bool isIdxSafeToSkipDuplicates; /* Open the index. */ idxrel = index_open(idxoid, RowExclusiveLock); - isIdxSafeToSkipDuplicates = (GetRelationIdentityOrPK(rel) == idxoid); - InitDirtySnapshot(snap); /* Build scan key. */ @@ -298,6 +303,25 @@ retry: return found; } +/* + * ABI-compatible wrapper to emulate old version of the above function. + * Do not call this version in new code. + */ +bool +RelationFindReplTupleByIndex(Relation rel, Oid idxoid, + LockTupleMode lockmode, + TupleTableSlot *searchslot, + TupleTableSlot *outslot) +{ + bool isIdxSafeToSkipDuplicates; + + isIdxSafeToSkipDuplicates = (GetRelationIdentityOrPK(rel) == idxoid); + + return RelationFindReplTupleByIndexExt(rel, idxoid, + isIdxSafeToSkipDuplicates, + lockmode, searchslot, outslot); +} + /* * Compare the tuples in the slots by checking if they have equal values. */ diff --git a/src/backend/replication/logical/relation.c b/src/backend/replication/logical/relation.c index 8794574f42a..535113881b0 100644 --- a/src/backend/replication/logical/relation.c +++ b/src/backend/replication/logical/relation.c @@ -55,7 +55,7 @@ typedef struct LogicalRepPartMapEntry } LogicalRepPartMapEntry; static Oid FindLogicalRepLocalIndex(Relation localrel, LogicalRepRelation *remoterel, - AttrMap *attrMap); + AttrMap *attrMap, bool *idxisreplident); /* * Relcache invalidation callback for our relation map cache. @@ -453,7 +453,8 @@ logicalrep_rel_open(LogicalRepRelId remoteid, LOCKMODE lockmode) * on the relation). */ entry->localindexoid = FindLogicalRepLocalIndex(entry->localrel, remoterel, - entry->attrmap); + entry->attrmap, + &entry->idxisreplident); entry->localrelvalid = true; } @@ -725,7 +726,8 @@ logicalrep_partition_open(LogicalRepRelMapEntry *root, * anything in the LogicalRepPartMapContext (hence CacheMemoryContext). */ entry->localindexoid = FindLogicalRepLocalIndex(partrel, remoterel, - entry->attrmap); + entry->attrmap, + &entry->idxisreplident); entry->localrelvalid = true; @@ -877,13 +879,19 @@ GetRelationIdentityOrPK(Relation rel) /* * Returns the index oid if we can use an index for subscriber. Otherwise, * returns InvalidOid. + * + * '*idxisreplident' is set to whether the returned index was chosen as the + * relation's replica identity or primary key, rather than as one usable + * for a REPLICA IDENTITY FULL remote relation. */ static Oid FindLogicalRepLocalIndex(Relation localrel, LogicalRepRelation *remoterel, - AttrMap *attrMap) + AttrMap *attrMap, bool *idxisreplident) { Oid idxoid; + *idxisreplident = false; + /* * We never need index oid for partitioned tables, always rely on leaf * partition's index. @@ -896,7 +904,10 @@ FindLogicalRepLocalIndex(Relation localrel, LogicalRepRelation *remoterel, */ idxoid = GetRelationIdentityOrPK(localrel); if (OidIsValid(idxoid)) + { + *idxisreplident = true; return idxoid; + } if (remoterel->replident == REPLICA_IDENTITY_FULL) { diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index 10c5dbed25f..ec057625370 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -147,6 +147,7 @@ #include #include +#include "access/htup_details.h" #include "access/table.h" #include "access/tableam.h" #include "access/twophase.h" @@ -385,15 +386,13 @@ static void apply_handle_insert_internal(ApplyExecutionData *edata, static void apply_handle_update_internal(ApplyExecutionData *edata, ResultRelInfo *relinfo, TupleTableSlot *remoteslot, - LogicalRepTupleData *newtup, - Oid localindexoid); + LogicalRepTupleData *newtup); static void apply_handle_delete_internal(ApplyExecutionData *edata, ResultRelInfo *relinfo, TupleTableSlot *remoteslot, - Oid localindexoid); + LogicalRepRelMapEntry *relmapentry); static bool FindReplTupleInLocalRel(ApplyExecutionData *edata, Relation localrel, - LogicalRepRelation *remoterel, - Oid localidxoid, + LogicalRepRelMapEntry *relmapentry, TupleTableSlot *remoteslot, TupleTableSlot **localslot); static void apply_handle_tuple_routing(ApplyExecutionData *edata, @@ -2513,7 +2512,7 @@ check_relation_updatable(LogicalRepRelMapEntry *rel) * We are in error mode so it's fine this is somewhat slow. It's better to * give user correct error. */ - if (OidIsValid(GetRelationIdentityOrPK(rel->localrel))) + if (rel->idxisreplident) { ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), @@ -2638,7 +2637,7 @@ apply_handle_update(StringInfo s) remoteslot, &newtup, CMD_UPDATE); else apply_handle_update_internal(edata, edata->targetRelInfo, - remoteslot, &newtup, rel->localindexoid); + remoteslot, &newtup); finish_edata(edata); @@ -2662,8 +2661,7 @@ static void apply_handle_update_internal(ApplyExecutionData *edata, ResultRelInfo *relinfo, TupleTableSlot *remoteslot, - LogicalRepTupleData *newtup, - Oid localindexoid) + LogicalRepTupleData *newtup) { EState *estate = edata->estate; LogicalRepRelMapEntry *relmapentry = edata->targetRel; @@ -2676,9 +2674,7 @@ apply_handle_update_internal(ApplyExecutionData *edata, EvalPlanQualInit(&epqstate, estate, NULL, NIL, -1, NIL); ExecOpenIndices(relinfo, false); - found = FindReplTupleInLocalRel(edata, localrel, - &relmapentry->remoterel, - localindexoid, + found = FindReplTupleInLocalRel(edata, localrel, relmapentry, remoteslot, &localslot); ExecClearTuple(remoteslot); @@ -2797,7 +2793,7 @@ apply_handle_delete(StringInfo s) ExecOpenIndices(relinfo, false); apply_handle_delete_internal(edata, relinfo, - remoteslot, rel->localindexoid); + remoteslot, rel); ExecCloseIndices(relinfo); } @@ -2823,11 +2819,10 @@ static void apply_handle_delete_internal(ApplyExecutionData *edata, ResultRelInfo *relinfo, TupleTableSlot *remoteslot, - Oid localindexoid) + LogicalRepRelMapEntry *relmapentry) { EState *estate = edata->estate; Relation localrel = relinfo->ri_RelationDesc; - LogicalRepRelation *remoterel = &edata->targetRel->remoterel; EPQState epqstate; TupleTableSlot *localslot; bool found; @@ -2839,7 +2834,7 @@ apply_handle_delete_internal(ApplyExecutionData *edata, !localrel->rd_rel->relhasindex || RelationGetIndexList(localrel) == NIL); - found = FindReplTupleInLocalRel(edata, localrel, remoterel, localindexoid, + found = FindReplTupleInLocalRel(edata, localrel, relmapentry, remoteslot, &localslot); /* If found delete it. */ @@ -2874,16 +2869,20 @@ apply_handle_delete_internal(ApplyExecutionData *edata, * the corresponding local relation using either replica identity index, * primary key, index or if needed, sequential scan. * + * 'relmapentry' is the relation map entry for 'localrel'; it tells which + * index to use, if any, and whether that index is the relation's replica + * identity or primary key. + * * Local tuple, if found, is returned in '*localslot'. */ static bool FindReplTupleInLocalRel(ApplyExecutionData *edata, Relation localrel, - LogicalRepRelation *remoterel, - Oid localidxoid, + LogicalRepRelMapEntry *relmapentry, TupleTableSlot *remoteslot, TupleTableSlot **localslot) { EState *estate = edata->estate; + Oid localidxoid = relmapentry->localindexoid; bool found; /* @@ -2895,23 +2894,40 @@ FindReplTupleInLocalRel(ApplyExecutionData *edata, Relation localrel, *localslot = table_slot_create(localrel, &estate->es_tupleTable); Assert(OidIsValid(localidxoid) || - (remoterel->replident == REPLICA_IDENTITY_FULL)); + (relmapentry->remoterel.replident == REPLICA_IDENTITY_FULL)); if (OidIsValid(localidxoid)) { #ifdef USE_ASSERT_CHECKING Relation idxrel = index_open(localidxoid, AccessShareLock); - /* Index must be PK, RI, or usable for REPLICA IDENTITY FULL tables */ - Assert(GetRelationIdentityOrPK(idxrel) == localidxoid || - IsIndexUsableForReplicaIdentityFull(BuildIndexInfo(idxrel), - edata->targetRel->attrmap)); + if (relmapentry->idxisreplident) + { + /* + * A replica identity or primary key index identifies the row on + * its own. The catalogs might no longer call it the identity, + * as DROP INDEX CONCURRENTLY or REINDEX CONCURRENTLY can commit + * while we hold only RowExclusiveLock on the table, but neither + * makes it non-unique or partial. + */ + Assert(idxrel->rd_index->indisunique); + Assert(heap_attisnull(idxrel->rd_indextuple, + Anum_pg_index_indpred, NULL)); + } + else + { + /* Otherwise every match is compared, so we need a whole row. */ + Assert(relmapentry->remoterel.replident == REPLICA_IDENTITY_FULL); + Assert(IsIndexUsableForReplicaIdentityFull(BuildIndexInfo(idxrel), + relmapentry->attrmap)); + } index_close(idxrel, AccessShareLock); #endif - found = RelationFindReplTupleByIndex(localrel, localidxoid, - LockTupleExclusive, - remoteslot, *localslot); + found = RelationFindReplTupleByIndexExt(localrel, localidxoid, + relmapentry->idxisreplident, + LockTupleExclusive, + remoteslot, *localslot); } else found = RelationFindReplTupleSeq(localrel, LockTupleExclusive, @@ -3011,8 +3027,7 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, case CMD_DELETE: apply_handle_delete_internal(edata, partrelinfo, - remoteslot_part, - part_entry->localindexoid); + remoteslot_part, part_entry); break; case CMD_UPDATE: @@ -3030,9 +3045,7 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, bool found; /* Get the matching local tuple from the partition. */ - found = FindReplTupleInLocalRel(edata, partrel, - &part_entry->remoterel, - part_entry->localindexoid, + found = FindReplTupleInLocalRel(edata, partrel, part_entry, remoteslot_part, &localslot); if (!found) { @@ -3127,8 +3140,7 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, /* DELETE old tuple found in the old partition. */ apply_handle_delete_internal(edata, partrelinfo, - localslot, - part_entry->localindexoid); + localslot, part_entry); /* INSERT new tuple into the new partition. */ diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index 7b8b07bd034..833f753179f 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -656,6 +656,11 @@ extern bool RelationFindReplTupleByIndex(Relation rel, Oid idxoid, LockTupleMode lockmode, TupleTableSlot *searchslot, TupleTableSlot *outslot); +extern bool RelationFindReplTupleByIndexExt(Relation rel, Oid idxoid, + bool isIdxSafeToSkipDuplicates, + LockTupleMode lockmode, + TupleTableSlot *searchslot, + TupleTableSlot *outslot); extern bool RelationFindReplTupleSeq(Relation rel, LockTupleMode lockmode, TupleTableSlot *searchslot, TupleTableSlot *outslot); diff --git a/src/include/replication/logicalrelation.h b/src/include/replication/logicalrelation.h index e687b40a566..d32674d505d 100644 --- a/src/include/replication/logicalrelation.h +++ b/src/include/replication/logicalrelation.h @@ -32,6 +32,13 @@ typedef struct LogicalRepRelMapEntry Relation localrel; /* relcache entry (NULL when closed) */ AttrMap *attrmap; /* map of local attributes to remote ones */ bool updatable; /* Can apply updates/deletes? */ + bool idxisreplident; /* whether localindexoid is the relation's + * replica identity or primary key, rather + * than an index usable for a REPLICA + * IDENTITY FULL remote relation. Placed + * here to fit in existing padding, which + * leaves the struct layout unchanged in + * the back branches. */ Oid localindexoid; /* which index to use, or InvalidOid if none */ /* Sync state. */ -- 2.43.0