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