Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions mysql-test/suite/heap/blob_update_key_rollback.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#
# Index consistency after a failed UPDATE on a HEAP table with BLOB
#
# heap_update() moves changed key entries to the new key values
# before writing the new blob chains. When a blob chain write then
# fails with "table is full", the key entries must be moved back to
# the old values, otherwise the index refers to records whose
# columns no longer match the indexed values.
#
SET @save_max_heap= @@max_heap_table_size;
SET max_heap_table_size = 16384;
#
# Hash index: single-row UPDATE fails writing the new blob chain
#
CREATE TABLE t1 (
id INT,
k VARCHAR(20),
b BLOB,
KEY k1 (k)
) ENGINE=HEAP;
INSERT INTO t1 SELECT seq, CONCAT('key', seq), REPEAT('a', 300)
FROM seq_1_to_10;
UPDATE t1 SET k= 'moved', b= REPEAT('z', 60000) WHERE id = 1;
ERROR HY000: The table 't1' is full
CHECK TABLE t1;
Table Op Msg_type Msg_text
test.t1 check status OK
# The row must still be found through the index under the old value
SELECT id, k, LENGTH(b) FROM t1 FORCE INDEX(k1) WHERE k = 'key1';
id k LENGTH(b)
1 key1 300
SELECT COUNT(*) FROM t1 FORCE INDEX(k1) WHERE k = 'moved';
COUNT(*)
0
DROP TABLE t1;
#
# BTREE index: same failure, rb-tree entries must be restored
#
CREATE TABLE t1 (
id INT,
k VARCHAR(20),
b BLOB,
KEY k1 (k) USING BTREE
) ENGINE=HEAP;
INSERT INTO t1 SELECT seq, CONCAT('key', seq), REPEAT('a', 300)
FROM seq_1_to_10;
UPDATE t1 SET k= 'moved', b= REPEAT('z', 60000) WHERE id = 1;
ERROR HY000: The table 't1' is full
CHECK TABLE t1;
Table Op Msg_type Msg_text
test.t1 check status OK
SELECT id, k, LENGTH(b) FROM t1 FORCE INDEX(k1) WHERE k = 'key1';
id k LENGTH(b)
1 key1 300
SELECT COUNT(*) FROM t1 FORCE INDEX(k1) WHERE k = 'moved';
COUNT(*)
0
DROP TABLE t1;
#
# Two indexed columns changed by the same failing UPDATE:
# entries in both indexes must be rolled back
#
CREATE TABLE t1 (
id INT,
k VARCHAR(20),
m VARCHAR(20),
b BLOB,
KEY k1 (k),
KEY k2 (m) USING BTREE
) ENGINE=HEAP;
INSERT INTO t1 SELECT seq, CONCAT('key', seq), CONCAT('mem', seq),
REPEAT('a', 300)
FROM seq_1_to_10;
UPDATE t1 SET k= 'moved', m= 'gone', b= REPEAT('z', 60000) WHERE id = 2;
ERROR HY000: The table 't1' is full
CHECK TABLE t1;
Table Op Msg_type Msg_text
test.t1 check status OK
SELECT id, k, m FROM t1 FORCE INDEX(k1) WHERE k = 'key2';
id k m
2 key2 mem2
SELECT id, k, m FROM t1 FORCE INDEX(k2) WHERE m = 'mem2';
id k m
2 key2 mem2
SELECT COUNT(*) FROM t1 FORCE INDEX(k1) WHERE k = 'moved';
COUNT(*)
0
SELECT COUNT(*) FROM t1 FORCE INDEX(k2) WHERE m = 'gone';
COUNT(*)
0
DROP TABLE t1;
#
# A second index on a column the UPDATE does NOT touch must be left
# alone by the rollback: only the changed key is moved back, the
# unchanged key's entries stay put and keep finding the row.
#
CREATE TABLE t1 (
id INT,
k VARCHAR(20),
fixed VARCHAR(20),
b BLOB,
KEY k1 (k),
KEY k2 (fixed)
) ENGINE=HEAP;
INSERT INTO t1 SELECT seq, CONCAT('key', seq), CONCAT('fix', seq),
REPEAT('a', 300)
FROM seq_1_to_10;
# Only k and b change; 'fixed' is untouched
UPDATE t1 SET k= 'moved', b= REPEAT('z', 60000) WHERE id = 3;
ERROR HY000: The table 't1' is full
CHECK TABLE t1;
Table Op Msg_type Msg_text
test.t1 check status OK
# Changed key rolled back to old value
SELECT id, k, fixed FROM t1 FORCE INDEX(k1) WHERE k = 'key3';
id k fixed
3 key3 fix3
SELECT COUNT(*) FROM t1 FORCE INDEX(k1) WHERE k = 'moved';
COUNT(*)
0
# Untouched key still resolves the row through its own index
SELECT id, k, fixed FROM t1 FORCE INDEX(k2) WHERE fixed = 'fix3';
id k fixed
3 key3 fix3
DROP TABLE t1;
#
# Multi-row UPDATE that fails part-way: rows updated before the
# failure keep their new key values, the failing row keeps its old
# ones, and every row must be reachable through the index by its
# current key value
#
CREATE TABLE t1 (
id INT,
k VARCHAR(20),
b BLOB,
KEY k1 (k)
) ENGINE=HEAP;
INSERT INTO t1 SELECT seq, CONCAT('key', seq), REPEAT('a', 300)
FROM seq_1_to_15;
# Each row grows its blob; the table fills up after some rows
UPDATE t1 SET k= CONCAT('new', id), b= REPEAT('z', 1500);
ERROR HY000: The table 't1' is full
CHECK TABLE t1;
Table Op Msg_type Msg_text
test.t1 check status OK
# Every row must be found via the index under its current key value
SELECT t.id FROM t1 t
WHERE NOT EXISTS (SELECT 1 FROM t1 i FORCE INDEX(k1)
WHERE i.k = t.k AND i.id = t.id)
ORDER BY t.id;
id
# Old and new key values must partition the table, with the
# failure hitting neither the first row nor after the last one.
# The exact fill point depends on platform record layout, so only
# the invariants are checked.
SELECT COUNT(*) AS total,
COUNT(IF(k LIKE 'new%', 1, NULL)) > 0 AS some_updated,
COUNT(IF(k LIKE 'key%', 1, NULL)) > 0 AS some_old,
COUNT(IF(k LIKE 'new%', 1, NULL)) +
COUNT(IF(k LIKE 'key%', 1, NULL)) = COUNT(*) AS partitioned
FROM t1;
total some_updated some_old partitioned
15 1 1 1
DROP TABLE t1;
SET max_heap_table_size = @save_max_heap;
159 changes: 159 additions & 0 deletions mysql-test/suite/heap/blob_update_key_rollback.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
--source include/have_sequence.inc

--echo #
--echo # Index consistency after a failed UPDATE on a HEAP table with BLOB
--echo #
--echo # heap_update() moves changed key entries to the new key values
--echo # before writing the new blob chains. When a blob chain write then
--echo # fails with "table is full", the key entries must be moved back to
--echo # the old values, otherwise the index refers to records whose
--echo # columns no longer match the indexed values.
--echo #

SET @save_max_heap= @@max_heap_table_size;
SET max_heap_table_size = 16384;

--echo #
--echo # Hash index: single-row UPDATE fails writing the new blob chain
--echo #

CREATE TABLE t1 (
id INT,
k VARCHAR(20),
b BLOB,
KEY k1 (k)
) ENGINE=HEAP;

INSERT INTO t1 SELECT seq, CONCAT('key', seq), REPEAT('a', 300)
FROM seq_1_to_10;

--error ER_RECORD_FILE_FULL
UPDATE t1 SET k= 'moved', b= REPEAT('z', 60000) WHERE id = 1;

CHECK TABLE t1;
--echo # The row must still be found through the index under the old value
SELECT id, k, LENGTH(b) FROM t1 FORCE INDEX(k1) WHERE k = 'key1';
SELECT COUNT(*) FROM t1 FORCE INDEX(k1) WHERE k = 'moved';
DROP TABLE t1;

--echo #
--echo # BTREE index: same failure, rb-tree entries must be restored
--echo #

CREATE TABLE t1 (
id INT,
k VARCHAR(20),
b BLOB,
KEY k1 (k) USING BTREE
) ENGINE=HEAP;

INSERT INTO t1 SELECT seq, CONCAT('key', seq), REPEAT('a', 300)
FROM seq_1_to_10;

--error ER_RECORD_FILE_FULL
UPDATE t1 SET k= 'moved', b= REPEAT('z', 60000) WHERE id = 1;

CHECK TABLE t1;
SELECT id, k, LENGTH(b) FROM t1 FORCE INDEX(k1) WHERE k = 'key1';
SELECT COUNT(*) FROM t1 FORCE INDEX(k1) WHERE k = 'moved';
DROP TABLE t1;

--echo #
--echo # Two indexed columns changed by the same failing UPDATE:
--echo # entries in both indexes must be rolled back
--echo #

CREATE TABLE t1 (
id INT,
k VARCHAR(20),
m VARCHAR(20),
b BLOB,
KEY k1 (k),
KEY k2 (m) USING BTREE
) ENGINE=HEAP;

INSERT INTO t1 SELECT seq, CONCAT('key', seq), CONCAT('mem', seq),
REPEAT('a', 300)
FROM seq_1_to_10;

--error ER_RECORD_FILE_FULL
UPDATE t1 SET k= 'moved', m= 'gone', b= REPEAT('z', 60000) WHERE id = 2;

CHECK TABLE t1;
SELECT id, k, m FROM t1 FORCE INDEX(k1) WHERE k = 'key2';
SELECT id, k, m FROM t1 FORCE INDEX(k2) WHERE m = 'mem2';
SELECT COUNT(*) FROM t1 FORCE INDEX(k1) WHERE k = 'moved';
SELECT COUNT(*) FROM t1 FORCE INDEX(k2) WHERE m = 'gone';
DROP TABLE t1;

--echo #
--echo # A second index on a column the UPDATE does NOT touch must be left
--echo # alone by the rollback: only the changed key is moved back, the
--echo # unchanged key's entries stay put and keep finding the row.
--echo #

CREATE TABLE t1 (
id INT,
k VARCHAR(20),
fixed VARCHAR(20),
b BLOB,
KEY k1 (k),
KEY k2 (fixed)
) ENGINE=HEAP;

INSERT INTO t1 SELECT seq, CONCAT('key', seq), CONCAT('fix', seq),
REPEAT('a', 300)
FROM seq_1_to_10;

--echo # Only k and b change; 'fixed' is untouched
--error ER_RECORD_FILE_FULL
UPDATE t1 SET k= 'moved', b= REPEAT('z', 60000) WHERE id = 3;

CHECK TABLE t1;
--echo # Changed key rolled back to old value
SELECT id, k, fixed FROM t1 FORCE INDEX(k1) WHERE k = 'key3';
SELECT COUNT(*) FROM t1 FORCE INDEX(k1) WHERE k = 'moved';
--echo # Untouched key still resolves the row through its own index
SELECT id, k, fixed FROM t1 FORCE INDEX(k2) WHERE fixed = 'fix3';
DROP TABLE t1;

--echo #
--echo # Multi-row UPDATE that fails part-way: rows updated before the
--echo # failure keep their new key values, the failing row keeps its old
--echo # ones, and every row must be reachable through the index by its
--echo # current key value
--echo #

CREATE TABLE t1 (
id INT,
k VARCHAR(20),
b BLOB,
KEY k1 (k)
) ENGINE=HEAP;

INSERT INTO t1 SELECT seq, CONCAT('key', seq), REPEAT('a', 300)
FROM seq_1_to_15;

--echo # Each row grows its blob; the table fills up after some rows
--error ER_RECORD_FILE_FULL
UPDATE t1 SET k= CONCAT('new', id), b= REPEAT('z', 1500);

CHECK TABLE t1;
--echo # Every row must be found via the index under its current key value
SELECT t.id FROM t1 t
WHERE NOT EXISTS (SELECT 1 FROM t1 i FORCE INDEX(k1)
WHERE i.k = t.k AND i.id = t.id)
ORDER BY t.id;
--echo # Old and new key values must partition the table, with the
--echo # failure hitting neither the first row nor after the last one.
--echo # The exact fill point depends on platform record layout, so only
--echo # the invariants are checked.
SELECT COUNT(*) AS total,
COUNT(IF(k LIKE 'new%', 1, NULL)) > 0 AS some_updated,
COUNT(IF(k LIKE 'key%', 1, NULL)) > 0 AS some_old,
COUNT(IF(k LIKE 'new%', 1, NULL)) +
COUNT(IF(k LIKE 'key%', 1, NULL)) = COUNT(*) AS partitioned
FROM t1;
DROP TABLE t1;

SET max_heap_table_size = @save_max_heap;
28 changes: 27 additions & 1 deletion storage/heap/hp_update.c
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new)
pos[share->visible]|= HP_ROW_HAS_CONT;
}
my_safe_afree(saved_chains, alloc_size);
goto err;
goto err_blob;
}
memcpy(pos + desc->offset + desc->packlength,
&first_run, sizeof(first_run));
Expand Down Expand Up @@ -252,6 +252,32 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new)
share->key_version++;
DBUG_RETURN(0);

err_blob:
{
/*
A blob chain write failed after the key loop above had already
moved all changed keys to the new values. The record at pos has
been restored to the old image; move every changed key back as
well, else the index refers to a record that no longer matches it.
Report the blob failure even if a rollback step fails, so the
caller never sees a misleading error from the recovery itself.
*/
int blob_errno= my_errno;
for (keydef= share->keydef; keydef < end; keydef++)
{
if (hp_rec_key_cmp(keydef, heap_new, old, NULL))
{
if ((*keydef->delete_key)(info, keydef, heap_new, pos, 0) ||
(*keydef->write_key)(info, keydef, old, pos))
break;
}
}
info->current_ptr= recovery_ptr;
info->current_hash_ptr= recovery_hash_ptr;
my_errno= blob_errno;
}
/* fall through: blob_errno is never HA_ERR_FOUND_DUPP_KEY */

err:
if (my_errno == HA_ERR_FOUND_DUPP_KEY)
{
Expand Down
Loading