Age Owner Branch data TLA Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : * conflict.c
3 : : * Support routines for logging conflicts.
4 : : *
5 : : * Copyright (c) 2024-2026, 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/genam.h"
19 : : #include "access/tableam.h"
20 : : #include "executor/executor.h"
21 : : #include "pgstat.h"
22 : : #include "replication/conflict.h"
23 : : #include "replication/worker_internal.h"
24 : : #include "storage/lmgr.h"
25 : : #include "utils/lsyscache.h"
26 : :
27 : : static const char *const ConflictTypeNames[] = {
28 : : [CT_INSERT_EXISTS] = "insert_exists",
29 : : [CT_UPDATE_ORIGIN_DIFFERS] = "update_origin_differs",
30 : : [CT_UPDATE_EXISTS] = "update_exists",
31 : : [CT_UPDATE_MISSING] = "update_missing",
32 : : [CT_DELETE_ORIGIN_DIFFERS] = "delete_origin_differs",
33 : : [CT_UPDATE_DELETED] = "update_deleted",
34 : : [CT_DELETE_MISSING] = "delete_missing",
35 : : [CT_MULTIPLE_UNIQUE_CONFLICTS] = "multiple_unique_conflicts"
36 : : };
37 : :
38 : : static int errcode_apply_conflict(ConflictType type);
39 : : static void errdetail_apply_conflict(EState *estate,
40 : : ResultRelInfo *relinfo,
41 : : ConflictType type,
42 : : TupleTableSlot *searchslot,
43 : : TupleTableSlot *localslot,
44 : : TupleTableSlot *remoteslot,
45 : : Oid indexoid, TransactionId localxmin,
46 : : ReplOriginId localorigin,
47 : : TimestampTz localts, StringInfo err_msg);
48 : : static void get_tuple_desc(EState *estate, ResultRelInfo *relinfo,
49 : : ConflictType type, char **key_desc,
50 : : TupleTableSlot *localslot, char **local_desc,
51 : : TupleTableSlot *remoteslot, char **remote_desc,
52 : : TupleTableSlot *searchslot, char **search_desc,
53 : : Oid indexoid);
54 : : static char *build_index_value_desc(EState *estate, Relation localrel,
55 : : TupleTableSlot *slot, Oid indexoid);
56 : :
57 : : /*
58 : : * Get the xmin and commit timestamp data (origin and timestamp) associated
59 : : * with the provided local row.
60 : : *
61 : : * Return true if the commit timestamp data was found, false otherwise.
62 : : */
63 : : bool
623 akapila@postgresql.o 64 :CBC 72311 : GetTupleTransactionInfo(TupleTableSlot *localslot, TransactionId *xmin,
65 : : ReplOriginId *localorigin, TimestampTz *localts)
66 : : {
67 : : Datum xminDatum;
68 : : bool isnull;
69 : :
70 : 72311 : xminDatum = slot_getsysattr(localslot, MinTransactionIdAttributeNumber,
71 : : &isnull);
72 : 72311 : *xmin = DatumGetTransactionId(xminDatum);
73 [ - + ]: 72311 : Assert(!isnull);
74 : :
75 : : /*
76 : : * The commit timestamp data is not available if track_commit_timestamp is
77 : : * disabled.
78 : : */
79 [ + + ]: 72311 : if (!track_commit_timestamp)
80 : : {
97 msawada@postgresql.o 81 :GNC 72251 : *localorigin = InvalidReplOriginId;
623 akapila@postgresql.o 82 :CBC 72251 : *localts = 0;
83 : 72251 : return false;
84 : : }
85 : :
86 : 60 : return TransactionIdGetCommitTsData(*xmin, localts, localorigin);
87 : : }
88 : :
89 : : /*
90 : : * This function is used to report a conflict while applying replication
91 : : * changes.
92 : : *
93 : : * 'searchslot' should contain the tuple used to search the local row to be
94 : : * updated or deleted.
95 : : *
96 : : * 'remoteslot' should contain the remote new tuple, if any.
97 : : *
98 : : * conflicttuples is a list of local rows that caused the conflict and the
99 : : * conflict related information. See ConflictTupleInfo.
100 : : *
101 : : * The caller must ensure that all the indexes passed in ConflictTupleInfo are
102 : : * locked so that we can fetch and display the conflicting key values.
103 : : */
104 : : void
105 : 60 : ReportApplyConflict(EState *estate, ResultRelInfo *relinfo, int elevel,
106 : : ConflictType type, TupleTableSlot *searchslot,
107 : : TupleTableSlot *remoteslot, List *conflicttuples)
108 : : {
109 : 60 : Relation localrel = relinfo->ri_RelationDesc;
110 : : StringInfoData err_detail;
111 : :
407 112 : 60 : initStringInfo(&err_detail);
113 : :
114 : : /* Form errdetail message by combining conflicting tuples information. */
115 [ + - + + : 209 : foreach_ptr(ConflictTupleInfo, conflicttuple, conflicttuples)
+ + ]
116 : 89 : errdetail_apply_conflict(estate, relinfo, type, searchslot,
407 akapila@postgresql.o 117 :ECB (38) : conflicttuple->slot, remoteslot,
118 : : conflicttuple->indexoid,
119 : : conflicttuple->xmin,
407 akapila@postgresql.o 120 :CBC 89 : conflicttuple->origin,
121 : : conflicttuple->ts,
122 : : &err_detail);
123 : :
608 124 : 60 : pgstat_report_subscription_conflict(MySubscription->oid, type);
125 : :
623 126 [ + - ]: 60 : ereport(elevel,
127 : : errcode_apply_conflict(type),
128 : : errmsg("conflict detected on relation \"%s.%s\": conflict=%s",
129 : : get_namespace_name(RelationGetNamespace(localrel)),
130 : : RelationGetRelationName(localrel),
131 : : ConflictTypeNames[type]),
132 : : errdetail_internal("%s", err_detail.data));
133 : 26 : }
134 : :
135 : : /*
136 : : * Find all unique indexes to check for a conflict and store them into
137 : : * ResultRelInfo.
138 : : */
139 : : void
140 : 128216 : InitConflictIndexes(ResultRelInfo *relInfo)
141 : : {
142 : 128216 : List *uniqueIndexes = NIL;
143 : :
144 [ + + ]: 236010 : for (int i = 0; i < relInfo->ri_NumIndices; i++)
145 : : {
146 : 107794 : Relation indexRelation = relInfo->ri_IndexRelationDescs[i];
147 : :
148 [ - + ]: 107794 : if (indexRelation == NULL)
623 akapila@postgresql.o 149 :UBC 0 : continue;
150 : :
151 : : /* Detect conflict only for unique indexes */
623 akapila@postgresql.o 152 [ + + ]:CBC 107794 : if (!relInfo->ri_IndexRelationInfo[i]->ii_Unique)
153 : 29 : continue;
154 : :
155 : : /* Don't support conflict detection for deferrable index */
156 [ - + ]: 107765 : if (!indexRelation->rd_index->indimmediate)
623 akapila@postgresql.o 157 :UBC 0 : continue;
158 : :
623 akapila@postgresql.o 159 :CBC 107765 : uniqueIndexes = lappend_oid(uniqueIndexes,
160 : : RelationGetRelid(indexRelation));
161 : : }
162 : :
163 : 128216 : relInfo->ri_onConflictArbiterIndexes = uniqueIndexes;
164 : 128216 : }
165 : :
166 : : /*
167 : : * Add SQLSTATE error code to the current conflict report.
168 : : */
169 : : static int
170 : 60 : errcode_apply_conflict(ConflictType type)
171 : : {
172 [ + + - ]: 60 : switch (type)
173 : : {
174 : 34 : case CT_INSERT_EXISTS:
175 : : case CT_UPDATE_EXISTS:
176 : : case CT_MULTIPLE_UNIQUE_CONFLICTS:
177 : 34 : return errcode(ERRCODE_UNIQUE_VIOLATION);
614 178 : 26 : case CT_UPDATE_ORIGIN_DIFFERS:
179 : : case CT_UPDATE_MISSING:
180 : : case CT_DELETE_ORIGIN_DIFFERS:
181 : : case CT_UPDATE_DELETED:
182 : : case CT_DELETE_MISSING:
623 183 : 26 : return errcode(ERRCODE_T_R_SERIALIZATION_FAILURE);
184 : : }
185 : :
623 akapila@postgresql.o 186 :UBC 0 : Assert(false);
187 : : return 0; /* silence compiler warning */
188 : : }
189 : :
190 : : /*
191 : : * Helper function to build the additional details for conflicting key,
192 : : * local row, remote row, and replica identity columns.
193 : : */
194 : : static void
1 akapila@postgresql.o 195 :GNC 123 : append_tuple_value_detail(StringInfo buf, List *tuple_values)
196 : : {
104 197 : 123 : bool first = true;
198 : :
199 [ + - - + ]: 123 : Assert(buf != NULL && tuple_values != NIL);
200 : :
201 [ + - + + : 491 : foreach_ptr(char, tuple_value, tuple_values)
+ + ]
202 : : {
203 : : /*
204 : : * Skip if the value is NULL. This means the current user does not
205 : : * have enough permissions to see all columns in the table. See
206 : : * get_tuple_desc().
207 : : */
208 [ + + ]: 245 : if (!tuple_value)
209 : 37 : continue;
210 : :
211 : : /* standard SQL punctuation, not translated */
1 212 [ + + ]: 208 : if (!first)
213 : 85 : appendStringInfoString(buf, ", ");
214 : :
104 215 : 208 : appendStringInfoString(buf, tuple_value);
216 : 208 : first = false;
217 : : }
218 : 123 : }
219 : :
220 : : /*
221 : : * Add an errdetail() line showing conflict detail.
222 : : *
223 : : * The DETAIL line comprises of two parts:
224 : : * 1. Explanation of the conflict type, including the origin and commit
225 : : * timestamp of the local row.
226 : : * 2. Display of conflicting key, local row, remote new row, and replica
227 : : * identity columns, if any. The remote old row is excluded as its
228 : : * information is covered in the replica identity columns.
229 : : */
230 : : static void
623 akapila@postgresql.o 231 :CBC 89 : errdetail_apply_conflict(EState *estate, ResultRelInfo *relinfo,
232 : : ConflictType type, TupleTableSlot *searchslot,
233 : : TupleTableSlot *localslot, TupleTableSlot *remoteslot,
234 : : Oid indexoid, TransactionId localxmin,
235 : : ReplOriginId localorigin, TimestampTz localts,
236 : : StringInfo err_msg)
237 : : {
238 : : StringInfoData err_detail;
239 : : StringInfoData tuple_buf;
240 : : char *origin_name;
104 akapila@postgresql.o 241 :GNC 89 : char *key_desc = NULL;
242 : 89 : char *local_desc = NULL;
243 : 89 : char *remote_desc = NULL;
244 : 89 : char *search_desc = NULL;
245 : :
246 : : /* Get key, replica identity, remote, and local value data */
247 : 89 : get_tuple_desc(estate, relinfo, type, &key_desc,
248 : : localslot, &local_desc,
249 : : remoteslot, &remote_desc,
250 : : searchslot, &search_desc,
251 : : indexoid);
252 : :
623 akapila@postgresql.o 253 :CBC 89 : initStringInfo(&err_detail);
1 akapila@postgresql.o 254 :GNC 89 : initStringInfo(&tuple_buf);
255 : :
256 : : /* Construct a detailed message describing the type of conflict */
623 akapila@postgresql.o 257 [ + + + + :CBC 89 : switch (type)
+ + - ]
258 : : {
259 : 63 : case CT_INSERT_EXISTS:
260 : : case CT_UPDATE_EXISTS:
261 : : case CT_MULTIPLE_UNIQUE_CONFLICTS:
407 262 [ + - - + ]: 63 : Assert(OidIsValid(indexoid) &&
263 : : CheckRelationOidLockedByMe(indexoid, RowExclusiveLock, true));
264 : :
104 akapila@postgresql.o 265 [ + + ]:GNC 63 : if (err_msg->len == 0)
266 : : {
1 267 : 34 : append_tuple_value_detail(&tuple_buf,
268 : : list_make2(remote_desc, search_desc));
269 : :
270 [ + - ]: 34 : if (tuple_buf.len)
271 : 34 : appendStringInfo(&err_detail, _("Could not apply remote change: %s.\n"),
272 : : tuple_buf.data);
273 : : else
1 akapila@postgresql.o 274 :UNC 0 : appendStringInfo(&err_detail, _("Could not apply remote change.\n"));
275 : :
276 : :
1 akapila@postgresql.o 277 :GNC 34 : resetStringInfo(&tuple_buf);
278 : : }
279 : :
280 : 63 : append_tuple_value_detail(&tuple_buf,
281 : : list_make2(key_desc, local_desc));
282 : :
623 akapila@postgresql.o 283 [ + + ]:CBC 63 : if (localts)
284 : : {
97 msawada@postgresql.o 285 [ - + ]:GNC 3 : if (localorigin == InvalidReplOriginId)
286 : : {
1 akapila@postgresql.o 287 [ # # ]:UNC 0 : if (tuple_buf.len)
288 : 0 : appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified locally in transaction %u at %s: %s."),
289 : : get_rel_name(indexoid),
290 : : localxmin, timestamptz_to_str(localts),
291 : : tuple_buf.data);
292 : : else
293 : 0 : appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified locally in transaction %u at %s."),
294 : : get_rel_name(indexoid),
295 : : localxmin, timestamptz_to_str(localts));
296 : : }
623 akapila@postgresql.o 297 [ + + ]:CBC 3 : else if (replorigin_by_oid(localorigin, true, &origin_name))
298 : : {
1 akapila@postgresql.o 299 [ + - ]:GNC 1 : if (tuple_buf.len)
300 : 1 : appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by origin \"%s\" in transaction %u at %s: %s."),
301 : : get_rel_name(indexoid), origin_name,
302 : : localxmin, timestamptz_to_str(localts),
303 : : tuple_buf.data);
304 : : else
1 akapila@postgresql.o 305 :UNC 0 : appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by origin \"%s\" in transaction %u at %s."),
306 : : get_rel_name(indexoid), origin_name,
307 : : localxmin, timestamptz_to_str(localts));
308 : : }
309 : :
310 : : /*
311 : : * The origin that modified this row has been removed. This
312 : : * can happen if the origin was created by a different apply
313 : : * worker and its associated subscription and origin were
314 : : * dropped after updating the row, or if the origin was
315 : : * manually dropped by the user.
316 : : */
317 : : else
318 : : {
1 akapila@postgresql.o 319 [ + - ]:GNC 2 : if (tuple_buf.len)
320 : 2 : appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by a non-existent origin in transaction %u at %s: %s."),
321 : : get_rel_name(indexoid),
322 : : localxmin, timestamptz_to_str(localts),
323 : : tuple_buf.data);
324 : : else
1 akapila@postgresql.o 325 :UNC 0 : appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by a non-existent origin in transaction %u at %s."),
326 : : get_rel_name(indexoid),
327 : : localxmin, timestamptz_to_str(localts));
328 : : }
329 : : }
330 : : else
331 : : {
1 akapila@postgresql.o 332 [ + - ]:GNC 60 : if (tuple_buf.len)
333 : 60 : appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified in transaction %u: %s."),
334 : : get_rel_name(indexoid), localxmin,
335 : : tuple_buf.data);
336 : : else
1 akapila@postgresql.o 337 :UNC 0 : appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified in transaction %u."),
338 : : get_rel_name(indexoid), localxmin);
339 : : }
340 : :
623 akapila@postgresql.o 341 :CBC 63 : break;
342 : :
614 343 : 3 : case CT_UPDATE_ORIGIN_DIFFERS:
1 akapila@postgresql.o 344 :GNC 3 : append_tuple_value_detail(&tuple_buf,
345 : : list_make3(local_desc, remote_desc,
346 : : search_desc));
347 : :
97 msawada@postgresql.o 348 [ + + ]: 3 : if (localorigin == InvalidReplOriginId)
349 : : {
1 akapila@postgresql.o 350 [ + - ]: 1 : if (tuple_buf.len)
351 : 1 : appendStringInfo(&err_detail, _("Updating the row that was modified locally in transaction %u at %s: %s."),
352 : : localxmin, timestamptz_to_str(localts),
353 : : tuple_buf.data);
354 : : else
1 akapila@postgresql.o 355 :UNC 0 : appendStringInfo(&err_detail, _("Updating the row that was modified locally in transaction %u at %s."),
356 : : localxmin, timestamptz_to_str(localts));
357 : : }
623 akapila@postgresql.o 358 [ + + ]:CBC 2 : else if (replorigin_by_oid(localorigin, true, &origin_name))
359 : : {
1 akapila@postgresql.o 360 [ + - ]:GNC 1 : if (tuple_buf.len)
361 : 1 : appendStringInfo(&err_detail, _("Updating the row that was modified by a different origin \"%s\" in transaction %u at %s: %s."),
362 : : origin_name, localxmin,
363 : : timestamptz_to_str(localts),
364 : : tuple_buf.data);
365 : : else
1 akapila@postgresql.o 366 :UNC 0 : appendStringInfo(&err_detail, _("Updating the row that was modified by a different origin \"%s\" in transaction %u at %s."),
367 : : origin_name, localxmin,
368 : : timestamptz_to_str(localts));
369 : : }
370 : :
371 : : /* The origin that modified this row has been removed. */
372 : : else
373 : : {
1 akapila@postgresql.o 374 [ + - ]:GNC 1 : if (tuple_buf.len)
375 : 1 : appendStringInfo(&err_detail, _("Updating the row that was modified by a non-existent origin in transaction %u at %s: %s."),
376 : : localxmin, timestamptz_to_str(localts),
377 : : tuple_buf.data);
378 : : else
1 akapila@postgresql.o 379 :UNC 0 : appendStringInfo(&err_detail, _("Updating the row that was modified by a non-existent origin in transaction %u at %s."),
380 : : localxmin, timestamptz_to_str(localts));
381 : : }
382 : :
623 akapila@postgresql.o 383 :GNC 3 : break;
384 : :
274 385 : 3 : case CT_UPDATE_DELETED:
1 386 : 3 : append_tuple_value_detail(&tuple_buf,
387 : : list_make2(remote_desc, search_desc));
388 : :
389 [ + - ]: 3 : if (tuple_buf.len)
390 : 3 : appendStringInfo(&err_detail, _("Could not find the row to be updated: %s.\n"),
391 : : tuple_buf.data);
392 : : else
1 akapila@postgresql.o 393 :UNC 0 : appendStringInfo(&err_detail, _("Could not find the row to be updated.\n"));
394 : :
274 akapila@postgresql.o 395 [ + - ]:GNC 3 : if (localts)
396 : : {
97 msawada@postgresql.o 397 [ + - ]: 3 : if (localorigin == InvalidReplOriginId)
104 akapila@postgresql.o 398 : 3 : appendStringInfo(&err_detail, _("The row to be updated was deleted locally in transaction %u at %s"),
399 : : localxmin, timestamptz_to_str(localts));
274 akapila@postgresql.o 400 [ # # ]:UNC 0 : else if (replorigin_by_oid(localorigin, true, &origin_name))
104 401 : 0 : appendStringInfo(&err_detail, _("The row to be updated was deleted by a different origin \"%s\" in transaction %u at %s"),
402 : : origin_name, localxmin, timestamptz_to_str(localts));
403 : :
404 : : /* The origin that modified this row has been removed. */
405 : : else
406 : 0 : appendStringInfo(&err_detail, _("The row to be updated was deleted by a non-existent origin in transaction %u at %s"),
407 : : localxmin, timestamptz_to_str(localts));
408 : : }
409 : : else
22 drowley@postgresql.o 410 : 0 : appendStringInfoString(&err_detail, _("The row to be updated was deleted"));
411 : :
274 akapila@postgresql.o 412 :CBC 3 : break;
413 : :
623 414 : 6 : case CT_UPDATE_MISSING:
1 akapila@postgresql.o 415 :GNC 6 : append_tuple_value_detail(&tuple_buf,
416 : : list_make2(remote_desc, search_desc));
417 : :
418 [ + - ]: 6 : if (tuple_buf.len)
419 : 6 : appendStringInfo(&err_detail, _("Could not find the row to be updated: %s."),
420 : : tuple_buf.data);
421 : : else
1 akapila@postgresql.o 422 :UNC 0 : appendStringInfo(&err_detail, _("Could not find the row to be updated."));
423 : :
623 akapila@postgresql.o 424 :CBC 6 : break;
425 : :
614 426 : 5 : case CT_DELETE_ORIGIN_DIFFERS:
1 akapila@postgresql.o 427 :GNC 5 : append_tuple_value_detail(&tuple_buf,
428 : : list_make3(local_desc, remote_desc,
429 : : search_desc));
430 : :
97 msawada@postgresql.o 431 [ + + ]: 5 : if (localorigin == InvalidReplOriginId)
432 : : {
1 akapila@postgresql.o 433 [ + - ]: 4 : if (tuple_buf.len)
434 : 4 : appendStringInfo(&err_detail, _("Deleting the row that was modified locally in transaction %u at %s: %s."),
435 : : localxmin, timestamptz_to_str(localts),
436 : : tuple_buf.data);
437 : : else
1 akapila@postgresql.o 438 :UNC 0 : appendStringInfo(&err_detail, _("Deleting the row that was modified locally in transaction %u at %s."),
439 : : localxmin, timestamptz_to_str(localts));
440 : : }
623 akapila@postgresql.o 441 [ + - ]:CBC 1 : else if (replorigin_by_oid(localorigin, true, &origin_name))
442 : : {
1 akapila@postgresql.o 443 [ + - ]:GNC 1 : if (tuple_buf.len)
444 : 1 : appendStringInfo(&err_detail, _("Deleting the row that was modified by a different origin \"%s\" in transaction %u at %s: %s."),
445 : : origin_name, localxmin,
446 : : timestamptz_to_str(localts),
447 : : tuple_buf.data);
448 : : else
1 akapila@postgresql.o 449 :UNC 0 : appendStringInfo(&err_detail, _("Deleting the row that was modified by a different origin \"%s\" in transaction %u at %s."),
450 : : origin_name, localxmin,
451 : : timestamptz_to_str(localts));
452 : : }
453 : :
454 : : /* The origin that modified this row has been removed. */
455 : : else
456 : : {
457 [ # # ]: 0 : if (tuple_buf.len)
458 : 0 : appendStringInfo(&err_detail, _("Deleting the row that was modified by a non-existent origin in transaction %u at %s: %s."),
459 : : localxmin, timestamptz_to_str(localts),
460 : : tuple_buf.data);
461 : : else
462 : 0 : appendStringInfo(&err_detail, _("Deleting the row that was modified by a non-existent origin in transaction %u at %s."),
463 : : localxmin, timestamptz_to_str(localts));
464 : : }
465 : :
623 akapila@postgresql.o 466 :CBC 5 : break;
467 : :
468 : 9 : case CT_DELETE_MISSING:
1 akapila@postgresql.o 469 :GNC 9 : append_tuple_value_detail(&tuple_buf,
470 : : list_make1(search_desc));
471 : :
472 [ + - ]: 9 : if (tuple_buf.len)
473 : 9 : appendStringInfo(&err_detail, _("Could not find the row to be deleted: %s."),
474 : : tuple_buf.data);
475 : : else
1 akapila@postgresql.o 476 :UNC 0 : appendStringInfo(&err_detail, _("Could not find the row to be deleted."));
477 : :
623 akapila@postgresql.o 478 :CBC 9 : break;
479 : : }
480 : :
481 [ - + ]: 89 : Assert(err_detail.len > 0);
482 : :
483 : : /*
484 : : * Insert a blank line to visually separate the new detail line from the
485 : : * existing ones.
486 : : */
407 487 [ + + ]: 89 : if (err_msg->len > 0)
488 : 29 : appendStringInfoChar(err_msg, '\n');
489 : :
389 drowley@postgresql.o 490 : 89 : appendStringInfoString(err_msg, err_detail.data);
623 akapila@postgresql.o 491 : 89 : }
492 : :
493 : : /*
494 : : * Extract conflicting key, local row, remote row, and replica identity
495 : : * columns. Results are set at xxx_desc.
496 : : *
497 : : * If the output is NULL, it indicates that the current user lacks permissions
498 : : * to view the columns involved.
499 : : */
500 : : static void
104 akapila@postgresql.o 501 :GNC 89 : get_tuple_desc(EState *estate, ResultRelInfo *relinfo, ConflictType type,
502 : : char **key_desc,
503 : : TupleTableSlot *localslot, char **local_desc,
504 : : TupleTableSlot *remoteslot, char **remote_desc,
505 : : TupleTableSlot *searchslot, char **search_desc,
506 : : Oid indexoid)
507 : : {
623 akapila@postgresql.o 508 :CBC 89 : Relation localrel = relinfo->ri_RelationDesc;
509 : 89 : Oid relid = RelationGetRelid(localrel);
510 : 89 : TupleDesc tupdesc = RelationGetDescr(localrel);
511 : 89 : char *desc = NULL;
512 : :
104 akapila@postgresql.o 513 [ + + - + :GNC 89 : Assert((localslot && local_desc) || (remoteslot && remote_desc) ||
+ + - + +
- - + ]
514 : : (searchslot && search_desc));
515 : :
516 : : /*
517 : : * Report the conflicting key values in the case of a unique constraint
518 : : * violation.
519 : : */
407 akapila@postgresql.o 520 [ + + + + :CBC 89 : if (type == CT_INSERT_EXISTS || type == CT_UPDATE_EXISTS ||
+ + ]
521 : : type == CT_MULTIPLE_UNIQUE_CONFLICTS)
522 : : {
623 523 [ + - - + ]: 63 : Assert(OidIsValid(indexoid) && localslot);
524 : :
104 akapila@postgresql.o 525 :GNC 63 : desc = build_index_value_desc(estate, localrel, localslot,
526 : : indexoid);
527 : :
623 akapila@postgresql.o 528 [ + - ]:CBC 63 : if (desc)
104 akapila@postgresql.o 529 :GNC 63 : *key_desc = psprintf(_("key %s"), desc);
530 : : }
531 : :
623 akapila@postgresql.o 532 [ + + ]:CBC 89 : if (localslot)
533 : : {
534 : : /*
535 : : * The 'modifiedCols' only applies to the new tuple, hence we pass
536 : : * NULL for the local row.
537 : : */
538 : 71 : desc = ExecBuildSlotValueDescription(relid, localslot, tupdesc,
539 : : NULL, 64);
540 : :
541 [ + - ]: 71 : if (desc)
104 akapila@postgresql.o 542 :GNC 71 : *local_desc = psprintf(_("local row %s"), desc);
543 : : }
544 : :
623 akapila@postgresql.o 545 [ + + ]:CBC 89 : if (remoteslot)
546 : : {
547 : : Bitmapset *modifiedCols;
548 : :
549 : : /*
550 : : * Although logical replication doesn't maintain the bitmap for the
551 : : * columns being inserted, we still use it to create 'modifiedCols'
552 : : * for consistency with other calls to ExecBuildSlotValueDescription.
553 : : *
554 : : * Note that generated columns are formed locally on the subscriber.
555 : : */
556 : 75 : modifiedCols = bms_union(ExecGetInsertedCols(relinfo, estate),
557 : 75 : ExecGetUpdatedCols(relinfo, estate));
104 akapila@postgresql.o 558 :GNC 75 : desc = ExecBuildSlotValueDescription(relid, remoteslot,
559 : : tupdesc, modifiedCols,
560 : : 64);
561 : :
623 akapila@postgresql.o 562 [ + - ]:CBC 75 : if (desc)
104 akapila@postgresql.o 563 :GNC 75 : *remote_desc = psprintf(_("remote row %s"), desc);
564 : : }
565 : :
623 akapila@postgresql.o 566 [ + + ]:CBC 89 : if (searchslot)
567 : : {
568 : : /*
569 : : * Note that while index other than replica identity may be used (see
570 : : * IsIndexUsableForReplicaIdentityFull for details) to find the tuple
571 : : * when applying update or delete, such an index scan may not result
572 : : * in a unique tuple and we still compare the complete tuple in such
573 : : * cases, thus such indexes are not used here.
574 : : */
575 : 30 : Oid replica_index = GetRelationIdentityOrPK(localrel);
576 : :
577 [ - + ]: 30 : Assert(type != CT_INSERT_EXISTS);
578 : :
579 : : /*
580 : : * If the table has a valid replica identity index, build the index
581 : : * key value string. Otherwise, construct the full tuple value for
582 : : * REPLICA IDENTITY FULL cases.
583 : : */
584 [ + + ]: 30 : if (OidIsValid(replica_index))
585 : 26 : desc = build_index_value_desc(estate, localrel, searchslot, replica_index);
586 : : else
587 : 4 : desc = ExecBuildSlotValueDescription(relid, searchslot, tupdesc, NULL, 64);
588 : :
589 [ + - ]: 30 : if (desc)
590 : : {
104 akapila@postgresql.o 591 [ + + ]:GNC 30 : if (OidIsValid(replica_index))
592 : 26 : *search_desc = psprintf(_("replica identity %s"), desc);
593 : : else
594 : 4 : *search_desc = psprintf(_("replica identity full %s"), desc);
595 : : }
596 : : }
623 akapila@postgresql.o 597 :GIC 89 : }
598 : :
599 : : /*
600 : : * Helper functions to construct a string describing the contents of an index
601 : : * entry. See BuildIndexValueDescription for details.
602 : : *
603 : : * The caller must ensure that the index with the OID 'indexoid' is locked so
604 : : * that we can fetch and display the conflicting key value.
605 : : */
606 : : static char *
623 akapila@postgresql.o 607 :CBC 89 : build_index_value_desc(EState *estate, Relation localrel, TupleTableSlot *slot,
608 : : Oid indexoid)
609 : : {
610 : : char *index_value;
611 : : Relation indexDesc;
612 : : Datum values[INDEX_MAX_KEYS];
613 : : bool isnull[INDEX_MAX_KEYS];
614 : 89 : TupleTableSlot *tableslot = slot;
615 : :
616 [ - + ]: 89 : if (!tableslot)
623 akapila@postgresql.o 617 :UBC 0 : return NULL;
618 : :
623 akapila@postgresql.o 619 [ - + ]:CBC 89 : Assert(CheckRelationOidLockedByMe(indexoid, RowExclusiveLock, true));
620 : :
621 : 89 : indexDesc = index_open(indexoid, NoLock);
622 : :
623 : : /*
624 : : * If the slot is a virtual slot, copy it into a heap tuple slot as
625 : : * FormIndexDatum only works with heap tuple slots.
626 : : */
627 [ + + ]: 89 : if (TTS_IS_VIRTUAL(slot))
628 : : {
629 : 17 : tableslot = table_slot_create(localrel, &estate->es_tupleTable);
630 : 17 : tableslot = ExecCopySlot(tableslot, slot);
631 : : }
632 : :
633 : : /*
634 : : * Initialize ecxt_scantuple for potential use in FormIndexDatum when
635 : : * index expressions are present.
636 : : */
637 [ + - ]: 89 : GetPerTupleExprContext(estate)->ecxt_scantuple = tableslot;
638 : :
639 : : /*
640 : : * The values/nulls arrays passed to BuildIndexValueDescription should be
641 : : * the results of FormIndexDatum, which are the "raw" input to the index
642 : : * AM.
643 : : */
644 : 89 : FormIndexDatum(BuildIndexInfo(indexDesc), tableslot, estate, values, isnull);
645 : :
646 : 89 : index_value = BuildIndexValueDescription(indexDesc, values, isnull);
647 : :
648 : 89 : index_close(indexDesc, NoLock);
649 : :
650 : 89 : return index_value;
651 : : }
|