LCOV - code coverage report
Current view: top level - src/backend/replication/logical - conflict.c (source / functions) Coverage Total Hit
Test: PostgreSQL 20devel Lines: 87.3 % 204 178
Test Date: 2026-07-17 01:15:41 Functions: 100.0 % 11 11
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 72.4 % 134 97

             Branch data     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 "catalog/heap.h"
      21                 :             : #include "catalog/pg_am.h"
      22                 :             : #include "catalog/pg_namespace.h"
      23                 :             : #include "catalog/toasting.h"
      24                 :             : #include "executor/executor.h"
      25                 :             : #include "pgstat.h"
      26                 :             : #include "replication/conflict.h"
      27                 :             : #include "replication/worker_internal.h"
      28                 :             : #include "storage/lmgr.h"
      29                 :             : #include "utils/lsyscache.h"
      30                 :             : 
      31                 :             : /*
      32                 :             :  * String representations for the supported conflict logging destinations.
      33                 :             :  */
      34                 :             : const char *const ConflictLogDestNames[] = {
      35                 :             :     [CONFLICT_LOG_DEST_LOG] = "log",
      36                 :             :     [CONFLICT_LOG_DEST_TABLE] = "table",
      37                 :             :     [CONFLICT_LOG_DEST_ALL] = "all"
      38                 :             : };
      39                 :             : 
      40                 :             : StaticAssertDecl(lengthof(ConflictLogDestNames) == CONFLICT_LOG_DEST_ALL + 1,
      41                 :             :                  "ConflictLogDestNames length mismatch");
      42                 :             : 
      43                 :             : 
      44                 :             : /* Structure to hold metadata for one column of the conflict log table */
      45                 :             : typedef struct ConflictLogColumnDef
      46                 :             : {
      47                 :             :     const char *attname;        /* Column name */
      48                 :             :     Oid         atttypid;       /* Data type OID */
      49                 :             : } ConflictLogColumnDef;
      50                 :             : 
      51                 :             : /*
      52                 :             :  * Schema definition for conflict log tables.
      53                 :             :  *
      54                 :             :  * Defines the fixed schema of the per-subscription conflict log table created
      55                 :             :  * in the pg_conflict namespace. Each entry specifies the column name and its
      56                 :             :  * type OID; the table is created in this column order by
      57                 :             :  * create_conflict_log_table().
      58                 :             :  *
      59                 :             :  * The tuple/key columns (replica_identity, remote_tuple, local_conflicts) are
      60                 :             :  * typed json rather than jsonb on purpose: they hold an exact audit snapshot
      61                 :             :  * of the applied tuples and replica identity, and json preserves the verbatim
      62                 :             :  * representation whereas jsonb would normalize it. Indexing them (jsonb's main
      63                 :             :  * advantage) wouldn't help anyway, as the conflict log is looked up by its
      64                 :             :  * scalar columns (relid, conflict_type, commit timestamp) while these json
      65                 :             :  * columns are per-conflict payload to inspect, not search keys.
      66                 :             :  */
      67                 :             : static const ConflictLogColumnDef ConflictLogSchema[] = {
      68                 :             :     {.attname = "relid", .atttypid = OIDOID},
      69                 :             :     {.attname = "schemaname", .atttypid = TEXTOID},
      70                 :             :     {.attname = "relname", .atttypid = TEXTOID},
      71                 :             :     {.attname = "conflict_type", .atttypid = TEXTOID},
      72                 :             :     {.attname = "remote_xid", .atttypid = XIDOID},
      73                 :             :     {.attname = "remote_commit_lsn", .atttypid = LSNOID},
      74                 :             :     {.attname = "remote_commit_ts", .atttypid = TIMESTAMPTZOID},
      75                 :             :     {.attname = "remote_origin", .atttypid = TEXTOID},
      76                 :             :     {.attname = "replica_identity_full", .atttypid = BOOLOID},
      77                 :             :     {.attname = "replica_identity", .atttypid = JSONOID},
      78                 :             :     {.attname = "remote_tuple", .atttypid = JSONOID},
      79                 :             :     {.attname = "local_conflicts", .atttypid = JSONARRAYOID}
      80                 :             : };
      81                 :             : 
      82                 :             : #define NUM_CONFLICT_ATTRS ((AttrNumber) lengthof(ConflictLogSchema))
      83                 :             : 
      84                 :             : static const char *const ConflictTypeNames[] = {
      85                 :             :     [CT_INSERT_EXISTS] = "insert_exists",
      86                 :             :     [CT_UPDATE_ORIGIN_DIFFERS] = "update_origin_differs",
      87                 :             :     [CT_UPDATE_EXISTS] = "update_exists",
      88                 :             :     [CT_UPDATE_MISSING] = "update_missing",
      89                 :             :     [CT_DELETE_ORIGIN_DIFFERS] = "delete_origin_differs",
      90                 :             :     [CT_UPDATE_DELETED] = "update_deleted",
      91                 :             :     [CT_DELETE_MISSING] = "delete_missing",
      92                 :             :     [CT_MULTIPLE_UNIQUE_CONFLICTS] = "multiple_unique_conflicts"
      93                 :             : };
      94                 :             : 
      95                 :             : static int  errcode_apply_conflict(ConflictType type);
      96                 :             : static void errdetail_apply_conflict(EState *estate,
      97                 :             :                                      ResultRelInfo *relinfo,
      98                 :             :                                      ConflictType type,
      99                 :             :                                      TupleTableSlot *searchslot,
     100                 :             :                                      TupleTableSlot *localslot,
     101                 :             :                                      TupleTableSlot *remoteslot,
     102                 :             :                                      Oid indexoid, TransactionId localxmin,
     103                 :             :                                      ReplOriginId localorigin,
     104                 :             :                                      TimestampTz localts, StringInfo err_msg);
     105                 :             : static void get_tuple_desc(EState *estate, ResultRelInfo *relinfo,
     106                 :             :                            ConflictType type, char **key_desc,
     107                 :             :                            TupleTableSlot *localslot, char **local_desc,
     108                 :             :                            TupleTableSlot *remoteslot, char **remote_desc,
     109                 :             :                            TupleTableSlot *searchslot, char **search_desc,
     110                 :             :                            Oid indexoid);
     111                 :             : static char *build_index_value_desc(EState *estate, Relation localrel,
     112                 :             :                                     TupleTableSlot *slot, Oid indexoid);
     113                 :             : 
     114                 :             : /*
     115                 :             :  * Builds the TupleDesc for the conflict log table.
     116                 :             :  */
     117                 :             : static TupleDesc
     118                 :          14 : create_conflict_log_table_tupdesc(void)
     119                 :             : {
     120                 :             :     TupleDesc   tupdesc;
     121                 :             : 
     122                 :          14 :     tupdesc = CreateTemplateTupleDesc(NUM_CONFLICT_ATTRS);
     123                 :             : 
     124         [ +  + ]:         182 :     for (int i = 0; i < NUM_CONFLICT_ATTRS; i++)
     125                 :         168 :         TupleDescInitEntry(tupdesc, i + 1,
     126                 :         168 :                            ConflictLogSchema[i].attname,
     127                 :         168 :                            ConflictLogSchema[i].atttypid,
     128                 :             :                            -1, 0);
     129                 :             : 
     130                 :          14 :     TupleDescFinalize(tupdesc);
     131                 :             : 
     132                 :          14 :     return tupdesc;
     133                 :             : }
     134                 :             : 
     135                 :             : /*
     136                 :             :  * Create a structured conflict log table for a subscription.
     137                 :             :  *
     138                 :             :  * The table is created within the system-managed 'pg_conflict' namespace to
     139                 :             :  * prevent users from manually dropping or altering it.  This also prevents
     140                 :             :  * accidental name collisions with user-created tables with the same name.
     141                 :             :  *
     142                 :             :  * The table name is generated automatically using the subscription's OID
     143                 :             :  * (e.g., "pg_conflict_log_<subid>") to ensure uniqueness within the
     144                 :             :  * cluster and to avoid collisions during subscription renames.
     145                 :             :  */
     146                 :             : Oid
     147                 :          14 : create_conflict_log_table(Oid subid, char *subname, Oid subowner)
     148                 :             : {
     149                 :             :     TupleDesc   tupdesc;
     150                 :             :     Oid         relid;
     151                 :             :     char        relname[NAMEDATALEN];
     152                 :             : 
     153                 :          14 :     snprintf(relname, NAMEDATALEN, "pg_conflict_log_%u", subid);
     154                 :             : 
     155                 :             :     /* Build the tuple descriptor for the new table. */
     156                 :          14 :     tupdesc = create_conflict_log_table_tupdesc();
     157                 :             : 
     158                 :             :     /* Create conflict log table. */
     159                 :          14 :     relid = heap_create_with_catalog(relname,
     160                 :             :                                      PG_CONFLICT_NAMESPACE,
     161                 :             :                                      0, /* tablespace */
     162                 :             :                                      InvalidOid,    /* relid */
     163                 :             :                                      InvalidOid,    /* reltypeid */
     164                 :             :                                      InvalidOid,    /* reloftypeid */
     165                 :             :                                      subowner,
     166                 :             :                                      HEAP_TABLE_AM_OID,
     167                 :             :                                      tupdesc,
     168                 :             :                                      NIL,
     169                 :             :                                      RELKIND_RELATION,
     170                 :             :                                      RELPERSISTENCE_PERMANENT,
     171                 :             :                                      false, /* shared_relation */
     172                 :             :                                      false, /* mapped_relation */
     173                 :             :                                      ONCOMMIT_NOOP,
     174                 :             :                                      (Datum) 0, /* reloptions */
     175                 :             :                                      false, /* use_user_acl */
     176                 :             :                                      false, /* allow_system_table_mods */
     177                 :             :                                      true,  /* is_internal */
     178                 :             :                                      InvalidOid,    /* relrewrite */
     179                 :             :                                      NULL); /* typaddress */
     180                 :             :     Assert(OidIsValid(relid));
     181                 :             : 
     182                 :             :     /* Release tuple descriptor memory. */
     183                 :          14 :     FreeTupleDesc(tupdesc);
     184                 :             : 
     185                 :             :     /*
     186                 :             :      * We must bump the command counter to make the newly-created relation
     187                 :             :      * tuple visible for opening.
     188                 :             :      */
     189                 :          14 :     CommandCounterIncrement();
     190                 :             : 
     191                 :             :     /*
     192                 :             :      * Create a TOAST table for the conflict log to support out-of-line
     193                 :             :      * storage of large json data.
     194                 :             :      */
     195                 :          14 :     NewRelationCreateToastTable(relid, (Datum) 0);
     196                 :             : 
     197         [ +  + ]:          14 :     ereport(NOTICE,
     198                 :             :             (errmsg("created conflict log table \"%s\" for subscription \"%s\"",
     199                 :             :                     get_qualified_objname(PG_CONFLICT_NAMESPACE, relname),
     200                 :             :                     subname)));
     201                 :             : 
     202                 :          14 :     return relid;
     203                 :             : }
     204                 :             : 
     205                 :             : /*
     206                 :             :  * Convert the string representation of a conflict logging destination to its
     207                 :             :  * corresponding enum value.
     208                 :             :  */
     209                 :             : ConflictLogDest
     210                 :          54 : GetConflictLogDest(const char *dest)
     211                 :             : {
     212                 :             :     /* NULL defaults to LOG. */
     213   [ +  -  +  + ]:          54 :     if (dest == NULL || pg_strcasecmp(dest, "log") == 0)
     214                 :          12 :         return CONFLICT_LOG_DEST_LOG;
     215                 :             : 
     216         [ +  + ]:          42 :     if (pg_strcasecmp(dest, "table") == 0)
     217                 :          25 :         return CONFLICT_LOG_DEST_TABLE;
     218                 :             : 
     219         [ +  + ]:          17 :     if (pg_strcasecmp(dest, "all") == 0)
     220                 :           9 :         return CONFLICT_LOG_DEST_ALL;
     221                 :             : 
     222                 :             :     /* Unrecognized string. */
     223         [ +  - ]:           8 :     ereport(ERROR,
     224                 :             :             (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     225                 :             :              errmsg("unrecognized conflict_log_destination value: \"%s\"", dest),
     226                 :             :              errhint("Valid values are \"log\", \"table\", and \"all\".")));
     227                 :             : }
     228                 :             : 
     229                 :             : /*
     230                 :             :  * Get the xmin and commit timestamp data (origin and timestamp) associated
     231                 :             :  * with the provided local row.
     232                 :             :  *
     233                 :             :  * Return true if the commit timestamp data was found, false otherwise.
     234                 :             :  */
     235                 :             : bool
     236                 :       72340 : GetTupleTransactionInfo(TupleTableSlot *localslot, TransactionId *xmin,
     237                 :             :                         ReplOriginId *localorigin, TimestampTz *localts)
     238                 :             : {
     239                 :             :     Datum       xminDatum;
     240                 :             :     bool        isnull;
     241                 :             : 
     242                 :       72340 :     xminDatum = slot_getsysattr(localslot, MinTransactionIdAttributeNumber,
     243                 :             :                                 &isnull);
     244                 :       72340 :     *xmin = DatumGetTransactionId(xminDatum);
     245                 :             :     Assert(!isnull);
     246                 :             : 
     247                 :             :     /*
     248                 :             :      * The commit timestamp data is not available if track_commit_timestamp is
     249                 :             :      * disabled.
     250                 :             :      */
     251         [ +  + ]:       72340 :     if (!track_commit_timestamp)
     252                 :             :     {
     253                 :       72254 :         *localorigin = InvalidReplOriginId;
     254                 :       72254 :         *localts = 0;
     255                 :       72254 :         return false;
     256                 :             :     }
     257                 :             : 
     258                 :          86 :     return TransactionIdGetCommitTsData(*xmin, localts, localorigin);
     259                 :             : }
     260                 :             : 
     261                 :             : /*
     262                 :             :  * This function is used to report a conflict while applying replication
     263                 :             :  * changes.
     264                 :             :  *
     265                 :             :  * 'searchslot' should contain the tuple used to search the local row to be
     266                 :             :  * updated or deleted.
     267                 :             :  *
     268                 :             :  * 'remoteslot' should contain the remote new tuple, if any.
     269                 :             :  *
     270                 :             :  * conflicttuples is a list of local rows that caused the conflict and the
     271                 :             :  * conflict related information. See ConflictTupleInfo.
     272                 :             :  *
     273                 :             :  * The caller must ensure that all the indexes passed in ConflictTupleInfo are
     274                 :             :  * locked so that we can fetch and display the conflicting key values.
     275                 :             :  */
     276                 :             : void
     277                 :          77 : ReportApplyConflict(EState *estate, ResultRelInfo *relinfo, int elevel,
     278                 :             :                     ConflictType type, TupleTableSlot *searchslot,
     279                 :             :                     TupleTableSlot *remoteslot, List *conflicttuples)
     280                 :             : {
     281                 :          77 :     Relation    localrel = relinfo->ri_RelationDesc;
     282                 :             :     StringInfoData err_detail;
     283                 :             : 
     284                 :          77 :     initStringInfo(&err_detail);
     285                 :             : 
     286                 :             :     /* Form errdetail message by combining conflicting tuples information. */
     287   [ +  -  +  +  :         275 :     foreach_ptr(ConflictTupleInfo, conflicttuple, conflicttuples)
                   +  + ]
     288                 :         121 :         errdetail_apply_conflict(estate, relinfo, type, searchslot,
     289                 :             :                                  conflicttuple->slot, remoteslot,
     290                 :             :                                  conflicttuple->indexoid,
     291                 :             :                                  conflicttuple->xmin,
     292                 :         121 :                                  conflicttuple->origin,
     293                 :             :                                  conflicttuple->ts,
     294                 :             :                                  &err_detail);
     295                 :             : 
     296                 :          77 :     pgstat_report_subscription_conflict(MySubscription->oid, type);
     297                 :             : 
     298         [ +  - ]:          77 :     ereport(elevel,
     299                 :             :             errcode_apply_conflict(type),
     300                 :             :             errmsg("conflict detected on relation \"%s.%s\": conflict=%s",
     301                 :             :                    get_namespace_name(RelationGetNamespace(localrel)),
     302                 :             :                    RelationGetRelationName(localrel),
     303                 :             :                    ConflictTypeNames[type]),
     304                 :             :             errdetail_internal("%s", err_detail.data));
     305                 :          29 : }
     306                 :             : 
     307                 :             : /*
     308                 :             :  * Find all unique indexes to check for a conflict and store them into
     309                 :             :  * ResultRelInfo.
     310                 :             :  */
     311                 :             : void
     312                 :      128639 : InitConflictIndexes(ResultRelInfo *relInfo)
     313                 :             : {
     314                 :      128639 :     List       *uniqueIndexes = NIL;
     315                 :             : 
     316         [ +  + ]:      236871 :     for (int i = 0; i < relInfo->ri_NumIndices; i++)
     317                 :             :     {
     318                 :      108232 :         Relation    indexRelation = relInfo->ri_IndexRelationDescs[i];
     319                 :             : 
     320         [ -  + ]:      108232 :         if (indexRelation == NULL)
     321                 :           0 :             continue;
     322                 :             : 
     323                 :             :         /* Detect conflict only for unique indexes */
     324         [ +  + ]:      108232 :         if (!relInfo->ri_IndexRelationInfo[i]->ii_Unique)
     325                 :          29 :             continue;
     326                 :             : 
     327                 :             :         /* Don't support conflict detection for deferrable index */
     328         [ -  + ]:      108203 :         if (!indexRelation->rd_index->indimmediate)
     329                 :           0 :             continue;
     330                 :             : 
     331                 :      108203 :         uniqueIndexes = lappend_oid(uniqueIndexes,
     332                 :             :                                     RelationGetRelid(indexRelation));
     333                 :             :     }
     334                 :             : 
     335                 :      128639 :     relInfo->ri_onConflictArbiterIndexes = uniqueIndexes;
     336                 :      128639 : }
     337                 :             : 
     338                 :             : /*
     339                 :             :  * Add SQLSTATE error code to the current conflict report.
     340                 :             :  */
     341                 :             : static int
     342                 :          77 : errcode_apply_conflict(ConflictType type)
     343                 :             : {
     344      [ +  +  - ]:          77 :     switch (type)
     345                 :             :     {
     346                 :          48 :         case CT_INSERT_EXISTS:
     347                 :             :         case CT_UPDATE_EXISTS:
     348                 :             :         case CT_MULTIPLE_UNIQUE_CONFLICTS:
     349                 :          48 :             return errcode(ERRCODE_UNIQUE_VIOLATION);
     350                 :          29 :         case CT_UPDATE_ORIGIN_DIFFERS:
     351                 :             :         case CT_UPDATE_MISSING:
     352                 :             :         case CT_DELETE_ORIGIN_DIFFERS:
     353                 :             :         case CT_UPDATE_DELETED:
     354                 :             :         case CT_DELETE_MISSING:
     355                 :          29 :             return errcode(ERRCODE_T_R_SERIALIZATION_FAILURE);
     356                 :             :     }
     357                 :             : 
     358                 :             :     Assert(false);
     359                 :           0 :     return 0;                   /* silence compiler warning */
     360                 :             : }
     361                 :             : 
     362                 :             : /*
     363                 :             :  * Helper function to build the additional details for conflicting key,
     364                 :             :  * local row, remote row, and replica identity columns.
     365                 :             :  */
     366                 :             : static void
     367                 :         169 : append_tuple_value_detail(StringInfo buf, List *tuple_values)
     368                 :             : {
     369                 :         169 :     bool        first = true;
     370                 :             : 
     371                 :             :     Assert(buf != NULL && tuple_values != NIL);
     372                 :             : 
     373   [ +  -  +  +  :         675 :     foreach_ptr(char, tuple_value, tuple_values)
                   +  + ]
     374                 :             :     {
     375                 :             :         /*
     376                 :             :          * Skip if the value is NULL. This means the current user does not
     377                 :             :          * have enough permissions to see all columns in the table. See
     378                 :             :          * get_tuple_desc().
     379                 :             :          */
     380         [ +  + ]:         337 :         if (!tuple_value)
     381                 :          51 :             continue;
     382                 :             : 
     383                 :             :         /* standard SQL punctuation, not translated */
     384         [ +  + ]:         286 :         if (!first)
     385                 :         117 :             appendStringInfoString(buf, ", ");
     386                 :             : 
     387                 :         286 :         appendStringInfoString(buf, tuple_value);
     388                 :         286 :         first = false;
     389                 :             :     }
     390                 :         169 : }
     391                 :             : 
     392                 :             : /*
     393                 :             :  * Add an errdetail() line showing conflict detail.
     394                 :             :  *
     395                 :             :  * The DETAIL line comprises of two parts:
     396                 :             :  * 1. Explanation of the conflict type, including the origin and commit
     397                 :             :  *    timestamp of the local row.
     398                 :             :  * 2. Display of conflicting key, local row, remote new row, and replica
     399                 :             :  *    identity columns, if any. The remote old row is excluded as its
     400                 :             :  *    information is covered in the replica identity columns.
     401                 :             :  */
     402                 :             : static void
     403                 :         121 : errdetail_apply_conflict(EState *estate, ResultRelInfo *relinfo,
     404                 :             :                          ConflictType type, TupleTableSlot *searchslot,
     405                 :             :                          TupleTableSlot *localslot, TupleTableSlot *remoteslot,
     406                 :             :                          Oid indexoid, TransactionId localxmin,
     407                 :             :                          ReplOriginId localorigin, TimestampTz localts,
     408                 :             :                          StringInfo err_msg)
     409                 :             : {
     410                 :             :     StringInfoData err_detail;
     411                 :             :     StringInfoData tuple_buf;
     412                 :             :     char       *origin_name;
     413                 :         121 :     char       *key_desc = NULL;
     414                 :         121 :     char       *local_desc = NULL;
     415                 :         121 :     char       *remote_desc = NULL;
     416                 :         121 :     char       *search_desc = NULL;
     417                 :             : 
     418                 :             :     /* Get key, replica identity, remote, and local value data */
     419                 :         121 :     get_tuple_desc(estate, relinfo, type, &key_desc,
     420                 :             :                    localslot, &local_desc,
     421                 :             :                    remoteslot, &remote_desc,
     422                 :             :                    searchslot, &search_desc,
     423                 :             :                    indexoid);
     424                 :             : 
     425                 :         121 :     initStringInfo(&err_detail);
     426                 :         121 :     initStringInfo(&tuple_buf);
     427                 :             : 
     428                 :             :     /* Construct a detailed message describing the type of conflict */
     429   [ +  +  +  +  :         121 :     switch (type)
                +  +  - ]
     430                 :             :     {
     431                 :          92 :         case CT_INSERT_EXISTS:
     432                 :             :         case CT_UPDATE_EXISTS:
     433                 :             :         case CT_MULTIPLE_UNIQUE_CONFLICTS:
     434                 :             :             Assert(OidIsValid(indexoid) &&
     435                 :             :                    CheckRelationOidLockedByMe(indexoid, RowExclusiveLock, true));
     436                 :             : 
     437         [ +  + ]:          92 :             if (err_msg->len == 0)
     438                 :             :             {
     439                 :          48 :                 append_tuple_value_detail(&tuple_buf,
     440                 :             :                                           list_make2(remote_desc, search_desc));
     441                 :             : 
     442         [ +  - ]:          48 :                 if (tuple_buf.len)
     443                 :          48 :                     appendStringInfo(&err_detail, _("Could not apply remote change: %s.\n"),
     444                 :             :                                      tuple_buf.data);
     445                 :             :                 else
     446                 :           0 :                     appendStringInfo(&err_detail, _("Could not apply remote change.\n"));
     447                 :             : 
     448                 :             : 
     449                 :          48 :                 resetStringInfo(&tuple_buf);
     450                 :             :             }
     451                 :             : 
     452                 :          92 :             append_tuple_value_detail(&tuple_buf,
     453                 :             :                                       list_make2(key_desc, local_desc));
     454                 :             : 
     455         [ +  + ]:          92 :             if (localts)
     456                 :             :             {
     457         [ -  + ]:           3 :                 if (localorigin == InvalidReplOriginId)
     458                 :             :                 {
     459         [ #  # ]:           0 :                     if (tuple_buf.len)
     460                 :           0 :                         appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified locally in transaction %u at %s: %s."),
     461                 :             :                                          get_rel_name(indexoid),
     462                 :             :                                          localxmin, timestamptz_to_str(localts),
     463                 :             :                                          tuple_buf.data);
     464                 :             :                     else
     465                 :           0 :                         appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified locally in transaction %u at %s."),
     466                 :             :                                          get_rel_name(indexoid),
     467                 :             :                                          localxmin, timestamptz_to_str(localts));
     468                 :             :                 }
     469         [ +  + ]:           3 :                 else if (replorigin_by_oid(localorigin, true, &origin_name))
     470                 :             :                 {
     471         [ +  - ]:           1 :                     if (tuple_buf.len)
     472                 :           1 :                         appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by origin \"%s\" in transaction %u at %s: %s."),
     473                 :             :                                          get_rel_name(indexoid), origin_name,
     474                 :             :                                          localxmin, timestamptz_to_str(localts),
     475                 :             :                                          tuple_buf.data);
     476                 :             :                     else
     477                 :           0 :                         appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by origin \"%s\" in transaction %u at %s."),
     478                 :             :                                          get_rel_name(indexoid), origin_name,
     479                 :             :                                          localxmin, timestamptz_to_str(localts));
     480                 :             :                 }
     481                 :             : 
     482                 :             :                 /*
     483                 :             :                  * The origin that modified this row has been removed. This
     484                 :             :                  * can happen if the origin was created by a different apply
     485                 :             :                  * worker and its associated subscription and origin were
     486                 :             :                  * dropped after updating the row, or if the origin was
     487                 :             :                  * manually dropped by the user.
     488                 :             :                  */
     489                 :             :                 else
     490                 :             :                 {
     491         [ +  - ]:           2 :                     if (tuple_buf.len)
     492                 :           2 :                         appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by a non-existent origin in transaction %u at %s: %s."),
     493                 :             :                                          get_rel_name(indexoid),
     494                 :             :                                          localxmin, timestamptz_to_str(localts),
     495                 :             :                                          tuple_buf.data);
     496                 :             :                     else
     497                 :           0 :                         appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified by a non-existent origin in transaction %u at %s."),
     498                 :             :                                          get_rel_name(indexoid),
     499                 :             :                                          localxmin, timestamptz_to_str(localts));
     500                 :             :                 }
     501                 :             :             }
     502                 :             :             else
     503                 :             :             {
     504         [ +  - ]:          89 :                 if (tuple_buf.len)
     505                 :          89 :                     appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified in transaction %u: %s."),
     506                 :             :                                      get_rel_name(indexoid), localxmin,
     507                 :             :                                      tuple_buf.data);
     508                 :             :                 else
     509                 :           0 :                     appendStringInfo(&err_detail, _("Key already exists in unique index \"%s\", modified in transaction %u."),
     510                 :             :                                      get_rel_name(indexoid), localxmin);
     511                 :             :             }
     512                 :             : 
     513                 :          92 :             break;
     514                 :             : 
     515                 :           3 :         case CT_UPDATE_ORIGIN_DIFFERS:
     516                 :           3 :             append_tuple_value_detail(&tuple_buf,
     517                 :             :                                       list_make3(local_desc, remote_desc,
     518                 :             :                                                  search_desc));
     519                 :             : 
     520         [ +  + ]:           3 :             if (localorigin == InvalidReplOriginId)
     521                 :             :             {
     522         [ +  - ]:           1 :                 if (tuple_buf.len)
     523                 :           1 :                     appendStringInfo(&err_detail, _("Updating the row that was modified locally in transaction %u at %s: %s."),
     524                 :             :                                      localxmin, timestamptz_to_str(localts),
     525                 :             :                                      tuple_buf.data);
     526                 :             :                 else
     527                 :           0 :                     appendStringInfo(&err_detail, _("Updating the row that was modified locally in transaction %u at %s."),
     528                 :             :                                      localxmin, timestamptz_to_str(localts));
     529                 :             :             }
     530         [ +  + ]:           2 :             else if (replorigin_by_oid(localorigin, true, &origin_name))
     531                 :             :             {
     532         [ +  - ]:           1 :                 if (tuple_buf.len)
     533                 :           1 :                     appendStringInfo(&err_detail, _("Updating the row that was modified by a different origin \"%s\" in transaction %u at %s: %s."),
     534                 :             :                                      origin_name, localxmin,
     535                 :             :                                      timestamptz_to_str(localts),
     536                 :             :                                      tuple_buf.data);
     537                 :             :                 else
     538                 :           0 :                     appendStringInfo(&err_detail, _("Updating the row that was modified by a different origin \"%s\" in transaction %u at %s."),
     539                 :             :                                      origin_name, localxmin,
     540                 :             :                                      timestamptz_to_str(localts));
     541                 :             :             }
     542                 :             : 
     543                 :             :             /* The origin that modified this row has been removed. */
     544                 :             :             else
     545                 :             :             {
     546         [ +  - ]:           1 :                 if (tuple_buf.len)
     547                 :           1 :                     appendStringInfo(&err_detail, _("Updating the row that was modified by a non-existent origin in transaction %u at %s: %s."),
     548                 :             :                                      localxmin, timestamptz_to_str(localts),
     549                 :             :                                      tuple_buf.data);
     550                 :             :                 else
     551                 :           0 :                     appendStringInfo(&err_detail, _("Updating the row that was modified by a non-existent origin in transaction %u at %s."),
     552                 :             :                                      localxmin, timestamptz_to_str(localts));
     553                 :             :             }
     554                 :             : 
     555                 :           3 :             break;
     556                 :             : 
     557                 :           3 :         case CT_UPDATE_DELETED:
     558                 :           3 :             append_tuple_value_detail(&tuple_buf,
     559                 :             :                                       list_make2(remote_desc, search_desc));
     560                 :             : 
     561         [ +  - ]:           3 :             if (tuple_buf.len)
     562                 :           3 :                 appendStringInfo(&err_detail, _("Could not find the row to be updated: %s.\n"),
     563                 :             :                                  tuple_buf.data);
     564                 :             :             else
     565                 :           0 :                 appendStringInfo(&err_detail, _("Could not find the row to be updated.\n"));
     566                 :             : 
     567         [ +  - ]:           3 :             if (localts)
     568                 :             :             {
     569         [ +  - ]:           3 :                 if (localorigin == InvalidReplOriginId)
     570                 :           3 :                     appendStringInfo(&err_detail, _("The row to be updated was deleted locally in transaction %u at %s"),
     571                 :             :                                      localxmin, timestamptz_to_str(localts));
     572         [ #  # ]:           0 :                 else if (replorigin_by_oid(localorigin, true, &origin_name))
     573                 :           0 :                     appendStringInfo(&err_detail, _("The row to be updated was deleted by a different origin \"%s\" in transaction %u at %s"),
     574                 :             :                                      origin_name, localxmin, timestamptz_to_str(localts));
     575                 :             : 
     576                 :             :                 /* The origin that modified this row has been removed. */
     577                 :             :                 else
     578                 :           0 :                     appendStringInfo(&err_detail, _("The row to be updated was deleted by a non-existent origin in transaction %u at %s"),
     579                 :             :                                      localxmin, timestamptz_to_str(localts));
     580                 :             :             }
     581                 :             :             else
     582                 :           0 :                 appendStringInfoString(&err_detail, _("The row to be updated was deleted"));
     583                 :             : 
     584                 :           3 :             break;
     585                 :             : 
     586                 :           9 :         case CT_UPDATE_MISSING:
     587                 :           9 :             append_tuple_value_detail(&tuple_buf,
     588                 :             :                                       list_make2(remote_desc, search_desc));
     589                 :             : 
     590         [ +  - ]:           9 :             if (tuple_buf.len)
     591                 :           9 :                 appendStringInfo(&err_detail, _("Could not find the row to be updated: %s."),
     592                 :             :                                  tuple_buf.data);
     593                 :             :             else
     594                 :           0 :                 appendStringInfo(&err_detail, _("Could not find the row to be updated."));
     595                 :             : 
     596                 :           9 :             break;
     597                 :             : 
     598                 :           5 :         case CT_DELETE_ORIGIN_DIFFERS:
     599                 :           5 :             append_tuple_value_detail(&tuple_buf,
     600                 :             :                                       list_make3(local_desc, remote_desc,
     601                 :             :                                                  search_desc));
     602                 :             : 
     603         [ +  + ]:           5 :             if (localorigin == InvalidReplOriginId)
     604                 :             :             {
     605         [ +  - ]:           4 :                 if (tuple_buf.len)
     606                 :           4 :                     appendStringInfo(&err_detail, _("Deleting the row that was modified locally in transaction %u at %s: %s."),
     607                 :             :                                      localxmin, timestamptz_to_str(localts),
     608                 :             :                                      tuple_buf.data);
     609                 :             :                 else
     610                 :           0 :                     appendStringInfo(&err_detail, _("Deleting the row that was modified locally in transaction %u at %s."),
     611                 :             :                                      localxmin, timestamptz_to_str(localts));
     612                 :             :             }
     613         [ +  - ]:           1 :             else if (replorigin_by_oid(localorigin, true, &origin_name))
     614                 :             :             {
     615         [ +  - ]:           1 :                 if (tuple_buf.len)
     616                 :           1 :                     appendStringInfo(&err_detail, _("Deleting the row that was modified by a different origin \"%s\" in transaction %u at %s: %s."),
     617                 :             :                                      origin_name, localxmin,
     618                 :             :                                      timestamptz_to_str(localts),
     619                 :             :                                      tuple_buf.data);
     620                 :             :                 else
     621                 :           0 :                     appendStringInfo(&err_detail, _("Deleting the row that was modified by a different origin \"%s\" in transaction %u at %s."),
     622                 :             :                                      origin_name, localxmin,
     623                 :             :                                      timestamptz_to_str(localts));
     624                 :             :             }
     625                 :             : 
     626                 :             :             /* The origin that modified this row has been removed. */
     627                 :             :             else
     628                 :             :             {
     629         [ #  # ]:           0 :                 if (tuple_buf.len)
     630                 :           0 :                     appendStringInfo(&err_detail, _("Deleting the row that was modified by a non-existent origin in transaction %u at %s: %s."),
     631                 :             :                                      localxmin, timestamptz_to_str(localts),
     632                 :             :                                      tuple_buf.data);
     633                 :             :                 else
     634                 :           0 :                     appendStringInfo(&err_detail, _("Deleting the row that was modified by a non-existent origin in transaction %u at %s."),
     635                 :             :                                      localxmin, timestamptz_to_str(localts));
     636                 :             :             }
     637                 :             : 
     638                 :           5 :             break;
     639                 :             : 
     640                 :           9 :         case CT_DELETE_MISSING:
     641                 :           9 :             append_tuple_value_detail(&tuple_buf,
     642                 :             :                                       list_make1(search_desc));
     643                 :             : 
     644         [ +  - ]:           9 :             if (tuple_buf.len)
     645                 :           9 :                 appendStringInfo(&err_detail, _("Could not find the row to be deleted: %s."),
     646                 :             :                                  tuple_buf.data);
     647                 :             :             else
     648                 :           0 :                 appendStringInfo(&err_detail, _("Could not find the row to be deleted."));
     649                 :             : 
     650                 :           9 :             break;
     651                 :             :     }
     652                 :             : 
     653                 :             :     Assert(err_detail.len > 0);
     654                 :             : 
     655                 :             :     /*
     656                 :             :      * Insert a blank line to visually separate the new detail line from the
     657                 :             :      * existing ones.
     658                 :             :      */
     659         [ +  + ]:         121 :     if (err_msg->len > 0)
     660                 :          44 :         appendStringInfoChar(err_msg, '\n');
     661                 :             : 
     662                 :         121 :     appendStringInfoString(err_msg, err_detail.data);
     663                 :         121 : }
     664                 :             : 
     665                 :             : /*
     666                 :             :  * Extract conflicting key, local row, remote row, and replica identity
     667                 :             :  * columns. Results are set at xxx_desc.
     668                 :             :  *
     669                 :             :  * If the output is NULL, it indicates that the current user lacks permissions
     670                 :             :  * to view the columns involved.
     671                 :             :  */
     672                 :             : static void
     673                 :         121 : get_tuple_desc(EState *estate, ResultRelInfo *relinfo, ConflictType type,
     674                 :             :                char **key_desc,
     675                 :             :                TupleTableSlot *localslot, char **local_desc,
     676                 :             :                TupleTableSlot *remoteslot, char **remote_desc,
     677                 :             :                TupleTableSlot *searchslot, char **search_desc,
     678                 :             :                Oid indexoid)
     679                 :             : {
     680                 :         121 :     Relation    localrel = relinfo->ri_RelationDesc;
     681                 :         121 :     Oid         relid = RelationGetRelid(localrel);
     682                 :         121 :     TupleDesc   tupdesc = RelationGetDescr(localrel);
     683                 :         121 :     char       *desc = NULL;
     684                 :             : 
     685                 :             :     Assert((localslot && local_desc) || (remoteslot && remote_desc) ||
     686                 :             :            (searchslot && search_desc));
     687                 :             : 
     688                 :             :     /*
     689                 :             :      * Report the conflicting key values in the case of a unique constraint
     690                 :             :      * violation.
     691                 :             :      */
     692   [ +  +  +  +  :         121 :     if (type == CT_INSERT_EXISTS || type == CT_UPDATE_EXISTS ||
                   +  + ]
     693                 :             :         type == CT_MULTIPLE_UNIQUE_CONFLICTS)
     694                 :             :     {
     695                 :             :         Assert(OidIsValid(indexoid) && localslot);
     696                 :             : 
     697                 :          92 :         desc = build_index_value_desc(estate, localrel, localslot,
     698                 :             :                                       indexoid);
     699                 :             : 
     700         [ +  - ]:          92 :         if (desc)
     701                 :          92 :             *key_desc = psprintf(_("key %s"), desc);
     702                 :             :     }
     703                 :             : 
     704         [ +  + ]:         121 :     if (localslot)
     705                 :             :     {
     706                 :             :         /*
     707                 :             :          * The 'modifiedCols' only applies to the new tuple, hence we pass
     708                 :             :          * NULL for the local row.
     709                 :             :          */
     710                 :         100 :         desc = ExecBuildSlotValueDescription(relid, localslot, tupdesc,
     711                 :             :                                              NULL, 64);
     712                 :             : 
     713         [ +  - ]:         100 :         if (desc)
     714                 :         100 :             *local_desc = psprintf(_("local row %s"), desc);
     715                 :             :     }
     716                 :             : 
     717         [ +  + ]:         121 :     if (remoteslot)
     718                 :             :     {
     719                 :             :         Bitmapset  *modifiedCols;
     720                 :             : 
     721                 :             :         /*
     722                 :             :          * Although logical replication doesn't maintain the bitmap for the
     723                 :             :          * columns being inserted, we still use it to create 'modifiedCols'
     724                 :             :          * for consistency with other calls to ExecBuildSlotValueDescription.
     725                 :             :          *
     726                 :             :          * Note that generated columns are formed locally on the subscriber.
     727                 :             :          */
     728                 :         107 :         modifiedCols = bms_union(ExecGetInsertedCols(relinfo, estate),
     729                 :         107 :                                  ExecGetUpdatedCols(relinfo, estate));
     730                 :         107 :         desc = ExecBuildSlotValueDescription(relid, remoteslot,
     731                 :             :                                              tupdesc, modifiedCols,
     732                 :             :                                              64);
     733                 :             : 
     734         [ +  - ]:         107 :         if (desc)
     735                 :         107 :             *remote_desc = psprintf(_("remote row %s"), desc);
     736                 :             :     }
     737                 :             : 
     738         [ +  + ]:         121 :     if (searchslot)
     739                 :             :     {
     740                 :             :         /*
     741                 :             :          * Note that while index other than replica identity may be used (see
     742                 :             :          * IsIndexUsableForReplicaIdentityFull for details) to find the tuple
     743                 :             :          * when applying update or delete, such an index scan may not result
     744                 :             :          * in a unique tuple and we still compare the complete tuple in such
     745                 :             :          * cases, thus such indexes are not used here.
     746                 :             :          */
     747                 :          33 :         Oid         replica_index = GetRelationIdentityOrPK(localrel);
     748                 :             : 
     749                 :             :         Assert(type != CT_INSERT_EXISTS);
     750                 :             : 
     751                 :             :         /*
     752                 :             :          * If the table has a valid replica identity index, build the index
     753                 :             :          * key value string. Otherwise, construct the full tuple value for
     754                 :             :          * REPLICA IDENTITY FULL cases.
     755                 :             :          */
     756         [ +  + ]:          33 :         if (OidIsValid(replica_index))
     757                 :          29 :             desc = build_index_value_desc(estate, localrel, searchslot, replica_index);
     758                 :             :         else
     759                 :           4 :             desc = ExecBuildSlotValueDescription(relid, searchslot, tupdesc, NULL, 64);
     760                 :             : 
     761         [ +  - ]:          33 :         if (desc)
     762                 :             :         {
     763         [ +  + ]:          33 :             if (OidIsValid(replica_index))
     764                 :          29 :                 *search_desc = psprintf(_("replica identity %s"), desc);
     765                 :             :             else
     766                 :           4 :                 *search_desc = psprintf(_("replica identity full %s"), desc);
     767                 :             :         }
     768                 :             :     }
     769                 :         121 : }
     770                 :             : 
     771                 :             : /*
     772                 :             :  * Helper functions to construct a string describing the contents of an index
     773                 :             :  * entry. See BuildIndexValueDescription for details.
     774                 :             :  *
     775                 :             :  * The caller must ensure that the index with the OID 'indexoid' is locked so
     776                 :             :  * that we can fetch and display the conflicting key value.
     777                 :             :  */
     778                 :             : static char *
     779                 :         121 : build_index_value_desc(EState *estate, Relation localrel, TupleTableSlot *slot,
     780                 :             :                        Oid indexoid)
     781                 :             : {
     782                 :             :     char       *index_value;
     783                 :             :     Relation    indexDesc;
     784                 :             :     Datum       values[INDEX_MAX_KEYS];
     785                 :             :     bool        isnull[INDEX_MAX_KEYS];
     786                 :         121 :     TupleTableSlot *tableslot = slot;
     787                 :             : 
     788         [ -  + ]:         121 :     if (!tableslot)
     789                 :           0 :         return NULL;
     790                 :             : 
     791                 :             :     Assert(CheckRelationOidLockedByMe(indexoid, RowExclusiveLock, true));
     792                 :             : 
     793                 :         121 :     indexDesc = index_open(indexoid, NoLock);
     794                 :             : 
     795                 :             :     /*
     796                 :             :      * If the slot is a virtual slot, copy it into a heap tuple slot as
     797                 :             :      * FormIndexDatum only works with heap tuple slots.
     798                 :             :      */
     799         [ +  + ]:         121 :     if (TTS_IS_VIRTUAL(slot))
     800                 :             :     {
     801                 :          20 :         tableslot = table_slot_create(localrel, &estate->es_tupleTable);
     802                 :          20 :         tableslot = ExecCopySlot(tableslot, slot);
     803                 :             :     }
     804                 :             : 
     805                 :             :     /*
     806                 :             :      * Initialize ecxt_scantuple for potential use in FormIndexDatum when
     807                 :             :      * index expressions are present.
     808                 :             :      */
     809         [ +  - ]:         121 :     GetPerTupleExprContext(estate)->ecxt_scantuple = tableslot;
     810                 :             : 
     811                 :             :     /*
     812                 :             :      * The values/nulls arrays passed to BuildIndexValueDescription should be
     813                 :             :      * the results of FormIndexDatum, which are the "raw" input to the index
     814                 :             :      * AM.
     815                 :             :      */
     816                 :         121 :     FormIndexDatum(BuildIndexInfo(indexDesc), tableslot, estate, values, isnull);
     817                 :             : 
     818                 :         121 :     index_value = BuildIndexValueDescription(indexDesc, values, isnull);
     819                 :             : 
     820                 :         121 :     index_close(indexDesc, NoLock);
     821                 :             : 
     822                 :         121 :     return index_value;
     823                 :             : }
        

Generated by: LCOV version 2.0-1