From 353e35cf912f156a979e4bc0a23cada8d47c197d Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Tue, 18 Aug 2026 00:05:31 -0400 Subject: [PATCH 1/3] MDEV-40803 Recursive CTE loses a row when its increment table converts `select_union_recursive::send_data()` builds each new row in the record buffer of the union result table and then writes it into the increment table. When that write overflows the in-memory table it asks for the increment table to be converted to an on-disk one. `create_internal_tmp_table_from_heap()` copies every stored row into the new table and then appends the row that did not fit, taking it from the `record[0]` of the table it was given, that is from `incr_table->record[0]`. Nothing ever fills that buffer: `TABLE::insert_all_rows_into_tmp_table()` reads into the destination's `record[0]` and the recursive scan reads the recursive result table. So the overflowing row was dropped and the empty record the table was opened with was appended in its place. The increment table is created with no grouping and no distinct flag, so it carries no key and the substituted row is not rejected. On the next iteration that row's columns are NULL, the recursion predicate evaluates to NULL, and it produces no descendants. That row and every row the recursion would have derived from it are missing from the answer, with no error and no warning. Copy the row into the increment table's own record buffer before asking for the conversion, so that this caller satisfies the same precondition that every other caller of `create_internal_tmp_table_from_heap()` already satisfies. Only `incr_table->s->reclength` bytes are copied: when the union result table uses a unique constraint its record carries a trailing `MARIA_UNIQUE_HASH_LENGTH` hash field, which is appended after the real columns and is not part of the increment table's record. Each shape in the test is run twice, once with the increment table small enough to stay in memory and once with it forced to convert, and the two runs are required to agree. Two properties are needed to expose the defect and neither is obvious: 1. `tmp_memory_table_size=0` cannot be used to force the conversion. It makes `Create_tmp_table::choose_engine()` build the table on disk from the start, so the conversion never happens. This is why the existing tests that set it never caught this. 2. The lost row has to be at a level of the recursion that still has descendants. The in-memory table is only checked for fullness at block boundaries, so a row width that pushes the conversion into the last level hides the defect entirely. --- mysql-test/main/cte_recursive.result | 227 +++++++++++++++++++++++++++ mysql-test/main/cte_recursive.test | 177 +++++++++++++++++++++ sql/sql_union.cc | 10 ++ 3 files changed, 414 insertions(+) diff --git a/mysql-test/main/cte_recursive.result b/mysql-test/main/cte_recursive.result index 9380237dce53b..30247c6d08231 100644 --- a/mysql-test/main/cte_recursive.result +++ b/mysql-test/main/cte_recursive.result @@ -6246,3 +6246,230 @@ SELECT 1 > ( WITH cte AS ( SELECT 1 FROM x ) SELECT 1 FROM cte ) ) SELECT 1 FROM x; ERROR HY000: Restrictions imposed on recursive definitions are violated for table 'x' # End of 10.11 tests +# +# Recursive CTE loses a row when the increment table is converted +# into an on-disk temporary table +# +CREATE TABLE t1 (n INT, pad CHAR(255) CHARACTER SET utf8mb4); +INSERT INTO t1 SELECT seq, REPEAT('x',255) FROM seq_1_to_64; +SET @save_tmp_memory_table_size= @@tmp_memory_table_size; +SET @save_max_heap_table_size= @@max_heap_table_size; +# Each query below is run twice. The first run leaves the increment +# table in memory, the second forces it to be converted to an on-disk +# table while it is being filled. Both runs must return the same rows. +# +# The increment table overflows while the anchor part fills it +# +SET SESSION tmp_memory_table_size= 16777216; +SET SESSION max_heap_table_size= 16777216; +FLUSH STATUS; +WITH RECURSIVE r AS +(SELECT 1 AS lvl, n, pad FROM t1 +UNION ALL +SELECT lvl+1, n, pad FROM r WHERE lvl < 3) +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads, SUM(n) AS sum_n +FROM r GROUP BY lvl ORDER BY lvl; +lvl cnt pads sum_n +1 64 64 2080 +2 64 64 2080 +3 64 64 2080 +# 0 = no temporary table was converted +SELECT VARIABLE_VALUE > 0 AS converted FROM information_schema.SESSION_STATUS +WHERE VARIABLE_NAME = 'Created_tmp_disk_tables'; +converted +0 +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +FLUSH STATUS; +WITH RECURSIVE r AS +(SELECT 1 AS lvl, n, pad FROM t1 +UNION ALL +SELECT lvl+1, n, pad FROM r WHERE lvl < 3) +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads, SUM(n) AS sum_n +FROM r GROUP BY lvl ORDER BY lvl; +lvl cnt pads sum_n +1 64 64 2080 +2 64 64 2080 +3 64 64 2080 +# 1 = a temporary table was converted +SELECT VARIABLE_VALUE > 0 AS converted FROM information_schema.SESSION_STATUS +WHERE VARIABLE_NAME = 'Created_tmp_disk_tables'; +converted +1 +# +# The increment table overflows during a recursive step +# +SET SESSION tmp_memory_table_size= 16777216; +SET SESSION max_heap_table_size= 16777216; +WITH RECURSIVE r AS +(SELECT 1 AS lvl, pad FROM t1 WHERE n = 1 +UNION ALL +SELECT r.lvl+1, t1.pad FROM r, t1 WHERE r.lvl < 3) +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads +FROM r GROUP BY lvl ORDER BY lvl; +lvl cnt pads +1 1 1 +2 64 64 +3 4096 4096 +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +WITH RECURSIVE r AS +(SELECT 1 AS lvl, pad FROM t1 WHERE n = 1 +UNION ALL +SELECT r.lvl+1, t1.pad FROM r, t1 WHERE r.lvl < 3) +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads +FROM r GROUP BY lvl ORDER BY lvl; +lvl cnt pads +1 1 1 +2 64 64 +3 4096 4096 +# +# The same, for a CTE that eliminates duplicates +# +SET SESSION tmp_memory_table_size= 16777216; +SET SESSION max_heap_table_size= 16777216; +WITH RECURSIVE r AS +(SELECT 1 AS lvl, n, pad FROM t1 +UNION +SELECT lvl+1, n, pad FROM r WHERE lvl < 3) +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads, SUM(n) AS sum_n +FROM r GROUP BY lvl ORDER BY lvl; +lvl cnt pads sum_n +1 64 64 2080 +2 64 64 2080 +3 64 64 2080 +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +WITH RECURSIVE r AS +(SELECT 1 AS lvl, n, pad FROM t1 +UNION +SELECT lvl+1, n, pad FROM r WHERE lvl < 3) +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads, SUM(n) AS sum_n +FROM r GROUP BY lvl ORDER BY lvl; +lvl cnt pads sum_n +1 64 64 2080 +2 64 64 2080 +3 64 64 2080 +# +# The same, when the result table's key does not fit an on-disk table +# and is replaced by a unique constraint. The result table's record +# then carries a trailing hash field that the increment table's record +# does not have, so the two records are of different length. +# +CREATE TABLE t2 (n INT, +p1 CHAR(200) CHARACTER SET utf8mb4, +p2 CHAR(200) CHARACTER SET utf8mb4, +p3 CHAR(200) CHARACTER SET utf8mb4); +INSERT INTO t2 +SELECT seq, REPEAT('a',200), REPEAT('b',200), REPEAT('c',200) FROM seq_1_to_64; +SET SESSION tmp_memory_table_size= 16777216; +SET SESSION max_heap_table_size= 16777216; +WITH RECURSIVE r AS +(SELECT 1 AS lvl, n, p1, p2, p3 FROM t2 +UNION +SELECT lvl+1, n, p1, p2, p3 FROM r WHERE lvl < 3) +SELECT lvl, COUNT(*) AS cnt, COUNT(p3) AS pads, SUM(n) AS sum_n +FROM r GROUP BY lvl ORDER BY lvl; +lvl cnt pads sum_n +1 64 64 2080 +2 64 64 2080 +3 64 64 2080 +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +WITH RECURSIVE r AS +(SELECT 1 AS lvl, n, p1, p2, p3 FROM t2 +UNION +SELECT lvl+1, n, p1, p2, p3 FROM r WHERE lvl < 3) +SELECT lvl, COUNT(*) AS cnt, COUNT(p3) AS pads, SUM(n) AS sum_n +FROM r GROUP BY lvl ORDER BY lvl; +lvl cnt pads sum_n +1 64 64 2080 +2 64 64 2080 +3 64 64 2080 +DROP TABLE t2; +# +# A prepared statement re-executed after its increment table was +# converted on an earlier execution +# +PREPARE s FROM " +WITH RECURSIVE r AS + (SELECT 1 AS lvl, n, pad FROM t1 + UNION ALL + SELECT lvl+1, n, pad FROM r WHERE lvl < 3) +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads, SUM(n) AS sum_n + FROM r GROUP BY lvl ORDER BY lvl"; +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +EXECUTE s; +lvl cnt pads sum_n +1 64 64 2080 +2 64 64 2080 +3 64 64 2080 +EXECUTE s; +lvl cnt pads sum_n +1 64 64 2080 +2 64 64 2080 +3 64 64 2080 +SET SESSION tmp_memory_table_size= 16777216; +SET SESSION max_heap_table_size= 16777216; +EXECUTE s; +lvl cnt pads sum_n +1 64 64 2080 +2 64 64 2080 +3 64 64 2080 +DROP PREPARE s; +# +# Mutually recursive CTEs +# +SET SESSION tmp_memory_table_size= 16777216; +SET SESSION max_heap_table_size= 16777216; +WITH RECURSIVE +a AS (SELECT 1 AS lvl, n, pad FROM t1 +UNION ALL +SELECT lvl+1, n, pad FROM b WHERE lvl < 4), +b AS (SELECT lvl, n, pad FROM a) +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads, SUM(n) AS sum_n +FROM a GROUP BY lvl ORDER BY lvl; +lvl cnt pads sum_n +1 64 64 2080 +2 64 64 2080 +3 64 64 2080 +4 64 64 2080 +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +WITH RECURSIVE +a AS (SELECT 1 AS lvl, n, pad FROM t1 +UNION ALL +SELECT lvl+1, n, pad FROM b WHERE lvl < 4), +b AS (SELECT lvl, n, pad FROM a) +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads, SUM(n) AS sum_n +FROM a GROUP BY lvl ORDER BY lvl; +lvl cnt pads sum_n +1 64 64 2080 +2 64 64 2080 +3 64 64 2080 +4 64 64 2080 +# +# INSERT ... SELECT, which takes a different warning-handling path +# when the increment table is filled +# +CREATE TABLE t3 (lvl INT, n INT, pad CHAR(255) CHARACTER SET utf8mb4); +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +INSERT INTO t3 +WITH RECURSIVE r AS +(SELECT 1 AS lvl, n, pad FROM t1 +UNION ALL +SELECT lvl+1, n, pad FROM r WHERE lvl < 3) +SELECT lvl, n, pad FROM r; +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads, SUM(n) AS sum_n +FROM t3 GROUP BY lvl ORDER BY lvl; +lvl cnt pads sum_n +1 64 64 2080 +2 64 64 2080 +3 64 64 2080 +DROP TABLE t3; +SET SESSION tmp_memory_table_size= @save_tmp_memory_table_size; +SET SESSION max_heap_table_size= @save_max_heap_table_size; +DROP TABLE t1; +# End of 13.1 tests diff --git a/mysql-test/main/cte_recursive.test b/mysql-test/main/cte_recursive.test index fe3e5e8ab03f5..feabe591a8625 100644 --- a/mysql-test/main/cte_recursive.test +++ b/mysql-test/main/cte_recursive.test @@ -4257,3 +4257,180 @@ WITH RECURSIVE x AS ( SELECT 1 FROM x; --echo # End of 10.11 tests + +--echo # +--echo # Recursive CTE loses a row when the increment table is converted +--echo # into an on-disk temporary table +--echo # + +--source include/have_sequence.inc + +CREATE TABLE t1 (n INT, pad CHAR(255) CHARACTER SET utf8mb4); +INSERT INTO t1 SELECT seq, REPEAT('x',255) FROM seq_1_to_64; + +SET @save_tmp_memory_table_size= @@tmp_memory_table_size; +SET @save_max_heap_table_size= @@max_heap_table_size; + +--echo # Each query below is run twice. The first run leaves the increment +--echo # table in memory, the second forces it to be converted to an on-disk +--echo # table while it is being filled. Both runs must return the same rows. + +--echo # +--echo # The increment table overflows while the anchor part fills it +--echo # +let $q= +WITH RECURSIVE r AS + (SELECT 1 AS lvl, n, pad FROM t1 + UNION ALL + SELECT lvl+1, n, pad FROM r WHERE lvl < 3) +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads, SUM(n) AS sum_n + FROM r GROUP BY lvl ORDER BY lvl; + +SET SESSION tmp_memory_table_size= 16777216; +SET SESSION max_heap_table_size= 16777216; +FLUSH STATUS; +eval $q; +--echo # 0 = no temporary table was converted +SELECT VARIABLE_VALUE > 0 AS converted FROM information_schema.SESSION_STATUS + WHERE VARIABLE_NAME = 'Created_tmp_disk_tables'; + +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +FLUSH STATUS; +eval $q; +--echo # 1 = a temporary table was converted +SELECT VARIABLE_VALUE > 0 AS converted FROM information_schema.SESSION_STATUS + WHERE VARIABLE_NAME = 'Created_tmp_disk_tables'; + +--echo # +--echo # The increment table overflows during a recursive step +--echo # +let $q= +WITH RECURSIVE r AS + (SELECT 1 AS lvl, pad FROM t1 WHERE n = 1 + UNION ALL + SELECT r.lvl+1, t1.pad FROM r, t1 WHERE r.lvl < 3) +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads + FROM r GROUP BY lvl ORDER BY lvl; + +SET SESSION tmp_memory_table_size= 16777216; +SET SESSION max_heap_table_size= 16777216; +eval $q; + +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +eval $q; + +--echo # +--echo # The same, for a CTE that eliminates duplicates +--echo # +let $q= +WITH RECURSIVE r AS + (SELECT 1 AS lvl, n, pad FROM t1 + UNION + SELECT lvl+1, n, pad FROM r WHERE lvl < 3) +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads, SUM(n) AS sum_n + FROM r GROUP BY lvl ORDER BY lvl; + +SET SESSION tmp_memory_table_size= 16777216; +SET SESSION max_heap_table_size= 16777216; +eval $q; + +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +eval $q; + +--echo # +--echo # The same, when the result table's key does not fit an on-disk table +--echo # and is replaced by a unique constraint. The result table's record +--echo # then carries a trailing hash field that the increment table's record +--echo # does not have, so the two records are of different length. +--echo # +CREATE TABLE t2 (n INT, + p1 CHAR(200) CHARACTER SET utf8mb4, + p2 CHAR(200) CHARACTER SET utf8mb4, + p3 CHAR(200) CHARACTER SET utf8mb4); +INSERT INTO t2 + SELECT seq, REPEAT('a',200), REPEAT('b',200), REPEAT('c',200) FROM seq_1_to_64; + +let $q= +WITH RECURSIVE r AS + (SELECT 1 AS lvl, n, p1, p2, p3 FROM t2 + UNION + SELECT lvl+1, n, p1, p2, p3 FROM r WHERE lvl < 3) +SELECT lvl, COUNT(*) AS cnt, COUNT(p3) AS pads, SUM(n) AS sum_n + FROM r GROUP BY lvl ORDER BY lvl; + +SET SESSION tmp_memory_table_size= 16777216; +SET SESSION max_heap_table_size= 16777216; +eval $q; + +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +eval $q; + +DROP TABLE t2; + +--echo # +--echo # A prepared statement re-executed after its increment table was +--echo # converted on an earlier execution +--echo # +PREPARE s FROM " +WITH RECURSIVE r AS + (SELECT 1 AS lvl, n, pad FROM t1 + UNION ALL + SELECT lvl+1, n, pad FROM r WHERE lvl < 3) +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads, SUM(n) AS sum_n + FROM r GROUP BY lvl ORDER BY lvl"; + +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +EXECUTE s; +EXECUTE s; +SET SESSION tmp_memory_table_size= 16777216; +SET SESSION max_heap_table_size= 16777216; +EXECUTE s; +DROP PREPARE s; + +--echo # +--echo # Mutually recursive CTEs +--echo # +let $q= +WITH RECURSIVE + a AS (SELECT 1 AS lvl, n, pad FROM t1 + UNION ALL + SELECT lvl+1, n, pad FROM b WHERE lvl < 4), + b AS (SELECT lvl, n, pad FROM a) +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads, SUM(n) AS sum_n + FROM a GROUP BY lvl ORDER BY lvl; + +SET SESSION tmp_memory_table_size= 16777216; +SET SESSION max_heap_table_size= 16777216; +eval $q; + +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +eval $q; + +--echo # +--echo # INSERT ... SELECT, which takes a different warning-handling path +--echo # when the increment table is filled +--echo # +CREATE TABLE t3 (lvl INT, n INT, pad CHAR(255) CHARACTER SET utf8mb4); +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +INSERT INTO t3 +WITH RECURSIVE r AS + (SELECT 1 AS lvl, n, pad FROM t1 + UNION ALL + SELECT lvl+1, n, pad FROM r WHERE lvl < 3) +SELECT lvl, n, pad FROM r; +SELECT lvl, COUNT(*) AS cnt, COUNT(pad) AS pads, SUM(n) AS sum_n + FROM t3 GROUP BY lvl ORDER BY lvl; +DROP TABLE t3; + +SET SESSION tmp_memory_table_size= @save_tmp_memory_table_size; +SET SESSION max_heap_table_size= @save_max_heap_table_size; +DROP TABLE t1; + +--echo # End of 13.1 tests diff --git a/sql/sql_union.cc b/sql/sql_union.cc index 8bf48bc4487ba..6e3b878cf9f0f 100644 --- a/sql/sql_union.cc +++ b/sql/sql_union.cc @@ -287,6 +287,16 @@ int select_union_recursive::send_data(List &values) if ((err= incr_table->file->ha_write_tmp_row(table->record[0]))) { bool is_duplicate; + /* + create_internal_tmp_table_from_heap() appends the row that did not fit + from the record[0] of the table it converts, so the row has to be in + the increment table's own record buffer. Only reclength bytes of the + increment table are copied: if the union result table uses a unique + constraint its record has a trailing hash field, which is not part of + the increment table's record (see the assert above). + */ + memcpy(incr_table->record[0], table->record[0], + incr_table->s->reclength); rc= create_internal_tmp_table_from_heap(thd, incr_table, tmp_table_param.start_recinfo, &tmp_table_param.recinfo, From 0c449dc6565fe46a340ba0fc4d98bd02fa3fc411 Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Tue, 18 Aug 2026 00:05:51 -0400 Subject: [PATCH 2/3] Cover converting a result table whose index has been disabled `create_internal_tmp_table_from_heap()` carries the disabled state of the in-memory table's index over to the table it creates: ``` if (table->file->indexes_are_disabled()) new_table.file->ha_disable_indexes(key_map(0), false); ``` That branch was never executed by the test suite. Measured with gcov over every test in the `main` suite that manipulates the temporary table size limits, it is the only reachable branch of that function that no test takes. A union reaches it: `st_select_lex_unit::exec()` drops the index of the result table once the last duplicate-eliminating part has been read, so a trailing `UNION ALL` part fills a table that has no index, and that table can then outgrow the in-memory limit. --- mysql-test/main/union.result | 35 +++++++++++++++++++++++++++++++++++ mysql-test/main/union.test | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/mysql-test/main/union.result b/mysql-test/main/union.result index 351beae4b1a19..f551c34cbd6e8 100644 --- a/mysql-test/main/union.result +++ b/mysql-test/main/union.result @@ -2848,3 +2848,38 @@ res 3 4 2 +# +# A result table that is converted to an on-disk table after its index +# has been disabled +# +CREATE TABLE t1 (a INT); +INSERT INTO t1 SELECT seq FROM seq_1_to_5000; +SET @save_tmp_memory_table_size= @@tmp_memory_table_size; +SET @save_max_heap_table_size= @@max_heap_table_size; +# The index of the result table is dropped once the last distinct part +# has been read, so the trailing UNION ALL part fills a table with no +# index, and that table then outgrows the in-memory limit. +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +SELECT COUNT(*) AS cnt, COUNT(DISTINCT a) AS distinct_a FROM ( +SELECT a FROM t1 WHERE a <= 100 +UNION +SELECT a FROM t1 WHERE a <= 100 +UNION ALL +SELECT a FROM t1) d; +cnt distinct_a +5100 5000 +SET SESSION tmp_memory_table_size= 16777216; +SET SESSION max_heap_table_size= 16777216; +SELECT COUNT(*) AS cnt, COUNT(DISTINCT a) AS distinct_a FROM ( +SELECT a FROM t1 WHERE a <= 100 +UNION +SELECT a FROM t1 WHERE a <= 100 +UNION ALL +SELECT a FROM t1) d; +cnt distinct_a +5100 5000 +SET SESSION tmp_memory_table_size= @save_tmp_memory_table_size; +SET SESSION max_heap_table_size= @save_max_heap_table_size; +DROP TABLE t1; +# End of 13.1 tests diff --git a/mysql-test/main/union.test b/mysql-test/main/union.test index 70a3d0fad5913..d10658d2ea9d4 100644 --- a/mysql-test/main/union.test +++ b/mysql-test/main/union.test @@ -2050,3 +2050,39 @@ select 1 as res union select 2 union all select 1 union distinct select 3; select 1 as res union select 2 union all select 1 union distinct select 3 union all select 2; select 1 as res union select 2 union all select 1 union distinct select 3 union all select 2 union distinct select 5; select truncate(seq/2,0)+1 as res from seq_1_to_6 union all select 2 union all select 1 union distinct select 3 union all select 2; + +--echo # +--echo # A result table that is converted to an on-disk table after its index +--echo # has been disabled +--echo # + +CREATE TABLE t1 (a INT); +INSERT INTO t1 SELECT seq FROM seq_1_to_5000; + +SET @save_tmp_memory_table_size= @@tmp_memory_table_size; +SET @save_max_heap_table_size= @@max_heap_table_size; + +--echo # The index of the result table is dropped once the last distinct part +--echo # has been read, so the trailing UNION ALL part fills a table with no +--echo # index, and that table then outgrows the in-memory limit. +let $q= +SELECT COUNT(*) AS cnt, COUNT(DISTINCT a) AS distinct_a FROM ( + SELECT a FROM t1 WHERE a <= 100 + UNION + SELECT a FROM t1 WHERE a <= 100 + UNION ALL + SELECT a FROM t1) d; + +SET SESSION tmp_memory_table_size= 16384; +SET SESSION max_heap_table_size= 16384; +eval $q; + +SET SESSION tmp_memory_table_size= 16777216; +SET SESSION max_heap_table_size= 16777216; +eval $q; + +SET SESSION tmp_memory_table_size= @save_tmp_memory_table_size; +SET SESSION max_heap_table_size= @save_max_heap_table_size; +DROP TABLE t1; + +--echo # End of 13.1 tests From 2f5882122ea2261b88621f0588bbd0dfc134e74c Mon Sep 17 00:00:00 2001 From: Arcadiy Ivanov Date: Tue, 18 Aug 2026 00:05:59 -0400 Subject: [PATCH 3/3] Drop the view left behind by cte_update_delete.test The test creates `v1` and never drops it, so it ends with a view still in the `test` database. MTR's check of the testcase then reports ``` tables_in_test +v1 ``` and fails the check both for this test and for whichever test happens to run next on the same worker, which is why the name it reports varies between runs. --- mysql-test/main/cte_update_delete.result | 1 + mysql-test/main/cte_update_delete.test | 1 + 2 files changed, 2 insertions(+) diff --git a/mysql-test/main/cte_update_delete.result b/mysql-test/main/cte_update_delete.result index 6cda90fb9e2cd..14fd4b09dc78f 100644 --- a/mysql-test/main/cte_update_delete.result +++ b/mysql-test/main/cte_update_delete.result @@ -1374,5 +1374,6 @@ a 15 20 25 +drop view v1; drop table t1; # End of 12.2 tests diff --git a/mysql-test/main/cte_update_delete.test b/mysql-test/main/cte_update_delete.test index ab6a0b8b1903c..d49c7546e7d63 100644 --- a/mysql-test/main/cte_update_delete.test +++ b/mysql-test/main/cte_update_delete.test @@ -594,6 +594,7 @@ with recursive cte as (select a from t1 union select a from cte where a < 2) delete from v1 using v1, cte where v1.a = cte.a; select * from t1 order by 1; +drop view v1; drop table t1; --echo # End of 12.2 tests