Skip to content

Commit 4bf1783

Browse files
committed
MDEV-41050 versioned DELETE via row_end index leaves a row undeleted
1. Aria/MyISAM engines A system-versioned DELETE on a MyISAM or Aria table indexed by row_end could leave the last current row undeleted. The delete scans the current rows via an equality search on row_end = MAX, which is driven by mi_rnext_same/maria_rnext_same. That function keeps the search's reference key in lastkey2 and uses the HA_STATE_RNEXT_SAME flag to remember it has already stored it. This reference-key mechanism is exactly what lets the scan modify rows it is walking without skipping them. Deleting a versioned row is an in-place update of row_end, and the update reuses lastkey2 as scratch space for the changed key, so it clears HA_STATE_RNEXT_SAME to request that rnext_same re-store its reference on the next call. However, TABLE::delete_row wraps the update in HA_EXTRA_REMEMBER_POS/HA_EXTRA_RESTORE_POS, and RESTORE_POS restored the whole saved info->update word, resurrecting the HA_STATE_RNEXT_SAME bit that the update had just cleared. As a result rnext_same skipped rebuilding its reference key and compared subsequent keys against the now-overwritten lastkey2, hitting a spurious end-of-file and terminating the scan one row early, defeating the engine's own protection against a Halloween-style skip. Fixed by preserving the current HA_STATE_RNEXT_SAME bit across RESTORE_POS instead of restoring the stale saved value. See also the HEAP fix below: same root cause, different per-engine mechanism. 2. HEAP engine The row loss also reproduces on the MEMORY (HEAP) engine. A system- versioned DELETE scans the current rows on the row_end index and turns each delete into an in-place update of row_end, so it modifies the very index it is walking. When the changed key is the scanned one (info->lastinx), hp_delete_key() repositions the cursor but heap_update() leaves info->update untouched, so HA_STATE_NEXT_FOUND from the preceding heap_rnext() stays set. The next heap_rnext() then sees current_ptr == 0 with that bit and takes the "!current_ptr && HA_STATE_NEXT_FOUND" guard as a false end-of-file, stopping one row early. Fixed by clearing HA_STATE_NEXT_FOUND when the scanned index key changed. HA_STATE_AKTIV is kept (unlike heap_delete): the row is updated, not removed, so a following op must not fail test_active(). The bit is only set after a heap_rnext(), so a plain single-row UPDATE never reaches this. See also the Aria/MyISAM fix above: same root cause, different per-engine mechanism. 3. Why the fix differs per engine, and InnoDB Aria and HEAP both trace their handler design back to MyISAM, hence the same root cause (stale scan bookkeeping after an in-place key change) in all three, fixed at each engine's own bookkeeping spot. InnoDB needs no fix: its persistent cursor survives concurrent index modification by design, already covered by this same test under the timestamp combination (default-storage-engine=innodb), which passes unmodified.
1 parent db8ec28 commit 4bf1783

5 files changed

Lines changed: 118 additions & 3 deletions

File tree

‎mysql-test/suite/versioning/r/delete.result‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,25 @@ f1 f3 check_row(row_start, row_end)
168168
2 2 HISTORICAL ROW
169169
# Cleanup
170170
drop tables t1, t2;
171+
#
172+
# MDEV-41050 versioned DELETE via row_end index leaves a row undeleted
173+
#
174+
create table t (
175+
id int primary key,
176+
row_start timestamp(6) generated always as row start,
177+
row_end timestamp(6) generated always as row end,
178+
period for system_time(row_start, row_end),
179+
key row_end_idx (row_end)
180+
) engine=ENGINE with system versioning;
181+
insert into t (id) values (1), (2), (3);
182+
delete from t force index (row_end_idx);
183+
# Expected: no current rows remain, all history rows sane.
184+
select id from t;
185+
id
186+
select id, check_row_ts(row_start, row_end) from t for system_time all order by id;
187+
id check_row_ts(row_start, row_end)
188+
1 HISTORICAL ROW
189+
2 HISTORICAL ROW
190+
3 HISTORICAL ROW
191+
drop table t;
192+
# End of 11.8 tests

‎mysql-test/suite/versioning/t/delete.test‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,32 @@ select f1, f3, check_row(row_start, row_end) from t1 for system_time all order b
121121
--echo # Cleanup
122122
drop tables t1, t2;
123123

124+
--echo #
125+
--echo # MDEV-41050 versioned DELETE via row_end index leaves a row undeleted
126+
--echo #
127+
--let $engine=`select @@default_storage_engine`
128+
# No dedicated Aria combination exists, so borrow trx_id's run for Aria coverage.
129+
if ($MTR_COMBINATION_TRX_ID)
130+
{
131+
--let $engine= Aria
132+
}
133+
134+
--replace_result $engine ENGINE
135+
eval create table t (
136+
id int primary key,
137+
row_start timestamp(6) generated always as row start,
138+
row_end timestamp(6) generated always as row end,
139+
period for system_time(row_start, row_end),
140+
key row_end_idx (row_end)
141+
) engine=$engine with system versioning;
142+
143+
insert into t (id) values (1), (2), (3);
144+
delete from t force index (row_end_idx);
145+
--echo # Expected: no current rows remain, all history rows sane.
146+
select id from t;
147+
select id, check_row_ts(row_start, row_end) from t for system_time all order by id;
148+
drop table t;
149+
150+
--echo # End of 11.8 tests
151+
124152
--source suite/versioning/common_finish.inc

‎storage/heap/hp_update.c‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new)
2323
HP_KEYDEF *keydef, *end, *p_lastinx;
2424
uchar *pos, *recovery_ptr;
2525
struct st_hp_hash_info *recovery_hash_ptr;
26-
my_bool auto_key_changed= 0, key_changed= 0;
26+
my_bool auto_key_changed= 0, key_changed= 0, lastinx_changed= 0;
2727
HP_SHARE *share= info->s;
2828
DBUG_ENTER("heap_update");
2929

@@ -40,14 +40,24 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new)
4040
recovery_hash_ptr= info->current_hash_ptr;
4141

4242
p_lastinx= share->keydef + info->lastinx;
43+
/* Re-index the record on every key whose value differs between old and new */
4344
for (keydef= share->keydef, end= keydef + share->keys; keydef < end; keydef++)
4445
{
46+
/* Skip keys that are unchanged by this update */
4547
if (hp_rec_key_cmp(keydef, old, heap_new))
4648
{
49+
/* Remove the old key entry, then insert the new one */
4750
if ((*keydef->delete_key)(info, keydef, old, pos, keydef == p_lastinx) ||
4851
(*keydef->write_key)(info, keydef, heap_new, pos))
4952
goto err;
5053
key_changed= 1;
54+
/*
55+
p_lastinx is the index the caller is currently scanning (info->lastinx),
56+
the one holding the live cursor. Detect when its key moved, so we can fix
57+
up the scan state below.
58+
*/
59+
if (keydef == p_lastinx)
60+
lastinx_changed= 1;
5161
if (share->auto_key == (uint) (keydef - share->keydef + 1))
5262
auto_key_changed= 1;
5363
}
@@ -63,6 +73,26 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new)
6373
heap_update_auto_increment(info, heap_new);
6474
if (key_changed)
6575
share->key_version++;
76+
if (lastinx_changed && (info->update & HA_STATE_NEXT_FOUND))
77+
{
78+
/*
79+
The scanned-index key of the current record changed in place (e.g. a
80+
versioned DELETE bumping row_end while scanning row_end), so the record
81+
left its position mid-scan. Clear HA_STATE_NEXT_FOUND, otherwise the next
82+
heap_rnext() hits the "!current_ptr && HA_STATE_NEXT_FOUND" guard in its
83+
hash (non-BTREE) branch, reports a false end-of-file and leaves a row
84+
behind. This is the same scan-state contract heap_delete() already
85+
maintains for heap_rnext() (it sets info->update there too); an
86+
in-place update that moves the scanned key must keep it consistent
87+
for the same reason. We only get here on an index scan; rnd scans
88+
(heap_scan()/heap_rrnd()) leave lastinx == -1, so despite also setting
89+
HA_STATE_NEXT_FOUND they never reach this. A plain single-row UPDATE has
90+
no preceding heap_rnext(), so the bit is unset and this is a no-op there.
91+
92+
See also: mi_extra.c/ma_extra.c HA_EXTRA_RESTORE_POS (MDEV-41050).
93+
*/
94+
info->update&= ~HA_STATE_NEXT_FOUND;
95+
}
6696
DBUG_RETURN(0);
6797

6898
err:

‎storage/maria/ma_extra.c‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ int maria_extra(MARIA_HA *info, enum ha_extra_function function,
4242
ulong cache_size;
4343
MARIA_SHARE *share= info->s;
4444
my_bool block_records= share->data_file_type == BLOCK_RECORD;
45+
uint save_update;
4546
DBUG_ENTER("maria_extra");
4647
DBUG_PRINT("enter",("function: %d",(int) function));
4748

@@ -209,7 +210,25 @@ int maria_extra(MARIA_HA *info, enum ha_extra_function function,
209210
bmove(info->last_key.data,
210211
info->last_key.data + share->base.max_key_length*2,
211212
info->save_lastkey_data_length + info->save_lastkey_ref_length);
212-
info->update= info->save_update | HA_STATE_WRITTEN;
213+
/*
214+
Preserve current HA_STATE_RNEXT_SAME state: a wrapped ha_update_row may
215+
have reused lastkey_buff2 and cleared the bit. Restoring the saved value
216+
would resurrect it, so maria_rnext_same would compare against a stale
217+
reference key and end the scan early, leading to Halloween-like skip.
218+
Only for RESTORE_POS: NO_KEYREAD ends a key-read scan and must restore
219+
save_update as is, otherwise the temporary scan's bit/reference leaks in.
220+
This targets only the known REMEMBER_POS/RESTORE_POS pair around a
221+
wrapped ha_update_row (TABLE::delete_row); it does not make the
222+
lastkey_buff2 reuse itself reentrant for other callers.
223+
224+
See also: hp_update.c HA_STATE_NEXT_FOUND, mi_extra.c (MDEV-41050).
225+
*/
226+
if (function == HA_EXTRA_RESTORE_POS)
227+
save_update= (info->save_update & ~HA_STATE_RNEXT_SAME) |
228+
(info->update & HA_STATE_RNEXT_SAME);
229+
else
230+
save_update= info->save_update;
231+
info->update= save_update | HA_STATE_WRITTEN;
213232
if (info->lastinx != info->save_lastinx) /* Index changed */
214233
{
215234
info->lastinx = info->save_lastinx;

‎storage/myisam/mi_extra.c‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ int mi_extra(MI_INFO *info, enum ha_extra_function function, void *extra_arg)
4040
int error=0;
4141
ulong cache_size;
4242
MYISAM_SHARE *share=info->s;
43+
uint save_update;
4344
DBUG_ENTER("mi_extra");
4445
DBUG_PRINT("enter",("function: %d",(int) function));
4546

@@ -200,7 +201,22 @@ int mi_extra(MI_INFO *info, enum ha_extra_function function, void *extra_arg)
200201
bmove((uchar*) info->lastkey,
201202
(uchar*) info->lastkey+share->base.max_key_length*2,
202203
info->save_lastkey_length);
203-
info->update= info->save_update | HA_STATE_WRITTEN;
204+
/*
205+
Preserve current HA_STATE_RNEXT_SAME state: a wrapped ha_update_row may
206+
have reused lastkey2 and cleared the bit. Restoring the saved value would
207+
resurrect it, so mi_rnext_same would compare against a stale reference key
208+
and end the scan early, leading to Halloween-like skip. Only for
209+
RESTORE_POS: NO_KEYREAD ends a key-read scan and must restore save_update
210+
as is, otherwise the temporary scan's bit/reference would leak in.
211+
212+
See also: hp_update.c HA_STATE_NEXT_FOUND, ma_extra.c (MDEV-41050).
213+
*/
214+
if (function == HA_EXTRA_RESTORE_POS)
215+
save_update= (info->save_update & ~HA_STATE_RNEXT_SAME) |
216+
(info->update & HA_STATE_RNEXT_SAME);
217+
else
218+
save_update= info->save_update;
219+
info->update= save_update | HA_STATE_WRITTEN;
204220
info->lastinx= info->save_lastinx;
205221
info->lastpos= info->save_lastpos;
206222
info->lastkey_length=info->save_lastkey_length;

0 commit comments

Comments
 (0)