Skip to content

Commit 44314c7

Browse files
committed
MDEV-15165 InnoDB purge for index on virtual column is trying to access an incomplete record
The algorithm change is based on a MySQL 8.0 fix for BUG #26818787: ASSERTION: DATA0DATA.IC:430:TUPLE by Krzysztof Kapuścik mysql/mysql-server@ee606e6 If a record had been inserted in place of a delete-marked purgeable record by modifying that record, and purge was accessing that record before the off-page columns were written, row_build_index_entry() would have returned NULL, causing a crash. row_vers_non_virtual_fields_equal(): Check whether all non-virtual fields of an index are equal. Replaces row_vers_non_vc_match(). A more complex version of this function was called row_vers_non_vc_index_entry_match() in the MySQL 8.0 fix. row_vers_impl_x_locked_low(): This change is not directly related to the reported problem, but apparently to the removal of the function row_vers_non_vc_match(). This function checks if a secondary index record was modified by a transaction that has not been committed yet. For comparing the non-virtual columns, construct a secondary index tuple from the table row. row_vers_vc_matches_cluster(): Replace row_vers_non_vc_match() with code that is equivalent to the row_vers_non_vc_index_entry_match() in the MySQL 8.0 fix. Also, deduplicate some code by using goto.
1 parent 29240b5 commit 44314c7

File tree

3 files changed

+137
-115
lines changed

3 files changed

+137
-115
lines changed

mysql-test/suite/gcol/r/innodb_virtual_debug_purge.result

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ connection default;
145145
update t set a = repeat('m', 16000) where a like "aaa%";
146146
connect lock_table, localhost, root;
147147
lock table t write;
148-
disconnect prevent_purge;
148+
connection prevent_purge;
149+
commit;
149150
connection default;
150151
InnoDB 0 transactions not purged
151152
disconnect lock_table;
@@ -154,5 +155,28 @@ commit;
154155
InnoDB 0 transactions not purged
155156
set global debug_dbug=@old_dbug;
156157
drop table t;
158+
#
159+
# MDEV-15165 InnoDB purge for index on virtual column
160+
# is trying to access an incomplete record
161+
#
162+
CREATE TABLE t1(
163+
u INT PRIMARY KEY, b BLOB, ug INT GENERATED ALWAYS AS (u) VIRTUAL,
164+
INDEX bug(b(100),ug)
165+
) ENGINE=InnoDB;
166+
INSERT INTO t1 (u,b) VALUES(1,REPEAT('a',16384));
167+
connection prevent_purge;
168+
start transaction with consistent snapshot;
169+
connection default;
170+
DELETE FROM t1;
171+
SET DEBUG_SYNC='blob_write_middle SIGNAL halfway WAIT_FOR purged';
172+
INSERT INTO t1 (u,b) VALUES(1,REPEAT('a',16384));
173+
connection prevent_purge;
174+
SET DEBUG_SYNC='now WAIT_FOR halfway';
175+
COMMIT;
176+
InnoDB 0 transactions not purged
177+
SET DEBUG_SYNC='now SIGNAL purged';
178+
disconnect prevent_purge;
179+
connection default;
180+
DROP TABLE t1;
157181
set debug_sync=reset;
158182
SET GLOBAL innodb_purge_rseg_truncate_frequency = @saved_frequency;

mysql-test/suite/gcol/t/innodb_virtual_debug_purge.test

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,8 @@ connection default;
184184
update t set a = repeat('m', 16000) where a like "aaa%";
185185
connect(lock_table, localhost, root);
186186
lock table t write;
187-
disconnect prevent_purge;
187+
connection prevent_purge;
188+
commit;
188189
connection default;
189190
--source ../../innodb/include/wait_all_purged.inc
190191
disconnect lock_table;
@@ -194,6 +195,32 @@ commit;
194195
set global debug_dbug=@old_dbug;
195196
drop table t;
196197

198+
--echo #
199+
--echo # MDEV-15165 InnoDB purge for index on virtual column
200+
--echo # is trying to access an incomplete record
201+
--echo #
202+
CREATE TABLE t1(
203+
u INT PRIMARY KEY, b BLOB, ug INT GENERATED ALWAYS AS (u) VIRTUAL,
204+
INDEX bug(b(100),ug)
205+
) ENGINE=InnoDB;
206+
INSERT INTO t1 (u,b) VALUES(1,REPEAT('a',16384));
207+
connection prevent_purge;
208+
start transaction with consistent snapshot;
209+
connection default;
210+
DELETE FROM t1;
211+
SET DEBUG_SYNC='blob_write_middle SIGNAL halfway WAIT_FOR purged';
212+
send INSERT INTO t1 (u,b) VALUES(1,REPEAT('a',16384));
213+
connection prevent_purge;
214+
SET DEBUG_SYNC='now WAIT_FOR halfway';
215+
COMMIT;
216+
--source ../../innodb/include/wait_all_purged.inc
217+
SET DEBUG_SYNC='now SIGNAL purged';
218+
disconnect prevent_purge;
219+
220+
connection default;
221+
reap;
222+
DROP TABLE t1;
223+
197224
--source include/wait_until_count_sessions.inc
198225
set debug_sync=reset;
199226
SET GLOBAL innodb_purge_rseg_truncate_frequency = @saved_frequency;

storage/innobase/row/row0vers.cc

Lines changed: 84 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*****************************************************************************
22
3-
Copyright (c) 1997, 2016, Oracle and/or its affiliates. All Rights Reserved.
3+
Copyright (c) 1997, 2017, Oracle and/or its affiliates. All Rights Reserved.
44
Copyright (c) 2017, 2018, MariaDB Corporation.
55
66
This program is free software; you can redistribute it and/or modify it under
@@ -45,24 +45,31 @@ Created 2/6/1997 Heikki Tuuri
4545
#include "lock0lock.h"
4646
#include "row0mysql.h"
4747

48-
/** Check whether all non-virtual columns in a virtual index match that of in
49-
the cluster index
50-
@param[in] index the secondary index
51-
@param[in] row the cluster index row in dtuple form
52-
@param[in] ext externally stored column prefix or NULL
53-
@param[in] ientry the secondary index entry
54-
@param[in,out] heap heap used to build virtual dtuple
55-
@param[in,out] n_non_v_col number of non-virtual columns in the index
56-
@return true if all matches, false otherwise */
48+
/** Check whether all non-virtual index fields are equal.
49+
@param[in] index the secondary index
50+
@param[in] a first index entry to compare
51+
@param[in] b second index entry to compare
52+
@return whether all non-virtual fields are equal */
5753
static
5854
bool
59-
row_vers_non_vc_match(
60-
dict_index_t* index,
61-
const dtuple_t* row,
62-
const row_ext_t* ext,
63-
const dtuple_t* ientry,
64-
mem_heap_t* heap,
65-
ulint* n_non_v_col);
55+
row_vers_non_virtual_fields_equal(
56+
const dict_index_t* index,
57+
const dfield_t* a,
58+
const dfield_t* b)
59+
{
60+
const dict_field_t* end = &index->fields[index->n_fields];
61+
62+
for (const dict_field_t* ifield = index->fields; ifield != end;
63+
ifield++) {
64+
if (!dict_col_is_virtual(ifield->col)
65+
&& cmp_dfield_dfield(a++, b++)) {
66+
return false;
67+
}
68+
}
69+
70+
return true;
71+
}
72+
6673
/** Determine if an active transaction has inserted or modified a secondary
6774
index record.
6875
@param[in] clust_rec clustered index record
@@ -233,15 +240,29 @@ row_vers_impl_x_locked_low(
233240
}
234241

235242
if (!cur_vrow) {
236-
ulint n_non_v_col = 0;
243+
/* Build index entry out of row */
244+
entry = row_build_index_entry(row, ext, index,
245+
heap);
246+
247+
/* entry could only be NULL (the
248+
clustered index record could contain
249+
BLOB pointers that are NULL) if we
250+
were accessing a freshly inserted
251+
record before it was fully inserted.
252+
prev_version cannot possibly be such
253+
an incomplete record, because its
254+
transaction would have to be committed
255+
in order for later versions of the
256+
record to be able to exist. */
257+
ut_ad(entry);
237258

238259
/* If the indexed virtual columns has changed,
239260
there must be log record to generate vrow.
240261
Otherwise, it is not changed, so no need
241262
to compare */
242-
if (row_vers_non_vc_match(
243-
index, row, ext, ientry, heap,
244-
&n_non_v_col) == 0) {
263+
if (!row_vers_non_virtual_fields_equal(
264+
index,
265+
ientry->fields, entry->fields)) {
245266
if (rec_del != vers_del) {
246267
break;
247268
}
@@ -411,61 +432,6 @@ row_vers_must_preserve_del_marked(
411432
return(!purge_sys->view.changes_visible(trx_id, name));
412433
}
413434

414-
/** Check whether all non-virtual columns in a virtual index match that of in
415-
the cluster index
416-
@param[in] index the secondary index
417-
@param[in] row the cluster index row in dtuple form
418-
@param[in] ext externally stored column prefix or NULL
419-
@param[in] ientry the secondary index entry
420-
@param[in,out] heap heap used to build virtual dtuple
421-
@param[in,out] n_non_v_col number of non-virtual columns in the index
422-
@return true if all matches, false otherwise */
423-
static
424-
bool
425-
row_vers_non_vc_match(
426-
dict_index_t* index,
427-
const dtuple_t* row,
428-
const row_ext_t* ext,
429-
const dtuple_t* ientry,
430-
mem_heap_t* heap,
431-
ulint* n_non_v_col)
432-
{
433-
const dfield_t* field1;
434-
dfield_t* field2;
435-
ulint n_fields = dtuple_get_n_fields(ientry);
436-
ulint ret = true;
437-
438-
*n_non_v_col = 0;
439-
440-
/* Build index entry out of row */
441-
dtuple_t* nentry = row_build_index_entry(row, ext, index, heap);
442-
443-
for (ulint i = 0; i < n_fields; i++) {
444-
const dict_field_t* ind_field = dict_index_get_nth_field(
445-
index, i);
446-
447-
const dict_col_t* col = ind_field->col;
448-
449-
/* Only check non-virtual columns */
450-
if (dict_col_is_virtual(col)) {
451-
continue;
452-
}
453-
454-
if (ret) {
455-
field1 = dtuple_get_nth_field(ientry, i);
456-
field2 = dtuple_get_nth_field(nentry, i);
457-
458-
if (cmp_dfield_dfield(field1, field2) != 0) {
459-
ret = false;
460-
}
461-
}
462-
463-
(*n_non_v_col)++;
464-
}
465-
466-
return(ret);
467-
}
468-
469435
/** build virtual column value from current cluster index record data
470436
@param[in,out] row the cluster index row in dtuple form
471437
@param[in] clust_index clustered index
@@ -616,8 +582,7 @@ that of current cluster index record, which is recreated from information
616582
stored in undo log
617583
@param[in] in_purge called by purge thread
618584
@param[in] rec record in the clustered index
619-
@param[in] row the cluster index row in dtuple form
620-
@param[in] ext externally stored column prefix or NULL
585+
@param[in] icentry the index entry built from a cluster row
621586
@param[in] clust_index cluster index
622587
@param[in] clust_offsets offsets on the cluster record
623588
@param[in] index the secondary index
@@ -633,8 +598,7 @@ bool
633598
row_vers_vc_matches_cluster(
634599
bool in_purge,
635600
const rec_t* rec,
636-
const dtuple_t* row,
637-
row_ext_t* ext,
601+
const dtuple_t* icentry,
638602
dict_index_t* clust_index,
639603
ulint* clust_offsets,
640604
dict_index_t* index,
@@ -659,15 +623,27 @@ row_vers_vc_matches_cluster(
659623
dfield_t* field2;
660624
ulint i;
661625

662-
tuple_heap = mem_heap_create(1024);
663-
664626
/* First compare non-virtual columns (primary keys) */
665-
if (!row_vers_non_vc_match(index, row, ext, ientry, tuple_heap,
666-
&n_non_v_col)) {
667-
mem_heap_free(tuple_heap);
668-
return(false);
627+
ut_ad(index->n_fields == n_fields);
628+
ut_ad(n_fields == dtuple_get_n_fields(icentry));
629+
{
630+
const dfield_t* a = ientry->fields;
631+
const dfield_t* b = icentry->fields;
632+
633+
for (const dict_field_t *ifield = index->fields,
634+
*const end = &index->fields[index->n_fields];
635+
ifield != end; ifield++, a++, b++) {
636+
if (!dict_col_is_virtual(ifield->col)) {
637+
if (cmp_dfield_dfield(a, b)) {
638+
return false;
639+
}
640+
n_non_v_col++;
641+
}
642+
}
669643
}
670644

645+
tuple_heap = mem_heap_create(1024);
646+
671647
ut_ad(n_fields > n_non_v_col);
672648

673649
*vrow = dtuple_create_with_vcol(v_heap ? v_heap : tuple_heap, 0, num_v);
@@ -936,27 +912,28 @@ row_vers_old_has_index_entry(
936912
entry = row_build_index_entry(
937913
row, ext, index, heap);
938914
if (entry && !dtuple_coll_cmp(ientry, entry)) {
939-
940-
mem_heap_free(heap);
941-
942-
if (v_heap) {
943-
mem_heap_free(v_heap);
944-
}
945-
946-
return(TRUE);
915+
goto safe_to_purge;
947916
}
948917
} else {
949-
if (row_vers_vc_matches_cluster(
950-
also_curr, rec, row, ext, clust_index,
951-
clust_offsets, index, ientry, roll_ptr,
952-
trx_id, NULL, &vrow, mtr)) {
953-
mem_heap_free(heap);
954-
955-
if (v_heap) {
956-
mem_heap_free(v_heap);
957-
}
958-
959-
return(TRUE);
918+
/* Build index entry out of row */
919+
entry = row_build_index_entry(row, ext, index, heap);
920+
/* entry could only be NULL if
921+
the clustered index record is an uncommitted
922+
inserted record whose BLOBs have not been
923+
written yet. The secondary index record
924+
can be safely removed, because it cannot
925+
possibly refer to this incomplete
926+
clustered index record. (Insert would
927+
always first be completed for the
928+
clustered index record, then proceed to
929+
secondary indexes.) */
930+
931+
if (entry && row_vers_vc_matches_cluster(
932+
also_curr, rec, entry,
933+
clust_index, clust_offsets,
934+
index, ientry, roll_ptr,
935+
trx_id, NULL, &vrow, mtr)) {
936+
goto safe_to_purge;
960937
}
961938
}
962939
clust_offsets = rec_get_offsets(rec, clust_index, NULL,
@@ -989,7 +966,7 @@ row_vers_old_has_index_entry(
989966
a different binary value in a char field, but the
990967
collation identifies the old and new value anyway! */
991968
if (entry && !dtuple_coll_cmp(ientry, entry)) {
992-
969+
safe_to_purge:
993970
mem_heap_free(heap);
994971

995972
if (v_heap) {
@@ -1086,13 +1063,7 @@ row_vers_old_has_index_entry(
10861063
and new value anyway! */
10871064

10881065
if (entry && !dtuple_coll_cmp(ientry, entry)) {
1089-
1090-
mem_heap_free(heap);
1091-
if (v_heap) {
1092-
mem_heap_free(v_heap);
1093-
}
1094-
1095-
return(TRUE);
1066+
goto safe_to_purge;
10961067
}
10971068
}
10981069

0 commit comments

Comments
 (0)