Skip to content

Commit 8001679

Browse files
FooBarriormidenok
authored andcommitted
MDEV-15990 handle timestamp-based collisions as well
Timestamp-versioned row deletion was exposed to a collisional problem: if current timestamp wasn't changed, then a sequence of row delete+insert could get a duplication error. A row delete would find another conflicting history row and return an error. This is true both for REPLACE and DELETE statements, however in REPLACE, the "optimized" path is usually taken, especially in the tests. There, delete+insert is substituted for a single versioned row update. In the end, both paths end up as ha_update_row + ha_write_row. The solution is to handle a history collision somehow. From the design perspective, the user shouldn't experience history rows loss, unless there's a technical limitation. To the contrary, trxid-based changes should never generate history for the same transaction, see MDEV-15427. If two operations on the same row happened too quickly, so that they happen at the same timestamp, the history row shouldn't be lost. We can still write a history row, though it'll have row_start == row_end. We cannot store more than one such historical row, as this will violate the unique constraint on row_end. So we will have to phisically delete the row if the history row is already available. In this commit: 1. Improve TABLE::delete_row to handle the history collision: if an update results with a duplicate error, delete a row for real. 2. use TABLE::delete_row in a non-optimistic path of REPLACE, where the system-versioned case now belongs entirely.
1 parent aeb2574 commit 8001679

File tree

9 files changed

+230
-51
lines changed

9 files changed

+230
-51
lines changed

mysql-test/suite/versioning/common.inc

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ if ($MTR_COMBINATION_TRX_ID)
6666
let $sys_datatype_expl= bigint(20) unsigned;
6767
let $sys_datatype_expl_uc= BIGINT(20) UNSIGNED;
6868
let $sys_datatype_max= 18446744073709551615;
69+
let $current_row= current_row;
70+
}
71+
if ($MTR_COMBINATION_TIMESTAMP)
72+
{
73+
let $current_row= current_row_ts;
6974
}
7075

7176
eval create or replace function current_row(sys_trx_end $sys_datatype_expl)
@@ -76,7 +81,7 @@ deterministic
7681
eval create or replace function current_row_ts(sys_trx_end timestamp(6))
7782
returns int
7883
deterministic
79-
return convert_tz(sys_trx_end, '+00:00', @@time_zone) = $sys_time_max;
84+
return convert_tz(sys_trx_end, @@time_zone, '+00:00') = $sys_time_max;
8085

8186
delimiter ~~;
8287
eval create or replace function check_row(row_start $sys_datatype_expl, row_end $sys_datatype_expl)
@@ -87,7 +92,7 @@ begin
8792
return "ERROR: row_end < row_start";
8893
elseif row_end = row_start then
8994
return "ERROR: row_end == row_start";
90-
elseif current_row(row_end) then
95+
elseif $current_row(row_end) then
9196
return "CURRENT ROW";
9297
end if;
9398
return "HISTORICAL ROW";
@@ -99,7 +104,7 @@ eval create or replace function check_row_slave(row_start $sys_datatype_expl, ro
99104
returns varchar(255)
100105
deterministic
101106
begin
102-
if current_row(row_end) then
107+
if $current_row(row_end) then
103108
return "CURRENT ROW";
104109
end if;
105110
return "HISTORICAL ROW";
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
--- replace.result
2+
+++ replace.reject
3+
@@ -75,7 +75,6 @@
4+
replace into t1 (pk,i) values (1,10),(1,100),(1,1000);
5+
select pk, i, check_row(row_start, row_end) from t1 for system_time all;
6+
pk i check_row(row_start, row_end)
7+
-1 10 ERROR: row_end == row_start
8+
1 1000 CURRENT ROW
9+
drop table t1;
10+
create or replace table t1 (
11+
@@ -94,7 +93,6 @@
12+
commit;
13+
select pk, i, check_row(row_start, row_end) from t1 for system_time all;
14+
pk i check_row(row_start, row_end)
15+
-1 3 ERROR: row_end == row_start
16+
1 300 CURRENT ROW
17+
drop table t1;
18+
# In this case there'll be no unique constraint on a historical row.
19+
@@ -114,8 +112,6 @@
20+
commit;
21+
select pk, i, check_row(row_start, row_end) from t1 for system_time all;
22+
pk i check_row(row_start, row_end)
23+
-1 3 ERROR: row_end == row_start
24+
-1 30 ERROR: row_end == row_start
25+
1 300 CURRENT ROW
26+
drop table t1;

mysql-test/suite/versioning/r/replace.result

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ connection default;
6363
replace into t1 values (1),(2);
6464
connection con1;
6565
replace into t1 values (1),(2);
66+
ERROR 23000: Duplicate entry '1' for key 'PRIMARY'
6667
drop table t1;
6768
#
6869
# MDEV-14794 Limitations which the row end as a part of PK imposes due to
@@ -95,13 +96,56 @@ drop table t1;
9596
# error (ER_DUP_ENTRY)
9697
#
9798
create or replace table t1 (
98-
pk int primary key, i int,
99-
row_start bigint unsigned as row start,
100-
row_end bigint unsigned as row end,
99+
pk int KEY_TYPE, i int,
100+
row_start SYS_DATATYPE as row start invisible,
101+
row_end SYS_DATATYPE as row end invisible,
101102
period for system_time(row_start, row_end)
102-
) engine=InnoDB with system versioning;
103+
) with system versioning;
103104
replace into t1 (pk,i) values (1,10),(1,100),(1,1000);
104-
select pk, i, row_end from t1 for system_time all;
105-
pk i row_end
106-
1 1000 18446744073709551615
105+
select pk, i, check_row(row_start, row_end) from t1 for system_time all;
106+
pk i check_row(row_start, row_end)
107+
1 1000 CURRENT ROW
108+
drop table t1;
109+
create or replace table t1 (
110+
pk int KEY_TYPE, i int,
111+
row_start SYS_DATATYPE as row start invisible,
112+
row_end SYS_DATATYPE as row end invisible,
113+
period for system_time(row_start, row_end)
114+
) with system versioning;
115+
set timestamp= (select unix_timestamp());
116+
begin;
117+
insert t1(pk, i) values(1,3);
118+
delete from t1;
119+
insert t1(pk, i) values(1,30);
120+
delete from t1;
121+
insert t1(pk, i) values(1,300);
122+
commit;
123+
select pk, i, check_row(row_start, row_end) from t1 for system_time all;
124+
pk i check_row(row_start, row_end)
125+
1 3 ERROR: row_end == row_start
126+
1 300 CURRENT ROW
107127
drop table t1;
128+
# In this case there'll be no unique constraint on a historical row.
129+
create or replace table t1 (
130+
pk int, i int,
131+
row_start SYS_DATATYPE as row start invisible,
132+
row_end SYS_DATATYPE as row end invisible,
133+
period for system_time(row_start, row_end)
134+
) with system versioning;
135+
set timestamp= (select unix_timestamp());
136+
begin;
137+
insert t1(pk, i) values(1,3);
138+
delete from t1;
139+
insert t1(pk, i) values(1,30);
140+
delete from t1;
141+
insert t1(pk, i) values(1,300);
142+
commit;
143+
select pk, i, check_row(row_start, row_end) from t1 for system_time all;
144+
pk i check_row(row_start, row_end)
145+
1 3 ERROR: row_end == row_start
146+
1 30 ERROR: row_end == row_start
147+
1 300 CURRENT ROW
148+
drop table t1;
149+
#
150+
# End of 10.5 tests
151+
#

mysql-test/suite/versioning/t/insert2.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,4 @@ select pk,i,row_end > '2038-01-01' from t1 for system_time all;
9191
drop table t1;
9292

9393
# Cleanup
94-
set @@time_zone=default;
94+
set @@time_zone=default;

mysql-test/suite/versioning/t/replace.test

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,11 @@ replace into t1 values (1),(2);
7878
--connection con1
7979
--error ER_DUP_ENTRY
8080
replace into t1 values (1),(2);
81-
8281
drop table t1;
8382

83+
--echo #
84+
--echo # MDEV-14794 Limitations which the row end as a part of PK imposes due to
85+
--echo # CURRENT_TIMESTAMP behavior and otherwise
8486
--echo #
8587
--echo # MDEV-15330 Server crash or assertion `table->insert_values' failure in write_record upon LOAD DATA
8688
--echo #
@@ -121,15 +123,56 @@ drop table t1;
121123
--echo # error (ER_DUP_ENTRY)
122124
--echo #
123125

124-
create or replace table t1 (
125-
pk int primary key, i int,
126-
row_start bigint unsigned as row start,
127-
row_end bigint unsigned as row end,
126+
--replace_result $sys_datatype_expl SYS_DATATYPE "$KEY_TYPE" KEY_TYPE
127+
eval create or replace table t1 (
128+
pk int $KEY_TYPE, i int,
129+
row_start $sys_datatype_expl as row start invisible,
130+
row_end $sys_datatype_expl as row end invisible,
128131
period for system_time(row_start, row_end)
129-
) engine=InnoDB with system versioning;
132+
) with system versioning;
130133
replace into t1 (pk,i) values (1,10),(1,100),(1,1000);
131-
select pk, i, row_end from t1 for system_time all;
134+
select pk, i, check_row(row_start, row_end) from t1 for system_time all;
135+
drop table t1;
136+
137+
--replace_result $sys_datatype_expl SYS_DATATYPE "$KEY_TYPE" KEY_TYPE
138+
eval create or replace table t1 (
139+
pk int $KEY_TYPE, i int,
140+
row_start $sys_datatype_expl as row start invisible,
141+
row_end $sys_datatype_expl as row end invisible,
142+
period for system_time(row_start, row_end)
143+
) with system versioning;
144+
set timestamp= (select unix_timestamp());
145+
begin;
146+
insert t1(pk, i) values(1,3);
147+
delete from t1;
148+
insert t1(pk, i) values(1,30);
149+
delete from t1;
150+
insert t1(pk, i) values(1,300);
151+
commit;
152+
select pk, i, check_row(row_start, row_end) from t1 for system_time all;
153+
drop table t1;
154+
155+
--echo # In this case there'll be no unique constraint on a historical row.
156+
--replace_result $sys_datatype_expl SYS_DATATYPE
157+
eval create or replace table t1 (
158+
pk int, i int,
159+
row_start $sys_datatype_expl as row start invisible,
160+
row_end $sys_datatype_expl as row end invisible,
161+
period for system_time(row_start, row_end)
162+
) with system versioning;
163+
set timestamp= (select unix_timestamp());
164+
begin;
165+
insert t1(pk, i) values(1,3);
166+
delete from t1;
167+
insert t1(pk, i) values(1,30);
168+
delete from t1;
169+
insert t1(pk, i) values(1,300);
170+
commit;
171+
select pk, i, check_row(row_start, row_end) from t1 for system_time all;
132172
drop table t1;
133173

174+
--echo #
175+
--echo # End of 10.5 tests
176+
--echo #
134177

135178
--source suite/versioning/common_finish.inc

sql/sql_delete.cc

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -289,33 +289,86 @@ int update_portion_of_time(THD *thd, TABLE *table,
289289
return res;
290290
}
291291

292-
inline
293-
int TABLE::delete_row()
292+
/**
293+
Delete a record stored in:
294+
replace= true: record[0]
295+
replace= false: record[1]
296+
297+
with regard to the treat_versioned flag, which can be false for a versioned
298+
table in case of versioned->versioned replication.
299+
300+
For a versioned case, we detect a few conditions, under which we should delete
301+
a row instead of updating it to a history row.
302+
This includes:
303+
* History deletion by user;
304+
* History collision, in case of REPLACE or very fast sequence of dmls
305+
so that timestamp doesn't change;
306+
* History collision in the parent table
307+
308+
A normal delete is processed here as well.
309+
*/
310+
template <bool replace>
311+
int TABLE::delete_row(bool treat_versioned)
294312
{
295-
if (!versioned(VERS_TIMESTAMP) ||
296-
!vers_end_field()->is_max())
297-
return file->ha_delete_row(record[0]);
313+
int err= 0;
314+
uchar *del_buf= record[replace ? 1 : 0];
315+
bool delete_row= !treat_versioned
316+
|| in_use->lex->vers_conditions.delete_history
317+
|| versioned(VERS_TRX_ID)
318+
|| !vers_end_field()->is_max(
319+
vers_end_field()->ptr_in_record(del_buf));
298320

299-
store_record(this, record[1]);
300-
vers_update_end();
301-
int err;
302321
if ((err= file->extra(HA_EXTRA_REMEMBER_POS)))
303322
return err;
304-
if ((err= file->ha_update_row(record[1], record[0])))
323+
324+
if (!delete_row)
305325
{
306-
/*
307-
MDEV-23644: we get HA_ERR_FOREIGN_DUPLICATE_KEY iff we already got
308-
history row with same trx_id which is the result of foreign key action,
309-
so we don't need one more history row.
310-
*/
311-
if (err == HA_ERR_FOREIGN_DUPLICATE_KEY)
312-
file->ha_delete_row(record[0]);
326+
if (replace)
327+
{
328+
store_record(this, record[2]);
329+
restore_record(this, record[1]);
330+
}
313331
else
314-
return err;
332+
{
333+
store_record(this, record[1]);
334+
}
335+
vers_update_end();
336+
err= file->ha_update_row(record[1], record[0]);
337+
if (unlikely(err))
338+
{
339+
/*
340+
MDEV-23644: we get HA_ERR_FOREIGN_DUPLICATE_KEY iff we already got
341+
history row with same trx_id which is the result of foreign key
342+
action, so we don't need one more history row.
343+
344+
Additionally, delete the row if versioned record already exists.
345+
This happens on replace, a very fast sequence of inserts and deletes,
346+
or if timestamp is frozen.
347+
*/
348+
delete_row= err == HA_ERR_FOUND_DUPP_KEY
349+
|| err == HA_ERR_FOUND_DUPP_UNIQUE
350+
|| err == HA_ERR_FOREIGN_DUPLICATE_KEY;
351+
if (!delete_row)
352+
return err;
353+
354+
if (!replace)
355+
del_buf= record[1];
356+
}
357+
358+
if (replace)
359+
restore_record(this, record[2]);
315360
}
316-
return file->extra(HA_EXTRA_RESTORE_POS);
361+
362+
if (delete_row)
363+
err= file->ha_delete_row(del_buf);
364+
365+
(void) file->extra(HA_EXTRA_RESTORE_POS);
366+
367+
return err;
317368
}
318369

370+
template int TABLE::delete_row<true>(bool treat_versioned);
371+
template int TABLE::delete_row<false>(bool treat_versioned);
319372

320373
/**
321374
@brief Special handling of single-table deletes after prepare phase

sql/sql_insert.cc

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2144,7 +2144,21 @@ int Write_record::replace_row(ha_rows *inserted, ha_rows *deleted)
21442144
error= table->file->ha_update_row(table->record[1], table->record[0]);
21452145

21462146
if (likely(!error))
2147+
{
21472148
++*deleted;
2149+
if (table->versioned() && table->vers_write)
2150+
{
2151+
if (versioned)
2152+
{
2153+
store_record(table, record[2]);
2154+
error= vers_insert_history_row(table);
2155+
restore_record(table, record[2]);
2156+
if (unlikely(error))
2157+
return on_ha_error(error);
2158+
}
2159+
++*inserted;
2160+
}
2161+
}
21482162
else if (error != HA_ERR_RECORD_IS_THE_SAME)
21492163
return on_ha_error(error);
21502164
break;
@@ -2160,16 +2174,8 @@ int Write_record::replace_row(ha_rows *inserted, ha_rows *deleted)
21602174
TRG_ACTION_BEFORE, true,
21612175
&trg_skip_row))
21622176
return restore_on_error();
2163-
if (!versioned)
2164-
error = table->file->ha_delete_row(table->record[1]);
2165-
else
2166-
{
2167-
store_record(table, record[2]);
2168-
restore_record(table, record[1]);
2169-
table->vers_update_end();
2170-
error = table->file->ha_update_row(table->record[1], table->record[0]);
2171-
restore_record(table, record[2]);
2172-
}
2177+
2178+
error= table->delete_row<true>(versioned);
21732179

21742180
if (unlikely(error))
21752181
return on_ha_error(error);

sql/sql_insert.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,11 @@ class Write_record
121121
{
122122
if (info->handle_duplicates == DUP_REPLACE)
123123
{
124-
bool has_delete_triggers= use_triggers &&
124+
const bool has_delete_triggers= use_triggers &&
125125
table->triggers->has_delete_triggers();
126-
bool referenced_by_fk= table->file->referenced_by_foreign_key();
127-
can_optimize= !referenced_by_fk && !has_delete_triggers
128-
&& !versioned && !table->versioned();
126+
const bool referenced_by_fk= table->file->referenced_by_foreign_key();
127+
can_optimize= !referenced_by_fk && !has_delete_triggers &&
128+
!table->versioned(VERS_TRX_ID);
129129
}
130130
}
131131
Write_record(THD *thd, TABLE *table, COPY_INFO *info,

sql/table.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1986,7 +1986,9 @@ struct TABLE
19861986
ha_rows *rows_inserted);
19871987
bool vers_check_update(List<Item> &items);
19881988
static bool check_period_overlaps(const KEY &key, const uchar *lhs, const uchar *rhs);
1989-
int delete_row();
1989+
inline int delete_row(){ return delete_row<false>(versioned(VERS_TIMESTAMP)); }
1990+
template <bool replace>
1991+
int delete_row(bool versioned);
19901992
/* Used in majority of DML (called from fill_record()) */
19911993
bool vers_update_fields();
19921994
/* Used in DELETE, DUP REPLACE and insert history row */

0 commit comments

Comments
 (0)