diff --git a/db/view/view.cc b/db/view/view.cc index 33bd2a3ca0..860fc6485f 100644 --- a/db/view/view.cc +++ b/db/view/view.cc @@ -1599,9 +1599,10 @@ future view_update_builder::on_results() { return should_stop_updates() ? stop() : advance_existings(); } - // If we have updates and it's a range tombstone, it removes nothing pre-exisiting, so we can ignore it if (_update && !_update->is_end_of_partition()) { - if (_update->is_clustering_row()) { + if (_update->is_range_tombstone_change()) { + _update_current_tombstone = _update->as_range_tombstone_change().tombstone(); + } else if (_update->is_clustering_row()) { _update->mutate_as_clustering_row(*_schema, [&] (clustering_row& cr) mutable { cr.apply(std::max(_update_partition_tombstone, _update_current_tombstone)); }); diff --git a/test/cqlpy/test_materialized_view.py b/test/cqlpy/test_materialized_view.py index 3af1dab8f7..8850d455c1 100644 --- a/test/cqlpy/test_materialized_view.py +++ b/test/cqlpy/test_materialized_view.py @@ -1556,6 +1556,42 @@ def test_mv_partition_tombstone_does_not_resurrect_range_deleted_row(cql, test_k assert [] == list(cql.execute(f"SELECT * FROM {table}")) assert [] == list(cql.execute(f"SELECT * FROM {mv}")) +# Test that a range delete in the same batch as an insert correctly covers +# rows within the deleted range in the materialized view and that it doesn't +# cover rows outside the deleted range. The view update builder must track +# range tombstone changes from the update stream so that all range tombstones +# are applied to the clustering rows that they cover. +# Without this, an inserted row within the range incorrectly survives in the +# view or is incorrectly deleted. +# Reproduces SCYLLADB-1555. +def test_mv_range_delete_and_insert_in_same_batch(cql, test_keyspace): + # Case 1: Insert within the range-deleted interval. The range tombstone + # should shadow the insert, leaving both base and view empty. + with new_test_table(cql, test_keyspace, + 'p int, c int, v int, w int, primary key (p, c)') as table: + with new_materialized_view(cql, table, '*', 'v, p, c', + 'v is not null and p is not null and c is not null') as mv: + cql.execute(f"BEGIN BATCH " + f"DELETE FROM {table} WHERE p = 1 AND c >= 1 AND c <= 3; " + f"INSERT INTO {table} (p, c, v) VALUES (1, 3, 3); " + f"APPLY BATCH") + assert [] == list(cql.execute(f"SELECT * FROM {table}")) + assert [] == list(cql.execute(f"SELECT * FROM {mv}")) + # Case 2: A pre-existing row within the range, and an insert outside it. + # The range delete should remove the existing row, but the new row at c=4 + # falls outside the range and should survive in both base and view. + with new_test_table(cql, test_keyspace, + 'p int, c int, v int, w int, primary key (p, c)') as table: + with new_materialized_view(cql, table, '*', 'v, p, c', + 'v is not null and p is not null and c is not null') as mv: + cql.execute(f"INSERT INTO {table} (p, c, v) VALUES (1, 2, 1)") + cql.execute(f"BEGIN BATCH " + f"DELETE FROM {table} WHERE p = 1 AND c >= 1 AND c <= 3; " + f"INSERT INTO {table} (p, c, v) VALUES (1, 4, 3); " + f"APPLY BATCH") + assert [] != list(cql.execute(f"SELECT * FROM {table}")) + assert [] != list(cql.execute(f"SELECT * FROM {mv}")) + # Test view representation in system.* tables def test_view_in_system_tables(cql, test_keyspace): with new_test_table(cql, test_keyspace, "p int PRIMARY KEY, v int") as base: