diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c index 13644328164..1fc68b0ecef 100644 --- a/src/backend/executor/execReplication.c +++ b/src/backend/executor/execReplication.c @@ -876,7 +876,7 @@ CheckAndReportConflict(ResultRelInfo *resultRelInfo, EState *estate, if (conflicttuples) ReportApplyConflict(estate, resultRelInfo, ERROR, list_length(conflicttuples) > 1 ? CT_MULTIPLE_UNIQUE_CONFLICTS : type, - searchslot, remoteslot, conflicttuples); + NULL, searchslot, remoteslot, conflicttuples); } /* diff --git a/src/backend/replication/logical/conflict.c b/src/backend/replication/logical/conflict.c index 9791a023201..ef19093001c 100644 --- a/src/backend/replication/logical/conflict.c +++ b/src/backend/replication/logical/conflict.c @@ -165,6 +165,7 @@ static Datum build_local_conflicts_json_array(List *conflicttuples); static void insert_conflict_log_tuple(EState *estate, Relation rel, Relation conflictlogrel, ConflictType conflict_type, + Oid searchindexoid, TupleTableSlot *searchslot, List *conflicttuples); static void lock_conflictlog_relation(Oid relid); @@ -320,6 +321,11 @@ GetTupleTransactionInfo(TupleTableSlot *localslot, TransactionId *xmin, * This function is used to report a conflict while applying replication * changes. * + * 'relmapentry' is the relation map entry that was used to search the local + * row, or NULL if no search was done (ERROR-level insert conflicts). If the + * search used the replica identity or primary key index, the caller must have + * locked it, which the search itself does, see FindReplTupleInLocalRel(). + * * 'searchslot' should contain the tuple used to search the local row to be * updated or deleted. * @@ -333,16 +339,27 @@ GetTupleTransactionInfo(TupleTableSlot *localslot, TransactionId *xmin, */ void ReportApplyConflict(EState *estate, ResultRelInfo *relinfo, int elevel, - ConflictType type, TupleTableSlot *searchslot, + ConflictType type, LogicalRepRelMapEntry *relmapentry, + TupleTableSlot *searchslot, TupleTableSlot *remoteslot, List *conflicttuples) { Relation localrel = relinfo->ri_RelationDesc; ConflictLogDest dest; + Oid searchindexoid = InvalidOid; Relation conflictlogrel; StringInfoData err_detail; pgstat_report_subscription_conflict(MySubscription->oid, type); + /* + * Use the replica identity or primary key index that the search actually + * used, which the caller has locked, rather than looking it up again. + * Concurrent DDL could have changed which index that is since the search, + * see comments in FindReplTupleInLocalRel(). + */ + if (relmapentry && relmapentry->idxisreplident) + searchindexoid = relmapentry->localindexoid; + conflictlogrel = get_conflictlog_dest_and_table(elevel, &dest); initStringInfo(&err_detail); @@ -406,6 +423,7 @@ ReportApplyConflict(EState *estate, ResultRelInfo *relinfo, int elevel, relinfo->ri_RelationDesc, conflictlogrel, type, + searchindexoid, searchslot, conflicttuples); table_close(conflictlogrel, NoLock); @@ -472,10 +490,7 @@ get_conflictlog_dest_and_table(int elevel, ConflictLogDest *log_dest) return NULL; } - /* - * Convert the text log destination to the internal enum. MySubscription - * already contains the data from pg_subscription. - */ + /* Convert the text log destination to the internal enum. */ *log_dest = GetConflictLogDest(MySubscription->conflictlogdest); /* Quick exit if a conflict log table was not requested. */ @@ -1012,7 +1027,6 @@ get_tuple_desc(EState *estate, ResultRelInfo *relinfo, ConflictType type, /* * Helper function to extract the "raw" index key Datums and their null flags * from a TupleTableSlot, given an already open index descriptor. - * This is the reusable core logic. */ static void build_index_datums_from_slot(EState *estate, Relation localrel, @@ -1023,19 +1037,17 @@ build_index_datums_from_slot(EState *estate, Relation localrel, TupleTableSlot *tableslot = slot; /* - * If the slot is a virtual slot, copy it into a heap tuple slot as - * FormIndexDatum only works with heap tuple slots. + * FormIndexDatum needs an EState whose per-tuple econtext's scantuple + * points to the slot holding the tuple, to evaluate any index expressions. + * It also requires a heap tuple slot, so copy a virtual slot into one, + * created in the EState's tuple table so that it is freed along with it. */ if (TTS_IS_VIRTUAL(slot)) { - /* Slot is created within the EState's tuple table */ tableslot = table_slot_create(localrel, &estate->es_tupleTable); tableslot = ExecCopySlot(tableslot, slot); } - /* - * Initialize ecxt_scantuple for potential use in FormIndexDatum - */ GetPerTupleExprContext(estate)->ecxt_scantuple = tableslot; /* Form the index datums */ @@ -1084,6 +1096,7 @@ build_index_value_desc(EState *estate, Relation localrel, TupleTableSlot *slot, * * The caller must ensure that the index with the OID 'indexid' is locked so * that we can safely fetch and construct the replica identity key values. + * See ReportApplyConflict() for how this holds. */ static Datum build_index_key_json(EState *estate, Relation localrel, @@ -1108,19 +1121,15 @@ build_index_key_json(EState *estate, Relation localrel, isnull); /* - * Build the JSON object here rather than handing the values to - * row_to_json(), and render each of them with its type's output function. - * - * row_to_json() would go through json_categorize_type(), which honours a - * user CREATE CAST (t AS json) for any type at or above - * FirstNormalObjectId. That cast is a function no other part of apply - * ever calls -- the publisher sends the value using the type's output - * function, the index compares it with the opclass, and neither asks for - * a json representation -- so its result is unrelated to the size of the - * value we received, and a tiny key could render a json value above the - * 1GB limit and error out the apply worker. Using the output function - * keeps us to the same representation the publisher already produced, as - * the server log does in BuildIndexValueDescription(). + * Build the JSON object here rather than passing the values to + * row_to_json(). For a type at or above FirstNormalObjectId, + * json_categorize_type() would use a user-defined cast to json if one + * exists. Nothing else in apply calls that cast, so its output is + * unrelated to the size of the value we received, and a small key could + * produce a json value over the 1GB limit and fail the apply worker. + * Rendering each value with its type's output function keeps us to the + * representation the publisher sent, and matches what the server log + * does in BuildIndexValueDescription(). * * Only the key attributes are recorded. FormIndexDatum() above also * fills in any non-key (INCLUDE) columns, but those are not part of the @@ -1359,6 +1368,7 @@ static void insert_conflict_log_tuple(EState *estate, Relation rel, Relation conflictlogrel, ConflictType conflict_type, + Oid searchindexoid, TupleTableSlot *searchslot, List *conflicttuples) { @@ -1423,10 +1433,10 @@ insert_conflict_log_tuple(EState *estate, Relation rel, */ Assert(!TupIsNull(searchslot)); - replica_index = GetRelationIdentityOrPK(rel); + replica_index = searchindexoid; /* - * If the table has a valid replica identity index, build the index JSON + * If the search used a replica identity index, build the index JSON * datum from key value. Otherwise, in REPLICA IDENTITY FULL cases, set * replica_identity_full to true and leave replica_identity NULL to avoid * serializing full tuples that could exceed memory allocation limits. diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index c46afc2f55b..56abc2cdc0b 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -3011,7 +3011,7 @@ apply_handle_update_internal(ApplyExecutionData *edata, conflicttuple.slot = localslot; ReportApplyConflict(estate, relinfo, LOG, CT_UPDATE_ORIGIN_DIFFERS, - remoteslot, newslot, + relmapentry, remoteslot, newslot, list_make1(&conflicttuple)); } @@ -3054,7 +3054,8 @@ apply_handle_update_internal(ApplyExecutionData *edata, * The tuple to be updated could not be found or was deleted. Do * nothing except for emitting a log message. */ - ReportApplyConflict(estate, relinfo, LOG, type, remoteslot, newslot, + ReportApplyConflict(estate, relinfo, LOG, type, relmapentry, + remoteslot, newslot, type == CT_UPDATE_DELETED ? list_make1(&conflicttuple) : NIL); } @@ -3198,7 +3199,7 @@ apply_handle_delete_internal(ApplyExecutionData *edata, { conflicttuple.slot = localslot; ReportApplyConflict(estate, relinfo, LOG, CT_DELETE_ORIGIN_DIFFERS, - remoteslot, NULL, + relmapentry, remoteslot, NULL, list_make1(&conflicttuple)); } @@ -3215,7 +3216,7 @@ apply_handle_delete_internal(ApplyExecutionData *edata, * emitting a log message. */ ReportApplyConflict(estate, relinfo, LOG, CT_DELETE_MISSING, - remoteslot, NULL, NIL); + relmapentry, remoteslot, NULL, NIL); } /* Cleanup. */ @@ -3575,7 +3576,8 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, * deleted. Do nothing except for emitting a log message. */ ReportApplyConflict(estate, partrelinfo, LOG, - type, remoteslot_part, newslot, + type, part_entry, + remoteslot_part, newslot, type == CT_UPDATE_DELETED ? list_make1(&conflicttuple) : NIL); return; @@ -3599,7 +3601,7 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, conflicttuple.slot = localslot; ReportApplyConflict(estate, partrelinfo, LOG, CT_UPDATE_ORIGIN_DIFFERS, - remoteslot_part, newslot, + part_entry, remoteslot_part, newslot, list_make1(&conflicttuple)); } diff --git a/src/include/replication/conflict.h b/src/include/replication/conflict.h index 326570ed639..5643052a133 100644 --- a/src/include/replication/conflict.h +++ b/src/include/replication/conflict.h @@ -15,6 +15,7 @@ /* Avoid including execnodes.h here */ typedef struct EState EState; +typedef struct LogicalRepRelMapEntry LogicalRepRelMapEntry; typedef struct ResultRelInfo ResultRelInfo; typedef struct TupleTableSlot TupleTableSlot; @@ -111,6 +112,7 @@ extern bool GetTupleTransactionInfo(TupleTableSlot *localslot, TimestampTz *localts); extern void ReportApplyConflict(EState *estate, ResultRelInfo *relinfo, int elevel, ConflictType type, + LogicalRepRelMapEntry *relmapentry, TupleTableSlot *searchslot, TupleTableSlot *remoteslot, List *conflicttuples);