Console View
|
Categories: connectors experimental galera main |
|
| connectors | experimental | galera | main | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
|
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40920 Say whether a value cut in a group reached the answer A TEXT value longer than `group_concat_max_len` is cut on its way into `blob_storage`, in `Field_blob::handle_group_concat()`. That happens while the group is being built, not when the answer is put together, so whether the answer is any shorter for it depends on whether the row carrying the value reaches the answer at all. Every such cut was reported as `ER_CUT_VALUE_GROUP_CONCAT`, which says that the answer lost something the user asked for, and that is true of only some of them. `Blob_mem_storage` now writes one byte in front of every value it stores and returns the pointer past it, so `was_cut()` answers for any value a reader holds a pointer to. `Field_blob::store()` sends every blob of a table that has a `Blob_mem_storage` through `handle_group_concat()`, so no value in that storage is without the byte, and `Field_blob::get_ptr()` on the record of a row hands back exactly the pointer that was stored. `dump_leaf_key()` reads the mark off each row it appends and sets `value_cut_in_result`. `val_str()` then reports: 1. **A warning**, `ER_CUT_VALUE_GROUP_CONCAT`, when a row that reached the answer carried a cut value. The answer is short by what was cut. The result being cut at `gconcat_max_len()` already gives that same warning, and a group that hits both is told once, not twice. 2. **A note**, `ER_CUT_VALUES_WHILE_PROCESSING`, when a value was cut but no row carrying one reached the answer. The answer may well be what a larger limit would have given. One note per aggregate is enough for a statement however many groups had a value cut, and `cleanup()` clears the mark so a statement run again gets its own. Reporting the loss as a warning keeps a strict `sql_mode` aborting on it, which it does because `THD::raise_condition()` promotes a warning and never promotes a note. `ST_COLLECT` is not affected. It reports `ER_CUT_VALUE_GROUP_CONCAT` itself, against `group_collect_max_len`. `main.gconcat_cut_note` covers the split with one group holding a short value and a long one, where a `LIMIT` alone decides which of them the answer is built from, over both the sort tree and the duplicate filter. `main.func_gconcat` shows the granularity: of five groups at `group_concat_max_len=499999`, the one holding exactly 499999 bytes is the one that does not warn. Note that `blob_storage` only exists when the aggregate has an `ORDER BY` or a `DISTINCT` and a blob field, so this is the only shape in which a value is cut this way. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40376 `Protocol::end_statement` assertion when window function fills HEAP tmp table `save_window_function_values()` did not take into account that one can get `HA_ERR_RECORD_FILE_FULL` on `ha_update_row()`. With blob support in heap, it can now happen more easily (expressions wider than 512 characters are promoted to TEXT in tmp tables, and each update of a blob result column allocates a continuation record). However, it could also happen with Aria tables, which is apparently not tested. The error was returned as a plain `true` with an empty diagnostics area: debug builds hit `Assertion '0'` in `Protocol::end_statement()`, release builds send a bare OK packet after the result set metadata, which the client mis-parses (appears as a hang / lost connection). Errors of the computation are now exposed: `save_window_function_values()` calls `handler::print_error()` for any failure it does not recover from, the previously ignored `ha_rnd_pos()` return values are checked and reported, and `compute_window_func()` distinguishes a read error from EOF and stops the row scan as soon as the per-row loop fails. The overflow itself is recovered from by converting the tmp table to the disk-based tmp engine in place: the rows keep their identity and their contents, only their positions change, and those are translated in the rowid sequence that the computation is built on. The filesort result of a window sort is a sequence of row positions that covers every row of the table exactly once (the sort is set up without a limit) and is the only place where the positions are kept: the row scan and all frame cursors read the rows through it. The conversion (`Window_rowid_remapper`) therefore copies the rows in the order of that sequence, which makes the new position of a row known as soon as the row has been written, and stores it back into the very slot the old position was read from. For the new position to always fit into the slot, a window sort stores the row positions in slots of a fixed 8 bytes (`Filesort::min_ref_length`, set to the new `TMP_TABLE_MAX_REF_LENGTH`), which holds the position of any engine an internal tmp table can use: a pointer into memory or a data file offset. `make_sortkey()` zero-pads a shorter position to its slot, and `SORT_INFO::ref_length` carries the slot width to the readers, so `init_read_record()` no longer re-derives it from `handler::ref_length`, which changes when the table is converted. Sorts that do not set a minimum, and engines whose positions are wider than 8 bytes, are unchanged byte for byte. A sequence held in memory is thereby rewritten in place, keeping its layout, so the frame cursors' places in it stay valid. A sequence held in a temporary file is written and read through an `IO_CACHE`, which encrypts the file when tmp file encryption is enabled, so the file can not be rewritten in place: the old sequence is streamed out of its cache and the translated sequence into a new cached temporary file, which then replaces the old one under the cache. The frame cursors' slave caches stay linked to the master through `next_file_user` across the replacement, and are re-created from the new master afterwards (`Frame_cursor::rowids_rewritten()`). Should re-creating one fail, the cursor forgets its already-released cache, so that it is not released a second time when the cursor is destroyed, and reports the failure. The row whose update did not fit is written from `record[0]`, which holds its new image, instead of being read from the table, so the conversion applies the update that failed. Blob values of `record[0]` that were read from the HEAP table can point into the handler's shared blob reassembly buffer (`hp_read_blobs()`, blob values that span multiple allocation blocks), which reading the other rows overwrites, so they are first given memory of their own. When the window sort was set up with a deferred filter (a HAVING clause deferred to the final ORDER BY sort), the sequence omits the rows the filter rejected and the conversion drops them: every later reader of the table applies the same filter, either directly or by reading the table through a sort that does, so such rows can never reach the result. `create_internal_tmp_table_from_heap()` gains a `Tmp_table_row_copier` hook for the copy from the HEAP table to the Aria or MyISAM table that replaces it. The plain copy loop and the append of the pending `record[0]` become `Tmp_table_default_copier`, used when the caller passes no copier, so both cases of the conversion run through the same code, defined together with `Window_rowid_remapper` just before the function. `ha_end_bulk_insert()` is called also when the copy fails, so the new handler does not keep bulk insert state when its table is dropped; the pending row is thereby written while the bulk insert is still active, which does not delay its duplicate detection, as bulk insert does not cache unique keys (`maria_init_bulk_insert()` skips `HA_NOSAME` keys). An ignored duplicate of the pending row is passed to the caller in `Tmp_table_row_copier::duplicate_row_error`, which sets `*is_duplicate`. Carrying the running computation over the conversion, instead of re-running it, has two visible consequences: - Compound expressions containing window functions (`items_to_copy`) are evaluated exactly once per row, as they are when the table does not overflow, so the conversion does not have to be refused when such an expression is non-deterministic (`RAND_TABLE_BIT`: a user variable assignment, a non-deterministic stored function); those statements produce their result. - The statement sorts once, so sort related aggregate warnings, e.g. the `max_sort_length` truncation counts, are reported for one pass. The rows are copied in the order of the sequence, so the converted table does not hold them in the order they were inserted in. This is safe even when that order is what the table was built for (`TABLE::keep_row_order`, set for `ROWNUM()` with GROUP BY or ORDER BY): before HEAP supported blobs, such a tmp table was created in the disk engine from the start and these statements simply ran, so refusing here would be a regression against that behavior. 1. `ROWNUM()` values are materialized into the tmp table rows during the fill, before the window computation begins; the copy moves them verbatim, so their pairing with the rows can not change. 2. The rowid sequence is translated in place, so the running window computation continues over exactly the same row order, and tie-sensitive window function values (`ROW_NUMBER`, frames over tied keys) keep the values the interrupted pass had already produced. 3. Consumers that scan the converted table afterwards see the rows in the copy order. Among rows with equal sort keys that order differs from the insertion order, but such tie order is unspecified and already differs between the memory and disk tmp engines. The converted table keeps `keep_row_order= true` so the copy order is also the order the disk engine preserves from then on. Tests, in `heap.blob_window_overflow` unless noted: a single window over all rows and a partitioned window overflow mid-computation and must transparently convert; the side-effecting window expression test verifies that the user variable is assigned exactly once per row, against the same query that does not overflow; a fourth test covers the sequence being an array in memory instead of a merge file; a fifth test covers the `keep_row_order` conversion, verifying that every `ROWNUM()` value keeps its insertion-order pairing with its row across the conversion; a sixth test covers blob values larger than a HEAP allocation block surviving the conversion of the pending row; `heap.blob_window_overflow_encrypt` runs the conversion with encrypted temporary files; `heap.blob_window_overflow_debug` covers the failure to re-create a slave cache of the sequence (`simulate_window_seq_slave_oom`), which previously hung the server in the cursor destructor. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40692 GROUP_CONCAT replays a group when an OFFSET skips every row Nothing says how many times a statement asks for the result of a group, and the answer must not depend on it. A `HAVING` clause on the alias is the shortest statement that asks twice, and it returns a different value than the same aggregate asked once: SELECT GROUP_CONCAT(a ORDER BY a LIMIT 2 OFFSET 4) v FROM t1; -> (empty) SELECT GROUP_CONCAT(a ORDER BY a LIMIT 2 OFFSET 4) v FROM t1 HAVING v LIKE '%'; -> a,b `val_str()` walks only while `result_finalized` is false, and `dump_leaf_key()` raises that flag for the first row it writes. A row that falls inside the offset is skipped by an earlier return, which decrements the offset counter and leaves the flag alone. The row-limit arm immediately above it does raise the flag before its own early return, so two adjacent early returns behave differently. A walk in which every row was skipped therefore writes nothing and records nothing. The next caller walks again with the offset already spent, and the rows skipped the first time are appended to a result buffer that was handed over once already. Once the duplicate filter has spilled to disk the second walk is worse than wrong. `Unique::reset()` documents the contract: Clear the tree and the file. You must call reset() if you want to reuse Unique after walk(). The first walk flushed the tree and emptied it, so the second flushes an empty tree, appending a chunk that holds no rows. `merge_walk()` reads nothing back from it and fails `DBUG_ASSERT(bytes_read)`. A build without assertions goes on to take keys from that chunk. Set `result_finalized` where the walk block ends, so that it records every path that has consumed the filter rather than only the paths that wrote a row. On the release branches only the form without `ORDER BY` reaches the duplicate filter. Since MDEV-21879 the `DISTINCT ... ORDER BY` combination builds its result the same way, so both forms can reach the assertion here. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40669 HEAP MIN_ROWS pre-sizes blocks past max_heap_table_size `init_block()` captures `requested_min_records` from the caller's `min_records` before the `min_records= MY_MIN(min_records, max_records)` clamp, then restores that raw value when the block allocation cap added by MDEV-40447 fires. `max_records` is derived from `max_heap_table_size` / `tmp_memory_table_size` and is the most rows the table can ever hold, so that clamp is what has always bounded a HEAP table's allocations. Restoring the pre-clamp value undoes it, and an unreachable `MIN_ROWS` pre-sizes the record block and every hash key block past the ceiling. With `max_heap_table_size=64M` and `MIN_ROWS=20000000` a one-row table allocates 1GB where it used to allocate 96MB; `MIN_ROWS=4294967295` at the shipped default 16MB ceiling allocates 4GB, bounded only by the `INT_MAX32` clamp on `memory_needed`. A few such tables exhaust memory. Fix: clamp `requested_min_records` to `max_records` as well. `MY_MIN` keeps 0 at 0, so "no `min_records` requested" stays distinguishable from an explicit `MIN_ROWS`, and the cap keeps ignoring the defaulted 1000-row heuristic. The cap and the clamp together give four regimes, and only the last one changes: 1. No `MIN_ROWS`: capped, sizing comes from the ceiling alone. 2. `MIN_ROWS` below the cap: capped. 3. `MIN_ROWS` above the cap but within `max_records`: pre-sizes to `MIN_ROWS`, past the cap, as MDEV-40447 intends. 4. `MIN_ROWS` at or above `max_records`: unreachable, so it degrades to plain ceiling-derived sizing. In case 4 the cap branch becomes a no-op, because `records_in_block` already equals `max_records`, so sizing returns to exactly what it was before MDEV-40447. `init_block()` no longer defaults `max_records` itself. The block sizing ceiling is derived once in `heap_create()` and the parameter is `const`, so the caller's value reaches `share->max_records` unchanged: 0 there means "no row limit", and `hp_alloc_from_tail()` skips the limit check only while it is 0. Tests: - `storage/heap/hp_test_block_size-t.c`: a four-case boundary walk asserting the exact `alloc_size` of each regime above at one ceiling-derived `max_records`, and a keyed case asserting that an unreachable `MIN_ROWS` clamps the hash key block as well as the record block (`sizeof(HASH_INFO)` gives that block its own `recbuffer` and its own cap). That one expectation selects on `SIZEOF_CHARP`: `sizeof(HASH_INFO)` is 24 on LP64 and 12 on ILP32, which halves `memory_needed` and rounds a whole power of two lower. The record-block sizes round the same on both widths. A `max_records=0` case covers the derived ceiling: the block is sized from it while `share->max_records` stays 0, and the table accepts far more rows than that default. - `mysql-test/suite/heap/min_rows_alloc.test`: the same regimes end to end across three ceilings, plus `MIN_ROWS` at the .frm maximum under the shipped default ceiling. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40591 Unexpected ER_NOT_KEYFILE or MSAN error in heap_check_heap `ha_heap::external_lock()` verifies the table with `heap_check_heap()` at `F_UNLCK`. That is safe on the ordinary unlock path, where `mysql_unlock_tables()` calls `unlock_external()` before `thr_multi_unlock()` and the lock is still held. It is not safe on either path that unlocks after a *failed* lock attempt, where the caller holds nothing at all while another connection is writing: 1. `mysql_lock_tables()` calls `unlock_external()` to balance the external locks it already took, because `thr_multi_lock()` timed out. 2. `lock_external()` unwinds the tables it has already locked, because a later table refused -- all before `thr_multi_lock()` runs at all. `ha_partition::external_lock()` unwinds its partitions the same way. MEMORY has no row-level concurrency control, so a scan taken outside the lock sees a writer's intermediate state by construction: `hp_alloc_from_tail()` publishes `total_records` at allocation time, before the slot is written, while the checker scans `[0, total_records + deleted)` and reads every slot's flags byte. Under MSAN that is a use of uninitialised `my_malloc()` memory; otherwise it is a spurious `total_records` mismatch. `heap_check_heap()` ends with `heap_mark_crashed()`, which sets `HEAP_STATE_CRASHED` in the **shared** `HP_SHARE`, so one bogus mid-write observation poisons a healthy table for every connection using it -- the reported `ER_NOT_KEYFILE`. MDEV-21373 disabled this check in 2021 for exactly this reason, by gating it on `EXTRA_DEBUG`. MDEV-38975 changed the gate to `EXTRA_HEAP_DEBUG` and defined that for every debug build, reviving the race. Rather than switch the check off wholesale again, only verify a table that this handle both holds a lock on and has changed under it: - `HP_INFO::lock_type` remembers the `ha_heap::external_lock()` argument, the way `MARIA_HA` and `MI_INFO` already do; - `HP_INFO::changed` is set by `heap_write()`, `heap_update()` and `heap_delete()`, and cleared by `ha_heap::external_lock()` on every grant, so it means "changed since this lock was taken"; - `table_is_locked_and_changed()` requires both. The change term is what separates the three unlock paths, because the lock type cannot: `ha_heap::external_lock()` records it before `thr_multi_lock()` runs, so it is armed on the two failing paths as well. Neither of them ever ran a row operation, so neither has changed anything. It has to be per handle rather than `HP_SHARE::changed`, which is true on exactly those paths, another connection being the one writing. Requiring a change also makes a debug build cheaper: the verification scans every record and every index, and now runs only after a statement that wrote to the table. Deriving this in the engine rather than repairing `lock_external()` also covers `ha_partition`, which reimplements the same unwind. A temporary table gets `F_EXTRA_LCK` and so counts as always locked: no other connection can reach its share. This covers the user's `CREATE TEMPORARY TABLE` and not only the optimizer's internal one -- an internal table frees its blob chains outright, whereas a user temporary table parks them, and `get_lock_data()` leaves it out of the lock set entirely, so it never reaches `external_lock()` at all. The `ALTER` copy target is temporary too, and additionally takes a direct `handler::ha_external_lock()` instead of going through the lock set. Redeeming a parked blob chain puts records back on the shared free list, so it needs the same protection, and both redemption points assert it. `hp_test_unlock_check-t` builds the lock states directly, in the order `ha_heap::external_lock()` builds them, so nothing here is raced. Four MTR tests cover the shapes it cannot reach: blob updates and deletes on a user `TEMPORARY` MEMORY table (`heap.blob_tmp_table`), `INSERT DELAYED` (`heap.blob_delayed_insert`), the `ALTER` copy target (`heap.blob_online_alter`), and one share locked twice in a lock set (`heap.blob_lock_twice`). No existing test exercised any of them. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Monty
monty@mariadb.org |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Added query_cache_use_rw_lock to allow concurrent query cache lookups The query cache protected all operations with one exclusive lock (m_cache_lock_status, guarded by structure_guard_mutex). This serialized every lookup, even though a lookup mainly reads the query cache structures. Added a new global variable, query_cache_use_rw_lock (default OFF). When set, send_result_to_client() takes a shared (read) lock instead of an exclusive one, which allows lookups to run concurrently. Having it as an option makes it possible to benchmark both alternatives with the same binary and then decide which one to keep. Implementation: - try_lock() has a new 'read_lock' argument. The new inline function try_read_lock() sets it from query_cache_use_rw_lock. - Readers are counted in m_readers and leave m_cache_lock_status as UNLOCKED. Writers wait until m_readers is 0 and are counted in m_waiting_writers. Waiting writers have priority over new readers, which ensures that invalidations are not starved by cache hits. - Readers wait on the new COND_cache_read_lock. wake_up_waiters() wakes one waiting writer, or all waiting readers if there is none. - unlock() is split into unlock() and unlock_internal(). The kind of lock to release is deduced from m_cache_lock_status, so none of the unlock() callers had to be changed. - send_result_to_client() updates the query list and the statistics under structure_guard_mutex when it only holds a read lock. With a write lock this is done as before, without taking the mutex. - If an engine requests invalidation during a lookup done with a read lock, the table key is copied and the invalidation is done after the lock is released, as invalidation requires a write lock. - Added the DEBUG_SYNC point "in_query_cache_hit" used by the new test. With query_cache_use_rw_lock=0 the code works as before. Did a simple sysbench run with 64 threads and 200000 simple select queries, all served from query cache. query_cache_use_rw_lock=1 gave a 2.25x speedup. The difference to an unmodied MariaDB version for the same test is 2.3 x faster. Co-Authored-By: Claude Opus 5 <[email protected]> |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Kristian Nielsen
knielsen@knielsen-hq.org |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-41217: Inefficient memory management by parallel slave The rpl_group_info object is used to keep track of a lot of different state in an event group/transaction across events. It is also used in parallel replication for a small set of values that are enqueued by the SQL driver thread for worker threads to handle. This wastes a lot of memory when a large number of event groups are enqueued (eg. in case of slave lag), since most of the rpl_group_info is not used in the queue. This patch changes the queue to contain only the rgi_queued_part object (ie. rgi->q), not the full rgi, saving the memory for the parts of rgi that are not needed in the queue. With this patch, there is one rgi for every worker thread independent of the amount of queued event groups (as well as one serial_rgi in the SQL driver thread). Signed-off-by: Kristian Nielsen <[email protected]> |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40585 Assertion `(data_len == 0) == (data_ptr == ((void *)0))' fails in hp_flush_unaliased_blob_free `hp_flush_unaliased_blob_free()` asserted that a zero-length blob in the record buffer carries a `NULL` data pointer. The SQL layer does not guarantee that direction of the invariant: - `Field_blob_compressed::store()` of a zero-length value allocates its scratch `String` first and then stores `(length = 0, ptr = value.ptr())`, leaving a stale non-`NULL` pointer. Plain `Field_blob::store()` zeroes the whole pack instead, which is why an uncompressed column does not reproduce this through SQL. - `Field_blob::unpack()` points a zero-length blob at the row-based replication event buffer, so the applier trips the same assertion on a slave-side `HEAP` table with a plain, uncompressed column. The assertion evaluates only for a column whose old chain was parked for deferred free, so the failing statement must both park a chain and write a zero-length blob: `REPLACE` over an existing row, `INSERT ... ON DUPLICATE KEY UPDATE`, or one replicated row-event group doing the same. A delete and an insert in separate statements redeem the parking through the record-less `hp_flush_pending_blob_free_impl()` and are unaffected. Debug builds only. Every decision in the engine -- here, in `hp_write_blobs()` and in `heap_update()` -- tests the stored length and never the pointer, so release builds store, free and adopt chains correctly and no wrong data is ever written. Keep the direction that is guaranteed, a non-empty blob must have a data pointer, and drop the reverse implication. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-41297 A stored empty blob never equals an all-space value Blob values for empty strings should return a pointer to an empty string and not NULL. `hp_materialize_one_blob()` returned NULL, which its callers `hp_rec_key_cmp()` and `hp_key_cmp()` read as an allocation failure. `hp_test_write_dup-t.c` was extended to test key reads on `TEXT` columns. This could not be done in MTR, as a `MEMORY` table cannot be created with a key on a `TEXT` column. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Monty
monty@mariadb.org |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40029 Add support for bit fields to HEAP This adds support for BIT_FIELD in record and keys for HEAP tables. The HEAP engine now has HA_CAN_BIT_FIELD set in table_flags() Multiple bugs in BIT field handling was found fixed. Some in HEAP table code, other bugs was affecting usage of BIT fields as keys. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40692 GROUP_CONCAT replays a group when an OFFSET skips every row Nothing says how many times a statement asks for the result of a group, and the answer must not depend on it. A `HAVING` clause on the alias is the shortest statement that asks twice, and it returns a different value than the same aggregate asked once: SELECT GROUP_CONCAT(a ORDER BY a LIMIT 2 OFFSET 4) v FROM t1; -> (empty) SELECT GROUP_CONCAT(a ORDER BY a LIMIT 2 OFFSET 4) v FROM t1 HAVING v LIKE '%'; -> a,b `val_str()` walks only while `result_finalized` is false, and `dump_leaf_key()` raises that flag for the first row it writes. A row that falls inside the offset is skipped by an earlier return, which decrements the offset counter and leaves the flag alone. The row-limit arm immediately above it does raise the flag before its own early return, so two adjacent early returns behave differently. A walk in which every row was skipped therefore writes nothing and records nothing. The next caller walks again with the offset already spent, and the rows skipped the first time are appended to a result buffer that was handed over once already. Once the duplicate filter has spilled to disk the second walk is worse than wrong. `Unique::reset()` documents the contract: Clear the tree and the file. You must call reset() if you want to reuse Unique after walk(). The first walk flushed the tree and emptied it, so the second flushes an empty tree, appending a chunk that holds no rows. `merge_walk()` reads nothing back from it and fails `DBUG_ASSERT(bytes_read)`. A build without assertions goes on to take keys from that chunk. Set `result_finalized` where the walk block ends, so that it records every path that has consumed the filter rather than only the paths that wrote a row. On the release branches only the form without `ORDER BY` reaches the duplicate filter. Since MDEV-21879 the `DISTINCT ... ORDER BY` combination builds its result the same way, so both forms can reach the assertion here. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Sergei Golubchik
serg@mariadb.org |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-41467 mbstream insufficient path validation in REMOVE and RENAME chunks apply the check from file_entry_new() also to other file operations also: swap names in the the rename error message |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Monty
monty@mariadb.org |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Fixed internal temporary buffer sizes to use tmp_memory_table_size tmp_memory_table_size is limiting the size of internal temporary memory tables. max_heap_table_size is there to limiting the size of explictely created memory tables. max_heap_table_size can be much larger than tmp_memory_table_size as the memory used by temporary tables is in the control of the user. This commit changes the usage of max_heap_table_size for internal buffers to min(max_heap_table_size, tmp_memory_table_size), like we do for internal temporary tables. This changes the in memory buffer allocations for: - GROUP_CONCAT() - Calculating the cost for scanning memory tables (the original code was wrong here as it used the wrong size for memory tables). - ANALYZE TABLE buffer sizes for calculating distinct column values Other things: - Add THD::ram_limitation() to provide consistent memory limitations in all code that used variables.tmp_memory_table_size as buffers. If tmp_memory_table_size == 0, then 8192 is used. This replaces Item_sum::ram_limitation which used 1024 as min buffer, which is way to little for any practical case. - Added security guard in heap_prepare_hp_create_info to ensure that max_table_size is calculated same way as in MariaDB server. - Fixed initial memory allocations for Item_func_group::concat which allocated 'max allowed memory' at start. Now it allocates only 1/16 of that memory at start. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40431 heap: classify rb-tree insert failures, mark the table crashed when key recovery fails `hp_rb_write_key()` reported **every** rejected `tree_insert()` as `HA_ERR_FOUND_DUPP_KEY`, although `tree_insert()` also returns NULL when the allocation of the tree node fails. An out of memory during a BTREE-indexed UPDATE was therefore reported as a duplicate: the user got `ER_DUP_ENTRY` with a fabricated value, possibly naming a key that is not unique at all, `handler::is_fatal_error()` treated the allocation failure as not fatal for callers asking for `HA_CHECK_DUP_KEY`, and the `HA_ERR_OUT_OF_MEM` / `ENOMEM` arms of the `heap_update()` recovery list were unreachable on the rb-tree path. `tree_insert()` now records why it returned NULL in the new `TREE::error` (`TREE_ERROR_OOM` or `TREE_ERROR_DUP_KEY`), and `hp_rb_write_key()` maps that to `HA_ERR_OUT_OF_MEM` or `HA_ERR_FOUND_DUPP_KEY` (`HA_ERR_INTERNAL_ERROR` defensively, should a new NULL source appear). With the classification corrected, the allocation failure lands in the `HA_ERR_OUT_OF_MEM` arm of the `heap_update()` recovery, which moves the already changed keys back, so correcting the error report does not trade the wrong message for a silently corrupt index. The recovery at `err:` keeps its explicit list of error codes: those are the errors we know how to recover from, and recovering from errors whose meaning we do not know is worse than stopping. What changes around it: 1. `info->errkey` is only set for `HA_ERR_FOUND_DUPP_KEY`, the single error it describes; every other failure leaves the `-1` that `err:` starts with. 2. When the recovery itself cannot restore a key -- the re-insert of an old key value fails -- or the failure is outside the list, the index no longer describes the data and the table is marked **crashed** in the new `HP_SHARE::state_changed` (bits and macros modeled on the `state.changed` of Maria, see `storage/maria/maria_def.h`). A crashed table refuses every lock acquisition, read and write with `HA_ERR_CRASHED` ("Index for table is corrupt"). `heap_check_heap()` does not trust the mark: it clears it, re-validates the whole structure and marks the table crashed again when it finds damage, so a check of an intact table drops a mark that no longer protects anything, while a genuinely damaged table stays refused. `hp_clear()` -- reached through TRUNCATE or DELETE without WHERE -- clears the state along with the data, because it rebuilds the indexes from nothing. Refusing a table known to be corrupt beats continuing and delivering wrong results. 3. `delete_key()` is assumed to succeed: it allocates no memory, so it cannot fail unless the table is already inconsistent, and building recovery logic and injected failures for that case would complicate the code for a scenario that cannot happen on a healthy table. If it ever does fail, the error falls outside the recovery list and the table is marked crashed, preserving the damaged state for analysis. The debug-build consistency check in `ha_heap::external_lock(F_UNLCK)` skips tables already marked crashed: their inconsistency is known and deliberate, and re-detecting it would raise a second error into a diagnostics area that can already hold OK, firing the `Diagnostics_area` assertion the check exists to prevent. The allocation failure is injected inside `tree_insert()` itself: `simulate_tree_insert_oom` fails every node allocation until the caller disarms it, `once_simulate_tree_insert_oom` fails only the next one and disarms itself. The two names must not be a prefix of one another, because `DBUG_SET()` matches an existing keyword by prefix and would silently merge instead of adding. Callers arm the keywords themselves and keep an inert guard keyword in the list, because a keyword list that becomes empty while debugging is on matches every keyword. The unit test `hp_test_update` drives the failures through the engine API: the classification and its duplicate-key counterpart, an already moved key being moved back, and the crashed lifecycle -- marking, refusal of reads and writes, the check that clears a stale mark on an intact table and keeps a damaged one crashed, and the reset on emptying. With the defects reintroduced, 8 of its assertions fail. `heap.update_key_rollback` covers the same from SQL; without the fix its first UPDATE reports `ER_DUP_ENTRY 'Duplicate entry 101 for key k2'` on a key that is not unique. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-41297 A stored empty blob never equals an all-space value Blob values for empty strings should return a pointer to an empty string and not NULL. `hp_materialize_one_blob()` returned NULL, which its callers `hp_rec_key_cmp()` and `hp_key_cmp()` read as an allocation failure. `hp_test_write_dup-t.c` was extended to test key reads on `TEXT` columns. This could not be done in MTR, as a `MEMORY` table cannot be created with a key on a `TEXT` column. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40447 Cap HEAP block allocations derived from the memory ceiling `init_block()` sizes every `HP_BLOCK` allocation as `max_records / heap_allocation_parts` records. `max_records` is normally computed from the table memory ceiling (`max_heap_table_size` / `tmp_memory_table_size`), not from any estimate of the expected number of rows, so a tuned-up ceiling turns directly into giant allocations: with `tmp_table_size=16G` the **first row written** to an internal temporary table allocates a 256MB..2GB block, and the per-key `HASH_INFO` blocks are inflated the same way. For create-and-drop-per-statement tables (`SHOW`/`INFORMATION_SCHEMA` materializations, `DISTINCT`/`GROUP BY` temporary tables) such allocations are served by `mmap()` and unmapped again on drop by common malloc implementations, so every statement pays page fault-in and zeroing, page-table teardown with TLB-shootdown IPIs, and process-wide `mmap_lock` serialization. On a `SHOW FULL COLUMNS` loop workload with `tmp_table_size=16G` this loses 36% QPS at 16 threads, 50% at 64 and 60% at 128; blob-bearing I_S temporary tables newly qualify for HEAP since MDEV-38975, which exposed the pre-existing sizing heuristic to this workload. Fix: cap ceiling-derived block allocations at `heap_max_allocation_block` (4MB): 1. 4MB stays within the range that mainstream allocators (glibc, jemalloc, tcmalloc, mimalloc) recycle from their free lists instead of returning to the kernel on free, so per-statement blocks are reused with no syscalls at steady state. The nearest boundary is jemalloc's `oversize_threshold` (8MB); glibc's dynamic mmap threshold adapts up to 32MB. 2. 4MB is large enough to keep typical blob values (up to ~1MB) in a single continuation run, preserving zero-copy blob reads. 3. At the default `tmp_table_size` (16MB) blocks come out at 1-2MB, so default-configuration sizing is unchanged; the cap only binds for ceilings above ~64MB. An explicit `min_records` (`CREATE TABLE ... MIN_ROWS=N`) is a real row count expectation and still pre-sizes beyond the cap. The cap compares against the **caller-supplied** `min_records`, not the defaulted "optimize for 1000 rows" value: for rows wider than ~4KB (`heap_max_allocation_block / 1000`) the 1000-row default exceeds `cap_records` and would otherwise silently override the cap (8KB rows -> 8MB blocks, 64KB rows -> 64MB blocks, and a row wider than the cap itself -> an `INT_MAX32`-clamped 1GB block). Wide internal temporary table rows are reachable without `MIN_ROWS`: fields wider than the VARCHAR limit become out-of-row blobs, but many inline columns (multi-table joins with `DISTINCT`/`GROUP BY`, wide `CHAR` columns) can sum past 4KB. Rows wider than the cap itself cannot honor it and degrade to the existing 10-records-per-block floor (e.g. 5MB rows -> 64MB blocks), which is as close to the cap as a block that must hold at least a few whole rows can get. Tables larger than 4MB simply allocate more blocks; the `HP_PTRS` block tree (128-ary) accommodates this with no depth issues. Tests: - `storage/heap/hp_test_block_size-t.c`: unit tests asserting the record and hash key block `alloc_size` cap with a ceiling-derived `max_records` (keyed and keyless), `MIN_ROWS` override, unchanged small-table sizing, and full functionality across the first-block boundary at the capped geometry (45K rows, key reads, `heap_check_heap`); wide-row scenarios asserting the cap holds for 8KB and 64KB rows despite the defaulted `min_records`, that rows wider than the cap degrade to the 10-record floor instead of the 1000-record geometry, and that an explicit `min_records` still overrides the cap for wide rows. - `mysql-test/main/tmp_table_heap_alloc.test`: end-to-end check that `Max_memory_used` stays bounded when a small `SELECT DISTINCT` on a TEXT column and a `SHOW FULL COLUMNS` materialize under a 4GB ceiling. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-21879 GROUP_CONCAT(DISTINCT ORDER BY) is wrong when Unique spills `Item_func_group_concat::add()` decided whether a row was a duplicate by checking whether `Unique::elements_in_tree()` had grown after `unique_add()`: uint count= unique_filter->elements_in_tree(); unique_filter->unique_add(get_record_pointer()); if (count == unique_filter->elements_in_tree()) row_eligible= FALSE; `Unique` flushes its whole in-memory tree to disk when it runs out of memory, and `elements_in_tree()` only counts what is still in memory. After the first flush the test says nothing about the rows that were already spilled. **MDEV-11563** made this harmless for `GROUP_CONCAT(DISTINCT x)` by building the result in `val_str()` from `unique_filter->walk()`, which merges the spilled parts back in. It left the `ORDER BY` case alone. There the result comes from the sort tree, which `add()` fills gated by `row_eligible`, so the defect is still fully live. Both directions of the failure are reachable, depending on how often the filter flushes relative to the insert: 1. Duplicates reach the result. 100 rows holding 50 distinct values give all 100 values back. 2. Rows are lost. 30 distinct rows of 2000 bytes give one value back. `JSON_ARRAYAGG(DISTINCT x ORDER BY y)` fails in the same way. Fixed by not filling the sort tree from `add()` when `DISTINCT` is used. `val_str()` now walks the merged `unique_filter` into the sort tree and then walks the sort tree, so the rows are sorted after the duplicate filtering is complete instead of during it. `Unique::walk()` merges everything it flushed, so the sort tree can be handed more rows than fit in memory. `insert_to_order_tree()` repacks it on the same memory budget `add()` used, and a walk that runs out of memory sets `result_cut`, so the user gets a cut value warning rather than a silently short result. **Behaviour change.** `ORDER BY` does not order rows that tie on the ordering expression, and which of them comes first changes here. It used to follow the order the rows were read in; it now follows the order the duplicate filter keeps them in. Unlike the old order, the new one depends on neither the memory available nor the physical row order. `main.gconcat_distinct_spill` checks that, and `main.func_gconcat` records one such tie. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40946 Two heap blob tests fail with the embedded server Both tests exercise server facilities that an embedded build does not have, so neither can run there. `INSERT DELAYED` has no delayed insert thread in an embedded build. The whole facility sits inside `#ifndef EMBEDDED_LIBRARY`, including the check that routes a delayed statement away from the ordinary insert path, so the statement is an ordinary insert and `DELAYED_WRITES` stays at 0. `ALTER TABLE ... LOCK=NONE` is never online in an embedded build. `online` is hard-wired to `false` when `HAVE_REPLICATION` is undefined, and `my_global.h` leaves it undefined for `EMBEDDED_LIBRARY`. The source lock is therefore not downgraded, the `alter_table_online_downgraded` sync point is never reached, and the test's `WAIT_FOR downgraded` runs out its `debug_sync` timeout. Skip both with `include/not_embedded.inc`, as `main.delayed` and `main.alter_table_online_debug` already do. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40634 Const MEMORY table's BLOB outlives the lock protecting it A single-row table is read once during optimization and its row kept in `record[0]` for the rest of the statement. `JOIN::optimize_stage2()` then releases the lock on every const table, on the premise stated in its own comment: *"It's safe to ignore result code as all tables where opened for read only."* That premise assumes a read leaves a **copy** of the row behind. MEMORY with a blob does not. `hp_read_blobs()` answers the read by pointing `record[0]` at the blob data inside `HP_SHARE` rather than copying it, so from the moment the lock is dropped another connection is free to overwrite, free or recycle those bytes -- and the statement goes on reading them. The result is a const table whose value changes in the middle of the statement using it, and a read of freed memory. Let a caller that keeps reading a row after the unlock ask for such tables to be left alone. `GET_LOCK_SKIP_ZERO_COPY_ROWS` drops them from the lock set `get_lock_data()` builds, exactly as `GET_LOCK_SKIP_SEQUENCES` already does, and the const-table unlock in `JOIN::optimize_stage2()` passes it. Which tables those are is for the engine to say rather than for the lock layer to infer. The 64-bit `table_flags()` space is full, so a second word `table_flags2()` carries the first such property, `HA2_CANNOT_ACCESS_ROWDATA_AFTER_UNLOCK`, and `ha_heap::open()` raises it for any table that has a blob. The skip has to be opt-in rather than a rule. `mysql_lock_remove()` also reaches `mysql_unlock_some_tables()`, and there the unlock is permanent and must not be skipped. A MEMORY blob const table now stays read-locked for the whole statement and blocks writers, which is the price every non-const MEMORY table already pays. The regression test parks the reader with `GET_LOCK()` rather than with a stored function. A stored function puts the statement into prelocked mode, and the const-table unlock is skipped entirely in that mode, so the code path under test would never run. The gate that parks it is taken with `--disable_ps2_protocol` in force. `--ps-protocol` executes every complete `SELECT` twice and compares the two result sets, and `GET_LOCK()` is recursive, so a doubled acquisition would outlive the single `RELEASE_LOCK()` that opens the gate again. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40033 Free-list contiguous block tracking for HEAP engine Replace the HEAP engine's O(records) per-record free-list traversal with O(blocks) by grouping contiguous deleted records into logical blocks. Each block uses two sentinel records (block-start at the lowest address, block-end at the highest) to represent an arbitrarily large contiguous group. Interior "dark" records are implicitly covered by the block's address range and have their metadata bytes cleared by `hp_clear_dark_records()`. **Block metadata layout** (stored inline in deleted records): - Byte 8 (`HP_DEL_FLAG_OFFSET`): `del_flag` -- `HP_DEL_BLOCK_START` (2) on the first record, `HP_DEL_BLOCK_END` (1) on the last, 0 on dark records and singles - Bytes 9-10 (`HP_DEL_COUNT_OFFSET`): `uint16` block record count, stored only on the block-start record **`visible` minimum** bumped from `sizeof(char*)` (8) to `HP_DEL_METADATA_SIZE` (11) for all tables. This has zero memory impact: `recbuffer = ALIGN(visible + 1, 8)` absorbs the shift entirely (16 bytes for all small records). **Delete neighbor coalescing**: `hp_push_free_block_coalesce()` (in `hp_delete.c`) normalizes the free-list head by treating a single record as a block of count 1, then checks adjacency in two directions (above/below). Handles all merge cases uniformly: single-to-single, single-to-block, block-to-single, and block-to-block. Combined count capped at `UINT_MAX16`; falls back to `hp_push_free_record` / `hp_push_free_block` when no adjacency. `hp_push_free_record_coalesce()` is a thin inline wrapper. **Dark record clearing** is centralized in `hp_clear_dark_records`, a strided loop that zeroes only the essential metadata bytes per record (`del_link`, `del_flag`, `visible`) rather than the entire `recbuffer`. Used by both `hp_push_free_block` and `hp_push_free_block_coalesce`. **New API in `heapdef.h`:** - Push: `hp_push_free_record()`, `hp_push_free_block()`, `hp_push_free_record_coalesce()` - Consume: `hp_pop_free_record()`, `hp_take_free_block()` - Traverse: `hp_is_free_block_end()`, `hp_free_block_first()`, `hp_is_free_block_start()`, `hp_free_block_start_count()` Non-inline functions: `hp_push_free_block()` and `hp_push_free_block_coalesce()` in `hp_delete.c`; `hp_take_free_block()` in `hp_write.c`. **Caller updates:** - `hp_delete.c`: `heap_delete()` calls `hp_push_free_record_coalesce()` for single-record deletes - `hp_write.c`: error-path push and `next_free_record_pos()` pop use `hp_push_free_record()` / `hp_pop_free_record()` - `hp_blob.c`: `hp_free_run_chain()` uses coalescing versions; `hp_take_free_list_runs()` extracted from Steps 1 and 3 of `hp_write_one_blob()`, parameterized by `min_avail`; blob allocation uses `hp_take_free_block()` / `hp_pop_free_record()` - `hp_scan.c`: batch-skip entire deleted blocks - `_check.c`: block-aware free-list count and scan - `hp_shrink_tail()`: reclaims entire blocks at the tail **Unit tests** (`hp_test_freelist-t.c`): 13 new tests (252 assertions): block formation, pop/shrink, collapse to single, partial block take, scan batch-skip, shrink-tail with blocks, interleaved singles and blocks, ascending/descending delete coalescing, non-adjacent gap, coalesced block reuse by blob insert, block-to-block via adjacent blob chains. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-41020 A `MEMORY` table refuses a row it just held `hp_alloc_from_tail()` tests the table memory ceiling before it decides whether the leaf it is about to use has to be allocated or is merely being reclaimed. Reclaiming adds no memory, so on that branch the test answers a question nobody asked. The state it misjudges is routine. `hp_find_free_hash()` allocates an index leaf without consulting `max_table_size`, and no leaf is smaller than `heap_min_allocation_block`, so a table with two hash indexes is already over a 32K ceiling once it holds its first row. That is legal: the ceiling is only tested when the record cursor lands on a leaf boundary, and the first row tests it while both counters are still zero. `hp_shrink_tail()` puts the cursor back on a leaf boundary whenever it empties the tail, and `data_length` goes on counting the leaf, which is still allocated. The next write re-reads that sum and reports `HA_ERR_RECORD_FILE_FULL` for a row the table held a moment earlier. Move the ceiling test to the arm that calls `hp_get_new_block()`. The `max_records` row-count test stays where it is, because it caps rows however the slot is obtained. **Why a master and a slave disagreed about the same statement.** `REPLACE INTO t SELECT * FROM t` feeds the row back out of the table, so the record buffer still points into the chain that the delete parked; the chain is adopted rather than freed and the cursor never moves. The slave builds the row from the replication event buffer, nothing points into the parked chain, it is freed, the tail empties and the write is refused. Replication is one way to reach the state, not the cause: a targeted `DELETE` and a re-`INSERT` on a single server reach it too. Tests: `heap.blob_delete_reinsert_ceiling` covers the single-server route and checks that a table that genuinely needs more memory is still refused; `heap.blob_replace_repl_ceiling` covers the reported master/slave divergence; `hp_test_freelist` test 22 pins the branch itself. Each fails without the change. **Storing the row limit as a ceiling.** `heap_create()` recorded its `max_records` argument in the share unchanged, so 0 arrived there still meaning "no limit" and every reader had to special-case it. The row-count test above carried that as a second condition, `&& info->max_records`. Write `NO_LIMIT_RECORDS` into the share instead. The share then always holds a real ceiling and the write path tests one value. That frees 0 to mean on the share what it says, a table that accepts no rows, which is the natural way to create one that is never written to; only `heap_create()`'s argument keeps using 0 for "no limit". `ha_heap::info()` multiplies `max_records` by the record length to report `MAX_DATA_LENGTH`. An unlimited table now reports `~(my_off_t) 0` rather than a product that would overflow. Nothing reaches that branch through SQL, because `ha_heap::create()` derives `max_records` from the memory ceiling and a `MAX_ROWS` clause only lowers it. Measured before and after, the reported `MAX_DATA_LENGTH` is unchanged for a plain `MEMORY` table and for one created with `MAX_ROWS`. The unit tests wrote the old convention themselves and move with it: `hp_test_freelist` assigned `share->max_records= 0` to lift the limit, which now reads as a limit of zero rows, and `hp_test_block_size` asserted the share kept the 0. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Kristian Nielsen
knielsen@knielsen-hq.org |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-41217: Inefficient memory management by parallel slave The rpl_group_info object is used to keep track of a lot of different state in an event group/transaction across events. It is also used in parallel replication for a small set of values that are enqueued by the SQL driver thread for worker threads to handle. This wastes a lot of memory when a large number of event groups are enqueued (eg. in case of slave lag), since most of the rpl_group_info is not used in the queue. This is a preparatory patch that splits out the part of rpl_group_info that is needed for the enqueueing into a separate rgi_queued_part object, without any functional or logic changes. This to separate a lot of mechanical changes from the actual logic modification for easier review: - Introduce struct rgi_queued_part and move a bunch of fields XXX into it from rpl_group_info. - Introduce rpl_group_info::q pointing to rgi_queued_part and allocate it in the constructor. - Change references like rgi->XXX into rgi->q->XXX. This refactoring provides the basis for a follow-up patch where parallel replication is changes so only the rgi_queued_part object is put in the queue, not the whole rpl_group_info. Signed-off-by: Kristian Nielsen <[email protected]> |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40802 COUNT(DISTINCT <blob>) fails when its tmp table converts `COUNT(DISTINCT)` collects the distinct values in a temporary table with a unique constraint over the aggregate's arguments, and treats a duplicate key error from the write as "value already seen": ```c if (!table->file->is_fatal_error(error, HA_CHECK_DUP)) return FALSE; // duplicate, not an error ``` For a blob argument the record holds only a pointer to the value, so `Aggregator_distinct::setup()` cannot use the `Unique` tree, which compares raw record bytes, and every value goes through that write instead. When such a write overflows the in-memory table, `create_internal_tmp_table_from_heap()` copies the stored rows to an on-disk table and then writes the row that overflowed, which until then was held in `record[0]` alone. Whether a duplicate key error on that last write is fatal is decided by the caller's `ignore_last_dupp_key_error` argument, and `Aggregator_distinct::add()` passed **0** three lines below the code that ignores the very same condition. The statement failed with ERROR 1169 (23000): Can't write, because of unique constraint, to table '(temporary)' Pass **1** instead, so that a duplicate arriving through the conversion is discarded exactly like one arriving through the ordinary write. The result is `table->file->stats.records` of that table, so not storing the duplicate is what makes the count right. The argument is the same upstream, where it is unreachable: a temporary table with a blob column was created on the on-disk engine to begin with, so the conversion was never entered for the only tables whose pending row can be a duplicate. Supporting blob columns in the in-memory engine made the table start in memory and convert. New tests `heap.count_distinct_blob_convert` and `heap.count_distinct_blob_convert_debug`. A write rejected as a duplicate returns its record to the free list and never reaches the allocation of the blob value, so only the first copy of a value makes the in-memory table grow, and the write that finds it full is the second copy of the value stored last. That holds only while a record slot is what the table runs out of first. Blob values come out of the same space, and only a write that is not a duplicate ever allocates one, so when a blob allocation is the one that hits the limit, the pending row is not a duplicate at all. Which of the two runs out first follows from how records and blob values pack together, not from any threshold on the value width. Of 24 measured combinations of width and `max_heap_table_size`, 20 convert but only 8 reach a duplicate pending row, so asserting that the table was converted does not establish that the ignored duplicate was reached. The first test uses widths measured to overflow on a record slot. The second removes the dependency on that measurement, injecting the duplicate through a new debug point in `Tmp_table_default_copier::copy_rows()`, beside the one the row copy loop already carries. Every value is present twice, so whichever copy the injected duplicate discards, the other one is still written and the count does not depend on which write overflowed. The status counter is read with the in-memory limit restored. The status table is materialized into a temporary table of its own, and its VARIABLE_VALUE column is wide enough to be stored as a blob, so under the shrunken limit that table can overflow and be converted as well, and would then report its own conversion. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40739 Server crashes in `spider_db_open_item_field` `spider_db_open_item_field()` looks a field's table up among the Spider tables of the query whenever the field does not belong to an internal temporary table: if (field->table->s->tmp_table != INTERNAL_TMP_TABLE) That is a hand-rolled copy of the server's own `TABLE_SHARE::is_optimizer_tmp_table()` predicate. Temporary tables created by `Create_tmp_table` are marked `RESULT_TMP_TABLE` rather than `INTERNAL_TMP_TABLE`, so a field of such a table passes the test, `spider_fields::find_table()` finds no holder for it, and the returned `NULL` is dereferenced. Only the second pass crashes. The first pass, which decides whether the group by handler can be created at all, does guard against a `NULL` holder. The two passes do not resolve to the same items, though: an `Item_direct_ref` is followed through `real_item()`, and between optimization and execution it is re-pointed at a field of the optimizer's result temporary table. Ask the server's accessor instead of restating it, so that the predicate keeps following the server's definition of an optimizer temporary table. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40523 Versioned UPDATE on a HEAP table with blobs corrupts the history row A system-versioned `UPDATE` of a blob column on a `HEAP` table stored garbage in the history row, and an `AFTER UPDATE` trigger reading `OLD.<blob>` saw the same garbage. Both values are durable: the history row is what `SELECT ... FOR SYSTEM_TIME ALL` returns, and both reach replicas through the row-based binlog image. No ASAN build is needed to reproduce either. ## How a versioned UPDATE reaches the engine The row is first updated in place with `ha_update_row(old_data, new_data)`. The SQL layer then calls `vers_insert_history_row()`, which restores the pre-update row from `record[1]` into `record[0]`, stamps it with the delete-time and calls `ha_write_row()`. So the record handed to `ha_write_row()` is a verbatim copy of the row the engine was just told to overwrite -- blob data pointer included. A versioned `DELETE` is not affected: `TABLE::delete_row()` stamps the end field with `vers_update_end()` and issues a single `ha_update_row()`. It writes no history row. ## Cause `heap_update()` and `heap_delete()` do not free the old blob chain outright. They park it, because the SQL layer keeps reading the pre-update row out of `record[1]` after `ha_update_row()` returns -- `binlog_log_row()` builds the before-image from it, and an `AFTER UPDATE` trigger reads `OLD.<blob>` from it. Those are zero-copy pointers straight into `HP_BLOCK`, so freeing the chain would make them dangle. `heap_write()` redeemed that parking unconditionally, before allocating. For the history row that is exactly the wrong moment, since it sources its blob from the chain the update just parked. The free put those records on the delete list, where the allocation immediately below handed them straight back as the history row's own chain -- with `hp_push_free_block()`'s free-list links already scribbled through the payload. Source and destination of the blob copy overlapped, and `record[1]` was left pointing at reused memory for the rest of the statement. ## What triggers the bug, and what reads the corrupted data Triggering statements are every `vers_insert_history_row()` caller reaching a `HEAP` table whose record buffer was filled by a read: single-table `UPDATE`, multi-table `UPDATE` (both the on-the-fly and the deferred `do_updates()` path), `INSERT ... ON DUPLICATE KEY UPDATE`, and the row-based replication applier. A versioned `UPDATE` that does not change the blob column is unaffected -- `heap_update()` keeps the chain and parks nothing. Three consumers then read corrupted data, all of them out of `record[1]` after the history-row write has recycled the chain: - the history row itself, as returned by `SELECT ... FOR SYSTEM_TIME ALL`; - the row-based binlog before-image, so the corruption reaches replicas; - `AFTER UPDATE` triggers reading `OLD.<blob>`, so whatever the trigger does with that value -- typically writing it to an audit table -- stores wrong data, and that write is itself replicated. The trigger case is the easiest to miss, because `LENGTH(OLD.<blob>)` is still correct: the length lives in the record buffer and survives, and only the payload has been recycled. A `BEFORE UPDATE` trigger on the same table reports the correct value, which localises the damage to the history-row write that happens between the two. ## Fix The parked chain already holds exactly the bytes the history row needs -- it is a verbatim copy of the same record. So instead of freeing it and allocating a duplicate, the new row adopts it: - `hp_flush_unaliased_blob_free()` redeems every parked chain **except** ones the record being written still sources blob data from. - `hp_write_blobs()` takes those over: the stored row points at the parked chain and no new chain is written. The pending slot is cleared only once every column has succeeded, so the rollback path can tell an adopted chain from an allocated one and leaves it parked rather than freeing it. Both record buffers stay valid, because the chain's contents are never disturbed. Adoption also needs no space at all, which matters at `max_heap_table_size`: the history row previously had to find room for a second copy of a blob that was already resident, and the parked chain was often the only reclaimable space. Aliasing is detected by exact pointer equality against the parked chain head. `hp_read_blobs()` hands out zero-copy pointers of exactly two forms -- the chain head, or `chain + recbuffer` -- and reassembles a multi-run chain into `info->blob_buff`, which can never alias, so the two comparisons in `hp_blob_sources_chain()` are complete and need no walk of the `HP_BLOCK` tree. Matching is per blob column, since `pending_blob_chains[i]` is the chain parked for column `i`, and that slot is `NULL` for a column the update did not change. An adopted chain may be longer than the adopting row's blob length: `UPDATE t SET f = LEFT(f,6)` leaves the shortened value pointing at the original data. That is harmless. `hp_read_blobs()` picks the chain layout from the chain's own flag byte rather than from the length, so a short read off a long chain still starts at the right offset, and `hp_free_run_chain()` walks `run_rec_count`/`next_cont` rather than the length, so the whole chain is still reclaimed. `REPLACE` was never affected: `HA_EXTRA_WRITE_CAN_REPLACE` makes `hp_read_blobs()` copy rather than hand out zero-copy pointers. Internal temporary tables never park -- they free their chains outright and do not allocate the array -- so `hp_write_blobs()` guards on it. A blob cannot itself be indexed in a `HEAP` table: `ha_heap` does not set `HA_CAN_INDEX_BLOBS`, so both `KEY (f(10))` and `UNIQUE (f)` are rejected at `CREATE` with `ER_BLOB_USED_AS_KEY`. There is therefore no versioned blob-as-key combination for adoption to get wrong. Blob key segments exist only for internal temporary tables, which never park a chain and are never versioned. ## Tests `storage/heap/hp_test_blob_alias-t.c` drives the sequence at the `heap_write()` API for a single-record chain, a zero-copy run and a multi-run chain, and checks both the stored row and the caller's buffer. It also pins adoption itself, by the stored row's chain pointer and by the growth of `block.last_allocated` across the write: exactly one record slot and no chain. The multi-run case is reassembled into `info->blob_buff` and so cannot alias; the test asserts which layout it got, so the coverage cannot silently degrade. `blob_vers_trigger` covers `OLD.<blob>` in triggers across single-table `UPDATE`, multiple blob columns, `ON DUPLICATE KEY UPDATE` and multi-table `UPDATE`, with `BEFORE UPDATE` alongside `AFTER UPDATE` on one table, and non-versioned `HEAP` and versioned `MyISAM` controls. `blob_versioning`, `blob_vers_odku`, `blob_vers_multi` and `blob_vers_repl` cover the history row itself for every `vers_insert_history_row()` caller and replication of the before-image. `blob_versioning` additionally covers the cases where only one of two blob columns changed, so the history-row write sees a mix of parked and empty slots; a blob shrunk in place with `LEFT()`, so the row and its history carry the same pointer with different lengths; an indexed table, so the key loop runs while `record[1]` still holds a zero-copy pointer and a `UNIQUE` key materializes stored blobs into `key_blob_buff`; and the two `ER_BLOB_USED_AS_KEY` rejections. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40378 heap: roll back key changes when a blob write fails in `heap_update()` `heap_update()` moves all changed key entries to the new key values **before** writing the new blob chains. When a blob chain write then failed (e.g. with `HA_ERR_RECORD_FILE_FULL`), the rollback restored the record bytes and blob chain pointers, but the `err:` label only undid key changes for `HA_ERR_FOUND_DUPP_KEY` -- historically the only possible failure once the key loop had run. The hash/btree entries were left keyed on the new values while pointing at a record holding the old values, corrupting the index: 1. index lookups by the old key value missed the row 2. `CHECK TABLE` reported the table corrupt 3. on debug builds the heap consistency check in `ha_heap::external_lock()` raised a second error into an already-set diagnostics area, firing a `Diagnostics_area` assertion on the next statement Fix: widen the `err:` recovery to run for `HA_ERR_RECORD_FILE_FULL`, `HA_ERR_OUT_OF_MEM` and `ENOMEM` as well, so a failure raised after the key loop also moves every changed key back to its old value. One recovery path now serves every failure that leaves keys moved to their new values, including any future error source in the key loop itself. The `err:` block assumed the failure happened **inside** the key loop, so that `keydef` addresses the partially processed keydef. A blob-chain write fails after that loop has run to completion, and therefore arrives with `keydef == keydef_end`. Reading `info->errkey` and `keydef->algorithm` from there addresses `share->keydef[share->keys]`; as `sizeof(HP_KEYDEF)` (888) far exceeds the key segments and blob descriptors that follow the keydef array, that read runs past the end of the `HP_SHARE` allocation, and the rollback sweep then dereferences a garbage `keydef->seg` in `hp_rec_key_cmp()`. So the recovery distinguishes the two failure sites: with `keydef == keydef_end` there is no partly updated key to repair and none to name in `info->errkey`, and the sweep starts at the last keydef instead. The same branch also covers a table with no keys at all (`share->keys == 0`), where the sweep has nothing to do. `info->errkey` is initialized to `-1` on entry to `err:`, so a failure that is not a key error can never expose a stale key number from an earlier operation. The original errno is captured before the recovery and restored after it, so that a rollback `write_key` failure (which `hp_rb_write_key()` reports as `HA_ERR_FOUND_DUPP_KEY` with a stale `errkey`) cannot mask it. The new test `heap.blob_update_key_rollback` exercises hash, BTREE, two changed indexes, an index on an unchanged column (which the rollback must leave untouched), a partial multi-row UPDATE, and a table with no indexes at all; each asserts the table stays consistent after the failure via `CHECK TABLE` and index lookups. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-41020 A `MEMORY` table refuses a row it just held `hp_alloc_from_tail()` tests the table memory ceiling before it decides whether the leaf it is about to use has to be allocated or is merely being reclaimed. Reclaiming adds no memory, so on that branch the test answers a question nobody asked. The state it misjudges is routine. `hp_find_free_hash()` allocates an index leaf without consulting `max_table_size`, and no leaf is smaller than `heap_min_allocation_block`, so a table with two hash indexes is already over a 32K ceiling once it holds its first row. That is legal: the ceiling is only tested when the record cursor lands on a leaf boundary, and the first row tests it while both counters are still zero. `hp_shrink_tail()` puts the cursor back on a leaf boundary whenever it empties the tail, and `data_length` goes on counting the leaf, which is still allocated. The next write re-reads that sum and reports `HA_ERR_RECORD_FILE_FULL` for a row the table held a moment earlier. Move the ceiling test to the arm that calls `hp_get_new_block()`. The `max_records` row-count test stays where it is, because it caps rows however the slot is obtained. **Why a master and a slave disagreed about the same statement.** `REPLACE INTO t SELECT * FROM t` feeds the row back out of the table, so the record buffer still points into the chain that the delete parked; the chain is adopted rather than freed and the cursor never moves. The slave builds the row from the replication event buffer, nothing points into the parked chain, it is freed, the tail empties and the write is refused. Replication is one way to reach the state, not the cause: a targeted `DELETE` and a re-`INSERT` on a single server reach it too. Tests: `heap.blob_delete_reinsert_ceiling` covers the single-server route and checks that a table that genuinely needs more memory is still refused; `heap.blob_replace_repl_ceiling` covers the reported master/slave divergence; `hp_test_freelist` test 22 pins the branch itself. Each fails without the change. **Storing the row limit as a ceiling.** `heap_create()` recorded its `max_records` argument in the share unchanged, so 0 arrived there still meaning "no limit" and every reader had to special-case it. The row-count test above carried that as a second condition, `&& info->max_records`. Write `NO_LIMIT_RECORDS` into the share instead. The share then always holds a real ceiling and the write path tests one value. That frees 0 to mean on the share what it says, a table that accepts no rows, which is the natural way to create one that is never written to; only `heap_create()`'s argument keeps using 0 for "no limit". `ha_heap::info()` multiplies `max_records` by the record length to report `MAX_DATA_LENGTH`. An unlimited table now reports `~(my_off_t) 0` rather than a product that would overflow. Nothing reaches that branch through SQL, because `ha_heap::create()` derives `max_records` from the memory ceiling and a `MAX_ROWS` clause only lowers it. Measured before and after, the reported `MAX_DATA_LENGTH` is unchanged for a plain `MEMORY` table and for one created with `MAX_ROWS`. The unit tests wrote the old convention themselves and move with it: `hp_test_freelist` assigned `share->max_records= 0` to lift the limit, which now reads as a limit of zero rows, and `hp_test_block_size` asserted the share kept the 0. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-41007 Warn when GROUP_CONCAT(DISTINCT) loses rows silently Give a warning when `GROUP_CONCAT(DISTINCT x)` or `JSON_ARRAYAGG(DISTINCT x)` returns only part of a group, or nothing at all, because the walk of the duplicate filter failed. The result is wrong rather than deliberately cut, and nothing was said about it. Both build their result in `val_str()` by walking `unique_filter`, and threw the walk's return value away. `Unique::walk()` reports its own failures through it, from allocating the merge buffer to reading back the chunks it merged, so a failure gave a short result, or an empty one, in silence. The return value cannot be used on its own. `dump_leaf_key()` also stops the walk, for two reasons that are not failures: it cuts the result at `group_concat_max_len`, which it already reports by setting `result_cut`, and it stops without losing anything once the `LIMIT` is used up. Reporting every non-zero return as a cut warns about `GROUP_CONCAT(DISTINCT a LIMIT 5)` returning exactly the five rows that were asked for. `dump_leaf_key()` now records that it was the one that stopped the walk, so `val_str()` warns only when the walk itself failed. The warning is a new one. `ER_CUT_VALUE_GROUP_CONCAT` reports the row the result was cut at, and there is no such row here: the walk failed before it delivered anything, and how much was lost is not known, so it would read `Row 0 was cut by group_concat()`. `ER_RESULT_CUT_BY_LIMIT` says that the result was cut and names the limit that cut it, which is the memory `Unique` was given to work in: the smaller of `tmp_memory_table_size` and `max_heap_table_size`. Not every failure is silent either. The merge buffer is allocated with `MY_WME` and the spill file is opened with `MY_WME`, so running out of memory or failing to read raises an error of its own. Only the guard at the top of `merge_walk()`, which refuses a merge buffer too small to hold one key per chunk, returns without saying anything. Warn only when no error was raised: where one was, the user has been told and the statement is failing, so describing the length of a result nobody will see adds nothing. The debug keyword `unique_walk_merge_fail` fails the merging walk quietly and `unique_walk_merge_error` fails it with an error raised. `main.gconcat_distinct_walk_fail` uses both. The `LIMIT` case needs no debug build and is checked in `main.gconcat_distinct_spill`. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Sergei Golubchik
serg@mariadb.org |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-41473 more mysql_json plugin OOB reads * check that `element_count` doesn't point beyond the data end also fixed: * UB in `expr<<28` shift in `read_variable_length()` * off-by-one in `parse_mysql_scalar()` * decimal sanity in `parse_mysql_scalar()` |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Monty
monty@mariadb.org |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Trivial optimziations for group_concat - Remove some if - Reorder code - More code comments (cherry picked from commit dc6a897961c311f981b150e4207ffc1390a219ef) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-38975: HEAP engine BLOB/TEXT/JSON/GEOMETRY support with indexable blob columns Remove the HA_NO_BLOBS restriction from the HEAP engine, allowing the optimizer to keep temporary tables with BLOB/TEXT columns in memory when they fit within max_heap_table_size / tmp_memory_table_size limits. Additionally, advertise HA_CAN_GEOMETRY so explicit CREATE TABLE ... ENGINE=MEMORY with GEOMETRY columns works. Unlike other HEAP blob implementations (e.g. Percona), this patch provides full HASH index support on blob columns, enabling efficient lookups, GROUP BY, and DISTINCT operations directly in HEAP without falling back to disk. Architecture ------------ BLOB data is stored using continuation records -- additional fixed-size records allocated from the same HP_BLOCK that holds regular rows. This reuses existing allocation, free list, and size accounting with minimal structural change, and avoids per-blob my_malloc() calls. The existing single-byte visibility flag is extended into a flags byte with bits for HP_ROW_HAS_CONT, HP_ROW_IS_CONT, HP_ROW_CONT_ZEROCOPY, HP_ROW_SINGLE_REC, and HP_ROW_MULTIPLE_REC. Continuation records are grouped into variable-length runs -- contiguous sequences within a leaf block. Only the first record of each run carries a 10-byte header (next_cont pointer + run_rec_count); inner records are pure payload. Three storage formats, detected by flag bits via inline predicates: Case A (HP_ROW_SINGLE_REC): single record, no header, data at offset 0. Zero-copy read. Case B (HP_ROW_CONT_ZEROCOPY): single run, multiple records. Header in rec 0, data contiguous in rec 1..N-1. Zero-copy read via chain + recbuffer. Case C (HP_ROW_MULTIPLE_REC): one or more runs linked via next_cont. Reassembly into blob_buff required. Run allocation uses a two-phase strategy: (1) peek-then-unlink walk of the free list detecting contiguous groups, (2) tail allocation from HP_BLOCK for remaining data. A Step 3 scavenge fallback walks the entire free list when tail allocation fails. HP_SHARE::total_records tracks all physical records (primary + continuation), while HP_SHARE::records remains the logical count used by hash bucket mapping. Reassembly buffer (HP_INFO::blob_buff) follows the same pattern as InnoDB's blob_heap -- allocated once, grown via my_realloc, freed on heap_reset()/close. Zero-copy cases (A/B) return pointers directly into HP_BLOCK with no copy. Full HASH index key handling for BLOB columns: hp_rec_hashnr(), hp_rec_key_cmp(), hp_key_cmp(), hp_make_key(), hp_hashnr() are extended for HA_BLOB_PART segments. Hash pre-check optimization skips expensive blob materialization when hashes differ. PAD SPACE collation semantics are preserved for blob key comparisons. Field_blob_key (Monty) produces HEAP-native key format (4-byte length + 8-byte data pointer) directly, eliminating key buffer translation between the SQL layer and HEAP engine. SQL layer changes ----------------- pick_engine() (new, extracted from choose_engine()): replaced the blob_fields check with a reclength > HA_MAX_REC_LENGTH guard. choose_engine() calls pick_engine() with the real reclength. pick_engine() is also called early in finalize() with reclength=0 to predict whether the engine will be HEAP, enabling blob-aware GROUP BY key setup that avoids unnecessary m_using_unique_constraint. finalize(): HEAP+blob uses fixed-width rows; GROUP BY key setup sets key_part_flag from field, uses item max_length for blob key sizing. store_length initialized for all GROUP BY key parts. key_type uses field->binary() to determine FIELDFLAG_BINARY vs text collation. DISTINCT key setup skips null-bits helper for HEAP. remove_duplicates(): blob check moved before HEAP check to fall through to remove_dup_with_compare(). Aggregator_distinct::add(): overflow-to-disk conversion via create_internal_tmp_table_from_heap() for non-dup write errors. Expression cache disabled for HEAP+blob (key format incompatibility). FULLTEXT early detection in mysql_derived_prepare(): forces disk engine via TMP_TABLE_FORCE_MYISAM when outer query uses MATCH. Deferred blob chain free (MDEV-39732): heap_delete() saves chain pointers to pending_blob_chains, flushed on next mutation or heap_reset()/close. Prevents dangling zero-copy pointers during binlog_log_row(). REPLACE safety (MDEV-39825): HP_SHARE::write_can_replace flag forces copy mode in hp_read_blobs(), preventing blob data corruption from freed-then-reused continuation records during REPLACE. Geometry GROUP_CONCAT fix (MDEV-39761): downgrade Field_geom to Field_blob for GROUP_CONCAT temp tables in both expression creation paths. Type_handler_geometry::type_handler_for_tmp_table() added. Geometry GROUP BY key fix (MDEV-39871): detect when new_key_field() produced non-blob Field_varstring for a blob column, replace with Field_blob_key. Performance ----------- Non-blob tables: zero regression. Every blob-specific code path is guarded by if (share->blob_count). No new allocations, no format changes, no hash function changes for non-blob keys. Blob tables: eliminates file creation/deletion overhead and page cache management. For single-run blobs (common case), the read path is entirely zero-copy. Limitations ----------- 1. No BTREE indexes on blob columns (HASH only) 2. No partial-key prefix indexing for blobs 3. 2x memory for Case C reads only (A/B are 1x) 4. No blob compression 5. 65,535 records per run (uint16 cap, auto-split) 6. max_heap_table_size applies to continuation records 7. Expression cache disabled for HEAP+blob 8. FULLTEXT forces disk engine Linked bugs fixed: - MDEV-39703: mroonga fulltext test ordering - MDEV-39723: ER_DUP_ENTRY on GROUP BY with blob column - MDEV-39724: crash in hp_is_single_rec with GROUP BY - MDEV-39732: slave crash in hp_free_run_chain on blob replication - MDEV-39761: Field_geom::store() assertion in GROUP_CONCAT - MDEV-39782: RBR ER_KEY_NOT_FOUND on HEAP blob UPDATE - MDEV-39825: blob data corruption on REPLACE into HEAP table - MDEV-39871: crash in my_hash_sort_bin on GROUP BY with geometry Reviewed by: Michael Widenius <[email protected]> Monty reviewed the entire patch. Areas where he suggested changes or contributed code: - Field_blob_key class (HEAP-native blob key format, 4-byte length + data pointer) - Duplicate key fix on HEAP-to-Aria conversion - hp_blob_key_length() uint32 fix - hp_rec_hashnr_stored removal - type_handler_for_tmp_table() param cleanup - Type_handler_geometry::type_handler_for_tmp_table() virtual - blob pointer bzero() - find_unique_row() double-materialization fix - Tail reclaim review - Batch tail allocation review - hp_update.c cleanup - Field_blob_compressed temp table fix - row_pack_length() dedup - pack_length_no_ptr() removal - Race condition fix in HEAP - MDEV-39703 mroonga test fix - MDEV-39825 write_can_replace optimization - is_text_key_segment removal (field->binary() simplification) - Documentation (Docs/internal-temporary-tables.txt) Contribution by: Alexander Barkov <[email protected]> Type_handler::make_and_init_table_field_ex() -- refactored temp table field creation from inline code in sql_select.cc into type handler virtual methods (sql_type.cc, sql_type_geom.cc), enabling clean per-type-handler field creation for HEAP blob promotion. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Monty
monty@mariadb.org |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Limit the memory used by GROUP_CONCAT() with ORDER BY GROUP_CONCAT() with ORDER BY collects all rows of the group in a TREE and only cuts it down in repack_tree(). The repack was triggered by (tree_len >> GCONCAT_REPACK_FACTOR) > thd->gconcat_max_len() with GCONCAT_REPACK_FACTOR 10, that is when the rows in the tree had produced 1024 * group_concat_max_len bytes, or 1G with the default settings. On top of that tree_len only counted the length of the strings, while the tree costs sizeof(TREE_ELEMENT) + reclength per row. For GROUP_CONCAT(int_col ORDER BY int_col) that is about 40 bytes per row against 6 bytes of result, so the tree had grown to several GB before the first repack. In practice the server ran out of memory first and the repack code was close to never used. The tree is now limited by the memory it has really allocated, tree->allocated, instead of by the length of the strings it holds. The limit is MY_MAX(thd->ram_limitation(), thd->gconcat_max_len()) and is never set so low that the tree can not hold a few rows. repack_tree() builds a new tree while the old one is still in memory, so the peak usage is the size we start the repack at plus the size we copy to. To keep the sum within the limit it is split into GCONCAT_TREE_PARTS parts; the repack starts when GCONCAT_TREE_REPACK_PARTS of them are used and copies to the remaining part. The part we do not copy to is also the room the tree has to grow before the next repack, which keeps the repacks amortized. Other changes: - tree_len is removed. It was only read by the old trigger. - repack_tree() decided that it had run out of memory by testing st.len <= st.maxlen after the walk. That test was only valid because the old trigger guaranteed that a complete copy had to overshoot st.maxlen. A repack triggered by memory can complete the walk with st.len far below st.maxlen, which would have failed the query with a wrong out of memory error. There is now an explicit flag for it. - The length that decides which rows to keep now also counts the separator that is put between two rows, so that it matches what val_str() will produce. - When the memory limit stops the copy, the result becomes shorter than group_concat_max_len. dump_leaf_key() can not detect this, as the result never reaches the maximum length. This is now remembered in result_cut and reported to the user. - All cut value reporting is moved to val_str(); dump_leaf_key() only marks that the result was cut. This removes the need to clear the truncated flag of table->blob_storage to avoid a duplicated warning, and gives one warning per group also when val_str() is called more than once for the same group, which repeated the warning before. - Added a function comment for repack_tree() that describes where the rows are cut away and why building a copy frees memory. Co-author: Arcadiy Ivanov <[email protected]> - Fixed a bug in copy_tree() |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Fix the `HA_NO_KEY_READ` blob key guard `HA_NO_KEY_READ` marks a key whose blob segment `heap_prepare_hp_create_info()` converted from the VARTEXT2 form, so that `heap_rkey()` refuses an index read on it. It never worked, because the mark was written to the wrong structure member. `heap_rkey()` tests `HP_KEYDEF::flag`, which is where the flag belongs: `HA_NO_KEY_READ` is declared among the key flags, not the key-seg flags. The assignment instead targeted `HA_KEYSEG::flag`, which no reader consults for this flag, so the guard could never fire. That member is also `uint16`, so bit 20 was discarded on assignment as well; `-Wall -Wextra` does not warn, only `-Wconversion` does, and it is not enabled. Write the flag to `keydef[key].flag` instead. `HP_KEYDEF::flag` is `uint` and holds bit 20, and `heap_create()` copies it into the share that `heap_rkey()` reads. `hp_test_key_setup-t` covers the marking, the unmarked case, that `heap_create()` does not lose the flag while folding its own bits into `keydef->flag`, and that `heap_rkey()` refuses a marked key while accepting an unmarked one. The last pair clears `my_assert` so the guard reports instead of aborting, the same way the server's `--debug-assert=0` does, and skips on builds without `DBUG_ASSERT`. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Monty
monty@mariadb.org |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Trivial optimziations for group_concat - Remove some if - Reorder code - More code comments (cherry picked from commit dc6a897961c311f981b150e4207ffc1390a219ef) |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Monty
monty@mariadb.org |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40030 Add support for CHECK TABLE for memory tables CHECK TABLE is now supported, but will only return ok or fail. Still good enough for testing heap table consistenty. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Fix CI regressions from MDEV-38975 forward-port to main Seven code fixes, a new test, and test re-recordings for issues found by CI on PR #5222. **NULL dereference in `create_tmp_field()`**: `SYS_REFCURSOR` plugin returns NULL from `make_new_field()` (cursor values cannot be materialized). The feature added `result->flags |= FIELD_PART_OF_TMP_UNIQUE` without a NULL check. Added `if (result)` guard. **xmltype identity loss and recursive CTE reclength mismatch in `Item_type_holder::create_tmp_field_ex()`**: the blob_key dispatch now requires both: (1) `type_handler_for_tmp_table()` returns `blob_key_type_handler()`, AND (2) `dynamic_cast<Type_handler_blob_common*>` confirms the original type is a native blob. Condition 1 excludes xmltype (its override returns itself). Condition 2 excludes VARCHAR types promoted via `varstring_type_handler()` -> `too_big_for_varchar()` -> `blob_type_handler()`. Without condition 2, wide VARCHAR in recursive CTEs (e.g. `cast('...' as varchar(1000))`) was promoted to `Field_blob_key` in the main UNION DISTINCT table (`part_of_unique_key=true`) but stayed as `Field_varchar` in the incremental table (`part_of_unique_key=false`), causing a `reclength` mismatch assertion in `select_union_recursive::send_data()` (`main.json_equals` crash). **Spurious `reclength > HA_MAX_REC_LENGTH` in `pick_engine()`**: the original `choose_engine()` (both 10.11 and upstream/main) never had a reclength check. MDEV-38975 introduced it when replacing the `blob_fields` condition. HEAP has no internal reclength limit -- `hp_create.c` stores `uint reclength` and allocates blocks of that size; `max_supported_record_length()` is only checked in `unireg.cc` during user-facing CREATE TABLE. I_S tables like SLAVE_STATUS routinely have reclength ~880KB (13 bare `Varchar()` columns). The check forced them to Aria where `fill_slave_status()` returned 0 rows. Removed the check and the unused `reclength` parameter from `pick_engine()`. **Multi-update `tmp_memory_table_size` override**: the 10.11 feature overrode `big_tables=FALSE` for multi-update dedup tables. The forward-port translated this as `tmp_memory_table_size=SIZE_T_MAX` when the variable was 0. But `big_tables=FALSE` was a soft "don't force disk" hint, while `tmp_memory_table_size=SIZE_T_MAX` overrides the user's explicit `tmp_memory_table_size=0` directive. Since main removed `big_tables` entirely (MDEV-19713), the override is not needed. Removed. **Zero-length key rejection in `check_tmp_key()`**: reject `key_len == 0` to prevent useless zero-length keys from being created by `add_tmp_key()`. Reachable when all key parts are CHAR(0) NOT NULL: `key_length()` returns 0, the field is not nullable (no HA_KEY_NULL_LENGTH) and not VARCHAR/BLOB/GEOMETRY (no HA_KEY_BLOB_LENGTH), so `fld_store_len` is 0 for every part. Without this guard, `check_tmp_key()` would accept the key (0 <= max_key_length), and the optimizer would create a ref key that cannot distinguish any rows. Added `heap.char0_key` test exercising this via a materialized derived table with CHAR(0) NOT NULL join columns. **Non-deterministic `column_compression` test**: HEAP blob support allows compressed VARCHAR/TEXT temp tables to stay in HEAP instead of falling to Aria, changing row iteration order. Added `--sorted_result` to the two MDEV-24726 subqueries that lack `ORDER BY`. Test changes: - `spatial_utility_function_collect`: added ORDER BY to window function that lacked it (results were engine-row-order-dependent) - `tmp_space_usage`: removed multi-update override; forced disk for MDEV-34016/34060 Aria-specific test sections (blob I_S tables now stay in MEMORY) - `blob_update_overflow`: replaced `SHOW STATUS LIKE 'Created_tmp_%'` with targeted I_S query (Created_tmp_files varies on sanitizer builds) - 'funcs_1.is_tables_is' ; re-recorded test as INFORMATION_SCHEMA.SLAVE_STATUS is now back as MEMORY table - `column_compression`: added `--sorted_result` for MDEV-24726 queries - `char0_key` (new): CHAR(0) NOT NULL derived table ref key rejection - Re-recorded 8 tests for expected "temp table stays in MEMORY" changes |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40920 Say whether a value cut in a group reached the answer A TEXT value longer than `group_concat_max_len` is cut on its way into `blob_storage`, in `Field_blob::handle_group_concat()`. That happens while the group is being built, not when the answer is put together, so whether the answer is any shorter for it depends on whether the row carrying the value reaches the answer at all. Every such cut was reported as `ER_CUT_VALUE_GROUP_CONCAT`, which says that the answer lost something the user asked for, and that is true of only some of them. `Blob_mem_storage` now writes one byte in front of every value it stores and returns the pointer past it, so `was_cut()` answers for any value a reader holds a pointer to. `Field_blob::store()` sends every blob of a table that has a `Blob_mem_storage` through `handle_group_concat()`, so no value in that storage is without the byte, and `Field_blob::get_ptr()` on the record of a row hands back exactly the pointer that was stored. `dump_leaf_key()` reads the mark off each row it appends and sets `value_cut_in_result`. `val_str()` then reports: 1. **A warning**, `ER_CUT_VALUE_GROUP_CONCAT`, when a row that reached the answer carried a cut value. The answer is short by what was cut. The result being cut at `gconcat_max_len()` already gives that same warning, and a group that hits both is told once, not twice. 2. **A note**, `ER_CUT_VALUES_WHILE_PROCESSING`, when a value was cut but no row carrying one reached the answer. The answer may well be what a larger limit would have given. One note per aggregate is enough for a statement however many groups had a value cut, and `cleanup()` clears the mark so a statement run again gets its own. Reporting the loss as a warning keeps a strict `sql_mode` aborting on it, which it does because `THD::raise_condition()` promotes a warning and never promotes a note. `ST_COLLECT` is not affected. It reports `ER_CUT_VALUE_GROUP_CONCAT` itself, against `group_collect_max_len`. `main.gconcat_cut_note` covers the split with one group holding a short value and a long one, where a `LIMIT` alone decides which of them the answer is built from, over both the sort tree and the duplicate filter. `main.func_gconcat` shows the granularity: of five groups at `group_concat_max_len=499999`, the one holding exactly 499999 bytes is the one that does not warn. Note that `blob_storage` only exists when the aggregate has an `ORDER BY` or a `DISTINCT` and a blob field, so this is the only shape in which a value is cut this way. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Fix the `HA_NO_KEY_READ` blob key guard `HA_NO_KEY_READ` marks a key whose blob segment `heap_prepare_hp_create_info()` converted from the VARTEXT2 form, so that `heap_rkey()` refuses an index read on it. It never worked, because the mark was written to the wrong structure member. `heap_rkey()` tests `HP_KEYDEF::flag`, which is where the flag belongs: `HA_NO_KEY_READ` is declared among the key flags, not the key-seg flags. The assignment instead targeted `HA_KEYSEG::flag`, which no reader consults for this flag, so the guard could never fire. That member is also `uint16`, so bit 20 was discarded on assignment as well; `-Wall -Wextra` does not warn, only `-Wconversion` does, and it is not enabled. Write the flag to `keydef[key].flag` instead. `HP_KEYDEF::flag` is `uint` and holds bit 20, and `heap_create()` copies it into the share that `heap_rkey()` reads. `hp_test_key_setup-t` covers the marking, the unmarked case, that `heap_create()` does not lose the flag while folding its own bits into `keydef->flag`, and that `heap_rkey()` refuses a marked key while accepting an unmarked one. The last pair clears `my_assert` so the guard reports instead of aborting, the same way the server's `--debug-assert=0` does, and skips on builds without `DBUG_ASSERT`. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Arcadiy Ivanov
arcadiy@ivanov.biz |
|
|
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
MDEV-40781 Duplicate row despite DISTINCT when tmp table converts `create_internal_tmp_table_from_heap()` writes the pending `record[0]`, the row whose write filled the in-memory table, into the new table. Since **MDEV-40376** (`636f154bb49`) that write happens *before* `ha_end_bulk_insert()` rather than after it. `ha_maria::start_bulk_insert()` disables **all** indexes of an internal temporary table that is about to receive at least `MARIA_MIN_ROWS_TO_DISABLE_INDEXES` (100) rows: ```c if (file->open_flags & HA_OPEN_INTERNAL_TABLE) { /* Internal table; If we get a duplicate something is very wrong */ file->update|= HA_STATE_CHANGED; index_disabled= share->base.keys > 0; maria_clear_all_keys_active(file->s->state.key_map); } ``` `maria_write()` then skips `_ma_check_unique()` entirely, so the unique constraint that implements `DISTINCT` for a key too wide to be an index is not enforced. The rows copied out of the in-memory table are already distinct and need no checking against each other, but the pending row is exactly the row whose duplicate status is unknown, and it was written inside that window. A `SELECT DISTINCT` over wide columns could therefore return a duplicate row. Note that the justification given in `636f154bb49` is not the mechanism at work here. It refers to the bulk insert key *tree*, a different branch of `ha_maria::start_bulk_insert()`; setting `bulk_insert_buffer_size=0` does not avoid the problem. The fix splits the copy in two: 1. `Tmp_table_row_copier` gains a second virtual, `write_pending_row()`, defaulting to a no-op. 2. `copy_rows()` now only copies the rows the in-memory table holds. 3. `create_internal_tmp_table_from_heap()` calls `ha_end_bulk_insert()` and then `write_pending_row()`, so the pending row is written with the indexes of the new table back in place and a duplicate of an already copied row is detected. `Window_rowid_remapper` keeps writing its pending row within `copy_rows()` and inherits the no-op default. Its new position is only known once the rows before it have been written, and nothing is lost by writing it with the indexes still disabled: it replaces a row that is already in the table rather than adding one, and an update of a window function value cannot collide with another row, as a deduplicating key is not built on the columns it changes. The new test covers `SELECT DISTINCT`, `SELECT DISTINCT ... ORDER BY`, `GROUP BY`, `UNION` and `INSERT ... SELECT DISTINCT`, and asserts that the conversion actually happened so that a future sizing change cannot silently void the coverage. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||