diff --git a/changelogs/unreleased/gh-10870-fix-vinyl-tx-stmt-overwrite-multikey.md b/changelogs/unreleased/gh-10870-fix-vinyl-tx-stmt-overwrite-multikey.md new file mode 100644 index 0000000000000000000000000000000000000000..1bee6e7deff8fbc851d0f539cacea648ed6a8be6 --- /dev/null +++ b/changelogs/unreleased/gh-10870-fix-vinyl-tx-stmt-overwrite-multikey.md @@ -0,0 +1,6 @@ +## bugfix/vinyl + +* Fixed a bug when a tuple could disappear from a multikey index in case it + replaced a tuple with duplicate multikey array entries created in the same + transaction. With the enabled `defer_deletes` space option, the bug could + also trigger a crash (gh-10869, gh-10870). diff --git a/src/box/vy_tx.c b/src/box/vy_tx.c index a0d85a67973d34cdd427b1ca79d6b52ab8d4b8e6..8f9acb94e2c38552898cbe27de2274aa98268b74 100644 --- a/src/box/vy_tx.c +++ b/src/box/vy_tx.c @@ -1071,6 +1071,23 @@ static int vy_tx_set_entry(struct vy_tx *tx, struct vy_lsm *lsm, struct vy_entry entry) { assert(vy_stmt_type(entry.stmt) != 0); + + struct txv *old = write_set_search_key(&tx->write_set, lsm, entry); + if (old != NULL && old->entry.stmt == entry.stmt) { + /* + * The inserted statement is already indexed in the write set. + * This may happen only if this is a multikey index and the + * indexed array has duplicate entries. Inserting a duplicate + * into the write set is pointless. Moreover, it may break + * assumptions taken by the optimizations applied below, like + * REPLACE + DELETE = NOP. Let's skip it. + */ + assert(lsm->cmp_def->is_multikey); + assert(!old->is_overwritten); + assert(old->entry.hint != entry.hint); + return 0; + } + /** * A statement in write set must have and unique lsn * in order to differ it from cachable statements in mem and run. @@ -1078,7 +1095,6 @@ vy_tx_set_entry(struct vy_tx *tx, struct vy_lsm *lsm, struct vy_entry entry) vy_stmt_set_lsn(entry.stmt, INT64_MAX); struct vy_entry applied = vy_entry_none(); - struct txv *old = write_set_search_key(&tx->write_set, lsm, entry); /* Found a match of the previous action of this transaction */ if (old != NULL && vy_stmt_type(entry.stmt) == IPROTO_UPSERT) { assert(lsm->index_id == 0); diff --git a/test/vinyl-luatest/gh_10820_tx_stmt_overwrite_test.lua b/test/vinyl-luatest/tx_stmt_overwrite_test.lua similarity index 75% rename from test/vinyl-luatest/gh_10820_tx_stmt_overwrite_test.lua rename to test/vinyl-luatest/tx_stmt_overwrite_test.lua index 47118417f9c60e2ce6008055ddd26b6019c025d9..ae96d664b44d636ebdb2247d558838ab9e37ef8b 100644 --- a/test/vinyl-luatest/gh_10820_tx_stmt_overwrite_test.lua +++ b/test/vinyl-luatest/tx_stmt_overwrite_test.lua @@ -39,6 +39,15 @@ g.before_each(function(cg) unique = params.unique, parts = {{3, 'unsigned'}}, }) + box.schema.space.create('test3', { + engine = 'vinyl', + defer_deletes = params.defer_deletes, + }) + box.space.test3:create_index('i1') + box.space.test3:create_index('i2', { + unique = params.unique, + parts = {{'[2][*]', 'unsigned'}}, + }) end, {cg.params}) end) @@ -50,9 +59,13 @@ g.after_each(function(cg) if box.space.test2 ~= nil then box.space.test2:drop() end + if box.space.test3 ~= nil then + box.space.test3:drop() + end end) end) +-- gh-10820 g.test_case_1 = function(cg) cg.server:exec(function() local s = box.space.test1 @@ -71,6 +84,7 @@ g.test_case_1 = function(cg) end) end +-- gh-10822 g.test_case_2 = function(cg) cg.server:exec(function() local s = box.space.test1 @@ -164,3 +178,43 @@ g.test_case_6 = function(cg) t.assert_covers(s.index.i3:stat(), stat) end) end + +-- gh-10869 +g.test_case_7 = function(cg) + cg.server:exec(function() + local s = box.space.test3 + s:insert({1, {10}}) + box.begin() + s:replace({1, {1, 1}}) + s:update({1}, {{'=', 2, {2, 3, 4}}}) + box.commit() + box.snapshot() + local tuple = {1, {2, 3, 4}} + t.assert_equals(s.index.i1:select({}, {fullscan = true}), + {tuple}) + t.assert_equals(s.index.i2:select({}, {fullscan = true}), + {tuple, tuple, tuple}) + t.assert_covers(s.index.i1:stat(), + {memory = {rows = 0}, disk = {rows = 1}}) + t.assert_covers(s.index.i2:stat(), + {memory = {rows = 0}, disk = {rows = 3}}) + end) +end + +-- gh-10870 +g.test_case_8 = function(cg) + cg.server:exec(function() + local s = box.space.test3 + box.begin() + s:replace({1, {1, 1}}) + s:update({1}, {{'=', 2, {1}}}) + box.commit() + box.snapshot() + local data = {{1, {1}}} + t.assert_equals(s.index.i1:select({}, {fullscan = true}), data) + t.assert_equals(s.index.i2:select({}, {fullscan = true}), data) + local stat = {memory = {rows = 0}, disk = {rows = 1}} + t.assert_covers(s.index.i1:stat(), stat) + t.assert_covers(s.index.i2:stat(), stat) + end) +end