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); }