From 004dc85f026be11b31a0ea303cdf50bea11b73a7 Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Tue, 28 Jul 2026 16:01:37 -0400 Subject: [PATCH] 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. --- include/my_sys.h | 14 + .../suite/heap/blob_window_overflow.result | 224 ++++++++++ .../suite/heap/blob_window_overflow.test | 286 +++++++++++++ .../heap/blob_window_overflow_debug.result | 33 ++ .../heap/blob_window_overflow_debug.test | 39 ++ .../heap/blob_window_overflow_encrypt.opt | 3 + .../heap/blob_window_overflow_encrypt.result | 33 ++ .../heap/blob_window_overflow_encrypt.test | 48 +++ sql/derived_handler.cc | 2 +- sql/filesort.cc | 16 +- sql/filesort.h | 18 +- sql/filesort_utils.cc | 2 +- sql/group_by_handler.cc | 2 +- sql/item_sum.cc | 3 +- sql/opt_subselect.cc | 3 +- sql/records.cc | 10 +- sql/sql_class.h | 7 + sql/sql_delete.cc | 27 +- sql/sql_expression_cache.cc | 3 +- sql/sql_select.cc | 385 +++++++++++++++--- sql/sql_select.h | 106 ++++- sql/sql_show.cc | 3 +- sql/sql_sort.h | 3 +- sql/sql_union.cc | 9 +- sql/sql_update.cc | 3 +- sql/sql_window.cc | 260 +++++++++++- sql/table.cc | 3 +- 27 files changed, 1445 insertions(+), 100 deletions(-) create mode 100644 mysql-test/suite/heap/blob_window_overflow.result create mode 100644 mysql-test/suite/heap/blob_window_overflow.test create mode 100644 mysql-test/suite/heap/blob_window_overflow_debug.result create mode 100644 mysql-test/suite/heap/blob_window_overflow_debug.test create mode 100644 mysql-test/suite/heap/blob_window_overflow_encrypt.opt create mode 100644 mysql-test/suite/heap/blob_window_overflow_encrypt.result create mode 100644 mysql-test/suite/heap/blob_window_overflow_encrypt.test diff --git a/include/my_sys.h b/include/my_sys.h index db766777de358..f47be15ec2dbc 100644 --- a/include/my_sys.h +++ b/include/my_sys.h @@ -500,6 +500,20 @@ typedef struct st_io_cache /* Used when caching files */ char prefix[3]; File file; /* file descriptor */ + /* + Circular, singly-linked list of IO_CACHEs that share the same + underlying file (the same 'file' descriptor and file position). + It links a master READ_CACHE to the slave caches created from it by + init_slave_io_cache(): each slave gets a private copy of the buffer + but reads from the master's file. NULL, the default, when + init_slave_io_cache() is not used. + + Because all members of the list share one file position, a seek done + through one cache affects the others: whenever a cache in the list + performs a real seek it walks the list and sets seek_not_done on + every other member so that they re-seek before their next I/O. + end_slave_io_cache() unlinks a cache from the list. + */ struct st_io_cache *next_file_user; /* seek_not_done is set by my_b_seek() to inform the upcoming read/write diff --git a/mysql-test/suite/heap/blob_window_overflow.result b/mysql-test/suite/heap/blob_window_overflow.result new file mode 100644 index 0000000000000..6c87c73948778 --- /dev/null +++ b/mysql-test/suite/heap/blob_window_overflow.result @@ -0,0 +1,224 @@ +# +# Window function computation must spill HEAP tmp table to Aria +# when storing computed values fills the table +# +# Window function results are written back into the sorted tmp +# table with ha_update_row(). A blob result column (an expression +# wider than 512 characters is promoted to TEXT in the tmp table) +# makes each such update allocate a blob continuation record; once +# this crosses max_heap_table_size the update fails with "table is +# full" and the server must transparently convert the tmp table to +# Aria, just as the write path already does, and carry the running +# computation over to the converted table. +# +# latin1 is used so that actual data size ~= declared column width: +# the memory arithmetic then holds whether or not the source VARCHAR +# column itself is stored as a blob in the HEAP tmp table. +# +SET @save_max_heap= @@max_heap_table_size; +SET @save_tmp= @@tmp_table_size; +SET max_heap_table_size = 4*1024*1024; +SET tmp_table_size = 4*1024*1024; +# +# Test 1: single window over all rows +# +# 800 rows of ~3KB fill the 4MB HEAP tmp table to ~2.4MB; storing +# the ~3KB computed value adds a blob continuation chain per row +# and overflows mid-computation. +# +CREATE TABLE t1 (a VARCHAR(3000)) CHARACTER SET latin1; +INSERT INTO t1 SELECT CONCAT(LPAD(seq, 8, '0'), REPEAT('x', 2900)) +FROM seq_1_to_800; +FLUSH STATUS; +# Should overflow during computation and convert HEAP->Aria +SELECT COUNT(*) AS cnt, COUNT(DISTINCT p) AS distinct_p, +MIN(LENGTH(p)) AS min_len, LEFT(MAX(p), 12) AS max_prefix +FROM (SELECT PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER () AS p +FROM t1) dt; +cnt distinct_p min_len max_prefix +800 1 2908 00000800xxxx +Warnings: +Warning 4202 800 values were longer than max_sort_length. Sorting used only the first 1024 bytes +# Verify HEAP->Aria conversion happened +SELECT variable_name, variable_value FROM information_schema.session_status +WHERE variable_name = 'Created_tmp_disk_tables'; +variable_name variable_value +CREATED_TMP_DISK_TABLES 1 +DROP TABLE t1; +# +# Test 2: partitioned window +# +# Same overflow, but the computation must also carry its partition +# tracking over the conversion (values are per partition). +# +CREATE TABLE t2 (g INT, a VARCHAR(3000)) CHARACTER SET latin1; +INSERT INTO t2 SELECT seq % 4, CONCAT(LPAD(seq, 8, '0'), REPEAT('y', 2900)) +FROM seq_1_to_800; +FLUSH STATUS; +SELECT COUNT(*) AS cnt, COUNT(DISTINCT p) AS partitions_seen, +MIN(LENGTH(p)) AS min_len, MAX(LENGTH(p)) AS max_len +FROM (SELECT PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY a) +OVER (PARTITION BY g) AS p +FROM t2) dt; +cnt partitions_seen min_len max_len +800 4 2908 2908 +Warnings: +Warning 4202 800 values were longer than max_sort_length. Sorting used only the first 1024 bytes +SELECT variable_name, variable_value FROM information_schema.session_status +WHERE variable_name = 'Created_tmp_disk_tables'; +variable_name variable_value +CREATED_TMP_DISK_TABLES 1 +DROP TABLE t2; +# +# Test 3: side-effecting window expression is evaluated once per row +# +# A compound expression containing a window function is evaluated +# row by row while the computed values are stored. The conversion +# must not make that pass start over: the user variable assignment +# below must be applied exactly once per row, the same as when the +# table does not overflow. +# +CREATE TABLE t3 (a VARCHAR(3000)) CHARACTER SET latin1; +INSERT INTO t3 SELECT CONCAT(LPAD(seq, 8, '0'), REPEAT('z', 2900)) +FROM seq_1_to_800; +FLUSH STATUS; +SET @c= 0; +SELECT COUNT(*) AS cnt, COUNT(DISTINCT e) AS distinct_e, +MIN(LENGTH(e)) AS min_len +FROM (SELECT CONCAT(@c:= @c+1, ':', +PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER ()) AS e +FROM t3) dt; +cnt distinct_e min_len +800 800 39 +Warnings: +Warning 4202 800 values were longer than max_sort_length. Sorting used only the first 1024 bytes +# One assignment per row, not one per row per pass +SELECT @c AS assignments; +assignments +800 +# Verify the overflow really happened +SELECT variable_name, variable_value FROM information_schema.session_status +WHERE variable_name = 'Created_tmp_disk_tables'; +variable_name variable_value +CREATED_TMP_DISK_TABLES 1 +# Same query without the overflow, for comparison +SET max_heap_table_size = 64*1024*1024; +SET tmp_table_size = 64*1024*1024; +FLUSH STATUS; +SET @c= 0; +SELECT COUNT(*) AS cnt, COUNT(DISTINCT e) AS distinct_e, +MIN(LENGTH(e)) AS min_len +FROM (SELECT CONCAT(@c:= @c+1, ':', +PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER ()) AS e +FROM t3) dt; +cnt distinct_e min_len +800 800 39 +Warnings: +Warning 4202 800 values were longer than max_sort_length. Sorting used only the first 1024 bytes +SELECT @c AS assignments; +assignments +800 +SELECT variable_name, variable_value FROM information_schema.session_status +WHERE variable_name = 'Created_tmp_disk_tables'; +variable_name variable_value +CREATED_TMP_DISK_TABLES 0 +DROP TABLE t3; +# +# Test 4: row positions held in memory instead of a temporary file +# +# The sort result is a sequence of row positions, which the +# conversion has to rewrite wherever it is kept. The tests above +# sort more than fits in the sort buffer, so the sequence is a merge +# file; with a sort buffer large enough it is an array in memory. +# +SET max_heap_table_size = 4*1024*1024; +SET tmp_table_size = 4*1024*1024; +SET @save_sort_buffer= @@sort_buffer_size; +SET sort_buffer_size = 16*1024*1024; +CREATE TABLE t4 (a VARCHAR(3000)) CHARACTER SET latin1; +INSERT INTO t4 SELECT CONCAT(LPAD(seq, 8, '0'), REPEAT('w', 2900)) +FROM seq_1_to_800; +FLUSH STATUS; +SELECT COUNT(*) AS cnt, COUNT(DISTINCT p) AS distinct_p, +MIN(LENGTH(p)) AS min_len, LEFT(MAX(p), 12) AS max_prefix +FROM (SELECT PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER () AS p +FROM t4) dt; +cnt distinct_p min_len max_prefix +800 1 2908 00000800wwww +Warnings: +Warning 4202 800 values were longer than max_sort_length. Sorting used only the first 1024 bytes +SELECT variable_name, variable_value FROM information_schema.session_status +WHERE variable_name = 'Created_tmp_disk_tables'; +variable_name variable_value +CREATED_TMP_DISK_TABLES 1 +DROP TABLE t4; +SET sort_buffer_size= @save_sort_buffer; +# +# Test 5: ROWNUM() makes the tmp table keep_row_order; the conversion +# must still happen and preserve the ROWNUM values +# +# ROWNUM() with ORDER BY asks the tmp table to preserve insertion +# order. The converted table holds the rows in the window sort order +# instead, which must not disturb the already-materialized ROWNUM +# values: they are assigned in insertion order during the fill, so +# with the reverse-ordered data below every row must satisfy +# rn = 801 - number(a) no matter how the rows are stored. +# +CREATE TABLE t5 (a VARCHAR(3000)) CHARACTER SET latin1; +INSERT INTO t5 SELECT CONCAT(LPAD(801-seq, 8, '0'), REPEAT('v', 2900)) +FROM seq_1_to_800; +FLUSH STATUS; +SELECT COUNT(*) AS cnt, MIN(rn) AS rn_min, MAX(rn) AS rn_max, +COUNT(DISTINCT rn) AS rn_distinct, +SUM(rn = 801 - CAST(LEFT(a, 8) AS UNSIGNED)) AS rn_pairing_ok, +MIN(LENGTH(p)) AS min_len +FROM (SELECT ROWNUM() AS rn, a, +PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER () AS p +FROM t5 ORDER BY a) dt; +cnt rn_min rn_max rn_distinct rn_pairing_ok min_len +800 1 800 800 800 2908 +Warnings: +Warning 4202 1600 values were longer than max_sort_length. Sorting used only the first 1024 bytes +# Verify HEAP->Aria conversion happened +SELECT variable_name, variable_value FROM information_schema.session_status +WHERE variable_name = 'Created_tmp_disk_tables'; +variable_name variable_value +CREATED_TMP_DISK_TABLES 2 +DROP TABLE t5; +# +# Test 6: blob values larger than a HEAP allocation block +# +# A blob value that spans multiple allocation blocks is reassembled +# into a buffer shared by all reads of the table. The row whose +# update overflowed the table is written to the converted table from +# its record buffer after the other rows have been read through that +# buffer: its blob values must survive those reads. Three distinct +# values must remain three distinct values. +# +SET max_heap_table_size = 12*1024*1024; +SET tmp_table_size = 12*1024*1024; +CREATE TABLE t6 (id INT, a MEDIUMTEXT) CHARACTER SET latin1; +INSERT INTO t6 SELECT seq, CONCAT(LPAD(seq, 8, '0'), REPEAT('v', 2000000)) +FROM seq_1_to_3; +FLUSH STATUS; +SELECT COUNT(*) AS cnt, +SUM(a = CONCAT(LEFT(a, 8), REPEAT('v', 2000000))) AS a_intact, +COUNT(DISTINCT LEFT(a, 8)) AS a_distinct, +SUM(p IS NULL OR p = CONCAT(LEFT(p, 8), REPEAT('v', 2000000))) +AS p_intact +FROM (SELECT a, LAG(a) OVER (ORDER BY a) AS p FROM t6) dt; +cnt a_intact a_distinct p_intact +3 3 3 3 +Warnings: +Warning 4202 3 values were longer than max_sort_length. Sorting used only the first 1024 bytes +# Verify HEAP->Aria conversion happened +SELECT variable_name, variable_value FROM information_schema.session_status +WHERE variable_name = 'Created_tmp_disk_tables'; +variable_name variable_value +CREATED_TMP_DISK_TABLES 1 +DROP TABLE t6; +# +# Cleanup +# +SET max_heap_table_size = @save_max_heap; +SET tmp_table_size = @save_tmp; diff --git a/mysql-test/suite/heap/blob_window_overflow.test b/mysql-test/suite/heap/blob_window_overflow.test new file mode 100644 index 0000000000000..3970659e52e4f --- /dev/null +++ b/mysql-test/suite/heap/blob_window_overflow.test @@ -0,0 +1,286 @@ +--source include/have_sequence.inc + +--echo # +--echo # Window function computation must spill HEAP tmp table to Aria +--echo # when storing computed values fills the table +--echo # +--echo # Window function results are written back into the sorted tmp +--echo # table with ha_update_row(). A blob result column (an expression +--echo # wider than 512 characters is promoted to TEXT in the tmp table) +--echo # makes each such update allocate a blob continuation record; once +--echo # this crosses max_heap_table_size the update fails with "table is +--echo # full" and the server must transparently convert the tmp table to +--echo # Aria, just as the write path already does, and carry the running +--echo # computation over to the converted table. +--echo # +--echo # latin1 is used so that actual data size ~= declared column width: +--echo # the memory arithmetic then holds whether or not the source VARCHAR +--echo # column itself is stored as a blob in the HEAP tmp table. +--echo # + +SET @save_max_heap= @@max_heap_table_size; +SET @save_tmp= @@tmp_table_size; + +SET max_heap_table_size = 4*1024*1024; +SET tmp_table_size = 4*1024*1024; + +--echo # +--echo # Test 1: single window over all rows +--echo # +--echo # 800 rows of ~3KB fill the 4MB HEAP tmp table to ~2.4MB; storing +--echo # the ~3KB computed value adds a blob continuation chain per row +--echo # and overflows mid-computation. +--echo # + +CREATE TABLE t1 (a VARCHAR(3000)) CHARACTER SET latin1; +INSERT INTO t1 SELECT CONCAT(LPAD(seq, 8, '0'), REPEAT('x', 2900)) +FROM seq_1_to_800; + +--disable_ps_protocol +--disable_ps2_protocol +--disable_view_protocol +--disable_cursor_protocol +FLUSH STATUS; + +--echo # Should overflow during computation and convert HEAP->Aria +SELECT COUNT(*) AS cnt, COUNT(DISTINCT p) AS distinct_p, + MIN(LENGTH(p)) AS min_len, LEFT(MAX(p), 12) AS max_prefix +FROM (SELECT PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER () AS p + FROM t1) dt; + +--echo # Verify HEAP->Aria conversion happened +--disable_warnings +SELECT variable_name, variable_value FROM information_schema.session_status + WHERE variable_name = 'Created_tmp_disk_tables'; +--enable_warnings +--enable_cursor_protocol +--enable_view_protocol +--enable_ps2_protocol +--enable_ps_protocol + +DROP TABLE t1; + +--echo # +--echo # Test 2: partitioned window +--echo # +--echo # Same overflow, but the computation must also carry its partition +--echo # tracking over the conversion (values are per partition). +--echo # + +CREATE TABLE t2 (g INT, a VARCHAR(3000)) CHARACTER SET latin1; +INSERT INTO t2 SELECT seq % 4, CONCAT(LPAD(seq, 8, '0'), REPEAT('y', 2900)) +FROM seq_1_to_800; + +--disable_ps_protocol +--disable_ps2_protocol +--disable_view_protocol +--disable_cursor_protocol +FLUSH STATUS; + +SELECT COUNT(*) AS cnt, COUNT(DISTINCT p) AS partitions_seen, + MIN(LENGTH(p)) AS min_len, MAX(LENGTH(p)) AS max_len +FROM (SELECT PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY a) + OVER (PARTITION BY g) AS p + FROM t2) dt; + +--disable_warnings +SELECT variable_name, variable_value FROM information_schema.session_status + WHERE variable_name = 'Created_tmp_disk_tables'; +--enable_warnings +--enable_cursor_protocol +--enable_view_protocol +--enable_ps2_protocol +--enable_ps_protocol + +DROP TABLE t2; + +--echo # +--echo # Test 3: side-effecting window expression is evaluated once per row +--echo # +--echo # A compound expression containing a window function is evaluated +--echo # row by row while the computed values are stored. The conversion +--echo # must not make that pass start over: the user variable assignment +--echo # below must be applied exactly once per row, the same as when the +--echo # table does not overflow. +--echo # + +CREATE TABLE t3 (a VARCHAR(3000)) CHARACTER SET latin1; +INSERT INTO t3 SELECT CONCAT(LPAD(seq, 8, '0'), REPEAT('z', 2900)) +FROM seq_1_to_800; + +--disable_ps_protocol +--disable_ps2_protocol +--disable_view_protocol +--disable_cursor_protocol +FLUSH STATUS; + +SET @c= 0; +SELECT COUNT(*) AS cnt, COUNT(DISTINCT e) AS distinct_e, + MIN(LENGTH(e)) AS min_len +FROM (SELECT CONCAT(@c:= @c+1, ':', + PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER ()) AS e + FROM t3) dt; +--echo # One assignment per row, not one per row per pass +SELECT @c AS assignments; + +--echo # Verify the overflow really happened +--disable_warnings +SELECT variable_name, variable_value FROM information_schema.session_status + WHERE variable_name = 'Created_tmp_disk_tables'; +--enable_warnings + +--echo # Same query without the overflow, for comparison +SET max_heap_table_size = 64*1024*1024; +SET tmp_table_size = 64*1024*1024; +FLUSH STATUS; + +SET @c= 0; +SELECT COUNT(*) AS cnt, COUNT(DISTINCT e) AS distinct_e, + MIN(LENGTH(e)) AS min_len +FROM (SELECT CONCAT(@c:= @c+1, ':', + PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER ()) AS e + FROM t3) dt; +SELECT @c AS assignments; + +--disable_warnings +SELECT variable_name, variable_value FROM information_schema.session_status + WHERE variable_name = 'Created_tmp_disk_tables'; +--enable_warnings +--enable_cursor_protocol +--enable_view_protocol +--enable_ps2_protocol +--enable_ps_protocol + +DROP TABLE t3; + +--echo # +--echo # Test 4: row positions held in memory instead of a temporary file +--echo # +--echo # The sort result is a sequence of row positions, which the +--echo # conversion has to rewrite wherever it is kept. The tests above +--echo # sort more than fits in the sort buffer, so the sequence is a merge +--echo # file; with a sort buffer large enough it is an array in memory. +--echo # + +SET max_heap_table_size = 4*1024*1024; +SET tmp_table_size = 4*1024*1024; +SET @save_sort_buffer= @@sort_buffer_size; +SET sort_buffer_size = 16*1024*1024; + +CREATE TABLE t4 (a VARCHAR(3000)) CHARACTER SET latin1; +INSERT INTO t4 SELECT CONCAT(LPAD(seq, 8, '0'), REPEAT('w', 2900)) +FROM seq_1_to_800; + +--disable_ps_protocol +--disable_ps2_protocol +--disable_view_protocol +--disable_cursor_protocol +FLUSH STATUS; + +SELECT COUNT(*) AS cnt, COUNT(DISTINCT p) AS distinct_p, + MIN(LENGTH(p)) AS min_len, LEFT(MAX(p), 12) AS max_prefix +FROM (SELECT PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER () AS p + FROM t4) dt; + +--disable_warnings +SELECT variable_name, variable_value FROM information_schema.session_status + WHERE variable_name = 'Created_tmp_disk_tables'; +--enable_warnings +--enable_cursor_protocol +--enable_view_protocol +--enable_ps2_protocol +--enable_ps_protocol + +DROP TABLE t4; +SET sort_buffer_size= @save_sort_buffer; + +--echo # +--echo # Test 5: ROWNUM() makes the tmp table keep_row_order; the conversion +--echo # must still happen and preserve the ROWNUM values +--echo # +--echo # ROWNUM() with ORDER BY asks the tmp table to preserve insertion +--echo # order. The converted table holds the rows in the window sort order +--echo # instead, which must not disturb the already-materialized ROWNUM +--echo # values: they are assigned in insertion order during the fill, so +--echo # with the reverse-ordered data below every row must satisfy +--echo # rn = 801 - number(a) no matter how the rows are stored. +--echo # + +CREATE TABLE t5 (a VARCHAR(3000)) CHARACTER SET latin1; +INSERT INTO t5 SELECT CONCAT(LPAD(801-seq, 8, '0'), REPEAT('v', 2900)) +FROM seq_1_to_800; + +--disable_ps_protocol +--disable_ps2_protocol +--disable_view_protocol +--disable_cursor_protocol +FLUSH STATUS; + +SELECT COUNT(*) AS cnt, MIN(rn) AS rn_min, MAX(rn) AS rn_max, + COUNT(DISTINCT rn) AS rn_distinct, + SUM(rn = 801 - CAST(LEFT(a, 8) AS UNSIGNED)) AS rn_pairing_ok, + MIN(LENGTH(p)) AS min_len +FROM (SELECT ROWNUM() AS rn, a, + PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER () AS p + FROM t5 ORDER BY a) dt; + +--echo # Verify HEAP->Aria conversion happened +--disable_warnings +SELECT variable_name, variable_value FROM information_schema.session_status + WHERE variable_name = 'Created_tmp_disk_tables'; +--enable_warnings +--enable_cursor_protocol +--enable_view_protocol +--enable_ps2_protocol +--enable_ps_protocol + +DROP TABLE t5; + +--echo # +--echo # Test 6: blob values larger than a HEAP allocation block +--echo # +--echo # A blob value that spans multiple allocation blocks is reassembled +--echo # into a buffer shared by all reads of the table. The row whose +--echo # update overflowed the table is written to the converted table from +--echo # its record buffer after the other rows have been read through that +--echo # buffer: its blob values must survive those reads. Three distinct +--echo # values must remain three distinct values. +--echo # + +SET max_heap_table_size = 12*1024*1024; +SET tmp_table_size = 12*1024*1024; + +CREATE TABLE t6 (id INT, a MEDIUMTEXT) CHARACTER SET latin1; +INSERT INTO t6 SELECT seq, CONCAT(LPAD(seq, 8, '0'), REPEAT('v', 2000000)) +FROM seq_1_to_3; + +--disable_ps_protocol +--disable_ps2_protocol +--disable_view_protocol +--disable_cursor_protocol +FLUSH STATUS; + +SELECT COUNT(*) AS cnt, + SUM(a = CONCAT(LEFT(a, 8), REPEAT('v', 2000000))) AS a_intact, + COUNT(DISTINCT LEFT(a, 8)) AS a_distinct, + SUM(p IS NULL OR p = CONCAT(LEFT(p, 8), REPEAT('v', 2000000))) + AS p_intact +FROM (SELECT a, LAG(a) OVER (ORDER BY a) AS p FROM t6) dt; + +--echo # Verify HEAP->Aria conversion happened +--disable_warnings +SELECT variable_name, variable_value FROM information_schema.session_status + WHERE variable_name = 'Created_tmp_disk_tables'; +--enable_warnings +--enable_cursor_protocol +--enable_view_protocol +--enable_ps2_protocol +--enable_ps_protocol + +DROP TABLE t6; + +--echo # +--echo # Cleanup +--echo # +SET max_heap_table_size = @save_max_heap; +SET tmp_table_size = @save_tmp; diff --git a/mysql-test/suite/heap/blob_window_overflow_debug.result b/mysql-test/suite/heap/blob_window_overflow_debug.result new file mode 100644 index 0000000000000..90bb2d512f179 --- /dev/null +++ b/mysql-test/suite/heap/blob_window_overflow_debug.result @@ -0,0 +1,33 @@ +# +# Failure to rebind the row position sequence during the HEAP to +# Aria conversion of a window function tmp table +# +# When re-creating a frame cursor's slave cache of the rewritten +# position sequence fails, the statement must fail with a reported +# error, and the cursor must not release the half-initialized cache +# a second time when it is destroyed. +# +SET @save_max_heap= @@max_heap_table_size; +SET @save_tmp= @@tmp_table_size; +SET max_heap_table_size = 4*1024*1024; +SET tmp_table_size = 4*1024*1024; +CREATE TABLE t1 (a VARCHAR(3000)) CHARACTER SET latin1; +INSERT INTO t1 SELECT CONCAT(LPAD(seq, 8, '0'), REPEAT('x', 2900)) +FROM seq_1_to_800; +SET debug_dbug='+d,simulate_window_seq_slave_oom'; +SELECT COUNT(*) FROM +(SELECT PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER () AS p +FROM t1) dt; +ERROR HY000: Out of memory. +SET debug_dbug=''; +# The same query must work after the failed attempt +SELECT COUNT(*) FROM +(SELECT PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER () AS p +FROM t1) dt; +COUNT(*) +800 +Warnings: +Warning 4202 800 values were longer than max_sort_length. Sorting used only the first 1024 bytes +DROP TABLE t1; +SET max_heap_table_size = @save_max_heap; +SET tmp_table_size = @save_tmp; diff --git a/mysql-test/suite/heap/blob_window_overflow_debug.test b/mysql-test/suite/heap/blob_window_overflow_debug.test new file mode 100644 index 0000000000000..1e1abacd41f8f --- /dev/null +++ b/mysql-test/suite/heap/blob_window_overflow_debug.test @@ -0,0 +1,39 @@ +--source include/have_debug.inc +--source include/have_sequence.inc + +--echo # +--echo # Failure to rebind the row position sequence during the HEAP to +--echo # Aria conversion of a window function tmp table +--echo # +--echo # When re-creating a frame cursor's slave cache of the rewritten +--echo # position sequence fails, the statement must fail with a reported +--echo # error, and the cursor must not release the half-initialized cache +--echo # a second time when it is destroyed. +--echo # + +SET @save_max_heap= @@max_heap_table_size; +SET @save_tmp= @@tmp_table_size; + +SET max_heap_table_size = 4*1024*1024; +SET tmp_table_size = 4*1024*1024; + +CREATE TABLE t1 (a VARCHAR(3000)) CHARACTER SET latin1; +INSERT INTO t1 SELECT CONCAT(LPAD(seq, 8, '0'), REPEAT('x', 2900)) +FROM seq_1_to_800; + +SET debug_dbug='+d,simulate_window_seq_slave_oom'; +--error ER_OUT_OF_RESOURCES +SELECT COUNT(*) FROM +(SELECT PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER () AS p + FROM t1) dt; +SET debug_dbug=''; + +--echo # The same query must work after the failed attempt +SELECT COUNT(*) FROM +(SELECT PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER () AS p + FROM t1) dt; + +DROP TABLE t1; + +SET max_heap_table_size = @save_max_heap; +SET tmp_table_size = @save_tmp; diff --git a/mysql-test/suite/heap/blob_window_overflow_encrypt.opt b/mysql-test/suite/heap/blob_window_overflow_encrypt.opt new file mode 100644 index 0000000000000..e4340cbef3dea --- /dev/null +++ b/mysql-test/suite/heap/blob_window_overflow_encrypt.opt @@ -0,0 +1,3 @@ +--plugin-load-add=file_key_management +--loose-file-key-management-filename=$MYSQL_TEST_DIR/std_data/keys.txt +--encrypt-tmp-files=ON diff --git a/mysql-test/suite/heap/blob_window_overflow_encrypt.result b/mysql-test/suite/heap/blob_window_overflow_encrypt.result new file mode 100644 index 0000000000000..9d43e20d08728 --- /dev/null +++ b/mysql-test/suite/heap/blob_window_overflow_encrypt.result @@ -0,0 +1,33 @@ +# +# Window function computation spilling a HEAP tmp table to Aria with +# encrypted temporary files +# +# The sorted row positions the computation runs over live in an +# IO_CACHE temporary file, which is encrypted here: the conversion +# must go through the cache instead of the file to translate them. +# +SET @save_max_heap= @@max_heap_table_size; +SET @save_tmp= @@tmp_table_size; +SET max_heap_table_size = 4*1024*1024; +SET tmp_table_size = 4*1024*1024; +CREATE TABLE t1 (a VARCHAR(3000)) CHARACTER SET latin1; +INSERT INTO t1 SELECT CONCAT(LPAD(seq, 8, '0'), REPEAT('x', 2900)) +FROM seq_1_to_800; +FLUSH STATUS; +# Should overflow during computation and convert HEAP->Aria +SELECT COUNT(*) AS cnt, COUNT(DISTINCT p) AS distinct_p, +MIN(LENGTH(p)) AS min_len, LEFT(MAX(p), 12) AS max_prefix +FROM (SELECT PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER () AS p +FROM t1) dt; +cnt distinct_p min_len max_prefix +800 1 2908 00000800xxxx +Warnings: +Warning 4202 800 values were longer than max_sort_length. Sorting used only the first 1024 bytes +# Verify HEAP->Aria conversion happened +SELECT variable_name, variable_value FROM information_schema.session_status +WHERE variable_name = 'Created_tmp_disk_tables'; +variable_name variable_value +CREATED_TMP_DISK_TABLES 1 +DROP TABLE t1; +SET max_heap_table_size = @save_max_heap; +SET tmp_table_size = @save_tmp; diff --git a/mysql-test/suite/heap/blob_window_overflow_encrypt.test b/mysql-test/suite/heap/blob_window_overflow_encrypt.test new file mode 100644 index 0000000000000..7be0db0628658 --- /dev/null +++ b/mysql-test/suite/heap/blob_window_overflow_encrypt.test @@ -0,0 +1,48 @@ +--source include/have_file_key_management.inc +--source include/have_sequence.inc + +--echo # +--echo # Window function computation spilling a HEAP tmp table to Aria with +--echo # encrypted temporary files +--echo # +--echo # The sorted row positions the computation runs over live in an +--echo # IO_CACHE temporary file, which is encrypted here: the conversion +--echo # must go through the cache instead of the file to translate them. +--echo # + +SET @save_max_heap= @@max_heap_table_size; +SET @save_tmp= @@tmp_table_size; + +SET max_heap_table_size = 4*1024*1024; +SET tmp_table_size = 4*1024*1024; + +CREATE TABLE t1 (a VARCHAR(3000)) CHARACTER SET latin1; +INSERT INTO t1 SELECT CONCAT(LPAD(seq, 8, '0'), REPEAT('x', 2900)) +FROM seq_1_to_800; + +--disable_ps_protocol +--disable_ps2_protocol +--disable_view_protocol +--disable_cursor_protocol +FLUSH STATUS; + +--echo # Should overflow during computation and convert HEAP->Aria +SELECT COUNT(*) AS cnt, COUNT(DISTINCT p) AS distinct_p, + MIN(LENGTH(p)) AS min_len, LEFT(MAX(p), 12) AS max_prefix +FROM (SELECT PERCENTILE_DISC(1) WITHIN GROUP (ORDER BY a) OVER () AS p + FROM t1) dt; + +--echo # Verify HEAP->Aria conversion happened +--disable_warnings +SELECT variable_name, variable_value FROM information_schema.session_status + WHERE variable_name = 'Created_tmp_disk_tables'; +--enable_warnings +--enable_cursor_protocol +--enable_view_protocol +--enable_ps2_protocol +--enable_ps_protocol + +DROP TABLE t1; + +SET max_heap_table_size = @save_max_heap; +SET tmp_table_size = @save_tmp; diff --git a/sql/derived_handler.cc b/sql/derived_handler.cc index cddd1200f5d0f..889b1ddad2dfe 100644 --- a/sql/derived_handler.cc +++ b/sql/derived_handler.cc @@ -73,7 +73,7 @@ int Pushdown_derived::execute() if (create_internal_tmp_table_from_heap(thd, table, tmp_table_param->start_recinfo, &tmp_table_param->recinfo, - err, 1, &is_duplicate)) + err, 1, &is_duplicate, NULL)) DBUG_RETURN(1); if (is_duplicate) continue; diff --git a/sql/filesort.cc b/sql/filesort.cc index a5883a3996c3f..7b6cd01163cf4 100644 --- a/sql/filesort.cc +++ b/sql/filesort.cc @@ -153,7 +153,8 @@ void Sort_param::init_for_filesort(TABLE *table, Filesort *filesort, } DBUG_ASSERT((using_addon_fields() == 0 || addon_length != 0)); - setup_lengths_and_limit(table, sortlen, addon_length, limit_rows_arg); + setup_lengths_and_limit(table, sortlen, addon_length, limit_rows_arg, + filesort->min_ref_length); accepted_rows= filesort->accepted_rows; } @@ -161,12 +162,13 @@ void Sort_param::init_for_filesort(TABLE *table, Filesort *filesort, void Sort_param::setup_lengths_and_limit(TABLE *table, uint sort_len_arg, uint addon_length_arg, - ha_rows limit_rows_arg) + ha_rows limit_rows_arg, + uint min_ref_length_arg) { sort_form= table; sort_length= sort_len_arg; limit_rows= limit_rows_arg; - ref_length= table->file->ref_length; + ref_length= MY_MAX(table->file->ref_length, min_ref_length_arg); if (addon_length_arg) { @@ -316,6 +318,7 @@ SORT_INFO *filesort(THD *thd, TABLE *table, Filesort *filesort, sort->addon_fields= param.addon_fields; sort->sort_keys= param.sort_keys; + sort->ref_length= param.ref_length; if (select && select->quick) thd->inc_status_sort_range(); @@ -1457,8 +1460,11 @@ static uint make_sortkey(Sort_param *param, uchar *to, uchar *ref_pos, } else { - /* Save filepos last */ - memcpy((uchar*) to, ref_pos, (size_t) param->ref_length); + /* Save filepos last, zero-padded to its slot (@see Filesort::min_ref_length) */ + uint ref_length= param->sort_form->file->ref_length; + memcpy((uchar*) to, ref_pos, (size_t) ref_length); + if (param->ref_length != ref_length) + bzero((uchar*) to + ref_length, param->ref_length - ref_length); to+= param->ref_length; } return static_cast(to - orig_to); diff --git a/sql/filesort.h b/sql/filesort.h index 6b82c841a23d1..1ad899fed8265 100644 --- a/sql/filesort.h +++ b/sql/filesort.h @@ -63,6 +63,14 @@ class Filesort: public Sql_alloc a call to table->file->position() using these table rowids. */ bool sort_positions; + /* + Minimum width of the row position slots stored with sort_positions. + A position shorter than its slot is zero-padded to it. Set to + TMP_TABLE_MAX_REF_LENGTH when the sorted table can be converted to + another engine while the sort result is in use, so that the positions + of the new engine fit into the slots (@see Window_rowid_remapper). + */ + uint min_ref_length; /* TRUE means all the fields of table of whose bitmap read_set is set need to be read while reading records in the sort buffer. @@ -86,6 +94,7 @@ class Filesort: public Sql_alloc own_select(false), using_pq(false), sort_positions(sort_positions_arg), + min_ref_length(0), set_all_read_bits(false), sort_keys(NULL), unpack(NULL) @@ -109,7 +118,7 @@ class SORT_INFO public: SORT_INFO() - :addon_fields(NULL), record_pointers(0), + :addon_fields(NULL), record_pointers(0), ref_length(0), sort_keys(NULL), sorted_result_in_fsbuf(FALSE) { @@ -144,6 +153,13 @@ class SORT_INFO LEX_STRING buffpek; /* Buffer for buffpek structures */ Addon_fields *addon_fields; /* Addon field descriptors */ uchar *record_pointers; /* If sorted in memory */ + /* + Width of the row position slots of io_cache / record_pointers. + filesort() sets it to Sort_param::ref_length; it stays 0 in a SORT_INFO + filled elsewhere, whose positions (if any) have the width of + handler::ref_length (@see init_read_record()). + */ + uint ref_length; Sort_keys *sort_keys; /* Sort key descriptors*/ /** diff --git a/sql/filesort_utils.cc b/sql/filesort_utils.cc index 0a0d7cc58464a..05155c2d405da 100644 --- a/sql/filesort_utils.cc +++ b/sql/filesort_utils.cc @@ -464,7 +464,7 @@ double cost_of_filesort(TABLE *table, ORDER *order_by, ha_rows rows_to_read, /* Fill in the Sort_param structure so we can compute the sort costs */ param.setup_lengths_and_limit(table, sort_len, addon_field_length, - limit_rows); + limit_rows, 0); costs.compute_sort_costs(¶m, rows_to_read, memory_available, with_addon_fields); diff --git a/sql/group_by_handler.cc b/sql/group_by_handler.cc index 7b998494af9ae..a43cef714df60 100644 --- a/sql/group_by_handler.cc +++ b/sql/group_by_handler.cc @@ -85,7 +85,7 @@ int Pushdown_query::execute(JOIN *join) start_recinfo, &join->tmp_table_param. recinfo, - err, 1, &is_duplicate)) + err, 1, &is_duplicate, NULL)) DBUG_RETURN(1); if (is_duplicate) continue; diff --git a/sql/item_sum.cc b/sql/item_sum.cc index c82e102d6f91e..64c06f45bf6b9 100644 --- a/sql/item_sum.cc +++ b/sql/item_sum.cc @@ -1003,6 +1003,7 @@ bool Aggregator_distinct::add() } if (unlikely((error= table->file->ha_write_tmp_row(table->record[0])))) { + bool is_duplicate; if (!table->file->is_fatal_error(error, HA_CHECK_DUP)) return FALSE; // duplicate, not an error /* @@ -1015,7 +1016,7 @@ bool Aggregator_distinct::add() if (create_internal_tmp_table_from_heap(table->in_use, table, tmp_table_param->start_recinfo, &tmp_table_param->recinfo, - error, 0, NULL)) + error, 0, &is_duplicate, NULL)) return TRUE; } return FALSE; diff --git a/sql/opt_subselect.cc b/sql/opt_subselect.cc index 220464cf0884d..75dba3e0b0c78 100644 --- a/sql/opt_subselect.cc +++ b/sql/opt_subselect.cc @@ -5180,7 +5180,8 @@ int SJ_TMP_TABLE::sj_weedout_check_row(THD *thd) bool is_duplicate; if (create_internal_tmp_table_from_heap(thd, tmp_table, start_recinfo, - &recinfo, error, 1, &is_duplicate)) + &recinfo, error, 1, &is_duplicate, + NULL)) DBUG_RETURN(-1); if (is_duplicate) DBUG_RETURN(1); diff --git a/sql/records.cc b/sql/records.cc index 442ddcd075214..c2fb091b28f49 100644 --- a/sql/records.cc +++ b/sql/records.cc @@ -204,7 +204,15 @@ bool init_read_record(READ_RECORD *info,THD *thd, TABLE *table, else { empty_record(table); - info->ref_length= (uint)table->file->ref_length; + /* + A sort can store the row positions in slots wider than a position of + the table's engine (@see Filesort::min_ref_length): read them at the + width they were stored with. The engine reads a position from the + first ref_length bytes of a slot, and handler::ref always has room + for a whole slot (@see handler::ha_open()). + */ + info->ref_length= ((filesort && filesort->ref_length) ? + filesort->ref_length : (uint)table->file->ref_length); } info->select=select; info->print_error=print_error; diff --git a/sql/sql_class.h b/sql/sql_class.h index d7dd18ea9f9ee..079ef07d11c49 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -6945,6 +6945,13 @@ inline uint tmp_table_max_key_length() { return MI_MAX_KEY_LENGTH; } inline uint tmp_table_max_key_parts() { return MI_MAX_KEY_SEG; } #endif +/* + The longest row position (handler::ref) of the engines that can hold an + internal temporary table: a pointer into memory (HEAP) or an offset in a + data file (Aria and MyISAM row pointers take at most 7 bytes). +*/ +#define TMP_TABLE_MAX_REF_LENGTH 8 + /* Param to create temporary tables when doing SELECT:s NOTE diff --git a/sql/sql_delete.cc b/sql/sql_delete.cc index cd106623b62c5..7adc2709974fa 100644 --- a/sql/sql_delete.cc +++ b/sql/sql_delete.cc @@ -1473,21 +1473,22 @@ int multi_delete::send_data(List &values) error= tmp_table->file->ha_write_tmp_row(tmp_table->record[0]); if (error) { - --found; - if (error != HA_ERR_FOUND_DUPP_KEY && - error != HA_ERR_FOUND_DUPP_UNIQUE) + --found; + if (error != HA_ERR_FOUND_DUPP_KEY && + error != HA_ERR_FOUND_DUPP_UNIQUE) + { + bool is_duplicate; + if (create_internal_tmp_table_from_heap(thd, tmp_table, + tmp_table_param[offset].start_recinfo, + &tmp_table_param[offset].recinfo, + error, 1, &is_duplicate, NULL)) { - if (create_internal_tmp_table_from_heap(thd, tmp_table, - tmp_table_param[offset].start_recinfo, - &tmp_table_param[offset].recinfo, - error, 1, NULL)) - { - do_delete= 0; - DBUG_RETURN(1); // Not a table_is_full error - } - found++; + do_delete= 0; + DBUG_RETURN(1); // Not a table_is_full error } - error= 0; + found++; + } + error= 0; } } } diff --git a/sql/sql_expression_cache.cc b/sql/sql_expression_cache.cc index 556d0c652c82e..bf2f92cf3e05c 100644 --- a/sql/sql_expression_cache.cc +++ b/sql/sql_expression_cache.cc @@ -320,10 +320,11 @@ my_bool Expression_cache_tmptable::put_value(Item *value) } else { + bool is_duplicate; if (create_internal_tmp_table_from_heap(table_thd, cache_table, cache_table_param.start_recinfo, &cache_table_param.recinfo, - error, 1, NULL)) + error, 1, &is_duplicate, NULL)) goto err2; } } diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 9a1bf2115fea0..af1f17bc1fa02 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -15717,11 +15717,13 @@ end_sj_materialize(JOIN *join, JOIN_TAB *join_tab, bool end_of_records) DBUG_RETURN(NESTED_LOOP_ERROR); /* purecov: inspected */ if (unlikely((error= table->file->ha_write_tmp_row(table->record[0])))) { + bool is_duplicate; /* create_myisam_from_heap will generate error if needed */ if (table->file->is_fatal_error(error, HA_CHECK_DUP) && create_internal_tmp_table_from_heap(thd, table, - sjm->sjm_table_param.start_recinfo, - &sjm->sjm_table_param.recinfo, error, 1, NULL)) + sjm->sjm_table_param.start_recinfo, + &sjm->sjm_table_param.recinfo, + error, 1, &is_duplicate, NULL)) DBUG_RETURN(NESTED_LOOP_ERROR); /* purecov: inspected */ } } @@ -24019,30 +24021,330 @@ bool create_internal_tmp_table(TABLE *table, KEY *org_keyinfo, /* - If a HEAP table gets full, create a internal table in MyISAM or Maria - and copy all rows to this + Default row copier of create_internal_tmp_table_from_heap(): copy the + rows in table scan order, then append the pending record[0], whose write + filled the in-memory table, as a new row. +*/ + +class Tmp_table_default_copier: public Tmp_table_row_copier +{ +public: + Tmp_table_default_copier(bool ignore_last_dupp_key_error_arg) + : ignore_last_dupp_key_error(ignore_last_dupp_key_error_arg) {} + + int copy_rows(TABLE *from, TABLE *to) override; + +private: + bool ignore_last_dupp_key_error; +}; + + +int Tmp_table_default_copier::copy_rows(TABLE *from, TABLE *to) +{ + THD *thd= from->in_use; + int write_err; + DBUG_ENTER("Tmp_table_default_copier::copy_rows"); + + /* + copy all old rows from heap table to MyISAM table + This is the only code that uses record[1] to read/write but this + is safe as this is a temporary MyISAM table without timestamp/autoincrement + or partitioning. + */ + while (!from->file->ha_rnd_next(to->record[1])) + { + write_err= to->file->ha_write_tmp_row(to->record[1]); + DBUG_EXECUTE_IF("raise_error", write_err= HA_ERR_FOUND_DUPP_KEY ;); + if (write_err) + DBUG_RETURN(write_err); + if (unlikely(thd->check_killed())) + DBUG_RETURN(-1); // The error is already reported + } + + /* copy row that filled HEAP table */ + if (unlikely((write_err= to->file->ha_write_tmp_row(from->record[0])))) + { + if (to->file->is_fatal_error(write_err, HA_CHECK_DUP) || + !ignore_last_dupp_key_error) + DBUG_RETURN(write_err); + duplicate_row_error= true; + } + DBUG_RETURN(0); +} + - In case of error, my_error() or handler::print_error() will be called. - Note that in case of error, table->file->ha_rnd_end() may have been called! +/* + The pending row is written from record[0], whose blob values can point + into the HEAP handler's shared reassembly buffer. Reading the other rows + of the table overwrites that buffer, so copy the values into memory that + stays valid until the pending row has been written. +*/ + +int Window_rowid_remapper::materialize_pending_blobs(TABLE *from) +{ + size_t total= 0; + uchar *pos; + uint *bf_end= from->s->blob_field + from->s->blob_fields; + DBUG_ENTER("Window_rowid_remapper::materialize_pending_blobs"); + + for (uint *bf= from->s->blob_field; bf < bf_end; bf++) + { + Field_blob *fb= (Field_blob*) from->field[*bf]; + if (!fb->is_null()) + total+= fb->get_length(); + } + if (!total) + DBUG_RETURN(0); + + if (!(pending_blob_buf= (uchar*) my_malloc(PSI_INSTRUMENT_ME, total, + MYF(MY_WME)))) + DBUG_RETURN(-1); // The error is reported + + pos= pending_blob_buf; + for (uint *bf= from->s->blob_field; bf < bf_end; bf++) + { + Field_blob *fb= (Field_blob*) from->field[*bf]; + uint32 length; + if (fb->is_null() || !(length= fb->get_length())) + continue; + memcpy(pos, fb->get_ptr(), length); + fb->set_ptr(length, pos); + pos+= length; + } + DBUG_RETURN(0); +} + + +int Window_rowid_remapper::copy_rows(TABLE *from, TABLE *to) +{ + THD *thd= from->in_use; + /* The rowid sequence is either an array in memory or a temporary file */ + const bool in_file= !sort->has_filesort_result_in_memory(); + /* A position occupies the first bytes of its possibly wider slot */ + const uint pos_length= from->file->ref_length; + uchar ref_buf[TMP_TABLE_MAX_REF_LENGTH]; + uchar new_slot[TMP_TABLE_MAX_REF_LENGTH]; + uchar *read_slot, *next_read_slot; + IO_CACHE new_seq; + int error; + my_bool found_pending __attribute__((unused))= FALSE; + DBUG_ENTER("Window_rowid_remapper::copy_rows"); + + new_ref_length= to->file->ref_length; + rows= (in_file ? + (ha_rows) (sort->io_cache.end_of_file / ref_length) : + sort->return_rows); + + DBUG_ASSERT(ref_length <= TMP_TABLE_MAX_REF_LENGTH); + /* + The sequence slots fit the position of any engine an internal tmp table + can use (@see Filesort::min_ref_length). Should that ever break, the + sequence can not be rewritten in place: give up on the conversion and + let the caller report that the table is full. + */ + if (new_ref_length > ref_length) + { + DBUG_ASSERT(0); + DBUG_RETURN(HA_ERR_RECORD_FILE_FULL); + } + + if (materialize_pending_blobs(from)) + DBUG_RETURN(-1); // The error is reported + + if (in_file) + { + if (open_cached_file(&new_seq, mysql_tmpdir, TEMP_PREFIX, + DISK_CHUNK_SIZE, MYF(MY_WME))) + DBUG_RETURN(-1); // The error is reported + if (reinit_io_cache(&sort->io_cache, READ_CACHE, 0, 0, 0)) + { + error= -1; // The error is reported + goto err; + } + } + + /* + The slots keep their width: a new position, which the check above + guarantees to fit, replaces the old one in the very slot it was read + from, zero-padded to the slot by new_slot. + */ + bzero(new_slot, sizeof(new_slot)); + read_slot= ref_buf; + next_read_slot= sort->record_pointers; + + for (ha_rows n= rows; n != 0; n--) + { + uchar *record; + my_bool is_pending; + + if (in_file) + { + if (my_b_read(&sort->io_cache, read_slot, ref_length)) + { + error= -1; // The error is reported + goto err; + } + } + else + { + read_slot= next_read_slot; + next_read_slot+= ref_length; + } + + if ((is_pending= !memcmp(read_slot, pending_ref, pos_length))) + record= from->record[0]; // Its new image, not stored yet + else + { + if ((error= from->file->ha_rnd_pos(from->record[1], read_slot))) + goto err; + record= from->record[1]; + } + + if ((error= to->file->ha_write_tmp_row(record))) + goto err; + to->file->position(record); + + /* + To keep the data at the same size as before, we store all row + pointers at the original ref length. Because of that we need to + use 'new_slot' as a temporary space, as new_ref_length may be less + than ref_length. + */ + memcpy(new_slot, to->file->ref, new_ref_length); + if (in_file) + { + if (my_b_write(&new_seq, new_slot, ref_length)) + { + error= -1; // The error is reported + goto err; + } + } + else + memcpy(read_slot, new_slot, ref_length); + + if (is_pending) + { + memcpy(new_pending_ref_buf, to->file->ref, new_ref_length); + found_pending= TRUE; + } + } + /* The row that did not fit is one of the rows of the sequence */ + DBUG_ASSERT(found_pending); + + if (unlikely(thd->check_killed())) + { + error= -1; // The error is reported + goto err; + } + + if (in_file) + { + /* + Frame cursors read the sequence through slave caches of the master + cache, linked to it through next_file_user. The link survives the + replacement of the master, on the same address, so that the slaves + can still be ended when the cursors are told that the rowids were + rewritten, which re-creates them from the new master. + */ + IO_CACHE *file_users= sort->io_cache.next_file_user; + /* + Make sure the file exists behind the cache: the readers of the + sequence seek in it, also when everything fits in the cache buffer. + */ + if (my_b_flush_io_cache(&new_seq, 1) || + reinit_io_cache(&new_seq, READ_CACHE, 0, 0, 0)) + { + error= -1; // The error is reported + goto err; + } + close_cached_file(&sort->io_cache); + sort->io_cache= new_seq; + /* + The new master lives at the same address as the old one, so the + circular next_file_user list of the slaves does not need adjusting. + */ + sort->io_cache.next_file_user= file_users; + } + DBUG_RETURN(0); + +err: + if (in_file) + close_cached_file(&new_seq); + DBUG_RETURN(error); +} + + +/* + Convert a HEAP tmp table that got full to a table in the on-disk tmp engine + + SYNOPSIS + create_internal_tmp_table_from_heap() + + thd Thread handle + table The HEAP table that ran out of memory. + On success it is converted in place to use + the on-disk engine (its handler and share + are swapped for the new table's). + start_recinfo Pointer to the first column definition of + the table (used to create the new table). + end_recinfo Pointer to the end of the column + definitions; may be updated by + create_internal_tmp_table(). + error The handler error that triggered the call. + Only HA_ERR_RECORD_FILE_FULL on a HEAP + table is handled; any other error/engine + is reported and treated as fatal. + ignore_last_dupp_key_error If TRUE, a duplicate-key error while + writing the pending row (see below) is not + treated as fatal; *is_duplicate is set + instead. Used e.g. for GROUP BY / DISTINCT. + is_duplicate Set to TRUE when the pending row was a + duplicate and the error was ignored, to + FALSE otherwise. Must be non-NULL. + copier Copies the rows; NULL for the default + copier, which appends the pending row: + table->record[0] holds a new row that + overflowed the HEAP table and still needs + to be written to the new table. A caller + for which the overflow happened while + updating an existing row passes its own + copier: table->record[0] then holds the + failed update's new image of a row that is + already in the table and must not be + appended as an extra row. + + DESCRIPTION + Creates a new internal temporary table using the on-disk tmp engine + (MyISAM or Aria) and copies all rows of the full HEAP table into it + with 'copier'. On success the HEAP table is dropped and 'table' is + updated in place to refer to the new on-disk table, so callers can + keep using the same TABLE pointer. + + RETURN + 0 Success; 'table' now uses the on-disk engine + 1 Error. my_error() or handler::print_error() has been called. + Note that in this case table->file->ha_rnd_end() may already have + been called. */ bool create_internal_tmp_table_from_heap(THD *thd, TABLE *table, TMP_ENGINE_COLUMNDEF *start_recinfo, - TMP_ENGINE_COLUMNDEF **recinfo, + TMP_ENGINE_COLUMNDEF **end_recinfo, int error, bool ignore_last_dupp_key_error, - bool *is_duplicate) + bool *is_duplicate, + Tmp_table_row_copier *copier) { TABLE new_table; TABLE_SHARE share; const char *save_proc_info; int write_err= 0; String tmp_alias; + Tmp_table_default_copier default_copier(ignore_last_dupp_key_error); DBUG_ENTER("create_internal_tmp_table_from_heap"); - if (is_duplicate) - *is_duplicate= FALSE; + *is_duplicate= FALSE; if (table->s->db_type() != heap_hton || error != HA_ERR_RECORD_FILE_FULL) { @@ -24072,7 +24374,7 @@ create_internal_tmp_table_from_heap(THD *thd, TABLE *table, new_table.no_rows= table->no_rows; if (create_internal_tmp_table(&new_table, table->key_info, start_recinfo, - recinfo, + end_recinfo, thd->lex->first_select_lex()->options | thd->variables.option_bits)) goto err2; @@ -24095,37 +24397,24 @@ create_internal_tmp_table_from_heap(THD *thd, TABLE *table, new_table.file->ha_start_bulk_insert(table->file->stats.records); } - /* - copy all old rows from heap table to MyISAM table - This is the only code that uses record[1] to read/write but this - is safe as this is a temporary MyISAM table without timestamp/autoincrement - or partitioning. - */ - while (!table->file->ha_rnd_next(new_table.record[1])) - { - write_err= new_table.file->ha_write_tmp_row(new_table.record[1]); - DBUG_EXECUTE_IF("raise_error", write_err= HA_ERR_FOUND_DUPP_KEY ;); - if (write_err) - goto err; - if (unlikely(thd->check_killed())) - goto err_killed; - } - if (!new_table.no_rows && (write_err= new_table.file->ha_end_bulk_insert())) - goto err; - /* copy row that filled HEAP table */ - if (unlikely((write_err=new_table.file->ha_write_tmp_row(table->record[0])))) + if (!copier) + copier= &default_copier; + write_err= copier->copy_rows(table, &new_table); + + if (!new_table.no_rows) { - if (new_table.file->is_fatal_error(write_err, HA_CHECK_DUP) || - !ignore_last_dupp_key_error) - goto err; - if (is_duplicate) - *is_duplicate= TRUE; + /* Also in case of errors, to end the bulk insert mode of the handler */ + int end_err= new_table.file->ha_end_bulk_insert(); + if (end_err && !write_err) + write_err= end_err; } - else + if (write_err) { - if (is_duplicate) - *is_duplicate= FALSE; + if (write_err < 0) + goto err_killed; // The error is already reported + goto err; } + *is_duplicate= copier->duplicate_row_error; /* remove heap table and change to use myisam table */ (void) table->file->ha_rnd_end(); @@ -26630,10 +26919,10 @@ end_write(JOIN *join, JOIN_TAB *join_tab __attribute__((unused)), if (likely(!table->file->is_fatal_error(error, HA_CHECK_DUP))) goto end; // Ignore duplicate keys bool is_duplicate; - if (create_internal_tmp_table_from_heap(join->thd, table, + if (create_internal_tmp_table_from_heap(join->thd, table, join_tab->tmp_table_param->start_recinfo, &join_tab->tmp_table_param->recinfo, - error, 1, &is_duplicate)) + error, 1, &is_duplicate, NULL)) DBUG_RETURN(NESTED_LOOP_ERROR); // Not a table_is_full error if (is_duplicate) goto end; @@ -26707,7 +26996,7 @@ convert_heap_to_aria_update(JOIN *join, JOIN_TAB *join_tab, if (create_internal_tmp_table_from_heap(join->thd, table, join_tab->tmp_table_param->start_recinfo, &join_tab->tmp_table_param->recinfo, - error, 1, &is_duplicate)) + error, 1, &is_duplicate, NULL)) return -1; DBUG_ASSERT(is_duplicate); table->file->get_dup_key(HA_ERR_FOUND_DUPP_KEY); @@ -26791,10 +27080,11 @@ end_update(JOIN *join, JOIN_TAB *join_tab __attribute__((unused)), DBUG_RETURN(NESTED_LOOP_ERROR); /* purecov: inspected */ if (unlikely((error= table->file->ha_write_tmp_row(table->record[0])))) { + bool is_duplicate; if (create_internal_tmp_table_from_heap(join->thd, table, join_tab->tmp_table_param->start_recinfo, &join_tab->tmp_table_param->recinfo, - error, 0, NULL)) + error, 0, &is_duplicate, NULL)) DBUG_RETURN(NESTED_LOOP_ERROR); // Not a table_is_full error /* Change method to update rows */ if (unlikely((error= table->file->ha_index_init(0, 0)))) @@ -26929,11 +27219,12 @@ end_write_group(JOIN *join, JOIN_TAB *join_tab __attribute__((unused)), if (!join_tab->having || join_tab->having->val_bool()) { int error= table->file->ha_write_tmp_row(table->record[0]); + bool is_duplicate; if (unlikely(error) && create_internal_tmp_table_from_heap(join->thd, table, join_tab->tmp_table_param->start_recinfo, &join_tab->tmp_table_param->recinfo, - error, 0, NULL)) + error, 0, &is_duplicate, NULL)) DBUG_RETURN(NESTED_LOOP_ERROR); } if (unlikely(join->rollup.state != ROLLUP::STATE_NONE)) @@ -31224,11 +31515,13 @@ int JOIN::rollup_write_data(uint idx, TMP_TABLE_PARAM *tmp_table_param_arg, if (unlikely((write_error= table_arg->file->ha_write_tmp_row(table_arg->record[0])))) { - if (create_internal_tmp_table_from_heap(thd, table_arg, + bool is_duplicate; + if (create_internal_tmp_table_from_heap(thd, table_arg, tmp_table_param_arg->start_recinfo, &tmp_table_param_arg->recinfo, - write_error, 0, NULL)) - return 1; + write_error, 0, &is_duplicate, + NULL)) + return 1; } } } diff --git a/sql/sql_select.h b/sql/sql_select.h index 2507a871a6005..2616a660da870 100644 --- a/sql/sql_select.h +++ b/sql/sql_select.h @@ -2678,11 +2678,111 @@ TABLE *create_tmp_table_for_schema(THD *thd, TMP_TABLE_PARAM *param, bool do_not_open, bool keep_row_order); void free_tmp_table(THD *thd, TABLE *entry); + +/* + The row copying step of create_internal_tmp_table_from_heap(): copies + the data from a HEAP table to the Aria or MyISAM table that replaces it. + + Callers that hold row positions of the in-memory table pass their own + copier that translates them into positions in the converted table, which + is only possible while both tables are open. +*/ +class Tmp_table_row_copier +{ +public: + virtual ~Tmp_table_row_copier() = default; + /* + Copy all rows of 'from' into 'to', including the pending record[0] that + did not fit into 'from'. + + @retval 0 all rows copied + @retval -1 failed, the error has been reported + @retval >0 failed with this handler error, which the caller reports + */ + virtual int copy_rows(TABLE *from, TABLE *to)= 0; + /* Set by copy_rows() when the pending record[0] was an ignored duplicate */ + bool duplicate_row_error= false; +}; + + +/* + Copy the rows of a window function tmp table that is converted from HEAP to + the disk-based tmp engine, translating the row positions that the running + 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, @see Window_funcs_sort::setup()) and it is the only place where the + positions are kept: the row scan and all frame cursors read rows through it. + + When the sort was set up with a deferred filter (a HAVING clause deferred + to the final ORDER BY sort, @see Window_funcs_computation::setup()), 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 anyway. + + The rows are copied in the order in which they appear in the sequence. The + new position of a row is then known as soon as the row has been written, + and is stored back into the slot the old position was read from: a window + sort stores the positions in slots that fit the position of any engine an + internal tmp table can use (@see Filesort::min_ref_length), so an + in-memory sequence is rewritten in place, keeping its layout. A sequence + kept in a temporary file is written and read through an IO_CACHE, which + encrypts the file when tmp file encryption is enabled, so it can not be + rewritten in place: the translated sequence is streamed into a new cached + temporary file that then replaces the old one under the cache. + + The row whose update did not fit into the HEAP table is written from + record[0], which holds its new image, instead of being read from the + table. Blob values of record[0] that were read from the HEAP table can + point into the handler's shared blob reassembly buffer + (@see hp_read_blobs()), which reading the other rows overwrites, so they + are first given memory of their own. +*/ + +class Window_rowid_remapper : public Tmp_table_row_copier +{ +public: + Window_rowid_remapper(SORT_INFO *sort_arg, const uchar *pending_ref_arg, + uint ref_length_arg) + : sort(sort_arg), pending_ref(pending_ref_arg), + ref_length(ref_length_arg), new_ref_length(0), rows(0), + pending_blob_buf(NULL) + { + bzero(new_pending_ref_buf, sizeof(new_pending_ref_buf)); + } + + ~Window_rowid_remapper() + { + my_free(pending_blob_buf); + } + + int copy_rows(TABLE *from, TABLE *to) override; + + /* Position of the row that did not fit, in the converted table */ + const uchar *new_pending_ref() const { return new_pending_ref_buf; } + +private: + int materialize_pending_blobs(TABLE *from); + + SORT_INFO *sort; + const uchar *pending_ref; + uint ref_length; // Ref_length from heap table + uint new_ref_length; // Ref_length for Aria table + ha_rows rows; + /* Owned copies of the pending row's blob values */ + uchar *pending_blob_buf; + uchar new_pending_ref_buf[TMP_TABLE_MAX_REF_LENGTH]; +}; + bool create_internal_tmp_table_from_heap(THD *thd, TABLE *table, TMP_ENGINE_COLUMNDEF *start_recinfo, - TMP_ENGINE_COLUMNDEF **recinfo, - int error, bool ignore_last_dupp_key_error, - bool *is_duplicate); + TMP_ENGINE_COLUMNDEF **end_recinfo, + int error, + bool ignore_last_dupp_key_error, + bool *is_duplicate, + Tmp_table_row_copier *copier); bool create_internal_tmp_table(TABLE *table, KEY *keyinfo, TMP_ENGINE_COLUMNDEF *start_recinfo, TMP_ENGINE_COLUMNDEF **recinfo, diff --git a/sql/sql_show.cc b/sql/sql_show.cc index cb03bd548cc7e..0d510886716a2 100644 --- a/sql/sql_show.cc +++ b/sql/sql_show.cc @@ -4212,10 +4212,11 @@ bool schema_table_store_record(THD *thd, TABLE *table) if (unlikely((error= table->file->ha_write_tmp_row(table->record[0])))) { TMP_TABLE_PARAM *param= table->pos_in_table_list->schema_table_param; + bool is_duplicate; if (unlikely(create_internal_tmp_table_from_heap(thd, table, param->start_recinfo, ¶m->recinfo, error, 0, - NULL))) + &is_duplicate, NULL))) return 1; } diff --git a/sql/sql_sort.h b/sql/sql_sort.h index 566bcf84d7577..15d3c2bdb7e5a 100644 --- a/sql/sql_sort.h +++ b/sql/sql_sort.h @@ -597,7 +597,8 @@ class Sort_param { void setup_lengths_and_limit(TABLE *table, uint sortlen, uint addon_length, - ha_rows limit_rows_arg); + ha_rows limit_rows_arg, + uint min_ref_length_arg); void (*unpack)(TABLE *); /// Enables the packing of addons if possible. void try_to_pack_addons(ulong max_length_for_sort_data); diff --git a/sql/sql_union.cc b/sql/sql_union.cc index 8bf48bc4487ba..9a9520aa7eb8b 100644 --- a/sql/sql_union.cc +++ b/sql/sql_union.cc @@ -290,7 +290,7 @@ int select_union_recursive::send_data(List &values) rc= create_internal_tmp_table_from_heap(thd, incr_table, tmp_table_param.start_recinfo, &tmp_table_param.recinfo, - err, 1, &is_duplicate); + err, 1, &is_duplicate, NULL); } } @@ -429,9 +429,10 @@ int select_unit::write_record() if (table->file->is_fatal_error(write_err, HA_CHECK_DUP)) { if (!create_internal_tmp_table_from_heap(thd, table, - tmp_table_param.start_recinfo, - &tmp_table_param.recinfo, - write_err, 1, &is_duplicate)) + tmp_table_param.start_recinfo, + &tmp_table_param.recinfo, + write_err, 1, &is_duplicate, + NULL)) return -2; return 1; } diff --git a/sql/sql_update.cc b/sql/sql_update.cc index 8f7d6fcfc089c..8f3acc3967e83 100644 --- a/sql/sql_update.cc +++ b/sql/sql_update.cc @@ -2557,10 +2557,11 @@ int multi_update::send_data(List ¬_used_values) if (error != HA_ERR_FOUND_DUPP_KEY && error != HA_ERR_FOUND_DUPP_UNIQUE) { + bool is_duplicate; if (create_internal_tmp_table_from_heap(thd, tmp_table, tmp_table_param[offset].start_recinfo, &tmp_table_param[offset].recinfo, - error, 1, NULL)) + error, 1, &is_duplicate, NULL)) { do_update= 0; DBUG_RETURN(1); // Not a table_is_full error diff --git a/sql/sql_window.cc b/sql/sql_window.cc index 526471ee44924..64b1067daf69c 100644 --- a/sql/sql_window.cc +++ b/sql/sql_window.cc @@ -858,6 +858,47 @@ class Rowid_seq_cursor } } + /* + The rowid sequence has been rewritten: the table was converted from HEAP + to a disk-based engine, all its rows got new positions and the sequence + was rebuilt from 'info', which is positioned at its start. + + The positions are rewritten in place, in slots that fit the positions + of either engine (@see Filesort::min_ref_length), so the layout of the + sequence and the place of this cursor in it do not change; only what + the cursor has cached from the old sequence has to be taken again. + */ + bool rowids_rewritten(READ_RECORD *info) + { + DBUG_ASSERT(info->ref_length == ref_length); + + /* + io_cache is a slave of the cache of the sequence file when the + sequence is kept in a temporary file, and NULL when the sequence is + an array in memory, @see init(). The array is rewritten in place, so + there is nothing to do for it; the file is replaced together with its + cache, and the slave has to be re-created from the new master. + */ + if (io_cache) + { + end_slave_io_cache(io_cache); + if (DBUG_IF("simulate_window_seq_slave_oom") || + init_slave_io_cache(info->io_cache, io_cache)) + { + /* + The cache is already ended: forget it, so that the destructor + does not end it a second time. + */ + my_free(io_cache); + io_cache= NULL; + my_error(ER_OUT_OF_RESOURCES, MYF(0)); + return true; + } + ref_buffer_valid= false; // The positions changed + } + return false; + } + virtual int next() { /* Allow multiple next() calls in EOF state. */ @@ -1111,10 +1152,20 @@ class Partition_read_cursor : public Table_read_cursor class Frame_cursor : public Sql_alloc { public: - Frame_cursor() : sum_functions(), perform_no_action(false) {} + Frame_cursor() : sum_functions(), seq_cursor(NULL), perform_no_action(false) + {} virtual void init(READ_RECORD *info) {}; + /* + @see Rowid_seq_cursor::rowids_rewritten(). A cursor that reads the rowid + sequence registers its reader in seq_cursor, in its constructor. + */ + virtual bool rowids_rewritten(READ_RECORD *info) + { + return seq_cursor ? seq_cursor->rowids_rewritten(info) : false; + } + bool add_sum_func(Item_sum* item) { return sum_functions.push_back(item); @@ -1204,6 +1255,9 @@ class Frame_cursor : public Sql_alloc /* Sum functions that this cursor handles. */ List sum_functions; + /* The rowid sequence reader of this cursor, if it has one */ + Rowid_seq_cursor *seq_cursor; + private: bool perform_no_action; }; @@ -1227,6 +1281,17 @@ class Cursor_manager fc->init(info); } + /* @see Rowid_seq_cursor::rowids_rewritten() */ + bool notify_cursors_rowids_rewritten(READ_RECORD *info) + { + List_iterator_fast iter(cursors); + Frame_cursor *fc; + while ((fc= iter++)) + if (fc->rowids_rewritten(info)) + return true; + return false; + } + void notify_cursors_partition_changed(ha_rows rownum) { List_iterator_fast iter(cursors); @@ -1301,6 +1366,7 @@ class Frame_range_n_top : public Frame_cursor cursor(thd, partition_list), n_val(n_val_arg), item_add(NULL), is_preceding(is_preceding_arg) { + seq_cursor= &cursor; DBUG_ASSERT(order_list->elements == 1); Item *src_expr= order_list->first->item[0]; if (order_list->first->direction == ORDER::ORDER_ASC) @@ -1440,6 +1506,7 @@ class Frame_range_n_bottom: public Frame_cursor cursor(thd, partition_list), n_val(n_val_arg), item_add(NULL), is_preceding(is_preceding_arg), added_values(false) { + seq_cursor= &cursor; DBUG_ASSERT(order_list->elements == 1); Item *src_expr= order_list->first->item[0]; @@ -1569,6 +1636,7 @@ class Frame_range_current_row_bottom: public Frame_cursor SQL_I_List *order_list) : cursor(thd, partition_list), peer_tracker(thd, order_list) { + seq_cursor= &cursor; } void init(READ_RECORD *info) override @@ -1666,7 +1734,9 @@ class Frame_range_current_row_top : public Frame_cursor SQL_I_List *order_list) : bound_tracker(thd, partition_list), cursor(), peer_tracker(thd, order_list), move(false) - {} + { + seq_cursor= &cursor; + } void init(READ_RECORD *info) override { @@ -1744,6 +1814,12 @@ class Frame_unbounded_preceding : public Frame_cursor void init(READ_RECORD *info) override {} + /* + The bound tracks its position as a row number only, which the rewrite + preserves: it holds nothing that is derived from the rowid sequence. + */ + bool rowids_rewritten(READ_RECORD *info) override { return false; } + void next_partition(ha_rows rownum) override { /* @@ -1782,7 +1858,10 @@ class Frame_unbounded_following : public Frame_cursor Frame_unbounded_following(THD *thd, SQL_I_List *partition_list, SQL_I_List *order_list) : - cursor(thd, partition_list) {} + cursor(thd, partition_list) + { + seq_cursor= &cursor; + } void init(READ_RECORD *info) override { @@ -1912,7 +1991,9 @@ class Frame_n_rows_preceding : public Frame_cursor public: Frame_n_rows_preceding(bool is_top_bound_arg, ha_rows n_rows_arg) : is_top_bound(is_top_bound_arg), n_rows(n_rows_arg), n_rows_behind(0) - {} + { + seq_cursor= &cursor; + } void init(READ_RECORD *info) override { @@ -2017,6 +2098,12 @@ class Frame_rows_current_row_bottom : public Frame_cursor Frame_rows_current_row_bottom() : curr_rownum(0) {} + /* + The bound tracks its position as a row number only, which the rewrite + preserves: it holds nothing that is derived from the rowid sequence. + */ + bool rowids_rewritten(READ_RECORD *info) override { return false; } + void pre_next_partition(ha_rows rownum) override { add_value_to_items(); @@ -2091,6 +2178,7 @@ class Frame_n_rows_following : public Frame_cursor is_top_bound(is_top_bound_arg), n_rows(n_rows_arg), cursor(thd, partition_list) { + seq_cursor= &cursor; } void init(READ_RECORD *info) override @@ -2218,7 +2306,10 @@ class Frame_scan_cursor : public Frame_cursor public: Frame_scan_cursor(const Frame_cursor &top_bound, const Frame_cursor &bottom_bound) : - top_bound(top_bound), bottom_bound(bottom_bound) {} + top_bound(top_bound), bottom_bound(bottom_bound) + { + seq_cursor= &cursor; + } void init(READ_RECORD *info) override { @@ -2300,7 +2391,10 @@ class Frame_positional_cursor : public Frame_cursor Frame_positional_cursor(const Frame_cursor &position_cursor) : position_cursor(position_cursor), top_bound(NULL), bottom_bound(NULL), offset(NULL), overflowed(false), - negative_offset(false) {} + negative_offset(false) + { + seq_cursor= &cursor; + } Frame_positional_cursor(const Frame_cursor &position_cursor, const Frame_cursor &top_bound, @@ -2309,7 +2403,10 @@ class Frame_positional_cursor : public Frame_cursor bool negative_offset) : position_cursor(position_cursor), top_bound(&top_bound), bottom_bound(&bottom_bound), offset(&offset), - negative_offset(negative_offset) {} + negative_offset(negative_offset) + { + seq_cursor= &cursor; + } void init(READ_RECORD *info) override { @@ -2789,17 +2886,107 @@ bool get_window_functions_required_cursors( return false; } + +/* + The tmp table got full while storing the computed window function values of + the current row. Convert it to the disk-based tmp engine and let the + computation continue on the converted table. + + The rows keep their identity and their contents, only their positions + change, and those are translated in the rowid sequence that the row scan + and the frame cursors read (@see Window_rowid_remapper). + + @param rowid_buf IN: position of the current row in the HEAP table + OUT: its position in the converted table + + @retval false the table was converted, the computation can go on + @retval true the conversion did not happen, the error is reported +*/ + +static bool spill_window_tmp_table(THD *thd, TABLE *tbl, SORT_INFO *sort, + READ_RECORD *info, + List &cursor_managers, + uchar *rowid_buf, int error) +{ + TMP_TABLE_PARAM *param= tbl->reginfo.join_tab->tmp_table_param; + const bool in_file= !sort->has_filesort_result_in_memory(); + const uint ref_length= info->ref_length; // Of a rowid sequence slot + Cursor_manager *cursor_manager; + ha_rows scan_rownum; + bool is_duplicate; + DBUG_ENTER("spill_window_tmp_table"); + + /* + The rows are copied in the order of the rowid sequence, which becomes + the order of the converted table: ask the tmp engine to keep it, instead + of letting a scan of the converted table return the rows in whatever + order the freed space happens to produce. + */ + tbl->keep_row_order= true; + + /* The row the scan is about to read, in the sequence */ + scan_rownum= in_file ? (ha_rows) (my_b_tell(info->io_cache) / ref_length) : + (ha_rows) ((info->cache_pos - sort->record_pointers) / + ref_length); + + Window_rowid_remapper remapper(sort, rowid_buf, ref_length); + + /* Release the scan of the handler that the conversion replaces */ + end_read_record(info); + + if (create_internal_tmp_table_from_heap(thd, tbl, param->start_recinfo, + ¶m->recinfo, error, 0, + &is_duplicate, &remapper)) + DBUG_RETURN(true); + + memcpy(rowid_buf, remapper.new_pending_ref(), tbl->file->ref_length); + + /* Read the rewritten sequence, and the rows through the new handler */ + if (init_read_record(info, thd, tbl, NULL, sort, 0, 1, FALSE)) + DBUG_RETURN(true); + + List_iterator_fast it(cursor_managers); + while ((cursor_manager= it++)) + if (cursor_manager->notify_cursors_rowids_rewritten(info)) + DBUG_RETURN(true); // Out of memory, reported + + /* Put the scan back on the row it was about to read */ + if (in_file) + { + if (reinit_io_cache(info->io_cache, READ_CACHE, + scan_rownum * info->ref_length, 0, 0)) + DBUG_RETURN(true); + } + else + info->cache_pos+= scan_rownum * info->ref_length; + + DBUG_RETURN(false); +} + + /** Helper function that takes a list of window functions and writes their values in the current table record. + + @param[out] out_error Set to the handler error code if the updated row + could not be stored because the in-memory tmp table + got full; the error is then not reported and the + caller is expected to convert the table to a + disk-based one and go on with the computation. */ static bool save_window_function_values(List& window_functions, - TABLE *tbl, uchar *rowid_buf) + TABLE *tbl, uchar *rowid_buf, int *out_error) { + int err; List_iterator_fast iter(window_functions); JOIN_TAB *join_tab= tbl->reginfo.join_tab; - tbl->file->ha_rnd_pos(tbl->record[0], rowid_buf); + *out_error= 0; + if ((err= tbl->file->ha_rnd_pos(tbl->record[0], rowid_buf))) + { + tbl->file->print_error(err, MYF(0)); + return true; + } store_record(tbl, record[1]); while (Item_window_func *item_win= iter++) item_win->save_in_field(item_win->result_field, true); @@ -2830,9 +3017,15 @@ bool save_window_function_values(List& window_functions, func->save_in_result_field(true); } - int err= tbl->file->ha_update_row(tbl->record[1], tbl->record[0]); + err= tbl->file->ha_update_row(tbl->record[1], tbl->record[0]); if (err && err != HA_ERR_RECORD_IS_THE_SAME) + { + if (err == HA_ERR_RECORD_FILE_FULL && tbl->s->db_type() == heap_hton) + *out_error= err; + else + tbl->file->print_error(err, MYF(0)); return true; + } return false; } @@ -2883,7 +3076,7 @@ bool compute_window_func(THD *thd, List_iterator_fast iter_win_funcs(window_functions); List_iterator_fast iter_cursor_managers(cursor_managers); bool ret= false; - uint err; + int err; READ_RECORD info; @@ -2909,12 +3102,22 @@ bool compute_window_func(THD *thd, List_iterator_fast iter_part_trackers(partition_trackers); ha_rows rownum= 0; - uchar *rowid_buf= (uchar*) my_malloc(PSI_INSTRUMENT_ME, tbl->file->ref_length, MYF(0)); + /* With room for a position of the engine the table can be converted to */ + uchar *rowid_buf= (uchar*) my_malloc(PSI_INSTRUMENT_ME, + filesort_result->ref_length, MYF(0)); while (true) { if ((err= info.read_record())) - break; // End of file. + { + /* + read_record() returns -1 at end of file; positive values are read + errors, for which the error has already been reported. + */ + if (err > 0) + ret= true; + break; + } /* Remember current row so that we can restore it before computing each window function. */ @@ -2952,19 +3155,35 @@ bool compute_window_func(THD *thd, /* Return to current row after notifying cursors for each window function. */ - if (tbl->file->ha_rnd_pos(tbl->record[0], rowid_buf)) + if ((err= tbl->file->ha_rnd_pos(tbl->record[0], rowid_buf))) { + tbl->file->print_error(err, MYF(0)); ret= true; break; } } + if (ret) + break; + /* We now have computed values for each window function. They can now be saved in the current row. */ - if (save_window_function_values(window_functions, tbl, rowid_buf)) + int spill_error; + if (save_window_function_values(window_functions, tbl, rowid_buf, + &spill_error)) { - ret= true; - break; + /* + The in-memory tmp table got full while storing the values: convert it + to a disk-based one, which gives every row a new position, and go on + with the computation on the converted table. + */ + if (!spill_error || + spill_window_tmp_table(thd, tbl, filesort_result, &info, + cursor_managers, rowid_buf, spill_error)) + { + ret= true; + break; + } } rownum++; } @@ -3181,6 +3400,13 @@ bool Window_funcs_sort::setup(THD *thd, SQL_SELECT *sel, sort_order= order; } filesort= new (thd->mem_root) Filesort(sort_order, HA_POS_ERROR, true, NULL); + /* + The sorted tmp table can be converted from HEAP to the disk-based tmp + engine while the computation runs on this sort's result: store the row + positions in slots that fit the positions of both engines, so that the + conversion can rewrite them in place (@see Window_rowid_remapper). + */ + filesort->min_ref_length= TMP_TABLE_MAX_REF_LENGTH; /* Apply the same condition that the subsequent sort has. */ filesort->select= sel; diff --git a/sql/table.cc b/sql/table.cc index 35c6620b9e956..5a68989c1a5b5 100644 --- a/sql/table.cc +++ b/sql/table.cc @@ -10016,7 +10016,8 @@ bool TABLE::insert_all_rows_into_tmp_table(THD *thd, create_internal_tmp_table_from_heap(thd, tmp_table, tmp_table_param->start_recinfo, &tmp_table_param->recinfo, - write_err, 1, &is_duplicate)) + write_err, 1, &is_duplicate, + NULL)) DBUG_RETURN(1); }