Skip to content

Commit e56f6c5

Browse files
committed
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 6353a80 commit e56f6c5

File tree

12 files changed

+213
-65
lines changed

12 files changed

+213
-65
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) = TIMESTAMP'2038-01-19 03:14:07.999999';
84+
return convert_tz(sys_trx_end, @@time_zone, '+00:00') = TIMESTAMP'2038-01-19 03:14:07.999999';
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";

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -393,10 +393,7 @@ SET FOREIGN_KEY_CHECKS = ON;
393393
SET SESSION SQL_MODE= 'NO_BACKSLASH_ESCAPES';
394394
SET timestamp = 8;
395395
LOAD DATA INFILE 't1.data' REPLACE INTO TABLE t1;
396-
Warnings:
397-
Warning 1265 Data truncated for column 'f12' at row 2
398-
Warning 1265 Data truncated for column 'f12' at row 4
399-
Warning 1265 Data truncated for column 'f12' at row 7
396+
ERROR 23000: Cannot add or update a child row: a foreign key constraint fails (`test`.`t1`, CONSTRAINT `t1_ibfk_1` FOREIGN KEY (`f13`) REFERENCES `t2` (`f`) ON DELETE SET NULL)
400397
SET timestamp = 9;
401398
REPLACE INTO t2 SELECT * FROM t2;
402399
DROP TABLE t1, t2;

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,3 @@ create or replace table t1 (pk int primary key) with system versioning;
7878
create trigger tr before insert on t1 for each row select 1 into @a;
7979
insert into t1 values (1),(2);
8080
drop table t1;
81-
create table t1 (pk int primary key, i int) with system versioning;
82-
replace into t1 values (1,10),(1,100),(1,1000);
83-
select pk,i,row_end > '2038-01-01' from t1 for system_time all;
84-
pk i row_end > '2038-01-01'
85-
1 1000 1
86-
drop table t1;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,15 +1283,15 @@ where table_name = 't1';
12831283
partition_name table_rows
12841284
p0 150
12851285
p1 60
1286-
pn 90
1286+
pn 91
12871287
rollback;
12881288
select partition_name, table_rows
12891289
from information_schema.partitions
12901290
where table_name = 't1';
12911291
partition_name table_rows
12921292
p0 0
12931293
p1 0
1294-
pn 90
1294+
pn 91
12951295
replace into t1 select seq from seq_1_to_10;
12961296
select partition_name, table_rows
12971297
from information_schema.partitions
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
@@ -95,13 +95,57 @@ drop table t1;
9595
# error (ER_DUP_ENTRY)
9696
#
9797
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,
98+
pk int KEY_TYPE, i int,
99+
row_start SYS_DATATYPE as row start invisible,
100+
row_end SYS_DATATYPE as row end invisible,
101101
period for system_time(row_start, row_end)
102-
) engine=InnoDB with system versioning;
102+
) with system versioning;
103103
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
104+
select pk, i, check_row(row_start, row_end) from t1 for system_time all;
105+
pk i check_row(row_start, row_end)
106+
1 10 ERROR: row_end == row_start
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/foreign.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ SET FOREIGN_KEY_CHECKS = ON;
419419

420420
SET SESSION SQL_MODE= 'NO_BACKSLASH_ESCAPES';
421421
SET timestamp = 8;
422+
--error ER_NO_REFERENCED_ROW_2
422423
LOAD DATA INFILE 't1.data' REPLACE INTO TABLE t1;
423424
SET timestamp = 9;
424425
REPLACE INTO t2 SELECT * FROM t2;

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

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,3 @@ create or replace table t1 (pk int primary key) with system versioning;
7878
create trigger tr before insert on t1 for each row select 1 into @a;
7979
insert into t1 values (1),(2);
8080
drop table t1;
81-
82-
#
83-
# MDEV-14794 Limitations which the row end as a part of PK imposes due to CURRENT_TIMESTAMP behavior and otherwise
84-
#
85-
create table t1 (pk int primary key, i int) with system versioning;
86-
replace into t1 values (1,10),(1,100),(1,1000);
87-
select pk,i,row_end > '2038-01-01' from t1 for system_time all;
88-
drop table t1;

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

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ replace into t1 values (1),(2);
7676
replace into t1 values (1),(2);
7777

7878
--connection con1
79-
--error ER_DUP_ENTRY
8079
replace into t1 values (1),(2);
8180
drop table t1;
8281

83-
drop table t1;
84-
82+
--echo #
83+
--echo # MDEV-14794 Limitations which the row end as a part of PK imposes due to
84+
--echo # CURRENT_TIMESTAMP behavior and otherwise
8585
--echo #
8686
--echo # MDEV-15330 Server crash or assertion `table->insert_values' failure in write_record upon LOAD DATA
8787
--echo #
@@ -122,15 +122,56 @@ drop table t1;
122122
--echo # error (ER_DUP_ENTRY)
123123
--echo #
124124

125-
create or replace table t1 (
126-
pk int primary key, i int,
127-
row_start bigint unsigned as row start,
128-
row_end bigint unsigned as row end,
125+
--replace_result $sys_datatype_expl SYS_DATATYPE "$KEY_TYPE" KEY_TYPE
126+
eval create or replace table t1 (
127+
pk int $KEY_TYPE, i int,
128+
row_start $sys_datatype_expl as row start invisible,
129+
row_end $sys_datatype_expl as row end invisible,
129130
period for system_time(row_start, row_end)
130-
) engine=InnoDB with system versioning;
131+
) with system versioning;
131132
replace into t1 (pk,i) values (1,10),(1,100),(1,1000);
132-
select pk, i, row_end from t1 for system_time all;
133+
select pk, i, check_row(row_start, row_end) from t1 for system_time all;
133134
drop table t1;
134135

136+
--replace_result $sys_datatype_expl SYS_DATATYPE "$KEY_TYPE" KEY_TYPE
137+
eval create or replace table t1 (
138+
pk int $KEY_TYPE, i int,
139+
row_start $sys_datatype_expl as row start invisible,
140+
row_end $sys_datatype_expl as row end invisible,
141+
period for system_time(row_start, row_end)
142+
) with system versioning;
143+
set timestamp= (select unix_timestamp());
144+
begin;
145+
insert t1(pk, i) values(1,3);
146+
delete from t1;
147+
insert t1(pk, i) values(1,30);
148+
delete from t1;
149+
insert t1(pk, i) values(1,300);
150+
commit;
151+
select pk, i, check_row(row_start, row_end) from t1 for system_time all;
152+
drop table t1;
153+
154+
--echo # In this case there'll be no unique constraint on a historical row.
155+
--replace_result $sys_datatype_expl SYS_DATATYPE
156+
eval create or replace table t1 (
157+
pk int, i int,
158+
row_start $sys_datatype_expl as row start invisible,
159+
row_end $sys_datatype_expl as row end invisible,
160+
period for system_time(row_start, row_end)
161+
) with system versioning;
162+
set timestamp= (select unix_timestamp());
163+
begin;
164+
insert t1(pk, i) values(1,3);
165+
delete from t1;
166+
insert t1(pk, i) values(1,30);
167+
delete from t1;
168+
insert t1(pk, i) values(1,300);
169+
commit;
170+
select pk, i, check_row(row_start, row_end) from t1 for system_time all;
171+
drop table t1;
172+
173+
--echo #
174+
--echo # End of 10.5 tests
175+
--echo #
135176

136177
--source suite/versioning/common_finish.inc

sql/sql_delete.cc

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -285,26 +285,80 @@ int update_portion_of_time(THD *thd, TABLE *table,
285285
return res;
286286
}
287287

288-
inline
289-
int TABLE::delete_row()
288+
/**
289+
Delete a record stored in:
290+
replace= true: record[0]
291+
replace= false: record[1]
292+
293+
with regard to the treat_versioned flag, which can be false for a versioned
294+
table in case of versioned->versioned replication.
295+
296+
For a versioned case, we detect a few conditions, under which we should delete
297+
a row instead of updating it to a history row.
298+
This includes:
299+
* History deletion by user;
300+
* History collision, in case of REPLACE or very fast sequence of dmls
301+
so that timestamp doesn't change;
302+
* History collision in the parent table
303+
304+
A normal delete is processed here as well.
305+
*/
306+
template <bool replace>
307+
int TABLE::delete_row(bool treat_versioned)
290308
{
291-
if (!versioned(VERS_TIMESTAMP) ||
292-
!vers_end_field()->is_max())
293-
return file->ha_delete_row(record[0]);
309+
int err= 0;
310+
uchar *del_buf= record[replace ? 1 : 0];
311+
bool delete_row= !treat_versioned
312+
|| in_use->lex->vers_conditions.delete_history
313+
|| versioned(VERS_TRX_ID)
314+
|| !vers_end_field()->is_max(
315+
vers_end_field()->ptr_in_record(del_buf));
316+
if (!delete_row)
317+
{
318+
if (replace)
319+
{
320+
store_record(this, record[2]);
321+
restore_record(this, record[1]);
322+
}
323+
else
324+
{
325+
store_record(this, record[1]);
326+
}
327+
vers_update_end();
328+
err= file->ha_update_row(record[1], record[0]);
329+
if (unlikely(err))
330+
{
331+
/*
332+
MDEV-23644: we get HA_ERR_FOREIGN_DUPLICATE_KEY iff we already got
333+
history row with same trx_id which is the result of foreign key
334+
action, so we don't need one more history row.
335+
336+
Additionally, delete the row if versioned record already exists.
337+
This happens on replace, a very fast sequence of inserts and deletes,
338+
or if timestamp is frozen.
339+
*/
340+
delete_row= err == HA_ERR_FOUND_DUPP_KEY
341+
|| err == HA_ERR_FOUND_DUPP_UNIQUE
342+
|| err == HA_ERR_FOREIGN_DUPLICATE_KEY;
343+
if (!delete_row)
344+
return err;
345+
346+
if (!replace)
347+
del_buf= record[1];
348+
}
349+
350+
if (replace)
351+
restore_record(this, record[2]);
352+
}
353+
354+
if (delete_row)
355+
err= file->ha_delete_row(del_buf);
294356

295-
store_record(this, record[1]);
296-
vers_update_end();
297-
int err= file->ha_update_row(record[1], record[0]);
298-
/*
299-
MDEV-23644: we get HA_ERR_FOREIGN_DUPLICATE_KEY iff we already got history
300-
row with same trx_id which is the result of foreign key action, so we
301-
don't need one more history row.
302-
*/
303-
if (err == HA_ERR_FOREIGN_DUPLICATE_KEY)
304-
return file->ha_delete_row(record[0]);
305357
return err;
306358
}
307359

360+
template int TABLE::delete_row<true>(bool treat_versioned);
361+
template int TABLE::delete_row<false>(bool treat_versioned);
308362

309363
/**
310364
Implement DELETE SQL word.

0 commit comments

Comments
 (0)