diff --git a/src/box/vy_tx.c b/src/box/vy_tx.c index cb9bbf587c92fdcd1173054d1812fd17fd51b22d..b1c39ff96d645e9c2720a49a6a45ebb4a532396c 100644 --- a/src/box/vy_tx.c +++ b/src/box/vy_tx.c @@ -865,6 +865,34 @@ vy_tx_set(struct vy_tx *tx, struct vy_index *index, struct tuple *stmt) vy_stmt_column_mask(old->stmt)); } + if (index->id > 0 && vy_stmt_type(stmt) == IPROTO_REPLACE && + old != NULL && vy_stmt_type(old->stmt) == IPROTO_DELETE) { + /* + * The column mask of an update operation may have a bit + * set even if the corresponding field doesn't actually + * get updated, because a column mask is generated + * only from update operations, before applying them. + * E.g. update(1, {{'+', 2, 0}}) doesn't modify the + * second field, but the column mask will say it does. + * + * To discard DELETE statements in the write iterator + * (see optimization #6), we turn a REPLACE into an + * INSERT in case the REPLACE was generated by an + * update that changed secondary key fields. So we + * can't tolerate inaccuracy in a column mask. + * + * So if the update didn't actually modify secondary + * key fields, i.e. DELETE and REPLACE generated by the + * update have the same key fields, we forcefully clear + * key bits in the column mask to ensure that no REPLACE + * statement will be written for this secondary key. + */ + uint64_t column_mask = vy_stmt_column_mask(stmt); + if (column_mask != UINT64_MAX) + vy_stmt_set_column_mask(stmt, column_mask & + ~index->cmp_def->column_mask); + } + v->overwritten = old; write_set_insert(&tx->write_set, v); tx->write_set_version++; diff --git a/test/vinyl/update_optimize.result b/test/vinyl/update_optimize.result index fbd42df04c8009d6d38531c1ead0198244a2125f..00242f4e92d1b413fd547b1d380132f178d4b882 100644 --- a/test/vinyl/update_optimize.result +++ b/test/vinyl/update_optimize.result @@ -711,3 +711,50 @@ lookups() space:drop() --- ... +-- +-- gh-3607: phantom tuples in secondary index if UPDATE does not +-- change key fields. +-- +s = box.schema.space.create('test', {engine = 'vinyl'}) +--- +... +_ = s:create_index('pk') +--- +... +_ = s:create_index('sk', {parts = {2, 'unsigned'}, run_count_per_level = 10}) +--- +... +s:insert{1, 10} +--- +- [1, 10] +... +box.snapshot() +--- +- ok +... +s:update(1, {{'=', 2, 10}}) +--- +- [1, 10] +... +s:delete(1) +--- +... +box.snapshot() +--- +- ok +... +s.index.sk:info().rows -- INSERT in the first run + DELETE the second run +--- +- 2 +... +s:insert{1, 20} +--- +- [1, 20] +... +s.index.sk:select() +--- +- - [1, 20] +... +s:drop() +--- +... diff --git a/test/vinyl/update_optimize.test.lua b/test/vinyl/update_optimize.test.lua index 32144172c4f1eeef6bd17553f3ca1df8a0bbd043..91bc97444e02c4ee6ed77f0b4da6d2d9302eef2e 100644 --- a/test/vinyl/update_optimize.test.lua +++ b/test/vinyl/update_optimize.test.lua @@ -234,3 +234,25 @@ space:update(1, {{'+', 5, 1}}) lookups() space:drop() + +-- +-- gh-3607: phantom tuples in secondary index if UPDATE does not +-- change key fields. +-- +s = box.schema.space.create('test', {engine = 'vinyl'}) +_ = s:create_index('pk') +_ = s:create_index('sk', {parts = {2, 'unsigned'}, run_count_per_level = 10}) + +s:insert{1, 10} +box.snapshot() + +s:update(1, {{'=', 2, 10}}) +s:delete(1) +box.snapshot() + +s.index.sk:info().rows -- INSERT in the first run + DELETE the second run + +s:insert{1, 20} +s.index.sk:select() + +s:drop()