PostgreSQL Source Code git master
conflict.c
Go to the documentation of this file.
1/*-------------------------------------------------------------------------
2 * conflict.c
3 * Support routines for logging conflicts.
4 *
5 * Copyright (c) 2024-2025, PostgreSQL Global Development Group
6 *
7 * IDENTIFICATION
8 * src/backend/replication/logical/conflict.c
9 *
10 * This file contains the code for logging conflicts on the subscriber during
11 * logical replication.
12 *-------------------------------------------------------------------------
13 */
14
15#include "postgres.h"
16
17#include "access/commit_ts.h"
18#include "access/tableam.h"
19#include "executor/executor.h"
20#include "pgstat.h"
23#include "storage/lmgr.h"
24#include "utils/lsyscache.h"
25
26static const char *const ConflictTypeNames[] = {
27 [CT_INSERT_EXISTS] = "insert_exists",
28 [CT_UPDATE_ORIGIN_DIFFERS] = "update_origin_differs",
29 [CT_UPDATE_EXISTS] = "update_exists",
30 [CT_UPDATE_MISSING] = "update_missing",
31 [CT_DELETE_ORIGIN_DIFFERS] = "delete_origin_differs",
32 [CT_DELETE_MISSING] = "delete_missing",
33 [CT_MULTIPLE_UNIQUE_CONFLICTS] = "multiple_unique_conflicts"
34};
35
37static void errdetail_apply_conflict(EState *estate,
38 ResultRelInfo *relinfo,
40 TupleTableSlot *searchslot,
41 TupleTableSlot *localslot,
42 TupleTableSlot *remoteslot,
43 Oid indexoid, TransactionId localxmin,
44 RepOriginId localorigin,
45 TimestampTz localts, StringInfo err_msg);
46static char *build_tuple_value_details(EState *estate, ResultRelInfo *relinfo,
48 TupleTableSlot *searchslot,
49 TupleTableSlot *localslot,
50 TupleTableSlot *remoteslot,
51 Oid indexoid);
52static char *build_index_value_desc(EState *estate, Relation localrel,
53 TupleTableSlot *slot, Oid indexoid);
54
55/*
56 * Get the xmin and commit timestamp data (origin and timestamp) associated
57 * with the provided local tuple.
58 *
59 * Return true if the commit timestamp data was found, false otherwise.
60 */
61bool
63 RepOriginId *localorigin, TimestampTz *localts)
64{
65 Datum xminDatum;
66 bool isnull;
67
69 &isnull);
70 *xmin = DatumGetTransactionId(xminDatum);
71 Assert(!isnull);
72
73 /*
74 * The commit timestamp data is not available if track_commit_timestamp is
75 * disabled.
76 */
78 {
79 *localorigin = InvalidRepOriginId;
80 *localts = 0;
81 return false;
82 }
83
84 return TransactionIdGetCommitTsData(*xmin, localts, localorigin);
85}
86
87/*
88 * This function is used to report a conflict while applying replication
89 * changes.
90 *
91 * 'searchslot' should contain the tuple used to search the local tuple to be
92 * updated or deleted.
93 *
94 * 'remoteslot' should contain the remote new tuple, if any.
95 *
96 * conflicttuples is a list of local tuples that caused the conflict and the
97 * conflict related information. See ConflictTupleInfo.
98 *
99 * The caller must ensure that all the indexes passed in ConflictTupleInfo are
100 * locked so that we can fetch and display the conflicting key values.
101 */
102void
103ReportApplyConflict(EState *estate, ResultRelInfo *relinfo, int elevel,
104 ConflictType type, TupleTableSlot *searchslot,
105 TupleTableSlot *remoteslot, List *conflicttuples)
106{
107 Relation localrel = relinfo->ri_RelationDesc;
108 StringInfoData err_detail;
109
110 initStringInfo(&err_detail);
111
112 /* Form errdetail message by combining conflicting tuples information. */
113 foreach_ptr(ConflictTupleInfo, conflicttuple, conflicttuples)
114 errdetail_apply_conflict(estate, relinfo, type, searchslot,
115 conflicttuple->slot, remoteslot,
116 conflicttuple->indexoid,
117 conflicttuple->xmin,
118 conflicttuple->origin,
119 conflicttuple->ts,
120 &err_detail);
121
123
124 ereport(elevel,
126 errmsg("conflict detected on relation \"%s.%s\": conflict=%s",
128 RelationGetRelationName(localrel),
130 errdetail_internal("%s", err_detail.data));
131}
132
133/*
134 * Find all unique indexes to check for a conflict and store them into
135 * ResultRelInfo.
136 */
137void
139{
140 List *uniqueIndexes = NIL;
141
142 for (int i = 0; i < relInfo->ri_NumIndices; i++)
143 {
144 Relation indexRelation = relInfo->ri_IndexRelationDescs[i];
145
146 if (indexRelation == NULL)
147 continue;
148
149 /* Detect conflict only for unique indexes */
150 if (!relInfo->ri_IndexRelationInfo[i]->ii_Unique)
151 continue;
152
153 /* Don't support conflict detection for deferrable index */
154 if (!indexRelation->rd_index->indimmediate)
155 continue;
156
157 uniqueIndexes = lappend_oid(uniqueIndexes,
158 RelationGetRelid(indexRelation));
159 }
160
161 relInfo->ri_onConflictArbiterIndexes = uniqueIndexes;
162}
163
164/*
165 * Add SQLSTATE error code to the current conflict report.
166 */
167static int
169{
170 switch (type)
171 {
172 case CT_INSERT_EXISTS:
173 case CT_UPDATE_EXISTS:
175 return errcode(ERRCODE_UNIQUE_VIOLATION);
181 }
182
183 Assert(false);
184 return 0; /* silence compiler warning */
185}
186
187/*
188 * Add an errdetail() line showing conflict detail.
189 *
190 * The DETAIL line comprises of two parts:
191 * 1. Explanation of the conflict type, including the origin and commit
192 * timestamp of the existing local tuple.
193 * 2. Display of conflicting key, existing local tuple, remote new tuple, and
194 * replica identity columns, if any. The remote old tuple is excluded as its
195 * information is covered in the replica identity columns.
196 */
197static void
199 ConflictType type, TupleTableSlot *searchslot,
200 TupleTableSlot *localslot, TupleTableSlot *remoteslot,
201 Oid indexoid, TransactionId localxmin,
202 RepOriginId localorigin, TimestampTz localts,
203 StringInfo err_msg)
204{
205 StringInfoData err_detail;
206 char *val_desc;
207 char *origin_name;
208
209 initStringInfo(&err_detail);
210
211 /* First, construct a detailed message describing the type of conflict */
212 switch (type)
213 {
214 case CT_INSERT_EXISTS:
215 case CT_UPDATE_EXISTS:
217 Assert(OidIsValid(indexoid) &&
219
220 if (localts)
221 {
222 if (localorigin == InvalidRepOriginId)
223 appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified locally in transaction %u at %s."),
224 get_rel_name(indexoid),
225 localxmin, timestamptz_to_str(localts));
226 else if (replorigin_by_oid(localorigin, true, &origin_name))
227 appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by origin \"%s\" in transaction %u at %s."),
228 get_rel_name(indexoid), origin_name,
229 localxmin, timestamptz_to_str(localts));
230
231 /*
232 * The origin that modified this row has been removed. This
233 * can happen if the origin was created by a different apply
234 * worker and its associated subscription and origin were
235 * dropped after updating the row, or if the origin was
236 * manually dropped by the user.
237 */
238 else
239 appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by a non-existent origin in transaction %u at %s."),
240 get_rel_name(indexoid),
241 localxmin, timestamptz_to_str(localts));
242 }
243 else
244 appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified in transaction %u."),
245 get_rel_name(indexoid), localxmin);
246
247 break;
248
250 if (localorigin == InvalidRepOriginId)
251 appendStringInfo(&err_detail, _("Updating the row that was modified locally in transaction %u at %s."),
252 localxmin, timestamptz_to_str(localts));
253 else if (replorigin_by_oid(localorigin, true, &origin_name))
254 appendStringInfo(&err_detail, _("Updating the row that was modified by a different origin \"%s\" in transaction %u at %s."),
255 origin_name, localxmin, timestamptz_to_str(localts));
256
257 /* The origin that modified this row has been removed. */
258 else
259 appendStringInfo(&err_detail, _("Updating the row that was modified by a non-existent origin in transaction %u at %s."),
260 localxmin, timestamptz_to_str(localts));
261
262 break;
263
265 appendStringInfo(&err_detail, _("Could not find the row to be updated."));
266 break;
267
269 if (localorigin == InvalidRepOriginId)
270 appendStringInfo(&err_detail, _("Deleting the row that was modified locally in transaction %u at %s."),
271 localxmin, timestamptz_to_str(localts));
272 else if (replorigin_by_oid(localorigin, true, &origin_name))
273 appendStringInfo(&err_detail, _("Deleting the row that was modified by a different origin \"%s\" in transaction %u at %s."),
274 origin_name, localxmin, timestamptz_to_str(localts));
275
276 /* The origin that modified this row has been removed. */
277 else
278 appendStringInfo(&err_detail, _("Deleting the row that was modified by a non-existent origin in transaction %u at %s."),
279 localxmin, timestamptz_to_str(localts));
280
281 break;
282
284 appendStringInfo(&err_detail, _("Could not find the row to be deleted."));
285 break;
286 }
287
288 Assert(err_detail.len > 0);
289
290 val_desc = build_tuple_value_details(estate, relinfo, type, searchslot,
291 localslot, remoteslot, indexoid);
292
293 /*
294 * Next, append the key values, existing local tuple, remote tuple and
295 * replica identity columns after the message.
296 */
297 if (val_desc)
298 appendStringInfo(&err_detail, "\n%s", val_desc);
299
300 /*
301 * Insert a blank line to visually separate the new detail line from the
302 * existing ones.
303 */
304 if (err_msg->len > 0)
305 appendStringInfoChar(err_msg, '\n');
306
307 appendStringInfo(err_msg, "%s", err_detail.data);
308}
309
310/*
311 * Helper function to build the additional details for conflicting key,
312 * existing local tuple, remote tuple, and replica identity columns.
313 *
314 * If the return value is NULL, it indicates that the current user lacks
315 * permissions to view the columns involved.
316 */
317static char *
320 TupleTableSlot *searchslot,
321 TupleTableSlot *localslot,
322 TupleTableSlot *remoteslot,
323 Oid indexoid)
324{
325 Relation localrel = relinfo->ri_RelationDesc;
326 Oid relid = RelationGetRelid(localrel);
327 TupleDesc tupdesc = RelationGetDescr(localrel);
328 StringInfoData tuple_value;
329 char *desc = NULL;
330
331 Assert(searchslot || localslot || remoteslot);
332
333 initStringInfo(&tuple_value);
334
335 /*
336 * Report the conflicting key values in the case of a unique constraint
337 * violation.
338 */
341 {
342 Assert(OidIsValid(indexoid) && localslot);
343
344 desc = build_index_value_desc(estate, localrel, localslot, indexoid);
345
346 if (desc)
347 appendStringInfo(&tuple_value, _("Key %s"), desc);
348 }
349
350 if (localslot)
351 {
352 /*
353 * The 'modifiedCols' only applies to the new tuple, hence we pass
354 * NULL for the existing local tuple.
355 */
356 desc = ExecBuildSlotValueDescription(relid, localslot, tupdesc,
357 NULL, 64);
358
359 if (desc)
360 {
361 if (tuple_value.len > 0)
362 {
363 appendStringInfoString(&tuple_value, "; ");
364 appendStringInfo(&tuple_value, _("existing local tuple %s"),
365 desc);
366 }
367 else
368 {
369 appendStringInfo(&tuple_value, _("Existing local tuple %s"),
370 desc);
371 }
372 }
373 }
374
375 if (remoteslot)
376 {
377 Bitmapset *modifiedCols;
378
379 /*
380 * Although logical replication doesn't maintain the bitmap for the
381 * columns being inserted, we still use it to create 'modifiedCols'
382 * for consistency with other calls to ExecBuildSlotValueDescription.
383 *
384 * Note that generated columns are formed locally on the subscriber.
385 */
386 modifiedCols = bms_union(ExecGetInsertedCols(relinfo, estate),
387 ExecGetUpdatedCols(relinfo, estate));
388 desc = ExecBuildSlotValueDescription(relid, remoteslot, tupdesc,
389 modifiedCols, 64);
390
391 if (desc)
392 {
393 if (tuple_value.len > 0)
394 {
395 appendStringInfoString(&tuple_value, "; ");
396 appendStringInfo(&tuple_value, _("remote tuple %s"), desc);
397 }
398 else
399 {
400 appendStringInfo(&tuple_value, _("Remote tuple %s"), desc);
401 }
402 }
403 }
404
405 if (searchslot)
406 {
407 /*
408 * Note that while index other than replica identity may be used (see
409 * IsIndexUsableForReplicaIdentityFull for details) to find the tuple
410 * when applying update or delete, such an index scan may not result
411 * in a unique tuple and we still compare the complete tuple in such
412 * cases, thus such indexes are not used here.
413 */
414 Oid replica_index = GetRelationIdentityOrPK(localrel);
415
417
418 /*
419 * If the table has a valid replica identity index, build the index
420 * key value string. Otherwise, construct the full tuple value for
421 * REPLICA IDENTITY FULL cases.
422 */
423 if (OidIsValid(replica_index))
424 desc = build_index_value_desc(estate, localrel, searchslot, replica_index);
425 else
426 desc = ExecBuildSlotValueDescription(relid, searchslot, tupdesc, NULL, 64);
427
428 if (desc)
429 {
430 if (tuple_value.len > 0)
431 {
432 appendStringInfoString(&tuple_value, "; ");
433 appendStringInfo(&tuple_value, OidIsValid(replica_index)
434 ? _("replica identity %s")
435 : _("replica identity full %s"), desc);
436 }
437 else
438 {
439 appendStringInfo(&tuple_value, OidIsValid(replica_index)
440 ? _("Replica identity %s")
441 : _("Replica identity full %s"), desc);
442 }
443 }
444 }
445
446 if (tuple_value.len == 0)
447 return NULL;
448
449 appendStringInfoChar(&tuple_value, '.');
450 return tuple_value.data;
451}
452
453/*
454 * Helper functions to construct a string describing the contents of an index
455 * entry. See BuildIndexValueDescription for details.
456 *
457 * The caller must ensure that the index with the OID 'indexoid' is locked so
458 * that we can fetch and display the conflicting key value.
459 */
460static char *
462 Oid indexoid)
463{
464 char *index_value;
465 Relation indexDesc;
467 bool isnull[INDEX_MAX_KEYS];
468 TupleTableSlot *tableslot = slot;
469
470 if (!tableslot)
471 return NULL;
472
474
475 indexDesc = index_open(indexoid, NoLock);
476
477 /*
478 * If the slot is a virtual slot, copy it into a heap tuple slot as
479 * FormIndexDatum only works with heap tuple slots.
480 */
481 if (TTS_IS_VIRTUAL(slot))
482 {
483 tableslot = table_slot_create(localrel, &estate->es_tupleTable);
484 tableslot = ExecCopySlot(tableslot, slot);
485 }
486
487 /*
488 * Initialize ecxt_scantuple for potential use in FormIndexDatum when
489 * index expressions are present.
490 */
491 GetPerTupleExprContext(estate)->ecxt_scantuple = tableslot;
492
493 /*
494 * The values/nulls arrays passed to BuildIndexValueDescription should be
495 * the results of FormIndexDatum, which are the "raw" input to the index
496 * AM.
497 */
498 FormIndexDatum(BuildIndexInfo(indexDesc), tableslot, estate, values, isnull);
499
500 index_value = BuildIndexValueDescription(indexDesc, values, isnull);
501
502 index_close(indexDesc, NoLock);
503
504 return index_value;
505}
Subscription * MySubscription
Definition: worker.c:299
const char * timestamptz_to_str(TimestampTz t)
Definition: timestamp.c:1861
Bitmapset * bms_union(const Bitmapset *a, const Bitmapset *b)
Definition: bitmapset.c:251
static Datum values[MAXATTR]
Definition: bootstrap.c:151
uint32 TransactionId
Definition: c.h:623
#define OidIsValid(objectId)
Definition: c.h:746
bool track_commit_timestamp
Definition: commit_ts.c:109
bool TransactionIdGetCommitTsData(TransactionId xid, TimestampTz *ts, RepOriginId *nodeid)
Definition: commit_ts.c:274
void ReportApplyConflict(EState *estate, ResultRelInfo *relinfo, int elevel, ConflictType type, TupleTableSlot *searchslot, TupleTableSlot *remoteslot, List *conflicttuples)
Definition: conflict.c:103
static void errdetail_apply_conflict(EState *estate, ResultRelInfo *relinfo, ConflictType type, TupleTableSlot *searchslot, TupleTableSlot *localslot, TupleTableSlot *remoteslot, Oid indexoid, TransactionId localxmin, RepOriginId localorigin, TimestampTz localts, StringInfo err_msg)
Definition: conflict.c:198
static const char *const ConflictTypeNames[]
Definition: conflict.c:26
static char * build_tuple_value_details(EState *estate, ResultRelInfo *relinfo, ConflictType type, TupleTableSlot *searchslot, TupleTableSlot *localslot, TupleTableSlot *remoteslot, Oid indexoid)
Definition: conflict.c:318
static char * build_index_value_desc(EState *estate, Relation localrel, TupleTableSlot *slot, Oid indexoid)
Definition: conflict.c:461
void InitConflictIndexes(ResultRelInfo *relInfo)
Definition: conflict.c:138
static int errcode_apply_conflict(ConflictType type)
Definition: conflict.c:168
bool GetTupleTransactionInfo(TupleTableSlot *localslot, TransactionId *xmin, RepOriginId *localorigin, TimestampTz *localts)
Definition: conflict.c:62
ConflictType
Definition: conflict.h:25
@ CT_MULTIPLE_UNIQUE_CONFLICTS
Definition: conflict.h:45
@ CT_DELETE_MISSING
Definition: conflict.h:42
@ CT_UPDATE_ORIGIN_DIFFERS
Definition: conflict.h:30
@ CT_INSERT_EXISTS
Definition: conflict.h:27
@ CT_UPDATE_EXISTS
Definition: conflict.h:33
@ CT_UPDATE_MISSING
Definition: conflict.h:36
@ CT_DELETE_ORIGIN_DIFFERS
Definition: conflict.h:39
int64 TimestampTz
Definition: timestamp.h:39
int errdetail_internal(const char *fmt,...)
Definition: elog.c:1230
int errcode(int sqlerrcode)
Definition: elog.c:853
int errmsg(const char *fmt,...)
Definition: elog.c:1070
#define _(x)
Definition: elog.c:90
#define ereport(elevel,...)
Definition: elog.h:149
char * ExecBuildSlotValueDescription(Oid reloid, TupleTableSlot *slot, TupleDesc tupdesc, Bitmapset *modifiedCols, int maxfieldlen)
Definition: execMain.c:2352
Bitmapset * ExecGetInsertedCols(ResultRelInfo *relinfo, EState *estate)
Definition: execUtils.c:1363
Bitmapset * ExecGetUpdatedCols(ResultRelInfo *relinfo, EState *estate)
Definition: execUtils.c:1384
#define GetPerTupleExprContext(estate)
Definition: executor.h:637
char * BuildIndexValueDescription(Relation indexRelation, const Datum *values, const bool *isnull)
Definition: genam.c:178
Assert(PointerIsAligned(start, uint64))
IndexInfo * BuildIndexInfo(Relation index)
Definition: index.c:2428
void FormIndexDatum(IndexInfo *indexInfo, TupleTableSlot *slot, EState *estate, Datum *values, bool *isnull)
Definition: index.c:2730
void index_close(Relation relation, LOCKMODE lockmode)
Definition: indexam.c:177
Relation index_open(Oid relationId, LOCKMODE lockmode)
Definition: indexam.c:133
int i
Definition: isn.c:74
List * lappend_oid(List *list, Oid datum)
Definition: list.c:375
bool CheckRelationOidLockedByMe(Oid relid, LOCKMODE lockmode, bool orstronger)
Definition: lmgr.c:351
#define NoLock
Definition: lockdefs.h:34
#define RowExclusiveLock
Definition: lockdefs.h:38
char * get_rel_name(Oid relid)
Definition: lsyscache.c:2011
char * get_namespace_name(Oid nspid)
Definition: lsyscache.c:3449
bool replorigin_by_oid(RepOriginId roident, bool missing_ok, char **roname)
Definition: origin.c:470
#define InvalidRepOriginId
Definition: origin.h:33
#define INDEX_MAX_KEYS
#define NIL
Definition: pg_list.h:68
#define foreach_ptr(type, var, lst)
Definition: pg_list.h:469
#define ERRCODE_T_R_SERIALIZATION_FAILURE
Definition: pgbench.c:77
void pgstat_report_subscription_conflict(Oid subid, ConflictType type)
uintptr_t Datum
Definition: postgres.h:69
static TransactionId DatumGetTransactionId(Datum X)
Definition: postgres.h:267
unsigned int Oid
Definition: postgres_ext.h:32
#define RelationGetRelid(relation)
Definition: rel.h:513
#define RelationGetDescr(relation)
Definition: rel.h:539
#define RelationGetRelationName(relation)
Definition: rel.h:547
#define RelationGetNamespace(relation)
Definition: rel.h:554
Oid GetRelationIdentityOrPK(Relation rel)
Definition: relation.c:891
void appendStringInfo(StringInfo str, const char *fmt,...)
Definition: stringinfo.c:145
void appendStringInfoString(StringInfo str, const char *s)
Definition: stringinfo.c:230
void appendStringInfoChar(StringInfo str, char ch)
Definition: stringinfo.c:242
void initStringInfo(StringInfo str)
Definition: stringinfo.c:97
List * es_tupleTable
Definition: execnodes.h:704
bool ii_Unique
Definition: execnodes.h:209
Definition: pg_list.h:54
Form_pg_index rd_index
Definition: rel.h:192
int ri_NumIndices
Definition: execnodes.h:478
List * ri_onConflictArbiterIndexes
Definition: execnodes.h:569
Relation ri_RelationDesc
Definition: execnodes.h:475
RelationPtr ri_IndexRelationDescs
Definition: execnodes.h:481
IndexInfo ** ri_IndexRelationInfo
Definition: execnodes.h:484
#define MinTransactionIdAttributeNumber
Definition: sysattr.h:22
TupleTableSlot * table_slot_create(Relation relation, List **reglist)
Definition: tableam.c:92
#define TTS_IS_VIRTUAL(slot)
Definition: tuptable.h:234
static Datum slot_getsysattr(TupleTableSlot *slot, int attnum, bool *isnull)
Definition: tuptable.h:416
static TupleTableSlot * ExecCopySlot(TupleTableSlot *dstslot, TupleTableSlot *srcslot)
Definition: tuptable.h:509
const char * type
uint16 RepOriginId
Definition: xlogdefs.h:65