mirror of
https://github.com/postgres/postgres.git
synced 2026-10-23 13:29:09 +03:00
Fix logical replication with lossy identity indexes.
A GiST index on a multirange stores only its bounding range, so different multiranges can produce the same index key. However, RelationFindReplTupleByIndex() and RelationFindDeletedTupleInfoByIndex() assume that matches through a primary key or replica identity index are exact and skip the equality check. This can cause updates or deletes to target the wrong row, and conflict detection to report update_deleted instead of update_missing. Fix this by rechecking candidates when the index scan sets xs_recheck. Compare only the index key columns, using the scan keys' equality operators and collations to match the index's notion of key equality. Backpatch to 18, where WITHOUT OVERLAPS was introduced. The change to RelationFindDeletedTupleInfoByIndex() applies only to 19 and later, where update_deleted conflict detection was added. Reported-by: Andres Freund <andres@anarazel.de> Author: Paul A. Jungwirth <pj@illuminatedcomputing.com> Author: Zhijie Hou <houzhijie22@gmail.com> Reviewed-by: Nisha Moond <nisha.moond412@gmail.com> Reviewed-by: kedar anavardekar <kedar.anavardekar@gmail.com> Reviewed-by: Amit Kapila <amit.kapila16@gmail.com> Discussion: https://postgr.es/m/kcyaok346iwocfdourf2oojgtz7ggxmh2ugis7qqhggs4qfzc2@cu6pkox4rfwp Discussion: https://postgr.es/m/e5nb5jbus2oa3pffmlo7pdvdckmchd54tqld4k3n6huyg5xxqn@7rounyvufjx6 Discussion: https://postgr.es/m/CA+renyWzTBC4GetPzJM1KqrejrmOWcGnn2NaA7tpee17HFxRPw@mail.gmail.com Backpatch-through: 18
This commit is contained in:
@@ -37,6 +37,8 @@
|
||||
|
||||
static bool tuples_equal(TupleTableSlot *slot1, TupleTableSlot *slot2,
|
||||
TypeCacheEntry **eq);
|
||||
static bool repl_index_key_equal(Relation idxrel, TupleTableSlot *slot1,
|
||||
TupleTableSlot *slot2, ScanKey skey);
|
||||
|
||||
/*
|
||||
* Setup a ScanKey for a search in the relation 'rel' for a tuple 'key' that
|
||||
@@ -176,8 +178,11 @@ should_refetch_tuple(TM_Result res, TM_FailureData *tmfd)
|
||||
* contents, and return true. Return false otherwise.
|
||||
*
|
||||
* 'skipduplicates' specifies whether the first matching tuple can be used
|
||||
* without comparing it against 'searchslot'. If false, all matching tuples are
|
||||
* compared against 'searchslot', which must contain a complete row.
|
||||
* without comparing it against 'searchslot', unless the index scan sets
|
||||
* xs_recheck (as with a lossy GiST index for WITHOUT OVERLAPS), in which case
|
||||
* only the index key columns are compared, using the index's own equality
|
||||
* semantics. If false, all matching tuples are compared against 'searchslot',
|
||||
* which must contain a complete row.
|
||||
*/
|
||||
bool
|
||||
RelationFindReplTupleByIndexExt(Relation rel, Oid idxoid,
|
||||
@@ -216,7 +221,8 @@ retry:
|
||||
{
|
||||
/*
|
||||
* Avoid expensive equality check if the index is primary key or
|
||||
* replica identity index.
|
||||
* replica identity index, unless the index scan indicates that a
|
||||
* recheck is needed.
|
||||
*/
|
||||
if (!skipduplicates)
|
||||
{
|
||||
@@ -226,6 +232,9 @@ retry:
|
||||
if (!tuples_equal(outslot, searchslot, eq))
|
||||
continue;
|
||||
}
|
||||
else if (scan->xs_recheck &&
|
||||
!repl_index_key_equal(idxrel, outslot, searchslot, skey))
|
||||
continue;
|
||||
|
||||
ExecMaterializeSlot(outslot);
|
||||
|
||||
@@ -362,6 +371,51 @@ tuples_equal(TupleTableSlot *slot1, TupleTableSlot *slot2,
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Compare the key columns of index 'idxrel' between the two slots, using the
|
||||
* equality operators and collations of the scan key 'skey' built by
|
||||
* build_replindex_scan_key().
|
||||
*/
|
||||
static bool
|
||||
repl_index_key_equal(Relation idxrel, TupleTableSlot *slot1,
|
||||
TupleTableSlot *slot2, ScanKey skey)
|
||||
{
|
||||
Assert(slot1->tts_tupleDescriptor->natts ==
|
||||
slot2->tts_tupleDescriptor->natts);
|
||||
|
||||
for (int i = 0; i < idxrel->rd_index->indnkeyatts; i++)
|
||||
{
|
||||
AttrNumber keycol = idxrel->rd_index->indkey.values[i];
|
||||
Datum value1;
|
||||
Datum value2;
|
||||
bool isnull1;
|
||||
bool isnull2;
|
||||
|
||||
value1 = slot_getattr(slot1, keycol, &isnull1);
|
||||
value2 = slot_getattr(slot2, keycol, &isnull2);
|
||||
|
||||
/*
|
||||
* If one value is NULL and other is not, then they are certainly not
|
||||
* equal
|
||||
*/
|
||||
if (isnull1 != isnull2)
|
||||
return false;
|
||||
|
||||
/*
|
||||
* If both are NULL, they can be considered equal.
|
||||
*/
|
||||
if (isnull1 || isnull2)
|
||||
continue;
|
||||
|
||||
if (!DatumGetBool(FunctionCall2Coll(&skey[i].sk_func,
|
||||
skey[i].sk_collation,
|
||||
value1, value2)))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Search the relation 'rel' for tuple using the sequential scan.
|
||||
*
|
||||
|
||||
@@ -628,4 +628,75 @@ is( $result, qq{[1,2)|[2000-01-01,2010-01-01)|a
|
||||
|
||||
drop_everything();
|
||||
|
||||
|
||||
# #################################
|
||||
# Test with a lossy REPLICA IDENTITY index (multirange key)
|
||||
#
|
||||
# A temporal key over a multirange column is backed by a lossy GiST index: two
|
||||
# distinct multiranges that share a bounding range are indistinguishable in the
|
||||
# index, so an index probe can return candidate tuples that are not exact
|
||||
# matches. The apply worker must recheck each candidate against the actual row
|
||||
# values, otherwise it could apply the change to the wrong row.
|
||||
# #################################
|
||||
|
||||
$node_publisher->safe_psql('postgres',
|
||||
"CREATE TABLE temporal_mltrng (id int4multirange, valid_at daterange, a text, PRIMARY KEY (id, valid_at WITHOUT OVERLAPS))"
|
||||
);
|
||||
$node_subscriber->safe_psql('postgres',
|
||||
"CREATE TABLE temporal_mltrng (id int4multirange, valid_at daterange, a text, PRIMARY KEY (id, valid_at WITHOUT OVERLAPS))"
|
||||
);
|
||||
|
||||
# Two different multiranges that share the bounding range [1,5), with the same
|
||||
# valid_at. They do not conflict (their ids are not equal), but the GiST index
|
||||
# cannot tell them apart without a recheck.
|
||||
$node_publisher->safe_psql(
|
||||
'postgres',
|
||||
"INSERT INTO temporal_mltrng (id, valid_at, a)
|
||||
VALUES ('{[1,5)}', '[2000-01-01,2010-01-01)', 'a'),
|
||||
('{[1,2),[3,5)}', '[2000-01-01,2010-01-01)', 'b')");
|
||||
|
||||
$node_publisher->safe_psql('postgres',
|
||||
"CREATE PUBLICATION pub1 FOR ALL TABLES");
|
||||
$node_subscriber->safe_psql('postgres',
|
||||
"CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr' PUBLICATION pub1"
|
||||
);
|
||||
$node_subscriber->wait_for_subscription_sync;
|
||||
|
||||
$result = $node_subscriber->safe_psql('postgres',
|
||||
"SELECT id, a FROM temporal_mltrng ORDER BY a");
|
||||
is( $result, qq{{[1,5)}|a
|
||||
{[1,2),[3,5)}|b}, 'synced temporal_mltrng lossy identity');
|
||||
|
||||
# Update each row. Each apply probes the identity index and gets *both* rows as
|
||||
# candidates; without a recheck the wrong row would be updated.
|
||||
$node_publisher->safe_psql('postgres',
|
||||
"UPDATE temporal_mltrng SET a = 'a2' WHERE id = '{[1,5)}'");
|
||||
$node_publisher->safe_psql('postgres',
|
||||
"UPDATE temporal_mltrng SET a = 'b2' WHERE id = '{[1,2),[3,5)}'");
|
||||
|
||||
$node_publisher->wait_for_catchup('sub1');
|
||||
|
||||
$result = $node_subscriber->safe_psql('postgres',
|
||||
"SELECT id, a FROM temporal_mltrng ORDER BY a");
|
||||
is( $result, qq{{[1,5)}|a2
|
||||
{[1,2),[3,5)}|b2}, 'replicated temporal_mltrng UPDATE to correct rows');
|
||||
|
||||
# Same for DELETE: remove only one of the two bounding-range twins.
|
||||
$node_publisher->safe_psql('postgres',
|
||||
"DELETE FROM temporal_mltrng WHERE id = '{[1,2),[3,5)}'");
|
||||
|
||||
$node_publisher->wait_for_catchup('sub1');
|
||||
|
||||
$result = $node_subscriber->safe_psql('postgres',
|
||||
"SELECT id, a FROM temporal_mltrng ORDER BY a");
|
||||
is($result, qq{{[1,5)}|a2},
|
||||
'replicated temporal_mltrng DELETE of correct row');
|
||||
|
||||
# cleanup
|
||||
|
||||
$node_publisher->safe_psql('postgres', "DROP TABLE temporal_mltrng");
|
||||
$node_subscriber->safe_psql('postgres', "DROP TABLE temporal_mltrng");
|
||||
$node_publisher->safe_psql('postgres', "DROP PUBLICATION pub1");
|
||||
$node_subscriber->safe_psql('postgres', "DROP SUBSCRIPTION sub1");
|
||||
|
||||
done_testing();
|
||||
|
||||
Reference in New Issue
Block a user