mirror of
https://github.com/MariaDB/server.git
synced 2026-10-23 18:49:29 +03:00
MDEV-40448 HEAP unique hash duplicate rejection re-hashes the full key value
A rejected duplicate insert into a HEAP unique hash index paid the full-value key hash twice: `hp_write_key()` hashed the key to insert it (the documented contract was "the record was still added and the caller must call hp_delete_key for it"), and `hp_delete_key()` then hashed the same record **again** just to locate the bucket to unlink from. For long key values (BLOB keys, MDEV-38975) the second scan dominates duplicate-heavy workloads: `SELECT DISTINCT` over 20-50KB blob values (~80% duplicates) regressed 25-32% vs Aria tmp tables at low concurrency, with `my_uca_hash_sort_utf8mb4` doing 1.83x the hashing work for the identical query. The hash caching added by `de1765fb64d` (`HASH_INFO::hash_of_key`) already made all chain surgery hash-free; the two full-value hashes per rejected row were the entire cost. Fix: 1. **Probe before insert** (`hp_write_key()`): compute the hash once at the top; for `HA_NOSAME` keys without NULL key parts walk the key's chain under the pre-insert mask comparing cached `HASH_INFO::hash_of_key` values, comparing full key values only on a hash match. On a duplicate return `HA_ERR_FOUND_DUPP_KEY` with nothing modified; otherwise proceed with the linear-hash split and insertion reusing the already-computed hash. Records with equal full hashes share a bucket under any mask, so probing the pre-insert chain finds any duplicate. A successful insert costs exactly one full-value hash, as before; a rejected duplicate drops from two full hashes + insert + undo delete to one hash + compare, the same as a lookup. 2. **Error-path contract change**: `hp_write_key()` no longer inserts the key on a duplicate, so `heap_write()` rolls back only the preceding keys (unconditional `keydef--`, as for BTREE/ENOMEM), and `heap_update()`'s duplicate path now re-inserts the old key for the failing keydef for both algorithms (previously BTREE-only) before rolling back earlier keys. 3. **Delete-side hash reuse** (`hp_delete_key()`): when the row being deleted was positioned via the same hash index (`flag` set and `info->current_hash_ptr->ptr_to_rec == recpos`), take the hash from the index entry instead of re-scanning the key value; a `DBUG_ASSERT` cross-checks the cached hash in debug builds. This removes the remaining full-value hash from `DELETE`/`UPDATE` of rows located through the index (`heap_rkey()` already hashed the key). 4. **`heap_rfirst()`/`heap_rlast()`**: clear `info->current_hash_ptr` when rejecting a hash index with `HA_ERR_WRONG_COMMAND`. Both functions retarget `info->lastinx` before the algorithm check, so a stale `current_hash_ptr` from a previous search on a different key could otherwise satisfy the new cached-hash guard in `hp_delete_key()` and send the bucket lookup to the wrong chain (reachable through the heap API only; the SQL layer never issues ordered reads on hash indexes). New unit test `hp_test_write_dup-t` (105 assertions) wraps the key charset's `hash_sort` collation handler in a counting shim, asserting the exact number of full-value hashes for every operation: 1 per insert attempt (successful or rejected), 1 total for index-read + delete, 1 for an index-positioned re-key. Behavioral coverage: single- and multi-key rollback for INSERT and UPDATE duplicates, NULL key parts, non-unique keys, delete after rejected `heap_rfirst()`/`heap_rlast()`, and a 500-row duplicate-heavy stress across linear-hash splits (exactly 500 hashes; was 827 before the fix). Benchmarked (16 threads, 120s runs, 8c/16t AMD 7840HS; `base` = preview-13.1 without MDEV-38975 = Aria tmp tables, `new` = unfixed, `fix` = this patch): | test (QPS) | base | new | fix | |---------------|------|--------------|---------------------| | `blob_case_c` | 4.65 | 3.36 (-28%) | 5.61 (+21% vs base) | | `blob_mixed` | 5.87 | 4.13 (-30%) | 6.92 (+18% vs base) | `hp_delete_key()` (38% inclusive before) is absent from the fixed profile; the low-concurrency regression becomes a win on top of the existing 2x+ high-concurrency wins.
This commit is contained in:
@@ -33,8 +33,9 @@ IF(WITH_UNIT_TESTS)
|
||||
ADD_EXECUTABLE(hp_test2 hp_test2.c)
|
||||
TARGET_LINK_LIBRARIES(hp_test2 heap mysys dbug strings)
|
||||
MY_ADD_TESTS(hp_test_hash hp_test_scan hp_test_freelist hp_test_concurrent
|
||||
hp_test_blob_alias hp_test_update
|
||||
hp_test_blob_alias hp_test_update hp_test_write_dup
|
||||
LINK_LIBRARIES heap mysys dbug strings)
|
||||
|
||||
INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}/sql
|
||||
${CMAKE_SOURCE_DIR}/include)
|
||||
|
||||
|
||||
@@ -279,7 +279,23 @@ int hp_delete_key(HP_INFO *info, register HP_KEYDEF *keyinfo,
|
||||
last_ptr=0;
|
||||
|
||||
/* Search after record with key */
|
||||
rec_hash= hp_rec_hashnr(0, keyinfo, record);
|
||||
if (flag && info->current_hash_ptr &&
|
||||
info->current_hash_ptr->ptr_to_rec == recpos)
|
||||
{
|
||||
/*
|
||||
The row was positioned via this key: flag means keyinfo is the
|
||||
last used index, and current_hash_ptr is only ever set by
|
||||
hp_search()/hp_search_next() on that index (scan and rnd-pos
|
||||
paths clear it). Every HASH_INFO entry stores the hash of its
|
||||
own record's key, so the entry for recpos gives the hash of
|
||||
'record' without re-scanning the key value (expensive for blob
|
||||
keys).
|
||||
*/
|
||||
rec_hash= info->current_hash_ptr->hash_of_key;
|
||||
DBUG_ASSERT(rec_hash == hp_rec_hashnr(0, keyinfo, record));
|
||||
}
|
||||
else
|
||||
rec_hash= hp_rec_hashnr(0, keyinfo, record);
|
||||
key_pos= hp_mask(rec_hash, blength, share->records + 1);
|
||||
pos= hp_find_hash(&keyinfo->block, key_pos);
|
||||
|
||||
|
||||
@@ -65,7 +65,14 @@ int heap_rfirst(HP_INFO *info, uchar *record, int inx)
|
||||
}
|
||||
else
|
||||
{
|
||||
/* We can't scan a non existing key value with hash index */
|
||||
/*
|
||||
We can't scan a non existing key value with hash index.
|
||||
lastinx was already retargeted to this key above; clear
|
||||
current_hash_ptr so that it cannot be misread as belonging to
|
||||
this key (hp_delete_key() reuses its cached hash when flag is
|
||||
set and current_hash_ptr matches the record being deleted).
|
||||
*/
|
||||
info->current_hash_ptr= 0;
|
||||
my_errno= HA_ERR_WRONG_COMMAND;
|
||||
DBUG_RETURN(my_errno);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,14 @@ int heap_rlast(HP_INFO *info, uchar *record, int inx)
|
||||
}
|
||||
else
|
||||
{
|
||||
/* We can't scan a non existing key value with hash index */
|
||||
/*
|
||||
We can't scan a non existing key value with hash index.
|
||||
lastinx was already retargeted to this key above; clear
|
||||
current_hash_ptr so that it cannot be misread as belonging to
|
||||
this key (hp_delete_key() reuses its cached hash when flag is
|
||||
set and current_hash_ptr matches the record being deleted).
|
||||
*/
|
||||
info->current_hash_ptr= 0;
|
||||
my_errno= HA_ERR_WRONG_COMMAND;
|
||||
DBUG_RETURN(my_errno);
|
||||
}
|
||||
|
||||
844
storage/heap/hp_test_write_dup-t.c
Normal file
844
storage/heap/hp_test_write_dup-t.c
Normal file
@@ -0,0 +1,844 @@
|
||||
/*
|
||||
Unit tests for unique hash key duplicate rejection cost and rollback
|
||||
correctness in the HEAP engine.
|
||||
|
||||
A hash index insert must hash the key value exactly once: the hash
|
||||
selects the bucket, so it is computed unconditionally for every
|
||||
insert. Historically a *rejected duplicate* paid the full-value
|
||||
hash twice: hp_write_key() hashed the key to insert it, and after
|
||||
the duplicate was detected the caller invoked hp_delete_key() to
|
||||
remove the just-inserted entry, which hashed the same key again to
|
||||
locate the bucket. For long key values (blob keys) the second scan
|
||||
of the value dominates duplicate-heavy workloads such as
|
||||
SELECT DISTINCT over blobs.
|
||||
|
||||
These tests wrap the key charset's hash_sort collation callback in
|
||||
a counting shim so every full-value hash of key data is observable,
|
||||
and assert:
|
||||
- a successful insert costs exactly one full-value hash (the
|
||||
duplicate probe must not add hashing);
|
||||
- a rejected duplicate insert costs exactly one full-value hash;
|
||||
- deleting or re-keying a row positioned via the same hash index
|
||||
reuses the hash cached in the index entry instead of re-hashing;
|
||||
plus rollback behavior: rejected inserts and updates leave the
|
||||
table exactly as before, including multi-key rollback, NULL key
|
||||
parts and non-unique keys.
|
||||
|
||||
Record layout (same as hp_test_helpers.h, plus a nullable variant):
|
||||
byte 0: null bitmap (1 byte, bit 2 = blob null)
|
||||
bytes 1-4: int4 field
|
||||
bytes 5-6: blob packlength=2 (length, little-endian)
|
||||
bytes 7-14: blob data pointer (portable_sizeof_char_ptr bytes)
|
||||
reclength = 15
|
||||
*/
|
||||
|
||||
#include <my_global.h>
|
||||
#include <my_sys.h>
|
||||
#include <m_string.h>
|
||||
#include <tap.h>
|
||||
#include "heap.h"
|
||||
#include "heapdef.h"
|
||||
|
||||
#define REC_NULL_OFFSET 0
|
||||
#define INT_OFFSET 1
|
||||
#define BLOB_OFFSET 5
|
||||
#define BLOB_PACKLEN 2
|
||||
#define REC_LENGTH 15
|
||||
|
||||
#define BLOB_KEY_LEN (4 + portable_sizeof_char_ptr)
|
||||
|
||||
/*
|
||||
In builds where DBUG_ASSERT evaluates its expression (debug builds,
|
||||
and DBUG_OFF builds compiled with DBUG_ASSERT_AS_PRINTF),
|
||||
hp_delete_key() cross-checks the cached hash against a recomputed
|
||||
one, which adds one counted hash_sort call per delete that takes the
|
||||
cached-hash path. The cached-path count assertions are therefore
|
||||
only distinguishing when DBUG_ASSERT compiles to nothing; the
|
||||
insert-path assertions distinguish in all builds.
|
||||
*/
|
||||
#if defined(DBUG_OFF) && !defined(DBUG_ASSERT_AS_PRINTF)
|
||||
#define DELETE_HASH_CHECK 0
|
||||
#else
|
||||
#define DELETE_HASH_CHECK 1
|
||||
#endif
|
||||
|
||||
/*
|
||||
Counting charset: a copy of latin1 whose collation handler routes
|
||||
hash_sort through a wrapper that increments hash_calls. Comparisons
|
||||
(strnncollsp) are left untouched, so the counter isolates hashing.
|
||||
*/
|
||||
static struct charset_info_st counting_cs;
|
||||
static struct my_collation_handler_st counting_coll;
|
||||
static ulong hash_calls;
|
||||
static void (*real_hash_sort)(my_hasher_st *hasher, CHARSET_INFO *cs,
|
||||
const uchar *key, size_t len);
|
||||
|
||||
static void counting_hash_sort(my_hasher_st *hasher, CHARSET_INFO *cs,
|
||||
const uchar *key, size_t len)
|
||||
{
|
||||
hash_calls++;
|
||||
real_hash_sort(hasher, cs, key, len);
|
||||
}
|
||||
|
||||
static void init_counting_charset(void)
|
||||
{
|
||||
counting_cs= my_charset_latin1;
|
||||
counting_coll= *my_charset_latin1.coll;
|
||||
real_hash_sort= counting_coll.hash_sort;
|
||||
counting_coll.hash_sort= counting_hash_sort;
|
||||
counting_cs.coll= &counting_coll;
|
||||
}
|
||||
|
||||
|
||||
static void build_record(uchar *rec, int32 int_val,
|
||||
const uchar *blob_data, uint16 blob_len,
|
||||
my_bool blob_is_null)
|
||||
{
|
||||
memset(rec, 0, REC_LENGTH);
|
||||
rec[REC_NULL_OFFSET]= blob_is_null ? 2 : 0;
|
||||
int4store(rec + INT_OFFSET, int_val);
|
||||
if (!blob_is_null)
|
||||
{
|
||||
int2store(rec + BLOB_OFFSET, blob_len);
|
||||
memcpy(rec + BLOB_OFFSET + BLOB_PACKLEN, &blob_data, sizeof(blob_data));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Packed key for heap_rkey on the blob key: 4-byte length + pointer */
|
||||
static void build_blob_key(uchar *key, const uchar *data, uint32 len)
|
||||
{
|
||||
memset(key, 0, BLOB_KEY_LEN);
|
||||
int4store(key, len);
|
||||
memcpy(key + 4, &data, sizeof(data));
|
||||
}
|
||||
|
||||
|
||||
static void init_blob_keyseg(HA_KEYSEG *seg, my_bool nullable)
|
||||
{
|
||||
memset(seg, 0, sizeof(*seg));
|
||||
seg->type= HA_KEYTYPE_VARTEXT4;
|
||||
seg->flag= HA_BLOB_PART | HA_VAR_LENGTH_PART;
|
||||
seg->start= BLOB_OFFSET;
|
||||
seg->length= BLOB_KEY_LEN;
|
||||
seg->bit_start= BLOB_PACKLEN;
|
||||
seg->charset= &counting_cs;
|
||||
if (nullable)
|
||||
{
|
||||
seg->null_bit= 2;
|
||||
seg->null_pos= REC_NULL_OFFSET;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void init_int_keyseg(HA_KEYSEG *seg)
|
||||
{
|
||||
memset(seg, 0, sizeof(*seg));
|
||||
seg->type= HA_KEYTYPE_BINARY;
|
||||
seg->start= INT_OFFSET;
|
||||
seg->length= 4;
|
||||
seg->charset= &my_charset_bin;
|
||||
}
|
||||
|
||||
|
||||
static void init_keydef(HP_KEYDEF *keydef, HA_KEYSEG *seg, uint length,
|
||||
uint8 flag)
|
||||
{
|
||||
memset(keydef, 0, sizeof(*keydef));
|
||||
keydef->keysegs= 1;
|
||||
keydef->seg= seg;
|
||||
keydef->algorithm= HA_KEY_ALG_HASH;
|
||||
keydef->flag= flag;
|
||||
keydef->length= length;
|
||||
}
|
||||
|
||||
|
||||
static int create_and_open(const char *name, uint keys, HP_KEYDEF *keydef,
|
||||
HP_SHARE **share, HP_INFO **info)
|
||||
{
|
||||
HP_CREATE_INFO ci;
|
||||
HP_BLOB_DESC blob_desc;
|
||||
my_bool unused;
|
||||
|
||||
blob_desc.offset= BLOB_OFFSET;
|
||||
blob_desc.packlength= BLOB_PACKLEN;
|
||||
|
||||
memset(&ci, 0, sizeof(ci));
|
||||
ci.keys= keys;
|
||||
ci.keydef= keydef;
|
||||
ci.reclength= REC_LENGTH;
|
||||
ci.max_records= 0;
|
||||
ci.min_records= 10;
|
||||
ci.max_table_size= 64 * 1024 * 1024;
|
||||
ci.blob_descs= &blob_desc;
|
||||
ci.blob_count= 1;
|
||||
|
||||
if (heap_create(name, &ci, share, &unused))
|
||||
return 1;
|
||||
if (!(*info= heap_open(name, 2)))
|
||||
return 1;
|
||||
heap_extra(*info, HA_EXTRA_NO_READCHECK);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Test 1: hashing cost of unique inserts and rejected duplicates.
|
||||
|
||||
Every insert must cost exactly one full-value hash: one for a
|
||||
successful insert (probe + insert share the hash) and one for a
|
||||
rejected duplicate (the undo delete that re-hashed the value must
|
||||
not exist). A rejected duplicate must leave the table unchanged
|
||||
and usable.
|
||||
*/
|
||||
|
||||
static void test_dup_insert_hash_cost(void)
|
||||
{
|
||||
HP_SHARE *share;
|
||||
HP_INFO *info;
|
||||
HP_KEYDEF keydef;
|
||||
HA_KEYSEG keyseg;
|
||||
uchar rec[REC_LENGTH];
|
||||
uchar blob_a[400], blob_b[400];
|
||||
ulong i, dup_errors;
|
||||
|
||||
memset(blob_a, 'a', sizeof(blob_a));
|
||||
memset(blob_b, 'b', sizeof(blob_b));
|
||||
|
||||
init_blob_keyseg(&keyseg, FALSE);
|
||||
init_keydef(&keydef, &keyseg, BLOB_KEY_LEN, HA_NOSAME);
|
||||
|
||||
if (create_and_open("t_dup_cost", 1, &keydef, &share, &info))
|
||||
{
|
||||
ok(0, "setup failed: %d", my_errno);
|
||||
skip(12, "setup failed");
|
||||
return;
|
||||
}
|
||||
ok(1, "created table with unique blob hash key");
|
||||
|
||||
build_record(rec, 1, blob_a, sizeof(blob_a), FALSE);
|
||||
hash_calls= 0;
|
||||
ok(heap_write(info, rec) == 0, "insert first row");
|
||||
ok(hash_calls == 1, "unique insert costs exactly one full-value hash "
|
||||
"(got %lu)", hash_calls);
|
||||
|
||||
build_record(rec, 2, blob_a, sizeof(blob_a), FALSE);
|
||||
hash_calls= 0;
|
||||
ok(heap_write(info, rec) == HA_ERR_FOUND_DUPP_KEY,
|
||||
"duplicate blob value rejected");
|
||||
ok(hash_calls == 1, "rejected duplicate costs exactly one full-value hash "
|
||||
"(got %lu)", hash_calls);
|
||||
ok(info->errkey == 0, "errkey is the failing key (got %d)", info->errkey);
|
||||
ok(share->records == 1, "table unchanged after rejection (records=%lu)",
|
||||
(ulong) share->records);
|
||||
|
||||
build_record(rec, 3, blob_b, sizeof(blob_b), FALSE);
|
||||
hash_calls= 0;
|
||||
ok(heap_write(info, rec) == 0, "different value inserted after rejection");
|
||||
ok(hash_calls == 1, "insert after rejection costs one hash (got %lu)",
|
||||
hash_calls);
|
||||
ok(share->records == 2, "records=2 after second insert");
|
||||
|
||||
dup_errors= 0;
|
||||
hash_calls= 0;
|
||||
for (i= 0; i < 50; i++)
|
||||
{
|
||||
build_record(rec, (int32) (100 + i), blob_a, sizeof(blob_a), FALSE);
|
||||
if (heap_write(info, rec) == HA_ERR_FOUND_DUPP_KEY)
|
||||
dup_errors++;
|
||||
}
|
||||
ok(dup_errors == 50, "all 50 repeated duplicates rejected (got %lu)",
|
||||
dup_errors);
|
||||
ok(hash_calls == 50, "50 rejections cost exactly 50 hashes (got %lu)",
|
||||
hash_calls);
|
||||
|
||||
ok(heap_check_heap(info, 0) == 0, "heap_check_heap OK");
|
||||
|
||||
heap_drop_table(info);
|
||||
heap_close(info);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Test 2: delete of a row positioned via the hash index must reuse the
|
||||
hash cached in the index entry (heap_rkey already hashed the key to
|
||||
find the row); delete of a row positioned via a table scan must fall
|
||||
back to computing the hash once.
|
||||
*/
|
||||
|
||||
static void test_delete_hash_cost(void)
|
||||
{
|
||||
HP_SHARE *share;
|
||||
HP_INFO *info;
|
||||
HP_KEYDEF keydef;
|
||||
HA_KEYSEG keyseg;
|
||||
uchar rec[REC_LENGTH];
|
||||
uchar key[BLOB_KEY_LEN];
|
||||
uchar blob_a[400], blob_b[400];
|
||||
|
||||
memset(blob_a, 'a', sizeof(blob_a));
|
||||
memset(blob_b, 'b', sizeof(blob_b));
|
||||
|
||||
init_blob_keyseg(&keyseg, FALSE);
|
||||
init_keydef(&keydef, &keyseg, BLOB_KEY_LEN, HA_NOSAME);
|
||||
|
||||
if (create_and_open("t_del_cost", 1, &keydef, &share, &info))
|
||||
{
|
||||
ok(0, "setup failed: %d", my_errno);
|
||||
skip(11, "setup failed");
|
||||
return;
|
||||
}
|
||||
ok(1, "created table");
|
||||
|
||||
build_record(rec, 1, blob_a, sizeof(blob_a), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "insert row a");
|
||||
build_record(rec, 2, blob_b, sizeof(blob_b), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "insert row b");
|
||||
|
||||
/* Position row b via the hash index, then delete it */
|
||||
build_blob_key(key, blob_b, sizeof(blob_b));
|
||||
hash_calls= 0;
|
||||
ok(heap_rkey(info, rec, 0, key, 1, HA_READ_KEY_EXACT) == 0,
|
||||
"positioned row b via hash key");
|
||||
ok(heap_delete(info, rec) == 0, "deleted row b");
|
||||
ok(hash_calls == 1 + DELETE_HASH_CHECK,
|
||||
"index-read + delete hash the key exactly once (got %lu)", hash_calls);
|
||||
ok(share->records == 1, "one row left");
|
||||
|
||||
/* Position the remaining row via a scan: delete must still work */
|
||||
heap_scan_init(info);
|
||||
{
|
||||
int found= 0;
|
||||
while (heap_scan(info, rec) == 0)
|
||||
{
|
||||
found= 1;
|
||||
break;
|
||||
}
|
||||
ok(found, "scan found remaining row");
|
||||
}
|
||||
hash_calls= 0;
|
||||
ok(heap_delete(info, rec) == 0, "deleted scan-positioned row");
|
||||
ok(hash_calls == 1,
|
||||
"scan-positioned delete computes the hash once (got %lu)", hash_calls);
|
||||
ok(share->records == 0, "table empty");
|
||||
|
||||
ok(heap_check_heap(info, 0) == 0, "heap_check_heap OK");
|
||||
|
||||
heap_drop_table(info);
|
||||
heap_close(info);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Test 3: update re-keying a row positioned via the hash index, and an
|
||||
update rejected as duplicate.
|
||||
|
||||
A successful re-key must hash only the new key value (the old key's
|
||||
hash is cached in the index entry). A rejected update must restore
|
||||
the old key and leave both rows intact.
|
||||
*/
|
||||
|
||||
static void test_update_rekey_and_dup(void)
|
||||
{
|
||||
HP_SHARE *share;
|
||||
HP_INFO *info;
|
||||
HP_KEYDEF keydef;
|
||||
HA_KEYSEG keyseg;
|
||||
uchar oldrec[REC_LENGTH], newrec[REC_LENGTH], rec[REC_LENGTH];
|
||||
uchar key[BLOB_KEY_LEN];
|
||||
uchar blob_x[300], blob_y[300], blob_z[300];
|
||||
|
||||
memset(blob_x, 'x', sizeof(blob_x));
|
||||
memset(blob_y, 'y', sizeof(blob_y));
|
||||
memset(blob_z, 'z', sizeof(blob_z));
|
||||
|
||||
init_blob_keyseg(&keyseg, FALSE);
|
||||
init_keydef(&keydef, &keyseg, BLOB_KEY_LEN, HA_NOSAME);
|
||||
|
||||
if (create_and_open("t_upd_dup", 1, &keydef, &share, &info))
|
||||
{
|
||||
ok(0, "setup failed: %d", my_errno);
|
||||
skip(15, "setup failed");
|
||||
return;
|
||||
}
|
||||
ok(1, "created table");
|
||||
|
||||
build_record(rec, 1, blob_x, sizeof(blob_x), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "insert row 1 (x)");
|
||||
build_record(rec, 2, blob_y, sizeof(blob_y), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "insert row 2 (y)");
|
||||
|
||||
/* Re-key row 2 from y to z, positioned via the index */
|
||||
build_blob_key(key, blob_y, sizeof(blob_y));
|
||||
ok(heap_rkey(info, oldrec, 0, key, 1, HA_READ_KEY_EXACT) == 0,
|
||||
"positioned row 2 via hash key");
|
||||
build_record(newrec, 2, blob_z, sizeof(blob_z), FALSE);
|
||||
hash_calls= 0;
|
||||
ok(heap_update(info, oldrec, newrec) == 0, "re-keyed row 2 from y to z");
|
||||
ok(hash_calls == 1 + DELETE_HASH_CHECK,
|
||||
"re-key hashes only the new key value (got %lu)", hash_calls);
|
||||
|
||||
build_blob_key(key, blob_z, sizeof(blob_z));
|
||||
ok(heap_rkey(info, rec, 0, key, 1, HA_READ_KEY_EXACT) == 0,
|
||||
"new key z finds the row");
|
||||
ok(sint4korr(rec + INT_OFFSET) == 2, "row content preserved");
|
||||
build_blob_key(key, blob_y, sizeof(blob_y));
|
||||
ok(heap_rkey(info, rec, 0, key, 1, HA_READ_KEY_EXACT) != 0,
|
||||
"old key y no longer finds a row");
|
||||
|
||||
/* Update row 2 to x: duplicate of row 1, must be rejected + rolled back */
|
||||
build_blob_key(key, blob_z, sizeof(blob_z));
|
||||
ok(heap_rkey(info, oldrec, 0, key, 1, HA_READ_KEY_EXACT) == 0,
|
||||
"re-positioned row 2 via key z");
|
||||
build_record(newrec, 2, blob_x, sizeof(blob_x), FALSE);
|
||||
ok(heap_update(info, oldrec, newrec) == HA_ERR_FOUND_DUPP_KEY,
|
||||
"update to duplicate value rejected");
|
||||
ok(info->errkey == 0, "errkey is the failing key (got %d)", info->errkey);
|
||||
ok(share->records == 2, "both rows still present");
|
||||
|
||||
ok(heap_rkey(info, rec, 0, key, 1, HA_READ_KEY_EXACT) == 0,
|
||||
"row 2 still reachable via key z after rejected update");
|
||||
build_blob_key(key, blob_x, sizeof(blob_x));
|
||||
ok(heap_rkey(info, rec, 0, key, 1, HA_READ_KEY_EXACT) == 0 &&
|
||||
sint4korr(rec + INT_OFFSET) == 1,
|
||||
"row 1 still reachable via key x");
|
||||
|
||||
ok(heap_check_heap(info, 0) == 0, "heap_check_heap OK");
|
||||
|
||||
heap_drop_table(info);
|
||||
heap_close(info);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Test 4: multi-key rollback. When a later key detects a duplicate,
|
||||
keys already written for the row must be removed again, and when the
|
||||
first key detects the duplicate, later keys must never be touched.
|
||||
*/
|
||||
|
||||
static void test_multikey_rollback(void)
|
||||
{
|
||||
HP_SHARE *share;
|
||||
HP_INFO *info;
|
||||
HP_KEYDEF keydef[2];
|
||||
HA_KEYSEG int_seg, blob_seg;
|
||||
uchar rec[REC_LENGTH];
|
||||
uchar int_key[4];
|
||||
uchar key[BLOB_KEY_LEN];
|
||||
uchar blob_x[200], blob_y[200], blob_z[200], blob_w[200];
|
||||
|
||||
memset(blob_x, 'x', sizeof(blob_x));
|
||||
memset(blob_y, 'y', sizeof(blob_y));
|
||||
memset(blob_z, 'z', sizeof(blob_z));
|
||||
memset(blob_w, 'w', sizeof(blob_w));
|
||||
|
||||
init_int_keyseg(&int_seg);
|
||||
init_keydef(&keydef[0], &int_seg, 4, HA_NOSAME);
|
||||
init_blob_keyseg(&blob_seg, FALSE);
|
||||
init_keydef(&keydef[1], &blob_seg, BLOB_KEY_LEN, HA_NOSAME);
|
||||
|
||||
if (create_and_open("t_multikey", 2, keydef, &share, &info))
|
||||
{
|
||||
ok(0, "setup failed: %d", my_errno);
|
||||
skip(13, "setup failed");
|
||||
return;
|
||||
}
|
||||
ok(1, "created table with unique int + unique blob hash keys");
|
||||
|
||||
build_record(rec, 1, blob_x, sizeof(blob_x), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "insert (1, x)");
|
||||
build_record(rec, 2, blob_y, sizeof(blob_y), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "insert (2, y)");
|
||||
|
||||
/* key 0 accepts 3, key 1 rejects y: key 0 must be rolled back */
|
||||
build_record(rec, 3, blob_y, sizeof(blob_y), FALSE);
|
||||
ok(heap_write(info, rec) == HA_ERR_FOUND_DUPP_KEY,
|
||||
"(3, y) rejected on blob key");
|
||||
ok(info->errkey == 1, "errkey is the blob key (got %d)", info->errkey);
|
||||
int4store(int_key, 3);
|
||||
ok(heap_rkey(info, rec, 0, int_key, 1, HA_READ_KEY_EXACT) != 0,
|
||||
"int key 3 rolled back");
|
||||
build_record(rec, 3, blob_z, sizeof(blob_z), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "(3, z) inserted after rollback");
|
||||
ok(share->records == 3, "records=3");
|
||||
|
||||
/* key 0 rejects 2 immediately: key 1 must never see w */
|
||||
build_record(rec, 2, blob_w, sizeof(blob_w), FALSE);
|
||||
ok(heap_write(info, rec) == HA_ERR_FOUND_DUPP_KEY,
|
||||
"(2, w) rejected on int key");
|
||||
ok(info->errkey == 0, "errkey is the int key (got %d)", info->errkey);
|
||||
build_blob_key(key, blob_w, sizeof(blob_w));
|
||||
ok(heap_rkey(info, rec, 1, key, 1, HA_READ_KEY_EXACT) != 0,
|
||||
"blob key w was never inserted");
|
||||
build_record(rec, 4, blob_w, sizeof(blob_w), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "(4, w) inserted");
|
||||
ok(share->records == 4, "records=4");
|
||||
|
||||
ok(heap_check_heap(info, 0) == 0, "heap_check_heap OK");
|
||||
|
||||
heap_drop_table(info);
|
||||
heap_close(info);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Test 5: multi-key update rollback. An update that changes two
|
||||
unique hash keys where the second key hits a duplicate must restore
|
||||
both original key values: the failing key was never inserted (only
|
||||
its old value is re-inserted), and the first key's new value must be
|
||||
removed and its old value re-inserted.
|
||||
*/
|
||||
|
||||
static void test_multikey_update_rollback(void)
|
||||
{
|
||||
HP_SHARE *share;
|
||||
HP_INFO *info;
|
||||
HP_KEYDEF keydef[2];
|
||||
HA_KEYSEG int_seg, blob_seg;
|
||||
uchar oldrec[REC_LENGTH], newrec[REC_LENGTH], rec[REC_LENGTH];
|
||||
uchar int_key[4];
|
||||
uchar key[BLOB_KEY_LEN];
|
||||
uchar blob_x[200], blob_y[200];
|
||||
|
||||
memset(blob_x, 'x', sizeof(blob_x));
|
||||
memset(blob_y, 'y', sizeof(blob_y));
|
||||
|
||||
init_int_keyseg(&int_seg);
|
||||
init_keydef(&keydef[0], &int_seg, 4, HA_NOSAME);
|
||||
init_blob_keyseg(&blob_seg, FALSE);
|
||||
init_keydef(&keydef[1], &blob_seg, BLOB_KEY_LEN, HA_NOSAME);
|
||||
|
||||
if (create_and_open("t_multikey_upd", 2, keydef, &share, &info))
|
||||
{
|
||||
ok(0, "setup failed: %d", my_errno);
|
||||
skip(11, "setup failed");
|
||||
return;
|
||||
}
|
||||
ok(1, "created table with unique int + unique blob hash keys");
|
||||
|
||||
build_record(rec, 1, blob_x, sizeof(blob_x), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "insert (1, x)");
|
||||
build_record(rec, 2, blob_y, sizeof(blob_y), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "insert (2, y)");
|
||||
|
||||
/* Update (1, x) to (3, y): int key 3 is written, blob key y dups */
|
||||
int4store(int_key, 1);
|
||||
ok(heap_rkey(info, oldrec, 0, int_key, 1, HA_READ_KEY_EXACT) == 0,
|
||||
"positioned row (1, x) via int key");
|
||||
build_record(newrec, 3, blob_y, sizeof(blob_y), FALSE);
|
||||
ok(heap_update(info, oldrec, newrec) == HA_ERR_FOUND_DUPP_KEY,
|
||||
"update (1, x) -> (3, y) rejected on blob key");
|
||||
ok(info->errkey == 1, "errkey is the blob key (got %d)", info->errkey);
|
||||
|
||||
int4store(int_key, 1);
|
||||
ok(heap_rkey(info, rec, 0, int_key, 1, HA_READ_KEY_EXACT) == 0 &&
|
||||
sint4korr(rec + INT_OFFSET) == 1,
|
||||
"int key 1 restored after rollback");
|
||||
int4store(int_key, 3);
|
||||
ok(heap_rkey(info, rec, 0, int_key, 1, HA_READ_KEY_EXACT) != 0,
|
||||
"int key 3 rolled back");
|
||||
build_blob_key(key, blob_x, sizeof(blob_x));
|
||||
ok(heap_rkey(info, rec, 1, key, 1, HA_READ_KEY_EXACT) == 0 &&
|
||||
sint4korr(rec + INT_OFFSET) == 1,
|
||||
"blob key x restored after rollback");
|
||||
build_blob_key(key, blob_y, sizeof(blob_y));
|
||||
ok(heap_rkey(info, rec, 1, key, 1, HA_READ_KEY_EXACT) == 0 &&
|
||||
sint4korr(rec + INT_OFFSET) == 2,
|
||||
"blob key y still finds row 2");
|
||||
|
||||
ok(share->records == 2, "records=2");
|
||||
|
||||
ok(heap_check_heap(info, 0) == 0, "heap_check_heap OK");
|
||||
|
||||
heap_drop_table(info);
|
||||
heap_close(info);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Test 6: delete after a rejected ordered read on a hash index.
|
||||
|
||||
heap_rfirst()/heap_rlast() retarget lastinx to the requested index
|
||||
before discovering it is a hash index and failing with
|
||||
HA_ERR_WRONG_COMMAND. They must not leave current_hash_ptr pointing
|
||||
into another key's hash block: hp_delete_key() trusts
|
||||
current_hash_ptr's cached hash when the row was positioned via the
|
||||
index it is deleting from, so a stale pointer from a different key
|
||||
would send the bucket lookup to the wrong chain and fail with
|
||||
HA_ERR_CRASHED.
|
||||
*/
|
||||
|
||||
static void test_delete_after_rejected_ordered_read(void)
|
||||
{
|
||||
HP_SHARE *share;
|
||||
HP_INFO *info;
|
||||
HP_KEYDEF keydef[2];
|
||||
HA_KEYSEG int_seg, blob_seg;
|
||||
uchar rec[REC_LENGTH], readbuf[REC_LENGTH];
|
||||
uchar int_key[4];
|
||||
uchar key[BLOB_KEY_LEN];
|
||||
uchar blob_x[200], blob_y[200];
|
||||
|
||||
memset(blob_x, 'x', sizeof(blob_x));
|
||||
memset(blob_y, 'y', sizeof(blob_y));
|
||||
|
||||
init_int_keyseg(&int_seg);
|
||||
init_keydef(&keydef[0], &int_seg, 4, HA_NOSAME);
|
||||
init_blob_keyseg(&blob_seg, FALSE);
|
||||
init_keydef(&keydef[1], &blob_seg, BLOB_KEY_LEN, HA_NOSAME);
|
||||
|
||||
if (create_and_open("t_stale_cursor", 2, keydef, &share, &info))
|
||||
{
|
||||
ok(0, "setup failed: %d", my_errno);
|
||||
skip(13, "setup failed");
|
||||
return;
|
||||
}
|
||||
ok(1, "created table with unique int + unique blob hash keys");
|
||||
|
||||
build_record(rec, 1, blob_x, sizeof(blob_x), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "insert (1, x)");
|
||||
build_record(rec, 2, blob_y, sizeof(blob_y), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "insert (2, y)");
|
||||
|
||||
/* Position row 1 via the blob key, then poison lastinx via rfirst */
|
||||
build_blob_key(key, blob_x, sizeof(blob_x));
|
||||
ok(heap_rkey(info, rec, 1, key, 1, HA_READ_KEY_EXACT) == 0,
|
||||
"positioned row 1 via blob key");
|
||||
ok(heap_rfirst(info, readbuf, 0) == HA_ERR_WRONG_COMMAND,
|
||||
"rfirst on hash int key rejected");
|
||||
ok(heap_delete(info, rec) == 0,
|
||||
"delete succeeds after rejected rfirst retargeted lastinx");
|
||||
ok(share->records == 1, "one row left");
|
||||
build_blob_key(key, blob_x, sizeof(blob_x));
|
||||
ok(heap_rkey(info, rec, 1, key, 1, HA_READ_KEY_EXACT) != 0,
|
||||
"deleted row unreachable via blob key");
|
||||
int4store(int_key, 2);
|
||||
ok(heap_rkey(info, rec, 0, int_key, 1, HA_READ_KEY_EXACT) == 0,
|
||||
"row 2 still reachable via int key");
|
||||
|
||||
/* Same shape with heap_rlast */
|
||||
build_blob_key(key, blob_y, sizeof(blob_y));
|
||||
ok(heap_rkey(info, rec, 1, key, 1, HA_READ_KEY_EXACT) == 0,
|
||||
"positioned row 2 via blob key");
|
||||
ok(heap_rlast(info, readbuf, 0) == HA_ERR_WRONG_COMMAND,
|
||||
"rlast on hash int key rejected");
|
||||
ok(heap_delete(info, rec) == 0,
|
||||
"delete succeeds after rejected rlast retargeted lastinx");
|
||||
ok(share->records == 0, "table empty");
|
||||
|
||||
ok(heap_check_heap(info, 0) == 0, "heap_check_heap OK");
|
||||
|
||||
heap_drop_table(info);
|
||||
heap_close(info);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Test 7: NULL key parts never conflict on a unique key; non-NULL
|
||||
duplicates in the same table are still rejected.
|
||||
*/
|
||||
|
||||
static void test_null_parts(void)
|
||||
{
|
||||
HP_SHARE *share;
|
||||
HP_INFO *info;
|
||||
HP_KEYDEF keydef;
|
||||
HA_KEYSEG keyseg;
|
||||
uchar rec[REC_LENGTH];
|
||||
uchar blob_x[200];
|
||||
|
||||
memset(blob_x, 'x', sizeof(blob_x));
|
||||
|
||||
init_blob_keyseg(&keyseg, TRUE);
|
||||
init_keydef(&keydef, &keyseg, 1 + BLOB_KEY_LEN, HA_NOSAME);
|
||||
|
||||
if (create_and_open("t_null_parts", 1, &keydef, &share, &info))
|
||||
{
|
||||
ok(0, "setup failed: %d", my_errno);
|
||||
skip(7, "setup failed");
|
||||
return;
|
||||
}
|
||||
ok(1, "created table with nullable unique blob hash key");
|
||||
|
||||
build_record(rec, 1, NULL, 0, TRUE);
|
||||
ok(heap_write(info, rec) == 0, "insert first NULL key");
|
||||
hash_calls= 0;
|
||||
build_record(rec, 2, NULL, 0, TRUE);
|
||||
ok(heap_write(info, rec) == 0, "insert second NULL key (NULLs never dup)");
|
||||
ok(hash_calls == 0, "NULL key value is never hashed (got %lu)", hash_calls);
|
||||
ok(share->records == 2, "records=2");
|
||||
|
||||
build_record(rec, 3, blob_x, sizeof(blob_x), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "insert non-NULL value");
|
||||
build_record(rec, 4, blob_x, sizeof(blob_x), FALSE);
|
||||
ok(heap_write(info, rec) == HA_ERR_FOUND_DUPP_KEY,
|
||||
"non-NULL duplicate still rejected");
|
||||
|
||||
ok(heap_check_heap(info, 0) == 0, "heap_check_heap OK");
|
||||
|
||||
heap_drop_table(info);
|
||||
heap_close(info);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Test 8: non-unique hash keys accept duplicates and never pay a
|
||||
duplicate probe beyond the single insert hash.
|
||||
*/
|
||||
|
||||
static void test_non_unique_dups(void)
|
||||
{
|
||||
HP_SHARE *share;
|
||||
HP_INFO *info;
|
||||
HP_KEYDEF keydef;
|
||||
HA_KEYSEG keyseg;
|
||||
uchar rec[REC_LENGTH];
|
||||
uchar blob_x[200];
|
||||
|
||||
memset(blob_x, 'x', sizeof(blob_x));
|
||||
|
||||
init_blob_keyseg(&keyseg, FALSE);
|
||||
init_keydef(&keydef, &keyseg, BLOB_KEY_LEN, 0);
|
||||
|
||||
if (create_and_open("t_non_unique", 1, &keydef, &share, &info))
|
||||
{
|
||||
ok(0, "setup failed: %d", my_errno);
|
||||
skip(5, "setup failed");
|
||||
return;
|
||||
}
|
||||
ok(1, "created table with non-unique blob hash key");
|
||||
|
||||
hash_calls= 0;
|
||||
build_record(rec, 1, blob_x, sizeof(blob_x), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "insert value");
|
||||
build_record(rec, 2, blob_x, sizeof(blob_x), FALSE);
|
||||
ok(heap_write(info, rec) == 0, "insert same value again");
|
||||
ok(hash_calls == 2, "two inserts cost two hashes (got %lu)", hash_calls);
|
||||
ok(share->records == 2, "records=2");
|
||||
|
||||
ok(heap_check_heap(info, 0) == 0, "heap_check_heap OK");
|
||||
|
||||
heap_drop_table(info);
|
||||
heap_close(info);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Test 9: duplicate-heavy insert stream across many hash table splits,
|
||||
then delete everything via index reads.
|
||||
|
||||
500 inserts of 173 distinct values: exactly 173 succeed, 327 are
|
||||
rejected, and the total hashing cost is exactly 500 (one hash per
|
||||
insert attempt, successful or not). The table stays consistent
|
||||
through the linear-hash splits interleaved with rejections.
|
||||
*/
|
||||
|
||||
static void test_mixed_stress(void)
|
||||
{
|
||||
HP_SHARE *share;
|
||||
HP_INFO *info;
|
||||
HP_KEYDEF keydef;
|
||||
HA_KEYSEG keyseg;
|
||||
uchar rec[REC_LENGTH];
|
||||
uchar key[BLOB_KEY_LEN];
|
||||
uchar blob_val[24];
|
||||
ulong i, successes= 0, dups= 0, delete_failures= 0;
|
||||
const ulong attempts= 500, distinct= 173;
|
||||
|
||||
init_blob_keyseg(&keyseg, FALSE);
|
||||
init_keydef(&keydef, &keyseg, BLOB_KEY_LEN, HA_NOSAME);
|
||||
|
||||
if (create_and_open("t_stress", 1, &keydef, &share, &info))
|
||||
{
|
||||
ok(0, "setup failed: %d", my_errno);
|
||||
skip(9, "setup failed");
|
||||
return;
|
||||
}
|
||||
ok(1, "created table");
|
||||
|
||||
hash_calls= 0;
|
||||
for (i= 0; i < attempts; i++)
|
||||
{
|
||||
int res;
|
||||
memset(blob_val, 'v', sizeof(blob_val));
|
||||
my_snprintf((char*) blob_val, sizeof(blob_val), "value-%03lu",
|
||||
i % distinct);
|
||||
build_record(rec, (int32) i, blob_val, sizeof(blob_val), FALSE);
|
||||
res= heap_write(info, rec);
|
||||
if (res == 0)
|
||||
successes++;
|
||||
else if (res == HA_ERR_FOUND_DUPP_KEY)
|
||||
dups++;
|
||||
}
|
||||
ok(successes == distinct, "%lu distinct values inserted (got %lu)",
|
||||
distinct, successes);
|
||||
ok(dups == attempts - distinct, "%lu duplicates rejected (got %lu)",
|
||||
attempts - distinct, dups);
|
||||
ok(hash_calls == attempts,
|
||||
"%lu insert attempts cost exactly %lu hashes (got %lu)",
|
||||
attempts, attempts, hash_calls);
|
||||
ok(share->records == distinct, "records=%lu", (ulong) share->records);
|
||||
ok(heap_check_heap(info, 0) == 0, "heap_check_heap OK after inserts");
|
||||
|
||||
hash_calls= 0;
|
||||
for (i= 0; i < distinct; i++)
|
||||
{
|
||||
memset(blob_val, 'v', sizeof(blob_val));
|
||||
my_snprintf((char*) blob_val, sizeof(blob_val), "value-%03lu", i);
|
||||
build_blob_key(key, blob_val, sizeof(blob_val));
|
||||
if (heap_rkey(info, rec, 0, key, 1, HA_READ_KEY_EXACT) ||
|
||||
heap_delete(info, rec))
|
||||
delete_failures++;
|
||||
}
|
||||
ok(delete_failures == 0, "all %lu rows deleted via index reads (%lu failed)",
|
||||
distinct, delete_failures);
|
||||
ok(hash_calls == distinct * (1 + DELETE_HASH_CHECK),
|
||||
"each index-read + delete hashes the key exactly once (got %lu)",
|
||||
hash_calls);
|
||||
ok(share->records == 0, "table empty");
|
||||
|
||||
ok(heap_check_heap(info, 0) == 0, "heap_check_heap OK after deletes");
|
||||
|
||||
heap_drop_table(info);
|
||||
heap_close(info);
|
||||
}
|
||||
|
||||
|
||||
int main(int argc __attribute__((unused)),
|
||||
char **argv __attribute__((unused)))
|
||||
{
|
||||
MY_INIT("hp_test_write_dup");
|
||||
plan(105);
|
||||
|
||||
init_counting_charset();
|
||||
|
||||
diag("Test 1: hashing cost of unique inserts and rejected duplicates");
|
||||
test_dup_insert_hash_cost();
|
||||
|
||||
diag("Test 2: hashing cost of deletes (index- and scan-positioned)");
|
||||
test_delete_hash_cost();
|
||||
|
||||
diag("Test 3: update re-key cost and update-to-duplicate rollback");
|
||||
test_update_rekey_and_dup();
|
||||
|
||||
diag("Test 4: multi-key rollback on duplicate");
|
||||
test_multikey_rollback();
|
||||
|
||||
diag("Test 5: multi-key update rollback on duplicate");
|
||||
test_multikey_update_rollback();
|
||||
|
||||
diag("Test 6: delete after rejected rfirst/rlast on a hash index");
|
||||
test_delete_after_rejected_ordered_read();
|
||||
|
||||
diag("Test 7: NULL key parts never conflict");
|
||||
test_null_parts();
|
||||
|
||||
diag("Test 8: non-unique hash keys accept duplicates");
|
||||
test_non_unique_dups();
|
||||
|
||||
diag("Test 9: duplicate-heavy stress across hash splits");
|
||||
test_mixed_stress();
|
||||
|
||||
my_end(0);
|
||||
return exit_status();
|
||||
}
|
||||
@@ -285,18 +285,21 @@ int heap_update(HP_INFO *info, const uchar *old, const uchar *heap_new)
|
||||
/* keydef points to the last key that failed */
|
||||
if (org_error == HA_ERR_FOUND_DUPP_KEY)
|
||||
info->errkey = (int) (keydef - share->keydef);
|
||||
if (keydef->algorithm == HA_KEY_ALG_BTREE)
|
||||
/*
|
||||
The failing key was never inserted: tree_insert() rejects
|
||||
duplicates and fails allocation before linking the node, and
|
||||
hash keys are probed for duplicates before insert. Re-insert
|
||||
the old key that was deleted just before the failed insert,
|
||||
then roll back the previously updated keys.
|
||||
*/
|
||||
if ((*keydef->write_key)(info, keydef, old, pos))
|
||||
{
|
||||
/* we don't need to delete non-inserted key from rb-tree */
|
||||
if ((*keydef->write_key)(info, keydef, old, pos))
|
||||
{
|
||||
heap_mark_crashed(share);
|
||||
if (++(share->records) == share->blength)
|
||||
share->blength+= share->blength;
|
||||
DBUG_RETURN(my_errno);
|
||||
}
|
||||
keydef--;
|
||||
heap_mark_crashed(share);
|
||||
if (++(share->records) == share->blength)
|
||||
share->blength+= share->blength;
|
||||
DBUG_RETURN(my_errno);
|
||||
}
|
||||
keydef--;
|
||||
}
|
||||
/* Restore all modified keys */
|
||||
while (keydef >= share->keydef)
|
||||
|
||||
@@ -100,15 +100,12 @@ err:
|
||||
DBUG_PRINT("info",("Duplicate key: %d", (int) (keydef - share->keydef)));
|
||||
info->errkey= (int) (keydef - share->keydef);
|
||||
/*
|
||||
We don't need to delete non-inserted key from rb-tree. Also, if
|
||||
we got ENOMEM, the key wasn't inserted, so don't try to delete it
|
||||
either. Otherwise for HASH index on HA_ERR_FOUND_DUPP_KEY the key
|
||||
was inserted and we have to delete it.
|
||||
The failing key was never inserted: tree_insert() rejects
|
||||
duplicates and fails allocation before linking the node, hash keys
|
||||
are probed for duplicates before insert, and on ENOMEM nothing was
|
||||
added. Roll back only the preceding keys.
|
||||
*/
|
||||
if (keydef->algorithm == HA_KEY_ALG_BTREE || my_errno == ENOMEM)
|
||||
{
|
||||
keydef--;
|
||||
}
|
||||
keydef--;
|
||||
|
||||
err_delete_written_keys:
|
||||
while (keydef >= share->keydef)
|
||||
@@ -346,8 +343,8 @@ uchar *next_free_record_pos(HP_SHARE *info)
|
||||
RETURN
|
||||
0 - OK
|
||||
-1 - Out of memory
|
||||
HA_ERR_FOUND_DUPP_KEY - Duplicate record on unique key. The record was
|
||||
still added and the caller must call hp_delete_key for it.
|
||||
HA_ERR_FOUND_DUPP_KEY - Duplicate record on unique key. Nothing was
|
||||
inserted; the index is unchanged.
|
||||
*/
|
||||
|
||||
int hp_write_key(HP_INFO *info, HP_KEYDEF *keyinfo,
|
||||
@@ -355,12 +352,42 @@ int hp_write_key(HP_INFO *info, HP_KEYDEF *keyinfo,
|
||||
{
|
||||
HP_SHARE *share = info->s;
|
||||
int flag;
|
||||
ulong halfbuff,hashnr,first_index;
|
||||
ulong halfbuff,hashnr,first_index,rec_hash;
|
||||
ulong UNINIT_VAR(hash_of_key), UNINIT_VAR(hash_of_key2);
|
||||
uchar *UNINIT_VAR(ptr_to_rec),*UNINIT_VAR(ptr_to_rec2);
|
||||
HASH_INFO *empty,*UNINIT_VAR(gpos),*UNINIT_VAR(gpos2),*pos;
|
||||
DBUG_ENTER("hp_write_key");
|
||||
|
||||
rec_hash= hp_rec_hashnr(0, keyinfo, record);
|
||||
|
||||
/*
|
||||
Reject a duplicate before modifying the index, so that the caller
|
||||
never has to undo the insert (the undo would hash the key value a
|
||||
second time, which is expensive for blob keys). The probe walks
|
||||
the key's chain under the current mask comparing cached hashes;
|
||||
the full value is compared only on a hash match. Records with
|
||||
equal hashes always share a bucket under any mask, so probing the
|
||||
pre-insert chain finds any duplicate.
|
||||
*/
|
||||
if ((keyinfo->flag & HA_NOSAME) && share->records &&
|
||||
(!(keyinfo->flag & HA_NULL_PART_KEY) ||
|
||||
!hp_if_null_in_key(keyinfo, record)))
|
||||
{
|
||||
ulong search_pos= hp_mask(rec_hash, share->blength, share->records);
|
||||
pos= hp_find_hash(&keyinfo->block, search_pos);
|
||||
/* Only entries whose home position is search_pos belong to the chain */
|
||||
if (hp_mask(pos->hash_of_key, share->blength, share->records) ==
|
||||
search_pos)
|
||||
{
|
||||
do
|
||||
{
|
||||
if (pos->hash_of_key == rec_hash &&
|
||||
!hp_rec_key_cmp(keyinfo, record, pos->ptr_to_rec, info))
|
||||
DBUG_RETURN(my_errno=HA_ERR_FOUND_DUPP_KEY);
|
||||
} while ((pos= pos->next_key));
|
||||
}
|
||||
}
|
||||
|
||||
flag=0;
|
||||
if (!(empty= hp_find_free_hash(share,&keyinfo->block,share->records)))
|
||||
DBUG_RETURN(-1); /* No more memory */
|
||||
@@ -499,13 +526,12 @@ int hp_write_key(HP_INFO *info, HP_KEYDEF *keyinfo,
|
||||
}
|
||||
/* Check if we are at the empty position */
|
||||
|
||||
hash_of_key= hp_rec_hashnr(0, keyinfo, record);
|
||||
pos=hp_find_hash(&keyinfo->block,
|
||||
hp_mask(hash_of_key, share->blength, share->records + 1));
|
||||
hp_mask(rec_hash, share->blength, share->records + 1));
|
||||
if (pos == empty)
|
||||
{
|
||||
pos->ptr_to_rec= recpos;
|
||||
pos->hash_of_key= hash_of_key;
|
||||
pos->hash_of_key= rec_hash;
|
||||
pos->next_key= 0;
|
||||
keyinfo->hash_buckets++;
|
||||
}
|
||||
@@ -518,7 +544,7 @@ int hp_write_key(HP_INFO *info, HP_KEYDEF *keyinfo,
|
||||
share->blength, share->records + 1));
|
||||
|
||||
pos->ptr_to_rec= recpos;
|
||||
pos->hash_of_key= hash_of_key;
|
||||
pos->hash_of_key= rec_hash;
|
||||
if (pos == gpos)
|
||||
pos->next_key=empty;
|
||||
else
|
||||
@@ -527,22 +553,6 @@ int hp_write_key(HP_INFO *info, HP_KEYDEF *keyinfo,
|
||||
pos->next_key= 0;
|
||||
hp_movelink(pos, gpos, empty);
|
||||
}
|
||||
|
||||
/* Check if duplicated keys */
|
||||
if ((keyinfo->flag & HA_NOSAME) && pos == gpos &&
|
||||
(!(keyinfo->flag & HA_NULL_PART_KEY) ||
|
||||
!hp_if_null_in_key(keyinfo, record)))
|
||||
{
|
||||
pos=empty;
|
||||
do
|
||||
{
|
||||
if (pos->hash_of_key == hash_of_key &&
|
||||
! hp_rec_key_cmp(keyinfo, record, pos->ptr_to_rec, info))
|
||||
{
|
||||
DBUG_RETURN(my_errno=HA_ERR_FOUND_DUPP_KEY);
|
||||
}
|
||||
} while ((pos=pos->next_key));
|
||||
}
|
||||
}
|
||||
DBUG_RETURN(0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user