diff --git a/mysql-test/suite/heap/blob_update_key_rollback.result b/mysql-test/suite/heap/blob_update_key_rollback.result new file mode 100644 index 0000000000000..e47ede0edb1e0 --- /dev/null +++ b/mysql-test/suite/heap/blob_update_key_rollback.result @@ -0,0 +1,186 @@ +# +# 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; +# +# Table with no indexes at all: the failed blob write must leave the +# record intact with nothing to roll back in the (empty) key set +# +CREATE TABLE t1 ( +id INT, +b BLOB +) ENGINE=HEAP; +INSERT INTO t1 SELECT seq, REPEAT('a', 300) FROM seq_1_to_10; +UPDATE t1 SET 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, LENGTH(b) FROM t1 WHERE id = 1; +id LENGTH(b) +1 300 +SELECT COUNT(*) FROM t1; +COUNT(*) +10 +DROP TABLE t1; +SET max_heap_table_size = @save_max_heap; diff --git a/mysql-test/suite/heap/blob_update_key_rollback.test b/mysql-test/suite/heap/blob_update_key_rollback.test new file mode 100644 index 0000000000000..b2ba7136cdb9a --- /dev/null +++ b/mysql-test/suite/heap/blob_update_key_rollback.test @@ -0,0 +1,179 @@ +--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; + +--echo # +--echo # Table with no indexes at all: the failed blob write must leave the +--echo # record intact with nothing to roll back in the (empty) key set +--echo # + +CREATE TABLE t1 ( + id INT, + b BLOB +) ENGINE=HEAP; + +INSERT INTO t1 SELECT seq, REPEAT('a', 300) FROM seq_1_to_10; + +--error ER_RECORD_FILE_FULL +UPDATE t1 SET b= REPEAT('z', 60000) WHERE id = 1; + +CHECK TABLE t1; +SELECT id, LENGTH(b) FROM t1 WHERE id = 1; +SELECT COUNT(*) FROM t1; +DROP TABLE t1; + +SET max_heap_table_size = @save_max_heap; diff --git a/storage/heap/hp_update.c b/storage/heap/hp_update.c index f822c8daaf803..aa40524649497 100644 --- a/storage/heap/hp_update.c +++ b/storage/heap/hp_update.c @@ -20,7 +20,7 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new) { - HP_KEYDEF *keydef, *end, *p_lastinx; + HP_KEYDEF *keydef, *keydef_end, *p_lastinx; uchar *pos, *recovery_ptr; struct st_hp_hash_info *recovery_hash_ptr; my_bool auto_key_changed= 0, key_changed= 0; @@ -41,7 +41,9 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new) recovery_hash_ptr= info->current_hash_ptr; p_lastinx= share->keydef + info->lastinx; - for (keydef= share->keydef, end= keydef + share->keys; keydef < end; keydef++) + for (keydef= share->keydef, keydef_end= keydef + share->keys; + keydef < keydef_end; + keydef++) { if (hp_rec_key_cmp(keydef, heap_new, old, NULL)) { @@ -253,20 +255,39 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new) DBUG_RETURN(0); err: - if (my_errno == HA_ERR_FOUND_DUPP_KEY) + info->errkey= -1; /* If not a key error */ + if (my_errno == HA_ERR_FOUND_DUPP_KEY || my_errno == HA_ERR_OUT_OF_MEM || + my_errno == ENOMEM || my_errno == HA_ERR_RECORD_FILE_FULL) { - info->errkey = (int) (keydef - share->keydef); - if (keydef->algorithm == HA_KEY_ALG_BTREE) + int org_error= my_errno; + if (keydef == keydef_end) { - /* we don't need to delete non-inserted key from rb-tree */ - if ((*keydef->write_key)(info, keydef, old, pos)) + /* + We got a failure after all key parts have been written, like + while writing blobs. Alternatively there are no keys. + Decrement keydef so that it points at the last key part or + before share->keydef. + */ + DBUG_ASSERT(share->keydef); + keydef--; + } + else + { + /* keydef points to the last key that failed */ + info->errkey = (int) (keydef - share->keydef); + if (keydef->algorithm == HA_KEY_ALG_BTREE) { - if (++(share->records) == share->blength) - share->blength+= share->blength; - DBUG_RETURN(my_errno); + /* we don't need to delete non-inserted key from rb-tree */ + if ((*keydef->write_key)(info, keydef, old, pos)) + { + if (++(share->records) == share->blength) + share->blength+= share->blength; + DBUG_RETURN(my_errno); + } + keydef--; } - keydef--; } + /* Restore all modified keys */ while (keydef >= share->keydef) { if (hp_rec_key_cmp(keydef, heap_new, old, NULL)) @@ -279,6 +300,7 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new) } info->current_ptr= recovery_ptr; info->current_hash_ptr= recovery_hash_ptr; + my_errno= org_error; } if (++(share->records) == share->blength) share->blength+= share->blength;