Message wording improvements

Use "row" instead of "tuple" for user-facing information for
logical replication conflicts.
master
Peter Eisentraut 2 weeks ago
parent 989b2e4d5c
commit 99234e9ddc
  1. 28
      doc/src/sgml/logical-replication.sgml
  2. 6
      src/backend/executor/execReplication.c
  3. 26
      src/backend/replication/logical/conflict.c
  4. 4
      src/include/replication/conflict.h
  5. 4
      src/test/subscription/t/001_rep_changes.pl
  6. 8
      src/test/subscription/t/013_partition.pl
  7. 2
      src/test/subscription/t/029_on_error.pl
  8. 4
      src/test/subscription/t/030_origin.pl
  9. 22
      src/test/subscription/t/035_conflicts.pl

@ -1824,7 +1824,7 @@ Publications:
<term><literal>update_missing</literal></term>
<listitem>
<para>
The tuple to be updated was not found. The update will simply be
The row to be updated was not found. The update will simply be
skipped in this scenario.
</para>
</listitem>
@ -1845,7 +1845,7 @@ Publications:
<term><literal>delete_missing</literal></term>
<listitem>
<para>
The tuple to be deleted was not found. The delete will simply be
The row to be deleted was not found. The delete will simply be
skipped in this scenario.
</para>
</listitem>
@ -1879,8 +1879,8 @@ DETAIL: <replaceable class="parameter">detailed_explanation</replaceable>.
<phrase>where <replaceable class="parameter">detail_values</replaceable> is one of:</phrase>
<literal>Key</literal> (<replaceable>column_name</replaceable> <optional>, ...</optional>)=(<replaceable>column_value</replaceable> <optional>, ...</optional>)
<literal>existing local tuple</literal> <optional>(<replaceable>column_name</replaceable> <optional>, ...</optional>)=</optional>(<replaceable>column_value</replaceable> <optional>, ...</optional>)
<literal>remote tuple</literal> <optional>(<replaceable>column_name</replaceable> <optional>, ...</optional>)=</optional>(<replaceable>column_value</replaceable> <optional>, ...</optional>)
<literal>existing local row</literal> <optional>(<replaceable>column_name</replaceable> <optional>, ...</optional>)=</optional>(<replaceable>column_value</replaceable> <optional>, ...</optional>)
<literal>remote row</literal> <optional>(<replaceable>column_name</replaceable> <optional>, ...</optional>)=</optional>(<replaceable>column_value</replaceable> <optional>, ...</optional>)
<literal>replica identity</literal> {(<replaceable>column_name</replaceable> <optional>, ...</optional>)=(<replaceable>column_value</replaceable> <optional>, ...</optional>) | full <optional>(<replaceable>column_name</replaceable> <optional>, ...</optional>)=</optional>(<replaceable>column_value</replaceable> <optional>, ...</optional>)}
</synopsis>
@ -1914,32 +1914,32 @@ DETAIL: <replaceable class="parameter">detailed_explanation</replaceable>.
<para>
<replaceable class="parameter">detailed_explanation</replaceable> includes
the origin, transaction ID, and commit timestamp of the transaction that
modified the existing local tuple, if available.
modified the existing local row, if available.
</para>
</listitem>
<listitem>
<para>
The <literal>Key</literal> section includes the key values of the local
tuple that violated a unique constraint for
row that violated a unique constraint for
<literal>insert_exists</literal>, <literal>update_exists</literal> or
<literal>multiple_unique_conflicts</literal> conflicts.
</para>
</listitem>
<listitem>
<para>
The <literal>existing local tuple</literal> section includes the local
tuple if its origin differs from the remote tuple for
The <literal>existing local row</literal> section includes the local
row if its origin differs from the remote row for
<literal>update_origin_differs</literal> or <literal>delete_origin_differs</literal>
conflicts, or if the key value conflicts with the remote tuple for
conflicts, or if the key value conflicts with the remote row for
<literal>insert_exists</literal>, <literal>update_exists</literal> or
<literal>multiple_unique_conflicts</literal> conflicts.
</para>
</listitem>
<listitem>
<para>
The <literal>remote tuple</literal> section includes the new tuple from
The <literal>remote row</literal> section includes the new row from
the remote insert or update operation that caused the conflict. Note that
for an update operation, the column value of the new tuple will be null
for an update operation, the column value of the new row will be null
if the value is unchanged and toasted.
</para>
</listitem>
@ -1947,7 +1947,7 @@ DETAIL: <replaceable class="parameter">detailed_explanation</replaceable>.
<para>
The <literal>replica identity</literal> section includes the replica
identity key values that were used to search for the existing local
tuple to be updated or deleted. This may include the full tuple value
row to be updated or deleted. This may include the full row value
if the local relation is marked with
<link linkend="sql-altertable-replica-identity-full"><literal>REPLICA IDENTITY FULL</literal></link>.
</para>
@ -1955,7 +1955,7 @@ DETAIL: <replaceable class="parameter">detailed_explanation</replaceable>.
<listitem>
<para>
<replaceable class="parameter">column_name</replaceable> is the column name.
For <literal>existing local tuple</literal>, <literal>remote tuple</literal>,
For <literal>existing local row</literal>, <literal>remote row</literal>,
and <literal>replica identity full</literal> cases, column names are
logged only if the user lacks the privilege to access all columns of
the table. If column names are present, they appear in the same order
@ -2012,7 +2012,7 @@ DETAIL: <replaceable class="parameter">detailed_explanation</replaceable>.
<screen>
ERROR: conflict detected on relation "public.test": conflict=insert_exists
DETAIL: Key already exists in unique index "t_pkey", which was modified locally in transaction 740 at 2024-06-26 10:47:04.727375+08.
Key (c)=(1); existing local tuple (1, 'local'); remote tuple (1, 'remote').
Key (c)=(1); existing local row (1, 'local'); remote row (1, 'remote').
CONTEXT: processing remote data for replication origin "pg_16395" during "INSERT" for replication target relation "public.test" in transaction 725 finished at 0/014C0378
</screen>
The LSN of the transaction that contains the change violating the constraint and

@ -852,10 +852,10 @@ ExecSimpleRelationInsert(ResultRelInfo *resultRelInfo,
conflictindexes, false);
/*
* Checks the conflict indexes to fetch the conflicting local tuple
* and reports the conflict. We perform this check here, instead of
* Checks the conflict indexes to fetch the conflicting local row and
* reports the conflict. We perform this check here, instead of
* performing an additional index scan before the actual insertion and
* reporting the conflict if any conflicting tuples are found. This is
* reporting the conflict if any conflicting rows are found. This is
* to avoid the overhead of executing the extra scan for each INSERT
* operation, even when no conflict arises, which could introduce
* significant overhead to replication, particularly in cases where

@ -55,7 +55,7 @@ static char *build_index_value_desc(EState *estate, Relation localrel,
/*
* Get the xmin and commit timestamp data (origin and timestamp) associated
* with the provided local tuple.
* with the provided local row.
*
* Return true if the commit timestamp data was found, false otherwise.
*/
@ -89,12 +89,12 @@ GetTupleTransactionInfo(TupleTableSlot *localslot, TransactionId *xmin,
* This function is used to report a conflict while applying replication
* changes.
*
* 'searchslot' should contain the tuple used to search the local tuple to be
* 'searchslot' should contain the tuple used to search the local row to be
* updated or deleted.
*
* 'remoteslot' should contain the remote new tuple, if any.
*
* conflicttuples is a list of local tuples that caused the conflict and the
* conflicttuples is a list of local rows that caused the conflict and the
* conflict related information. See ConflictTupleInfo.
*
* The caller must ensure that all the indexes passed in ConflictTupleInfo are
@ -191,9 +191,9 @@ errcode_apply_conflict(ConflictType type)
*
* The DETAIL line comprises of two parts:
* 1. Explanation of the conflict type, including the origin and commit
* timestamp of the existing local tuple.
* 2. Display of conflicting key, existing local tuple, remote new tuple, and
* replica identity columns, if any. The remote old tuple is excluded as its
* timestamp of the existing local row.
* 2. Display of conflicting key, existing local row, remote new row, and
* replica identity columns, if any. The remote old row is excluded as its
* information is covered in the replica identity columns.
*/
static void
@ -313,7 +313,7 @@ errdetail_apply_conflict(EState *estate, ResultRelInfo *relinfo,
localslot, remoteslot, indexoid);
/*
* Next, append the key values, existing local tuple, remote tuple and
* Next, append the key values, existing local row, remote row, and
* replica identity columns after the message.
*/
if (val_desc)
@ -331,7 +331,7 @@ errdetail_apply_conflict(EState *estate, ResultRelInfo *relinfo,
/*
* Helper function to build the additional details for conflicting key,
* existing local tuple, remote tuple, and replica identity columns.
* existing local row, remote row, and replica identity columns.
*
* If the return value is NULL, it indicates that the current user lacks
* permissions to view the columns involved.
@ -373,7 +373,7 @@ build_tuple_value_details(EState *estate, ResultRelInfo *relinfo,
{
/*
* The 'modifiedCols' only applies to the new tuple, hence we pass
* NULL for the existing local tuple.
* NULL for the existing local row.
*/
desc = ExecBuildSlotValueDescription(relid, localslot, tupdesc,
NULL, 64);
@ -383,12 +383,12 @@ build_tuple_value_details(EState *estate, ResultRelInfo *relinfo,
if (tuple_value.len > 0)
{
appendStringInfoString(&tuple_value, "; ");
appendStringInfo(&tuple_value, _("existing local tuple %s"),
appendStringInfo(&tuple_value, _("existing local row %s"),
desc);
}
else
{
appendStringInfo(&tuple_value, _("Existing local tuple %s"),
appendStringInfo(&tuple_value, _("Existing local row %s"),
desc);
}
}
@ -415,11 +415,11 @@ build_tuple_value_details(EState *estate, ResultRelInfo *relinfo,
if (tuple_value.len > 0)
{
appendStringInfoString(&tuple_value, "; ");
appendStringInfo(&tuple_value, _("remote tuple %s"), desc);
appendStringInfo(&tuple_value, _("remote row %s"), desc);
}
else
{
appendStringInfo(&tuple_value, _("Remote tuple %s"), desc);
appendStringInfo(&tuple_value, _("Remote row %s"), desc);
}
}
}

@ -57,7 +57,7 @@ typedef enum
#define CONFLICT_NUM_TYPES (CT_MULTIPLE_UNIQUE_CONFLICTS + 1)
/*
* Information for the existing local tuple that caused the conflict.
* Information for the existing local row that caused the conflict.
*/
typedef struct ConflictTupleInfo
{
@ -69,7 +69,7 @@ typedef struct ConflictTupleInfo
* the conflict */
RepOriginId origin; /* origin identifier of the modification */
TimestampTz ts; /* timestamp of when the modification on the
* conflicting local tuple occurred */
* conflicting local row occurred */
} ConflictTupleInfo;
extern bool GetTupleTransactionInfo(TupleTableSlot *localslot,

@ -365,10 +365,10 @@ $node_publisher->wait_for_catchup('tap_sub');
my $logfile = slurp_file($node_subscriber->logfile, $log_location);
ok( $logfile =~
qr/conflict detected on relation "public.tab_full_pk": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated.*\n.*Remote tuple \(1, quux\); replica identity \(a\)=\(1\)/m,
qr/conflict detected on relation "public.tab_full_pk": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated.*\n.*Remote row \(1, quux\); replica identity \(a\)=\(1\)/m,
'update target row is missing');
ok( $logfile =~
qr/conflict detected on relation "public.tab_full": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated.*\n.*Remote tuple \(26\); replica identity full \(25\)/m,
qr/conflict detected on relation "public.tab_full": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated.*\n.*Remote row \(26\); replica identity full \(25\)/m,
'update target row is missing');
ok( $logfile =~
qr/conflict detected on relation "public.tab_full_pk": conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be deleted.*\n.*Replica identity \(a\)=\(2\)/m,

@ -368,7 +368,7 @@ $node_publisher->wait_for_catchup('sub2');
my $logfile = slurp_file($node_subscriber1->logfile(), $log_location);
ok( $logfile =~
qr/conflict detected on relation "public.tab1_2_2": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated.*\n.*Remote tuple \(null, 4, quux\); replica identity \(a\)=\(4\)/,
qr/conflict detected on relation "public.tab1_2_2": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated.*\n.*Remote row \(null, 4, quux\); replica identity \(a\)=\(4\)/,
'update target row is missing in tab1_2_2');
ok( $logfile =~
qr/conflict detected on relation "public.tab1_1": conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be deleted.*\n.*Replica identity \(a\)=\(1\)/,
@ -781,7 +781,7 @@ $node_publisher->wait_for_catchup('sub2');
$logfile = slurp_file($node_subscriber1->logfile(), $log_location);
ok( $logfile =~
qr/conflict detected on relation "public.tab2_1": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated.*\n.*Remote tuple \(pub_tab2, quux, 5\); replica identity \(a\)=\(5\)/,
qr/conflict detected on relation "public.tab2_1": conflict=update_missing.*\n.*DETAIL:.* Could not find the row to be updated.*\n.*Remote row \(pub_tab2, quux, 5\); replica identity \(a\)=\(5\)/,
'update target row is missing in tab2_1');
ok( $logfile =~
qr/conflict detected on relation "public.tab2_1": conflict=delete_missing.*\n.*DETAIL:.* Could not find the row to be deleted.*\n.*Replica identity \(a\)=\(1\)/,
@ -802,8 +802,8 @@ $node_publisher->wait_for_catchup('sub_viaroot');
$logfile = slurp_file($node_subscriber1->logfile(), $log_location);
ok( $logfile =~
qr/conflict detected on relation "public.tab2_1": conflict=update_origin_differs.*\n.*DETAIL:.* Updating the row that was modified locally in transaction [0-9]+ at .*\n.*Existing local tuple \(yyy, null, 3\); remote tuple \(pub_tab2, quux, 3\); replica identity \(a\)=\(3\)/,
'updating a tuple that was modified by a different origin');
qr/conflict detected on relation "public.tab2_1": conflict=update_origin_differs.*\n.*DETAIL:.* Updating the row that was modified locally in transaction [0-9]+ at .*\n.*Existing local row \(yyy, null, 3\); remote row \(pub_tab2, quux, 3\); replica identity \(a\)=\(3\)/,
'updating a row that was modified by a different origin');
# The remaining tests no longer test conflict detection.
$node_subscriber1->append_conf('postgresql.conf',

@ -30,7 +30,7 @@ sub test_skip_lsn
# ERROR with its CONTEXT when retrieving this information.
my $contents = slurp_file($node_subscriber->logfile, $offset);
$contents =~
qr/conflict detected on relation "public.tbl".*\n.*DETAIL:.* Key already exists in unique index "tbl_pkey", modified by .*origin.* transaction \d+ at .*\n.*Key \(i\)=\(\d+\); existing local tuple .*; remote tuple .*\n.*CONTEXT:.* for replication target relation "public.tbl" in transaction \d+, finished at ([[:xdigit:]]+\/[[:xdigit:]]+)/m
qr/conflict detected on relation "public.tbl".*\n.*DETAIL:.* Key already exists in unique index "tbl_pkey", modified by .*origin.* transaction \d+ at .*\n.*Key \(i\)=\(\d+\); existing local row .*; remote row .*\n.*CONTEXT:.* for replication target relation "public.tbl" in transaction \d+, finished at ([[:xdigit:]]+\/[[:xdigit:]]+)/m
or die "could not get error-LSN";
my $lsn = $1;

@ -163,7 +163,7 @@ is($result, qq(32), 'The node_A data replicated to node_B');
$node_C->safe_psql('postgres', "UPDATE tab SET a = 33 WHERE a = 32;");
$node_B->wait_for_log(
qr/conflict detected on relation "public.tab": conflict=update_origin_differs.*\n.*DETAIL:.* Updating the row that was modified by a different origin ".*" in transaction [0-9]+ at .*\n.*Existing local tuple \(32\); remote tuple \(33\); replica identity \(a\)=\(32\)/
qr/conflict detected on relation "public.tab": conflict=update_origin_differs.*\n.*DETAIL:.* Updating the row that was modified by a different origin ".*" in transaction [0-9]+ at .*\n.*Existing local row \(32\); remote row \(33\); replica identity \(a\)=\(32\)/
);
$node_B->safe_psql('postgres', "DELETE FROM tab;");
@ -179,7 +179,7 @@ is($result, qq(33), 'The node_A data replicated to node_B');
$node_C->safe_psql('postgres', "DELETE FROM tab WHERE a = 33;");
$node_B->wait_for_log(
qr/conflict detected on relation "public.tab": conflict=delete_origin_differs.*\n.*DETAIL:.* Deleting the row that was modified by a different origin ".*" in transaction [0-9]+ at .*\n.*Existing local tuple \(33\); replica identity \(a\)=\(33\)/
qr/conflict detected on relation "public.tab": conflict=delete_origin_differs.*\n.*DETAIL:.* Deleting the row that was modified by a different origin ".*" in transaction [0-9]+ at .*\n.*Existing local row \(33\); replica identity \(a\)=\(33\)/
);
# The remaining tests no longer test conflict detection.

@ -79,11 +79,11 @@ $node_publisher->safe_psql('postgres',
$node_subscriber->wait_for_log(
qr/conflict detected on relation \"public.conf_tab\": conflict=multiple_unique_conflicts.*
.*Key already exists in unique index \"conf_tab_pkey\".*
.*Key \(a\)=\(2\); existing local tuple \(2, 2, 2\); remote tuple \(2, 3, 4\).*
.*Key \(a\)=\(2\); existing local row \(2, 2, 2\); remote row \(2, 3, 4\).*
.*Key already exists in unique index \"conf_tab_b_key\".*
.*Key \(b\)=\(3\); existing local tuple \(3, 3, 3\); remote tuple \(2, 3, 4\).*
.*Key \(b\)=\(3\); existing local row \(3, 3, 3\); remote row \(2, 3, 4\).*
.*Key already exists in unique index \"conf_tab_c_key\".*
.*Key \(c\)=\(4\); existing local tuple \(4, 4, 4\); remote tuple \(2, 3, 4\)./,
.*Key \(c\)=\(4\); existing local row \(4, 4, 4\); remote row \(2, 3, 4\)./,
$log_offset);
pass('multiple_unique_conflicts detected during insert');
@ -111,11 +111,11 @@ $node_publisher->safe_psql('postgres',
$node_subscriber->wait_for_log(
qr/conflict detected on relation \"public.conf_tab\": conflict=multiple_unique_conflicts.*
.*Key already exists in unique index \"conf_tab_pkey\".*
.*Key \(a\)=\(6\); existing local tuple \(6, 6, 6\); remote tuple \(6, 7, 8\).*
.*Key \(a\)=\(6\); existing local row \(6, 6, 6\); remote row \(6, 7, 8\).*
.*Key already exists in unique index \"conf_tab_b_key\".*
.*Key \(b\)=\(7\); existing local tuple \(7, 7, 7\); remote tuple \(6, 7, 8\).*
.*Key \(b\)=\(7\); existing local row \(7, 7, 7\); remote row \(6, 7, 8\).*
.*Key already exists in unique index \"conf_tab_c_key\".*
.*Key \(c\)=\(8\); existing local tuple \(8, 8, 8\); remote tuple \(6, 7, 8\)./,
.*Key \(c\)=\(8\); existing local row \(8, 8, 8\); remote row \(6, 7, 8\)./,
$log_offset);
pass('multiple_unique_conflicts detected during update');
@ -139,9 +139,9 @@ $node_publisher->safe_psql('postgres',
$node_subscriber->wait_for_log(
qr/conflict detected on relation \"public.conf_tab_2_p1\": conflict=multiple_unique_conflicts.*
.*Key already exists in unique index \"conf_tab_2_p1_pkey\".*
.*Key \(a\)=\(55\); existing local tuple \(55, 2, 3\); remote tuple \(55, 2, 3\).*
.*Key \(a\)=\(55\); existing local row \(55, 2, 3\); remote row \(55, 2, 3\).*
.*Key already exists in unique index \"conf_tab_2_p1_a_b_key\".*
.*Key \(a, b\)=\(55, 2\); existing local tuple \(55, 2, 3\); remote tuple \(55, 2, 3\)./,
.*Key \(a, b\)=\(55, 2\); existing local row \(55, 2, 3\); remote row \(55, 2, 3\)./,
$log_offset);
pass('multiple_unique_conflicts detected on a leaf partition during insert');
@ -314,7 +314,7 @@ my $logfile = slurp_file($node_B->logfile(), $log_location);
ok( $logfile =~
qr/conflict detected on relation "public.tab": conflict=delete_origin_differs.*
.*DETAIL:.* Deleting the row that was modified locally in transaction [0-9]+ at .*
.*Existing local tuple \(1, 3\); replica identity \(a\)=\(1\)/,
.*Existing local row \(1, 3\); replica identity \(a\)=\(1\)/,
'delete target row was modified in tab');
$log_location = -s $node_A->logfile;
@ -327,7 +327,7 @@ $logfile = slurp_file($node_A->logfile(), $log_location);
ok( $logfile =~
qr/conflict detected on relation "public.tab": conflict=update_deleted.*
.*DETAIL:.* The row to be updated was deleted locally in transaction [0-9]+ at .*
.*Remote tuple \(1, 3\); replica identity \(a\)=\(1\)/,
.*Remote row \(1, 3\); replica identity \(a\)=\(1\)/,
'update target row was deleted in tab');
# Remember the next transaction ID to be assigned
@ -383,7 +383,7 @@ $logfile = slurp_file($node_A->logfile(), $log_location);
ok( $logfile =~
qr/conflict detected on relation "public.tab": conflict=update_deleted.*
.*DETAIL:.* The row to be updated was deleted locally in transaction [0-9]+ at .*
.*Remote tuple \(2, 4\); replica identity full \(2, 2\)/,
.*Remote row \(2, 4\); replica identity full \(2, 2\)/,
'update target row was deleted in tab');
###############################################################################

Loading…
Cancel
Save