diff --git a/mysql-test/main/except_all.result b/mysql-test/main/except_all.result index a457e8a4ab1..0cc1ef69a39 100644 --- a/mysql-test/main/except_all.result +++ b/mysql-test/main/except_all.result @@ -39,8 +39,8 @@ a b select * from t1 except all select * from t1 union all select * from t1 union all select * from t1 except all select * from t2; a b 4 4 -2 2 4 4 +2 2 4 4 4 4 4 4 diff --git a/storage/heap/_check.c b/storage/heap/_check.c index 1837a2b9877..4d3cd79e55d 100644 --- a/storage/heap/_check.c +++ b/storage/heap/_check.c @@ -47,6 +47,7 @@ int heap_check_heap(const HP_INFO *info, my_bool print_status) ulong records=0, deleted=0, cont_count=0, pos, next_block; ulong del_link_count; uchar *del_ptr; + my_bool block_count_error_printed= FALSE; HP_SHARE *share=info->s; uchar *current_ptr= info->current_ptr; DBUG_ENTER("heap_check_heap"); @@ -71,13 +72,25 @@ int heap_check_heap(const HP_INFO *info, my_bool print_status) error= 1; } - /* Verify free list length matches share->deleted */ + /* Verify free list record count matches share->deleted */ del_link_count= 0; - for (del_ptr= share->del_link; del_ptr; del_ptr= *((uchar**) del_ptr)) - del_link_count++; + for (del_ptr= share->del_link; del_ptr; ) + { + if (hp_is_free_block_end(del_ptr)) + { + uchar *first= hp_free_block_first(del_ptr); + del_link_count+= hp_free_block_start_count(first); + del_ptr= *((uchar**) first); + } + else + { + del_link_count++; + del_ptr= *((uchar**) del_ptr); + } + } if (del_link_count != share->deleted) { - DBUG_PRINT("error",("free list length %lu != share->deleted %lu", + DBUG_PRINT("error",("free list record count %lu != share->deleted %lu", del_link_count, (ulong) share->deleted)); error= 1; } @@ -105,7 +118,72 @@ int heap_check_heap(const HP_INFO *info, my_bool print_status) current_ptr= hp_find_block(&share->block, pos); if (!current_ptr[share->visible]) - deleted++; + { + if (hp_is_free_block_start(current_ptr)) + { + uint16 block_count= hp_free_block_start_count(current_ptr); + uchar *block_last; + + if (block_count < 2 || + pos + block_count > share->total_records + share->deleted) + { + if (!block_count_error_printed) + { + DBUG_PRINT("error", + ("Block at pos: %lu invalid count: %u " + "(total_records+deleted=%lu)", + pos, (uint) block_count, + (ulong)(share->total_records + share->deleted))); + block_count_error_printed= TRUE; + } + error= 1; + deleted++; + goto next_record; + } + + block_last= current_ptr + + (uint32)(block_count - 1) * share->block.recbuffer; + + if (!(block_last[HP_DEL_FLAG_OFFSET] & HP_DEL_BLOCK_END)) + { + DBUG_PRINT("error", + ("Block at pos %lu: last record missing " + "HP_DEL_BLOCK_END flag", pos)); + error= 1; + } +#ifndef DBUG_OFF + { + uchar *dark; + for (dark= current_ptr + share->block.recbuffer; + dark < block_last; dark+= share->block.recbuffer) + { + if (dark[HP_DEL_FLAG_OFFSET] != 0) + { + DBUG_PRINT("error", + ("Dark record at pos %lu+%u has non-zero del_flag", + pos, + (uint)((dark - current_ptr) / + share->block.recbuffer))); + error= 1; + } + if (dark[share->visible] != 0) + { + DBUG_PRINT("error", + ("Dark record at pos %lu+%u has non-zero visible", + pos, + (uint)((dark - current_ptr) / + share->block.recbuffer))); + error= 1; + } + } + } +#endif + deleted+= block_count; + pos+= block_count - 1; + } + else + deleted++; + } else if (hp_is_cont(current_ptr, share->visible)) { /* @@ -157,6 +235,7 @@ int heap_check_heap(const HP_INFO *info, my_bool print_status) } } } +next_record: ; } if (records != share->records || deleted != share->deleted) diff --git a/storage/heap/heapdef.h b/storage/heap/heapdef.h index 1e77dc8f105..e5d6115ef64 100644 --- a/storage/heap/heapdef.h +++ b/storage/heap/heapdef.h @@ -46,7 +46,7 @@ C_MODE_START Continuation run header: next_cont pointer + run_rec_count. Stored at the beginning of the first blob segment in each run. */ -#define HP_CONT_REC_COUNT_SIZE sizeof(uint16) +#define HP_CONT_REC_COUNT_SIZE 2 #define HP_CONT_HEADER_SIZE (sizeof(uchar*) + HP_CONT_REC_COUNT_SIZE) /* @@ -93,12 +93,13 @@ static inline my_bool hp_is_multi_run(const uchar *rec, uint visible) /* Continuation run header accessors. Read next_cont pointer and run_rec_count from the first record of a run. + In heap, all delete/chain link pointers in records are 8 byte aligned + and thus safe to access directly. */ + static inline const uchar *hp_cont_next(const uchar *chain) { - const uchar *next; - memcpy(&next, chain, sizeof(uchar*)); - return next; + return *((const uchar**) chain); } static inline uint16 hp_cont_rec_count(const uchar *chain) @@ -151,6 +152,148 @@ static inline uint32 hp_blob_min_run_bytes(uint32 blob_length) return length; } +/* + Delete-list block metadata. + + Deleted records store a del_link pointer in bytes 0..sizeof(uchar*)-1 + and a flags byte at offset 'visible'. Block coalescing reuses bytes + after del_link for block metadata: + byte HP_DEL_FLAG_OFFSET (8): del_flag (block-start / block-end bits) + bytes HP_DEL_COUNT_OFFSET (9-10): block record count (uint16, LE) + + HP_DEL_METADATA_SIZE (11) is the minimum 'visible' offset needed to + guarantee these bytes never overlap with the flags byte. +*/ +#define HP_DEL_FLAG_OFFSET sizeof(uchar*) +#define HP_DEL_BLOCK_END 1 /* bit 0: last record of block */ +#define HP_DEL_BLOCK_START 2 /* bit 1: first record of block */ +#define HP_DEL_COUNT_OFFSET (sizeof(uchar*) + 1) +#define HP_DEL_METADATA_SIZE (sizeof(uchar*) + 1 + HP_CONT_REC_COUNT_SIZE) + +/* + Free-list block predicates and accessors. + A contiguous group of N >= 2 deleted records is represented as a block: + rec[0] (first): del_flag = HP_DEL_BLOCK_START, del_link = next entry, + count at bytes 9-10 + rec[1..N-2] (dark): bytes 0-10 zeroed, pos[visible] = 0 + rec[N-1] (last): del_flag = HP_DEL_BLOCK_END, del_link = &rec[0] + share->del_link = &rec[N-1] + Single deleted records have del_flag = 0 (unchanged behavior). +*/ + +static inline my_bool hp_is_free_block_end(const uchar *pos) +{ + return (pos[HP_DEL_FLAG_OFFSET] & HP_DEL_BLOCK_END) != 0; +} + +static inline uchar *hp_free_block_first(uchar *pos) +{ + return *((uchar**) pos); +} + +static inline my_bool hp_is_free_block_start(const uchar *pos) +{ + return (pos[HP_DEL_FLAG_OFFSET] & HP_DEL_BLOCK_START) != 0; +} + +static inline uint16 hp_free_block_start_count(const uchar *pos) +{ + return uint2korr(pos + HP_DEL_COUNT_OFFSET); +} + +/* + Clear the metadata bytes of dark records (records between block-start + and block-end that are not individually on the free list). + Uses a strided loop so only the essential bytes per record are touched. + For short record lengths a contiguous bzero over the full range would + be faster, but the crossover point has not been measured. +*/ + +static inline void hp_clear_dark_records(uchar *from, uchar *to, + uint recbuffer, uint visible) +{ + uchar *pos; + for (pos= from; pos < to; pos+= recbuffer) + { + *((uchar**) pos)= NULL; + pos[HP_DEL_FLAG_OFFSET]= 0; + pos[visible]= 0; + } +} + +/* + Push a single deleted record onto the free list. + Replaces the inlined 3-line pattern in heap_delete(), heap_write() + error path, and hp_free_run_chain() for single-record runs. +*/ + +static inline void hp_push_free_record(HP_SHARE *share, uchar *pos) +{ + *((uchar**) pos)= share->del_link; + pos[HP_DEL_FLAG_OFFSET]= 0; + share->del_link= pos; + pos[share->visible]= 0; + share->deleted++; + share->total_records--; +} + +/* + Push a contiguous block of deleted records onto the free list. + Defined in hp_delete.c. +*/ +extern void hp_push_free_block(HP_SHARE *share, uchar *first, uint16 count); + +/* + Push a contiguous block (count >= 2) or single record (count == 1) + onto the free list, coalescing with the head entry if adjacent. + Defined in hp_delete.c. +*/ +extern void hp_push_free_block_coalesce(HP_SHARE *share, uchar *first, + uint16 count); + +/* + Push a deleted record onto the free list, coalescing with the head + entry if adjacent. Delegates to hp_push_free_block_coalesce with + count=1; both merge branches produce correct results for a single + record (dark-record clearing ranges become empty no-ops where no + dark records exist). +*/ + +static inline void hp_push_free_record_coalesce(HP_SHARE *share, uchar *pos) +{ + hp_push_free_block_coalesce(share, pos, 1); +} + +/* + Take 'count' contiguous records from the head block. + Defined in hp_write.c. +*/ +extern uchar *hp_take_free_block(HP_SHARE *share, uint16 count); + +/* + Pop one record from the head of the free list. + If the head is a block-end, takes the last record and shrinks + or collapses the block. +*/ + +static inline uchar *hp_pop_free_record(HP_SHARE *share) +{ + uchar *pos= share->del_link; + + DBUG_ASSERT(pos); + + if (!hp_is_free_block_end(pos)) + { + /* Single record */ + share->del_link= *((uchar**) pos); + share->deleted--; + share->total_records++; + return pos; + } + + return hp_take_free_block(share, 1); +} + /* Some extern variables */ extern LIST *heap_open_list,*heap_share_list; diff --git a/storage/heap/hp_blob.c b/storage/heap/hp_blob.c index e0237ffc319..d004c26ff1e 100644 --- a/storage/heap/hp_blob.c +++ b/storage/heap/hp_blob.c @@ -82,11 +82,30 @@ void hp_shrink_tail(HP_SHARE *share) while (share->del_link == tail_pos) { - share->del_link= *((uchar**) share->del_link); - share->deleted--; - block->last_allocated--; - tail_pos-= recbuffer; - block_pos--; + ulong reclaim_count; + + if (hp_is_free_block_end(share->del_link)) + { + /* + Block entry at tail: reclaim the entire block at once. + Blocks from hp_free_run_chain() are always within a single + leaf (hp_alloc_from_tail caps at the leaf boundary), so the + block cannot span leaf boundaries. + */ + uchar *first= hp_free_block_first(share->del_link); + reclaim_count= hp_free_block_start_count(first); + share->del_link= *((uchar**) first); + } + else + { + reclaim_count= 1; + share->del_link= *((uchar**) share->del_link); + } + + block->last_allocated-= reclaim_count; + tail_pos-= reclaim_count * recbuffer; + block_pos-= reclaim_count; + share->deleted-= reclaim_count; /* When the current leaf block becomes empty (block_pos has reached 0), @@ -153,14 +172,12 @@ void hp_flush_pending_blob_free_impl(HP_INFO *info) void hp_free_run_chain(HP_SHARE *share, uchar *chain) { - uint recbuffer= share->block.recbuffer; uint visible= share->visible; while (chain) { uchar *next_run; uint16 run_rec_count; - uint16 j; if (hp_is_single_rec(chain, visible)) { @@ -171,19 +188,14 @@ void hp_free_run_chain(HP_SHARE *share, uchar *chain) else { /* Case B/C: header present with next_cont and run_rec_count */ - memcpy(&next_run, chain, sizeof(uchar*)); - run_rec_count= uint2korr(chain + sizeof(uchar*)); + next_run= (uchar*) hp_cont_next(chain); + run_rec_count= hp_cont_rec_count(chain); } - for (j= 0; j < run_rec_count; j++) - { - uchar *pos= chain + j * recbuffer; - *((uchar**) pos)= share->del_link; - share->del_link= pos; - pos[visible]= 0; - share->deleted++; - share->total_records--; - } + if (run_rec_count == 1) + hp_push_free_record_coalesce(share, chain); + else + hp_push_free_block_coalesce(share, chain, run_rec_count); chain= next_run; } @@ -286,42 +298,68 @@ static void hp_write_run_data(HP_SHARE *share, const uchar *data, /* - Unlink a contiguous group from the delete list and write blob data into it. - Does not support zerocopy (always uses HP_ROW_MULTIPLE_REC format). + Take runs from the free list and write blob data into them. + + Iterates the free list, accepting blocks with at least min_avail + records and singles (avail=1). Stops when all data is written, + the free list is exhausted, or a too-small entry is found. @param share Table share @param data_ptr Blob data @param data_len Total blob data length - @param run_start Lowest address of the contiguous group - @param run_count Number of contiguous records in the group + @param min_avail Minimum block size to accept (1 = accept anything) @param data_offset [in/out] Current offset into blob data - @param first_run [out] Pointer to first run; undefined until return + @param first_run [in/out] Pointer to first run @param prev_run_start [in/out] Pointer to previous run's start */ -static void hp_unlink_and_write_run(HP_SHARE *share, const uchar *data_ptr, - uint32 data_len, uchar *run_start, - uint16 run_count, uint32 *data_offset, - uchar **first_run, uchar **prev_run_start) +static void hp_take_free_list_runs(HP_SHARE *share, const uchar *data_ptr, + uint32 data_len, uint16 min_avail, + uint32 *data_offset, + uchar **first_run, uchar **prev_run_start) { - uint recbuffer= share->block.recbuffer; - DBUG_ASSERT(share->del_link == run_start + (run_count-1) * recbuffer); - DBUG_ASSERT(share->del_link >= run_start && - share->del_link < run_start + run_count * recbuffer); + uint32 recbuffer= share->block.recbuffer; + uint32 first_payload= share->visible - HP_CONT_HEADER_SIZE; - share->del_link= *(uchar**) (share->del_link - - (run_count-1) * recbuffer); - share->deleted-= run_count; - share->total_records+= run_count; + while (share->del_link && *data_offset < data_len) + { + uchar *pos= share->del_link; + uchar *run_start; + uint16 avail; + uint16 take; + uint32 remaining= data_len - *data_offset; + uint32 remaining_records= + (remaining <= first_payload ? 1 : + 1 + (remaining - first_payload + recbuffer - 1) / recbuffer); - hp_write_run_data(share, data_ptr, data_len, run_start, - run_count, HP_ROW_MULTIPLE_REC, data_offset); + if (hp_is_free_block_end(pos)) + avail= hp_free_block_start_count(hp_free_block_first(pos)); + else + avail= 1; - if (*prev_run_start) - memcpy(*prev_run_start, &run_start, sizeof(run_start)); - else - *first_run= run_start; - *prev_run_start= run_start; + if (avail < min_avail) + break; + + take= (uint16) MY_MIN((uint32) avail, + MY_MIN(remaining_records, (uint32) UINT_MAX16)); + + if (avail > 1) + run_start= hp_take_free_block(share, take); + else + { + DBUG_ASSERT(take == 1); + run_start= hp_pop_free_record(share); + } + + hp_write_run_data(share, data_ptr, data_len, run_start, + take, HP_ROW_MULTIPLE_REC, data_offset); + + if (*prev_run_start) + memcpy(*prev_run_start, &run_start, sizeof(run_start)); + else + *first_run= run_start; + *prev_run_start= run_start; + } } @@ -387,75 +425,16 @@ int hp_write_one_blob(HP_SHARE *share, const uchar *data_ptr, /* Step 1: Try to allocate contiguous runs from the top of the delete list. - Peek at delete list records by walking next pointers without unlinking. - Track contiguous groups (descending addresses - LIFO order from - hp_free_run_chain). On discontinuity: if the group qualifies - (>= min_run_records), unlink and use it; if it doesn't, the tail - of the delete_link is too small. Instead of continue searching - for a larger block, we stop searching. + With block-coalesced free lists, each block entry encodes contiguity + directly (count + boundaries). No per-record walk needed -- just + inspect each free-list entry and take from it if large enough. + + remaining_records is recomputed each iteration because each take + creates a separate run with its own HP_CONT_HEADER_SIZE overhead, + so the per-run payload is less than in a single-run estimate. */ - { - uchar *run_start; - uint16 run_count= 1; - uchar *pos; - uint32 max_run= MY_MIN(total_records_needed, UINT_MAX16); - - if ((run_start= share->del_link)) - { - pos= *((uchar**) run_start); - run_count= 1; - for (; pos ; pos= *((uchar**) pos)) - { - /* - Only check descending direction: hp_free_run_chain() frees - records in ascending address order (j=0..N), so LIFO pushes - them onto the delete list in reverse - consecutive delete - list entries have descending addresses. Ascending adjacency - from unrelated deletes is ignored intentionally; we only - recover runs that were freed together. - */ - if (run_count == total_records_needed) - break; /* Use this run */ - - if (pos == run_start - recbuffer) - { - run_start= pos; - run_count++; - if (run_count < max_run) - continue; - if (run_count == total_records_needed) - break; /* Use this run */ - /* run_count is now UINT_MAX16 */ - } - - /* - Discontinuity. If the accumulated group qualifies, use it. - If not, the top of the delete list is fragmented - give up entirely. - */ - if (run_count < min_run_records) - break; - hp_unlink_and_write_run(share, data_ptr, data_len, run_start, - run_count, &data_offset, - &first_run, &prev_run_start); - - pos= share->del_link; - total_records_needed-= run_count; - - /* This cannot be last run */ - DBUG_ASSERT(data_offset < data_len && pos); - DBUG_ASSERT(total_records_needed != 0); - - run_start= pos; - run_count= 1; - } - - /* Handle the last group after the loop ends */ - if (run_count >= min_run_records && data_offset < data_len) - hp_unlink_and_write_run(share, data_ptr, data_len, run_start, - run_count, &data_offset, - &first_run, &prev_run_start); - } - } + hp_take_free_list_runs(share, data_ptr, data_len, min_run_records, + &data_offset, &first_run, &prev_run_start); /* Step 2: Allocate remaining data from the block tail. @@ -541,41 +520,10 @@ int hp_write_one_blob(HP_SHARE *share, const uchar *data_ptr, Step 3: Free-list scavenge fallback. When the tail is full but there are deleted records on the free list, - walk the entire free list accepting any contiguous group (even a - single slot). This produces maximally fragmented chains (many short - runs, Case C), which are slower to read but correct. Failing with - table-full when free slots exist is worse than a fragmented chain. + take whatever blocks or singles remain, accepting any size. */ - while (data_offset < data_len && share->del_link) - { - uchar *run_start= share->del_link; - uchar *pos= *((uchar**) run_start); - uint16 run_count= 1; - uint32 remaining= data_len - data_offset; - uint32 remaining_records= - (remaining <= first_payload ? 1 : - 1 + (remaining - first_payload + recbuffer - 1) / recbuffer); - uint32 max_run= MY_MIN(remaining_records, UINT_MAX16); - - /* Traverse the delete link list */ - for (; pos; pos= *((uchar**) pos)) - { - if (run_count >= max_run) - break; - if (pos == run_start - recbuffer) - { - /* Found block directly before current block. Combine them */ - run_start= pos; - run_count++; - continue; - } - break; - } - - hp_unlink_and_write_run(share, data_ptr, data_len, run_start, - run_count, &data_offset, - &first_run, &prev_run_start); - } + hp_take_free_list_runs(share, data_ptr, data_len, 1, + &data_offset, &first_run, &prev_run_start); if (data_offset < data_len) { @@ -686,7 +634,6 @@ static void hp_reassemble_chain(const uchar *chain, uint32 data_len, uint32 remaining= data_len; while (chain && remaining > 0) { - uint16 rec; uint16 run_rec_count; uint32 chunk; const uchar *next_cont; @@ -703,16 +650,19 @@ static void hp_reassemble_chain(const uchar *chain, uint32 data_len, remaining-= chunk; /* Inner records: recbuffer stride, no flags byte */ - for (rec= 1; rec < run_rec_count; rec++) { - const uchar *rec_ptr= chain + rec * recbuffer; - DBUG_ASSERT(remaining != 0); - chunk= recbuffer; - if (chunk > remaining) - chunk= remaining; - memcpy(out, rec_ptr, chunk); - out+= chunk; - remaining-= chunk; + const uchar *rec_end= chain + (uint32) run_rec_count * recbuffer; + const uchar *rec_ptr; + for (rec_ptr= chain + recbuffer; rec_ptr < rec_end; rec_ptr+= recbuffer) + { + DBUG_ASSERT(remaining != 0); + chunk= recbuffer; + if (chunk > remaining) + chunk= remaining; + memcpy(out, rec_ptr, chunk); + out+= chunk; + remaining-= chunk; + } } chain= next_cont; diff --git a/storage/heap/hp_create.c b/storage/heap/hp_create.c index c0f22a92cd7..639af285751 100644 --- a/storage/heap/hp_create.c +++ b/storage/heap/hp_create.c @@ -71,20 +71,13 @@ int heap_create(const char *name, HP_CREATE_INFO *create_info, DBUG_PRINT("info",("Initializing new table")); /* - We have to store sometimes uchar* del_link in records, - so the visible_offset must be least at sizeof(uchar*) + Deleted records store del_link (sizeof(uchar*) bytes), a block + flags byte, and a uint16 block count. visible_offset must be + at least HP_DEL_METADATA_SIZE so that these fields never overlap + the flags byte at offset 'visible'. This also satisfies the + blob continuation header requirement (HP_CONT_HEADER_SIZE + 1). */ - visible_offset= MY_MAX(reclength, sizeof (char*)); - /* - Blob tables store continuation run headers (next_cont pointer + - run_slots count = HP_CONT_HEADER_SIZE bytes) in each run's first - slot. Ensure at least 1 byte of payload beyond the header, - otherwise hp_write_run_data() underflows computing - chunk = visible - HP_CONT_HEADER_SIZE. Only matters for - pathological single-TINYBLOB tables (reclength as low as 9). - */ - if (create_info->blob_count) - visible_offset= MY_MAX(visible_offset, HP_CONT_HEADER_SIZE + 1); + visible_offset= MY_MAX(reclength, HP_DEL_METADATA_SIZE); for (i= key_segs= max_length= 0, keyinfo= keydef; i < keys; i++, keyinfo++) { @@ -337,10 +330,16 @@ static int keys_compare(void *heap_rb_, const void *key1_, size_t hp_memory_needed_per_row(size_t reclength) { - /* Data needed for storing record + pointer to records */ - reclength= MY_MAX(reclength, sizeof(char*)); - /* The + 1 below is for the delete marker at the end of record*/ - reclength= MY_ALIGN(reclength+1, sizeof(char*)); + /* + Must accommodate del_link + del_flag + block count. + The + 1 below is for the required visibility byte at the end of each record. + */ + reclength= MY_MAX(reclength + 1, HP_DEL_METADATA_SIZE); + /* + Record has to be aligned for faster memcpy and also for allowing + direct access to delete link/chain at start of record. + */ + reclength= MY_ALIGN(reclength, sizeof(char*)); return reclength; } diff --git a/storage/heap/hp_delete.c b/storage/heap/hp_delete.c index 96cd5c75434..5dbd3590c8c 100644 --- a/storage/heap/hp_delete.c +++ b/storage/heap/hp_delete.c @@ -17,6 +17,127 @@ #include "heapdef.h" +/* + Push a contiguous block of deleted records onto the free list. + 'first' is the lowest address; the block spans count records + at stride share->block.recbuffer. +*/ + +void hp_push_free_block(HP_SHARE *share, uchar *first, uint16 count) +{ + uint recbuffer= share->block.recbuffer; + uint visible= share->visible; + uchar *last= first + (uint32)(count - 1) * recbuffer; + + DBUG_ASSERT(count >= 2); + + /* First record: block-start with chain to next free-list entry */ + *((uchar**) first)= share->del_link; + first[HP_DEL_FLAG_OFFSET]= HP_DEL_BLOCK_START; + int2store(first + HP_DEL_COUNT_OFFSET, count); + first[visible]= 0; + + /* Dark records: clear metadata bytes at stride recbuffer */ + hp_clear_dark_records(first + recbuffer, last, recbuffer, visible); + + /* Last record: block-end with back-pointer to first */ + *((uchar**) last)= first; + last[HP_DEL_FLAG_OFFSET]= HP_DEL_BLOCK_END; + last[visible]= 0; + + share->del_link= last; + share->deleted+= count; + share->total_records-= count; +} + + +/* + Push a contiguous block (count >= 2) or single record (count == 1) + onto the free list, coalescing with the head entry if adjacent. + Normalizes the head by treating a single record as a block of count 1, + then checks adjacency in two directions (above/below). + Falls back to hp_push_free_block/hp_push_free_record when no adjacency. +*/ + +void hp_push_free_block_coalesce(HP_SHARE *share, uchar *first, + uint16 count) +{ + uchar *head= share->del_link; + uint recbuffer= share->block.recbuffer; + uchar *last= first + (uint32)(count - 1) * recbuffer; + + DBUG_ASSERT(count >= 1); + + if (head) + { + /* Normalize head: treat single record as a block of count 1 */ + uchar *head_first; + uint16 head_count; + uint32 combined; + + if (hp_is_free_block_end(head)) + { + head_first= hp_free_block_first(head); + head_count= hp_free_block_start_count(head_first); + } + else + { + head_first= head; + head_count= 1; + } + combined= (uint32) head_count + count; + + if (combined <= UINT_MAX16) + { + if (head == first - recbuffer) + { + /* Head immediately below new block: extend upward */ + hp_clear_dark_records((head_count > 1 ? head : first), last, + recbuffer, share->visible); + + *((uchar**) last)= head_first; + last[HP_DEL_FLAG_OFFSET]= HP_DEL_BLOCK_END; + last[share->visible]= 0; + + head_first[HP_DEL_FLAG_OFFSET]= HP_DEL_BLOCK_START; + int2store(head_first + HP_DEL_COUNT_OFFSET, (uint16) combined); + + share->del_link= last; + share->deleted+= count; + share->total_records-= count; + return; + } + + if (head_first == last + recbuffer) + { + /* Head immediately above new block: extend downward */ + uchar *next_entry= *((uchar**) head_first); + + hp_clear_dark_records(first + recbuffer, head_first + recbuffer, + recbuffer, share->visible); + + *((uchar**) first)= next_entry; + first[HP_DEL_FLAG_OFFSET]= HP_DEL_BLOCK_START; + int2store(first + HP_DEL_COUNT_OFFSET, (uint16) combined); + first[share->visible]= 0; + + *((uchar**) head)= first; + head[HP_DEL_FLAG_OFFSET]= HP_DEL_BLOCK_END; + + share->deleted+= count; + share->total_records-= count; + return; + } + } + } + + if (count == 1) + hp_push_free_record(share, first); + else + hp_push_free_block(share, first, count); +} + + int heap_delete(HP_INFO *info, const uchar *record) { uchar *pos; @@ -83,11 +204,7 @@ int heap_delete(HP_INFO *info, const uchar *record) } } info->update=HA_STATE_DELETED; - *((uchar**) pos)=share->del_link; - share->del_link=pos; - pos[share->visible]=0; /* Record deleted */ - share->deleted++; - share->total_records--; + hp_push_free_record_coalesce(share, pos); share->key_version++; #if !defined(DBUG_OFF) && defined(EXTRA_HEAP_DEBUG) DBUG_EXECUTE("check_heap",heap_check_heap(info, 0);); diff --git a/storage/heap/hp_scan.c b/storage/heap/hp_scan.c index 486a146eec9..12347c9bf01 100644 --- a/storage/heap/hp_scan.c +++ b/storage/heap/hp_scan.c @@ -93,6 +93,16 @@ retry: } if (!info->current_ptr[share->visible]) { + if (hp_is_free_block_start(info->current_ptr)) + { + uint16 block_count= hp_free_block_start_count(info->current_ptr); + uint skip; + DBUG_ASSERT(block_count > 1); + skip= block_count - 1; + info->current_record+= skip; + info->current_ptr+= skip * share->block.recbuffer; + goto retry; + } DBUG_PRINT("warning",("Found deleted record")); info->update= HA_STATE_PREV_FOUND | HA_STATE_NEXT_FOUND; DBUG_RETURN(my_errno=HA_ERR_RECORD_DELETED); diff --git a/storage/heap/hp_test_freelist-t.c b/storage/heap/hp_test_freelist-t.c index d3eb16f9ca4..9edecdad551 100644 --- a/storage/heap/hp_test_freelist-t.c +++ b/storage/heap/hp_test_freelist-t.c @@ -800,11 +800,1010 @@ static void test_block_reuse_after_reclaim(void) } +/* + Test: block formation after blob chain free. + + Insert a blob row (100 bytes, 8 continuation records) and a guard + row. Delete the blob row. The continuation chain becomes a block + on the free list. Verify block-start/end flags, count, dark record + zeroing, and heap_check_heap. +*/ + +static void test_block_formation(void) +{ + HP_SHARE *share; + HP_INFO *info; + uchar rec[REC_LENGTH]; + uchar blob_data[100]; + uchar *pos0, *pos8; + uint visible; + uint16 j; + + memset(blob_data, 'B', sizeof(blob_data)); + + if (create_and_open("test_block_form", &share, &info)) + { + ok(0, "setup failed: %d", my_errno); + skip(9, "setup failed"); + return; + } + + visible= share->visible; + + /* Insert blob row (primary at pos 0, chain at pos 1-8) */ + build_record(rec, 1, blob_data, sizeof(blob_data)); + ok(heap_write(info, rec) == 0, "insert blob row"); + + /* Guard row at pos 9 prevents hp_shrink_tail from reclaiming */ + build_record(rec, 2, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert guard row"); + + /* Delete the blob row (deferred blob free for non-internal table) */ + { + uchar key[4]; + int4store(key, 1); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found blob row for deletion"); + ok(heap_delete(info, rec) == 0, "deleted blob row"); + } + + /* Flush deferred blob free to materialize the block */ + hp_flush_pending_blob_free(info); + + /* + Free list: block(pos0-pos8, count=9) -> NULL + The primary (pos0) coalesced with the chain block (pos1-8) + via hp_push_free_block_coalesce in hp_free_run_chain. + */ + pos0= hp_find_block(&share->block, 0); + pos8= hp_find_block(&share->block, 8); + + ok(hp_is_free_block_start(pos0), + "pos 0 has HP_DEL_BLOCK_START flag (primary coalesced with chain)"); + ok(hp_free_block_start_count(pos0) == 9, + "block count == 9 (got %u)", hp_free_block_start_count(pos0)); + ok(hp_is_free_block_end(pos8), + "pos 8 has HP_DEL_BLOCK_END flag"); + ok(hp_free_block_first(pos8) == pos0, + "block-end del_link points to block-start (pos0)"); + + /* Verify dark records (pos 1-7) have del_flag == 0 and visible == 0 */ + { + my_bool dark_ok= TRUE; + for (j= 1; j <= 7; j++) + { + uchar *dark= hp_find_block(&share->block, j); + if (dark[HP_DEL_FLAG_OFFSET] != 0 || dark[visible] != 0) + dark_ok= FALSE; + } + ok(dark_ok, "dark records (pos 1-7) have zeroed metadata"); + } + + ok(heap_check_heap(info, 0) == 0, + "heap_check_heap validates block structure"); + + heap_drop_table(info); + heap_close(info); +} + + +/* + Test: hp_pop_free_record from a block shrinks it. + + After creating a block(8) on the free list, pop records one at a time. + Verify the block shrinks correctly: block-end moves, count decreases, + dark records are promoted. +*/ + +static void test_pop_from_block(void) +{ + HP_SHARE *share; + HP_INFO *info; + uchar rec[REC_LENGTH]; + uchar blob_data[100]; + uchar *pos0, *pos7; + + memset(blob_data, 'P', sizeof(blob_data)); + + if (create_and_open("test_pop_block", &share, &info)) + { + ok(0, "setup failed: %d", my_errno); + skip(8, "setup failed"); + return; + } + + /* Insert blob row + guard */ + build_record(rec, 1, blob_data, sizeof(blob_data)); + ok(heap_write(info, rec) == 0, "insert blob row"); + build_record(rec, 2, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert guard row"); + + /* Delete blob row */ + { + uchar key[4]; + int4store(key, 1); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found blob row"); + ok(heap_delete(info, rec) == 0, "deleted blob row"); + } + hp_flush_pending_blob_free(info); + /* Free list: block(pos0-pos8, count=9) -> NULL (primary coalesced) */ + + ok(hp_is_free_block_end(share->del_link), + "free list head is block-end after flush"); + + /* Pop from block head by inserting a non-blob row */ + build_record(rec, 3, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, + "insert non-blob row (pops block-end pos8)"); + + /* Block should have shrunk: 9 -> 8, new end at pos7 */ + pos0= hp_find_block(&share->block, 0); + pos7= hp_find_block(&share->block, 7); + ok(share->del_link == pos7 && + hp_is_free_block_end(pos7) && + hp_free_block_start_count(pos0) == 8, + "block shrank to 8 records (end at pos7, count=%u)", + hp_free_block_start_count(pos0)); + + /* Pop again: 8 -> 7 */ + build_record(rec, 4, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, + "insert non-blob row (pops from block again)"); + + ok(heap_check_heap(info, 0) == 0, + "heap_check_heap validates after pops"); + + heap_drop_table(info); + heap_close(info); +} + + +/* + Test: block collapse to single record on pop. + + Create a 3-record block (primary + 2 continuation records from a + 16-byte blob, coalesced via hp_push_free_block_coalesce). Pop + records until only one remains and it collapses to a single. +*/ + +static void test_block_collapse_to_single(void) +{ + HP_SHARE *share; + HP_INFO *info; + uchar rec[REC_LENGTH]; + uchar blob_data[16]; + uchar *pos0; + + memset(blob_data, 'C', sizeof(blob_data)); + + if (create_and_open("test_block_collapse", &share, &info)) + { + ok(0, "setup failed: %d", my_errno); + skip(8, "setup failed"); + return; + } + + /* Insert 16-byte blob (2 continuation records: pos 1-2) */ + build_record(rec, 1, blob_data, sizeof(blob_data)); + ok(heap_write(info, rec) == 0, "insert 16-byte blob row"); + + /* Guard row at pos 3 */ + build_record(rec, 2, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert guard row"); + + /* Delete blob row */ + { + uchar key[4]; + int4store(key, 1); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found blob row"); + ok(heap_delete(info, rec) == 0, "deleted blob row"); + } + hp_flush_pending_blob_free(info); + /* + Free list: block(pos0-pos2, count=3) -> NULL + Primary coalesced with chain via hp_push_free_block_coalesce. + */ + + /* Pop from 3-record block: takes pos2, block shrinks to 2 */ + build_record(rec, 3, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "pop from 3-record block"); + + /* Pop again: takes pos1, collapses pos0 to single */ + build_record(rec, 4, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "pop from 2-record block"); + + pos0= hp_find_block(&share->block, 0); + ok(share->del_link == pos0 && pos0[HP_DEL_FLAG_OFFSET] == 0, + "remaining record collapsed to single (del_flag=0)"); + + /* Pop pos0 (single) */ + build_record(rec, 5, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "pop collapsed single"); + ok(share->del_link == NULL, "free list empty after all pops"); + + ok(heap_check_heap(info, 0) == 0, + "heap_check_heap validates after collapse"); + + heap_drop_table(info); + heap_close(info); +} + + +/* + Test: partial block take via blob reinsert. + + Create a block(8) on the free list, then insert a new blob that + needs fewer records. hp_take_free_block takes a partial block. + Verify the remaining block is correct. +*/ + +static void test_partial_block_take(void) +{ + HP_SHARE *share; + HP_INFO *info; + uchar rec[REC_LENGTH]; + uchar blob_data_big[100]; + uchar blob_data_small[50]; + uchar *pos0; + ulong alloc_before; + + memset(blob_data_big, 'G', sizeof(blob_data_big)); + memset(blob_data_small, 'S', sizeof(blob_data_small)); + + if (create_and_open("test_partial_take", &share, &info)) + { + ok(0, "setup failed: %d", my_errno); + skip(7, "setup failed"); + return; + } + + /* Insert 100-byte blob (pos 0=primary, pos 1-8=chain) + guard */ + build_record(rec, 1, blob_data_big, sizeof(blob_data_big)); + ok(heap_write(info, rec) == 0, "insert 100-byte blob"); + build_record(rec, 2, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert guard row"); + + alloc_before= (ulong) share->block.last_allocated; + + /* Delete blob row */ + { + uchar key[4]; + int4store(key, 1); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found blob row"); + ok(heap_delete(info, rec) == 0, "deleted blob row"); + } + hp_flush_pending_blob_free(info); + /* Free list: block(pos0-pos8, count=9) -> NULL (primary coalesced) */ + + /* Insert 50-byte blob: pops from coalesced block for chain + primary */ + build_record(rec, 3, blob_data_small, sizeof(blob_data_small)); + ok(heap_write(info, rec) == 0, "insert 50-byte blob (partial block take)"); + + /* No tail growth: all from free list */ + ok(share->block.last_allocated == alloc_before, + "last_allocated unchanged (all from free list)"); + + /* Remaining block should be smaller */ + pos0= hp_find_block(&share->block, 0); + ok(hp_is_free_block_start(pos0) || share->del_link == pos0, + "remaining records still on free list (block or single)"); + + ok(heap_check_heap(info, 0) == 0, + "heap_check_heap validates after partial take"); + + /* Verify blob data integrity */ + { + uchar key[4]; + uchar read_buf[REC_LENGTH]; + uint32 read_len; + const uchar *read_ptr; + + int4store(key, 3); + ok(heap_rkey(info, read_buf, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found reinserted row"); + read_len= uint2korr(read_buf + BLOB_OFFSET); + memcpy(&read_ptr, read_buf + BLOB_OFFSET + BLOB_PACKLEN, sizeof(read_ptr)); + ok(read_len == 50 && memcmp(read_ptr, blob_data_small, 50) == 0, + "blob data matches after partial block take"); + } + + heap_drop_table(info); + heap_close(info); +} + + +/* + Test: scan batch-skip for deleted blocks. + + Insert active rows, then a blob row, then more active rows. Delete + the blob row to create a block in the middle. Scan should return + only the active rows, batch-skipping the block. +*/ + +static void test_scan_batch_skip_block(void) +{ + HP_SHARE *share; + HP_INFO *info; + uchar rec[REC_LENGTH]; + uchar blob_data[100]; + int scan_count; + int err; + int32 i; + + memset(blob_data, 'K', sizeof(blob_data)); + + if (create_and_open("test_scan_skip", &share, &info)) + { + ok(0, "setup failed: %d", my_errno); + skip(5, "setup failed"); + return; + } + + /* Insert 3 active rows (pos 0, 1, 2) */ + for (i= 0; i < 3; i++) + { + build_record(rec, i, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert row %d", (int) i); + } + + /* Insert blob row (primary at pos 3, chain at pos 4-11) */ + build_record(rec, 100, blob_data, sizeof(blob_data)); + ok(heap_write(info, rec) == 0, "insert blob row"); + + /* Insert 2 more active rows (pos 12, 13) */ + for (i= 10; i < 12; i++) + { + build_record(rec, i, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert row %d", (int) i); + } + + ok(share->records == 6, "6 primary rows before delete"); + + /* Delete the blob row: chain at pos 4-11 becomes block(8) */ + { + uchar key[4]; + int4store(key, 100); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found blob row"); + ok(heap_delete(info, rec) == 0, "deleted blob row"); + } + + ok(share->records == 5, "5 primary rows after delete"); + + /* Flush deferred blob free so chain becomes a block */ + hp_flush_pending_blob_free(info); + + /* Scan: should return exactly 5 rows */ + heap_scan_init(info); + scan_count= 0; + while ((err= heap_scan(info, rec)) != HA_ERR_END_OF_FILE) + { + if (err == 0) + scan_count++; + } + ok(scan_count == 5, "scan returned %d rows (expected 5)", scan_count); + + ok(heap_check_heap(info, 0) == 0, + "heap_check_heap validates with block in middle"); + + heap_drop_table(info); + heap_close(info); +} + + +/* + Test: hp_shrink_tail reclaims entire block at once. + + Insert a blob row at the tail (no guard row). Delete it. + hp_shrink_tail should reclaim the entire block in one step, + not record-by-record. +*/ + +static void test_shrink_tail_with_block(void) +{ + HP_SHARE *share; + HP_INFO *info; + uchar rec[REC_LENGTH]; + uchar blob_data[100]; + ulong alloc_after_insert, alloc_after_delete; + + memset(blob_data, 'H', sizeof(blob_data)); + + if (create_and_open("test_shrink_block", &share, &info)) + { + ok(0, "setup failed: %d", my_errno); + skip(5, "setup failed"); + return; + } + + /* Insert blob row: primary at pos 0, chain at pos 1-8 */ + build_record(rec, 1, blob_data, sizeof(blob_data)); + ok(heap_write(info, rec) == 0, "insert blob row"); + alloc_after_insert= (ulong) share->block.last_allocated; + + /* Delete blob row, then flush to trigger block free + shrink_tail */ + { + uchar key[4]; + int4store(key, 1); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found blob row"); + ok(heap_delete(info, rec) == 0, "deleted blob row"); + } + hp_flush_pending_blob_free(info); + + alloc_after_delete= (ulong) share->block.last_allocated; + /* + hp_flush_pending_blob_free -> hp_free_run_chain (pushes block) + -> hp_shrink_tail reclaims block + primary, leaving last_allocated=0. + */ + ok(alloc_after_delete == 0, + "hp_shrink_tail reclaimed all records (before=%lu, after=%lu)", + alloc_after_insert, alloc_after_delete); + ok(share->deleted == 0, "deleted == 0 (all reclaimed)"); + ok(share->del_link == NULL, "free list empty after full reclaim"); + + ok(heap_check_heap(info, 0) == 0, + "heap_check_heap validates after full reclaim"); + + heap_drop_table(info); + heap_close(info); +} + + +/* + Test: interleaved singles and blocks on free list. + + Create a free list with mixed entries: a single record, then a + block, then another single. Verify traversal and heap_check_heap. +*/ + +static void test_interleaved_singles_and_blocks(void) +{ + HP_SHARE *share; + HP_INFO *info; + uchar rec[REC_LENGTH]; + uchar blob_data[100]; + ulong del_count; + + memset(blob_data, 'I', sizeof(blob_data)); + + if (create_and_open("test_interleaved", &share, &info)) + { + ok(0, "setup failed: %d", my_errno); + skip(6, "setup failed"); + return; + } + + /* + Layout after inserts: + pos 0: non-blob row (key=10) + pos 1-8: blob chain (key=20, 100-byte blob) + pos 9: blob primary (key=20) + Actually, wait -- primary comes first. Let me re-think. + + Actually, heap_write puts the primary at the first free position, + then blob chains extend from there. So: + pos 0: primary of row key=10 (non-blob) + pos 1: primary of row key=20 (blob row) + pos 2-9: blob chain of row key=20 (8 continuation records) + pos 10: primary of row key=30 (non-blob) + */ + build_record(rec, 10, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert row 10 (non-blob)"); + build_record(rec, 20, blob_data, sizeof(blob_data)); + ok(heap_write(info, rec) == 0, "insert row 20 (blob)"); + build_record(rec, 30, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert row 30 (non-blob, guard)"); + + /* Delete row 10 (non-blob at pos 0) -> single on free list */ + { + uchar key[4]; + int4store(key, 10); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found row 10"); + ok(heap_delete(info, rec) == 0, "deleted row 10 (single)"); + } + + /* Delete row 20 (blob at pos 1) -> deferred blob chain free */ + { + uchar key[4]; + int4store(key, 20); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found row 20"); + ok(heap_delete(info, rec) == 0, "deleted row 20 (blob -> block)"); + } + hp_flush_pending_blob_free(info); + + /* + Free list: + pos9 (block-end, 8 records at pos2-pos9) -> + pos1 (single, blob primary) -> pos0 (single) -> NULL + Total deleted = 10 (8 block + 2 singles) + */ + del_count= share->deleted; + ok(del_count == 10, + "total deleted records == 10 (got %lu)", del_count); + + /* Verify free list traversal counts correctly */ + ok(heap_check_heap(info, 0) == 0, + "heap_check_heap validates interleaved singles and blocks"); + + heap_drop_table(info); + heap_close(info); +} + + +/* + Test: delete coalescing forms block from sequential ascending deletes. + + Delete rows 0, 1, 2 in ascending order. First delete pushes a single. + Second delete finds head adjacent below pos, forms a 2-block. + Third delete finds block-end adjacent below pos, extends to 3. +*/ + +static void test_delete_coalesce_ascending(void) +{ + HP_SHARE *share; + HP_INFO *info; + uchar rec[REC_LENGTH]; + uchar *pos0, *pos1, *pos2; + int32 i; + + if (create_and_open("test_coalesce_asc", &share, &info)) + { + ok(0, "setup failed: %d", my_errno); + skip(13, "setup failed"); + return; + } + + /* Insert 5 non-blob rows: pos 0-4 */ + for (i= 0; i < 5; i++) + { + build_record(rec, i, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert row %d", (int) i); + } + + pos0= hp_find_block(&share->block, 0); + pos1= hp_find_block(&share->block, 1); + pos2= hp_find_block(&share->block, 2); + + /* Delete row 0: push single */ + { + uchar key[4]; + int4store(key, 0); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found row 0"); + ok(heap_delete(info, rec) == 0, "deleted row 0"); + } + ok(share->del_link == pos0 && pos0[HP_DEL_FLAG_OFFSET] == 0, + "row 0 is single on free list"); + + /* Delete row 1: adjacent above head, form 2-block */ + { + uchar key[4]; + int4store(key, 1); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found row 1"); + ok(heap_delete(info, rec) == 0, "deleted row 1"); + } + ok(share->del_link == pos1 && hp_is_free_block_end(pos1), + "coalesced into 2-block (end at pos1)"); + ok(hp_is_free_block_start(pos0) && hp_free_block_start_count(pos0) == 2, + "block-start at pos0, count=2"); + + /* Delete row 2: adjacent above block-end, extend to 3 */ + { + uchar key[4]; + int4store(key, 2); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found row 2"); + ok(heap_delete(info, rec) == 0, "deleted row 2"); + } + ok(share->del_link == pos2 && hp_is_free_block_end(pos2), + "extended to 3-block (end at pos2)"); + ok(hp_free_block_start_count(pos0) == 3, + "block count == 3"); + ok(pos1[HP_DEL_FLAG_OFFSET] == 0, + "pos1 is dark (del_flag=0)"); + + ok(heap_check_heap(info, 0) == 0, + "heap_check_heap validates ascending coalesce"); + + heap_drop_table(info); + heap_close(info); +} + + +/* + Test: delete coalescing forms block from sequential descending deletes. + + Delete rows 4, 3, 2 in descending order. First delete pushes single. + Second finds head adjacent above pos, forms 2-block. + Third extends downward to 3. +*/ + +static void test_delete_coalesce_descending(void) +{ + HP_SHARE *share; + HP_INFO *info; + uchar rec[REC_LENGTH]; + uchar *pos2, *pos3, *pos4; + int32 i; + + if (create_and_open("test_coalesce_desc", &share, &info)) + { + ok(0, "setup failed: %d", my_errno); + skip(13, "setup failed"); + return; + } + + for (i= 0; i < 5; i++) + { + build_record(rec, i, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert row %d", (int) i); + } + + pos2= hp_find_block(&share->block, 2); + pos3= hp_find_block(&share->block, 3); + pos4= hp_find_block(&share->block, 4); + + /* Delete row 4: push single */ + { + uchar key[4]; + int4store(key, 4); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found row 4"); + ok(heap_delete(info, rec) == 0, "deleted row 4"); + } + ok(share->del_link == pos4 && pos4[HP_DEL_FLAG_OFFSET] == 0, + "row 4 is single on free list"); + + /* Delete row 3: adjacent below head, form 2-block */ + { + uchar key[4]; + int4store(key, 3); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found row 3"); + ok(heap_delete(info, rec) == 0, "deleted row 3"); + } + ok(share->del_link == pos4 && hp_is_free_block_end(pos4), + "coalesced into 2-block (end at pos4)"); + ok(hp_is_free_block_start(pos3) && hp_free_block_start_count(pos3) == 2, + "block-start at pos3, count=2"); + + /* Delete row 2: adjacent below block-start, extend downward to 3 */ + { + uchar key[4]; + int4store(key, 2); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found row 2"); + ok(heap_delete(info, rec) == 0, "deleted row 2"); + } + ok(share->del_link == pos4 && hp_is_free_block_end(pos4), + "extended to 3-block (end still at pos4)"); + ok(hp_free_block_start_count(pos2) == 3, + "block-start moved to pos2, count=3"); + ok(pos3[HP_DEL_FLAG_OFFSET] == 0, + "pos3 is dark (del_flag=0)"); + + ok(heap_check_heap(info, 0) == 0, + "heap_check_heap validates descending coalesce"); + + heap_drop_table(info); + heap_close(info); +} + + +/* + Test: non-adjacent deletes do not coalesce. + + Delete rows 0 and 2 (gap at row 1). Both should remain singles. +*/ + +static void test_delete_no_coalesce_gap(void) +{ + HP_SHARE *share; + HP_INFO *info; + uchar rec[REC_LENGTH]; + uchar *pos0, *pos2; + int32 i; + + if (create_and_open("test_no_coalesce", &share, &info)) + { + ok(0, "setup failed: %d", my_errno); + skip(7, "setup failed"); + return; + } + + for (i= 0; i < 5; i++) + { + build_record(rec, i, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert row %d", (int) i); + } + + pos0= hp_find_block(&share->block, 0); + pos2= hp_find_block(&share->block, 2); + + { + uchar key[4]; + int4store(key, 0); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found row 0"); + ok(heap_delete(info, rec) == 0, "deleted row 0"); + } + + { + uchar key[4]; + int4store(key, 2); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found row 2"); + ok(heap_delete(info, rec) == 0, "deleted row 2"); + } + + ok(share->del_link == pos2 && pos2[HP_DEL_FLAG_OFFSET] == 0, + "pos2 is single (no coalesce)"); + ok(*((uchar**) pos2) == pos0 && pos0[HP_DEL_FLAG_OFFSET] == 0, + "pos0 is single (no coalesce), linked from pos2"); + + ok(heap_check_heap(info, 0) == 0, + "heap_check_heap validates non-adjacent singles"); + + heap_drop_table(info); + heap_close(info); +} + + +/* + Test: coalesced block reused by blob insert. + + Delete 5 adjacent non-blob rows, coalescing into a 5-block. + Insert a blob row that fits in 4 continuation records. + The blob allocator should take from the coalesced block. +*/ + +static void test_coalesced_block_reuse(void) +{ + HP_SHARE *share; + HP_INFO *info; + uchar rec[REC_LENGTH]; + uchar blob_data[50]; + ulong alloc_before; + int32 i; + + memset(blob_data, 'R', sizeof(blob_data)); + + if (create_and_open("test_coalesce_reuse", &share, &info)) + { + ok(0, "setup failed: %d", my_errno); + skip(10, "setup failed"); + return; + } + + /* Insert 7 rows: 0-6 */ + for (i= 0; i < 7; i++) + { + build_record(rec, i, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert row %d", (int) i); + } + + alloc_before= (ulong) share->block.last_allocated; + + /* Delete rows 1-5 (ascending) -> coalesced 5-block */ + for (i= 1; i <= 5; i++) + { + uchar key[4]; + int4store(key, i); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found row %d", (int) i); + ok(heap_delete(info, rec) == 0, "deleted row %d", (int) i); + } + + ok(share->deleted == 5, "5 deleted records (coalesced block)"); + ok(hp_is_free_block_end(share->del_link), + "free list head is block-end"); + ok(hp_free_block_start_count( + hp_free_block_first(share->del_link)) == 5, + "coalesced block count == 5"); + + /* Insert blob row: should take from the coalesced block */ + build_record(rec, 100, blob_data, sizeof(blob_data)); + ok(heap_write(info, rec) == 0, "insert blob row (reuse coalesced block)"); + + ok(share->block.last_allocated == alloc_before, + "last_allocated unchanged: blob reused coalesced block"); + + /* Verify blob data integrity */ + { + uchar key[4]; + uchar read_buf[REC_LENGTH]; + uint32 read_len; + const uchar *read_ptr; + + int4store(key, 100); + ok(heap_rkey(info, read_buf, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found blob row"); + read_len= uint2korr(read_buf + BLOB_OFFSET); + memcpy(&read_ptr, read_buf + BLOB_OFFSET + BLOB_PACKLEN, sizeof(read_ptr)); + ok(read_len == 50 && memcmp(read_ptr, blob_data, 50) == 0, + "blob data matches after coalesced block reuse"); + } + + ok(heap_check_heap(info, 0) == 0, + "heap_check_heap validates after coalesced block reuse"); + + heap_drop_table(info); + heap_close(info); +} + + +/* + Test: scan correctly handles coalesced blocks from delete. + + Insert 8 rows. Delete the middle 4 (rows 2-5) via ascending deletes + to form a coalesced block. Scan should return only the 4 active rows, + batch-skipping the coalesced block. +*/ + +static void test_scan_coalesced_block(void) +{ + HP_SHARE *share; + HP_INFO *info; + uchar rec[REC_LENGTH]; + int32 i; + int scan_count, err; + + if (create_and_open("test_scan_coalesce", &share, &info)) + { + ok(0, "setup failed: %d", my_errno); + skip(8, "setup failed"); + return; + } + + for (i= 0; i < 8; i++) + { + build_record(rec, i, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert row %d", (int) i); + } + + /* Delete rows 2-5 ascending -> coalesced 4-block */ + for (i= 2; i <= 5; i++) + { + uchar key[4]; + int4store(key, i); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found row %d", (int) i); + ok(heap_delete(info, rec) == 0, "deleted row %d", (int) i); + } + + ok(share->records == 4, "4 active rows after deletes"); + + heap_scan_init(info); + scan_count= 0; + while ((err= heap_scan(info, rec)) != HA_ERR_END_OF_FILE) + { + if (err == 0) + scan_count++; + } + ok(scan_count == 4, "scan returned %d rows (expected 4)", scan_count); + + ok(heap_check_heap(info, 0) == 0, + "heap_check_heap validates scan with coalesced block"); + + heap_drop_table(info); + heap_close(info); +} + + +/* + Test: block-to-block coalescing via adjacent blob chain free. + + Insert two adjacent blob rows A (pos0-8) and B (pos9-17). + Delete both. When B's chain (pos10-17) is freed, the primary (pos9) + has already coalesced with A's chain block via record coalescing. + The block push of B's chain then merges with the existing block + via hp_push_free_block_coalesce (block-to-block merge). + + Result: a single block spanning pos0-17 (count=18). +*/ + +static void test_block_to_block_coalesce(void) +{ + HP_SHARE *share; + HP_INFO *info; + uchar rec[REC_LENGTH]; + uchar blob_data[100]; + uchar *pos0, *pos17; + + memset(blob_data, 'M', sizeof(blob_data)); + + if (create_and_open("test_blk_blk_coal", &share, &info)) + { + ok(0, "setup failed: %d", my_errno); + skip(12, "setup failed"); + return; + } + + /* Insert blob A (primary pos0, chain pos1-8) */ + build_record(rec, 1, blob_data, sizeof(blob_data)); + ok(heap_write(info, rec) == 0, "insert blob A"); + + /* Insert blob B (primary pos9, chain pos10-17) */ + build_record(rec, 2, blob_data, sizeof(blob_data)); + ok(heap_write(info, rec) == 0, "insert blob B"); + + /* Guard row at pos18 */ + build_record(rec, 3, (const uchar*) "", 0); + ok(heap_write(info, rec) == 0, "insert guard row"); + + ok(share->block.last_allocated == 19, + "19 records allocated (2*9 blob + 1 guard)"); + + /* Delete blob A: primary pos0 pushed, chains deferred */ + { + uchar key[4]; + int4store(key, 1); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found blob A"); + ok(heap_delete(info, rec) == 0, "deleted blob A"); + } + + /* + Delete blob B: first flushes A's chains, then pushes B's primary, + then defers B's chains. + + Flush of A: hp_free_run_chain pushes pos1-8 as block. + hp_push_free_block_coalesce: head is pos0 (single), adjacent below + -> merge into block(pos0-8, count=9). + + Then B's primary pos9 pushed via hp_push_free_record_coalesce: + head is block-end pos8, pos9 == pos8 + rb -> extend upward to + block(pos0-9, count=10). + + B's chains deferred. + */ + { + uchar key[4]; + int4store(key, 2); + ok(heap_rkey(info, rec, 0, key, 4, HA_READ_KEY_EXACT) == 0, + "found blob B"); + ok(heap_delete(info, rec) == 0, "deleted blob B"); + } + + /* + Flush B's chains: hp_free_run_chain pushes pos10-17 (8 records). + hp_push_free_block_coalesce: head is block-end pos9, new block + first=pos10, pos9 == pos10 - rb -> block-to-block merge! + Result: block(pos0-17, count=18). + */ + hp_flush_pending_blob_free(info); + + pos0= hp_find_block(&share->block, 0); + pos17= hp_find_block(&share->block, 17); + + ok(share->del_link == pos17 && hp_is_free_block_end(pos17), + "free list head is block-end at pos17"); + ok(hp_free_block_first(pos17) == pos0, + "block-end points to pos0 (block-start)"); + ok(hp_free_block_start_count(pos0) == 18, + "block count == 18 (two blob rows fully coalesced, got %u)", + hp_free_block_start_count(pos0)); + + ok(heap_check_heap(info, 0) == 0, + "heap_check_heap validates block-to-block coalesce"); + + heap_drop_table(info); + heap_close(info); +} + + int main(int argc __attribute__((unused)), char **argv __attribute__((unused))) { MY_INIT("hp_test_freelist"); - plan(81); + plan(252); diag("Test 1: free-list contiguity detects groups > 2 records"); test_freelist_contiguity_multirecord(); @@ -830,6 +1829,45 @@ int main(int argc __attribute__((unused)), diag("Test 8: orphaned blocks reused after reclaim"); test_block_reuse_after_reclaim(); + diag("Test 9: block formation after blob chain free"); + test_block_formation(); + + diag("Test 10: pop from block shrinks it"); + test_pop_from_block(); + + diag("Test 11: block collapse to single on pop"); + test_block_collapse_to_single(); + + diag("Test 12: partial block take via blob reinsert"); + test_partial_block_take(); + + diag("Test 13: scan batch-skip for deleted blocks"); + test_scan_batch_skip_block(); + + diag("Test 14: hp_shrink_tail reclaims entire block at once"); + test_shrink_tail_with_block(); + + diag("Test 15: interleaved singles and blocks on free list"); + test_interleaved_singles_and_blocks(); + + diag("Test 16: delete coalescing - ascending adjacent deletes"); + test_delete_coalesce_ascending(); + + diag("Test 17: delete coalescing - descending adjacent deletes"); + test_delete_coalesce_descending(); + + diag("Test 18: delete coalescing - non-adjacent deletes stay separate"); + test_delete_no_coalesce_gap(); + + diag("Test 19: coalesced block reused by blob insert"); + test_coalesced_block_reuse(); + + diag("Test 20: scan batch-skip with coalesced delete block"); + test_scan_coalesced_block(); + + diag("Test 21: block-to-block coalescing via adjacent blob chains"); + test_block_to_block_coalesce(); + my_end(0); return exit_status(); } diff --git a/storage/heap/hp_test_helpers.h b/storage/heap/hp_test_helpers.h index 5e114df488c..6a9c7ab46bd 100644 --- a/storage/heap/hp_test_helpers.h +++ b/storage/heap/hp_test_helpers.h @@ -7,7 +7,7 @@ bytes 5-6: blob packlength=2 (length, little-endian) bytes 7-14: blob data pointer (portable_sizeof_char_ptr = 8 bytes always) reclength = 15 - visible_offset = MAX(15, 8) = 15 + visible_offset = MAX(15, HP_DEL_METADATA_SIZE=11) = 15 recbuffer = ALIGN(15 + 1, 8) = 16 The pointer slot is always portable_sizeof_char_ptr (8) bytes in the record diff --git a/storage/heap/hp_write.c b/storage/heap/hp_write.c index 6229abc4ea8..5c30caef4c6 100644 --- a/storage/heap/hp_write.c +++ b/storage/heap/hp_write.c @@ -114,11 +114,7 @@ err_delete_written_keys: and hp_write_blobs(). The slot at pos still contains stale data from the delete list, so hp_free_blobs would chase garbage chain pointers. */ - share->deleted++; - share->total_records--; - *((uchar**) pos)=share->del_link; - share->del_link=pos; - pos[share->visible]= 0; /* Record deleted */ + hp_push_free_record(share, pos); DBUG_RETURN(my_errno); } /* heap_write */ @@ -232,6 +228,57 @@ uchar *hp_alloc_from_tail(HP_SHARE *info, uint *blocks) } +/* + Take 'count' contiguous records from the head block. + The head must be a block-end with at least 'count' records. + Returns the lowest address of the taken range. +*/ + +uchar *hp_take_free_block(HP_SHARE *share, uint16 count) +{ + uchar *pos= share->del_link; + uchar *first; + uint recbuffer= share->block.recbuffer; + uint16 block_count, remaining; + uchar *taken_start; + + DBUG_ASSERT(pos && hp_is_free_block_end(pos)); + + first= hp_free_block_first(pos); + block_count= hp_free_block_start_count(first); + + DBUG_ASSERT(count >= 1 && count <= block_count); + + remaining= block_count - count; + taken_start= first + (uint32) remaining * recbuffer; + + if (remaining == 0) + { + /* Block fully consumed */ + share->del_link= *((uchar**) first); + } + else if (remaining == 1) + { + /* Collapse to single record; first's del_link already has next entry */ + first[HP_DEL_FLAG_OFFSET]= 0; + share->del_link= first; + } + else + { + /* Shrink block */ + uchar *new_last= taken_start - recbuffer; + *((uchar**) new_last)= first; + new_last[HP_DEL_FLAG_OFFSET]= HP_DEL_BLOCK_END; + int2store(first + HP_DEL_COUNT_OFFSET, remaining); + share->del_link= new_last; + } + + share->deleted-= count; + share->total_records+= count; + return taken_start; +} + + /* Find where to place a new record. @@ -250,10 +297,7 @@ uchar *next_free_record_pos(HP_SHARE *info) if (info->del_link) { - pos=info->del_link; - info->del_link= *((uchar**) pos); - info->deleted--; - info->total_records++; + pos= hp_pop_free_record(info); DBUG_PRINT("exit",("Used old position: %p", pos)); DBUG_RETURN(pos); }