From 02b12f4d7a0e590471734f060888c2a235c93cbc Mon Sep 17 00:00:00 2001 From: OmarGamal10 Date: Tue, 17 Mar 2026 02:19:09 +0200 Subject: [PATCH] MDEV-38819 Logic Inconsistency between Direct HAVING Query and Derived Table Relocation This fix restores consistency between the two logically equivalent queries by deep cloning the condition tree before pushing it down from HAVING into WHERE. The issue was caused by the original condition being shared between different optimizer and execution passes. --- mysql-test/main/having_cond_pushdown.result | 131 ++++++++++++++++++++ mysql-test/main/having_cond_pushdown.test | 33 +++++ sql/sql_lex.cc | 11 +- 3 files changed, 171 insertions(+), 4 deletions(-) diff --git a/mysql-test/main/having_cond_pushdown.result b/mysql-test/main/having_cond_pushdown.result index 6b488c2b78812..235a5a0b643c7 100644 --- a/mysql-test/main/having_cond_pushdown.result +++ b/mysql-test/main/having_cond_pushdown.result @@ -6090,4 +6090,135 @@ EXPLAIN } } drop table t1, t2; +# +# MDEV-38819: Logic Inconsistency between Direct HAVING Query and Derived Table Relocation +# +CREATE TABLE t0(c0 INT, c1 INT UNIQUE); +CREATE TABLE t1(c0 VARCHAR(100), c1 INT UNIQUE); +INSERT INTO t1 VALUES ('C', -1833670268); +INSERT INTO t1 VALUES ('\\', 1046230419); +INSERT INTO t0 VALUES (500, -1016012686); +SELECT t1.c1 AS g0, t0.c0 AS g1, t0.c1 AS g2 +FROM t1 STRAIGHT_JOIN t0 +GROUP BY t1.c1, t0.c0, t0.c1 +HAVING g2 NOT IN (g0, g0 != g1); +g0 g1 g2 +-1833670268 500 -1016012686 +1046230419 500 -1016012686 +explain format=json SELECT t1.c1 AS g0, t0.c0 AS g1, t0.c1 AS g2 +FROM t1 STRAIGHT_JOIN t0 +GROUP BY t1.c1, t0.c0, t0.c1 +HAVING g2 NOT IN (g0, g0 != g1); +EXPLAIN +{ + "query_block": { + "select_id": 1, + "filesort": { + "sort_key": "t1.c1, t0.c0, t0.c1", + "temporary_table": { + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "index", + "key": "c1", + "key_length": "5", + "used_key_parts": ["c1"], + "rows": 2, + "filtered": 100, + "using_index": true + } + }, + { + "range-checked-for-each-record": { + "keys": ["c1"], + "table": { + "table_name": "t0", + "access_type": "ALL", + "possible_keys": ["c1"], + "rows": 1, + "filtered": 100 + } + } + } + ] + } + } + } +} +SELECT g0, g1, g2 +FROM ( +SELECT t1.c1 AS g0, t0.c0 AS g1, t0.c1 AS g2, +t0.c1 NOT IN (t1.c1, t1.c1 != t0.c0) AS ref1 +FROM t1 STRAIGHT_JOIN t0 +GROUP BY t1.c1, t0.c0, t0.c1 +) AS s +WHERE ref1; +g0 g1 g2 +-1833670268 500 -1016012686 +1046230419 500 -1016012686 +explain format=json SELECT g0, g1, g2 +FROM ( +SELECT t1.c1 AS g0, t0.c0 AS g1, t0.c1 AS g2, +t0.c1 NOT IN (t1.c1, t1.c1 != t0.c0) AS ref1 +FROM t1 STRAIGHT_JOIN t0 +GROUP BY t1.c1, t0.c0, t0.c1 +) AS s +WHERE ref1; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "nested_loop": [ + { + "table": { + "table_name": "", + "access_type": "ALL", + "rows": 2, + "filtered": 100, + "attached_condition": "s.ref1 <> 0", + "materialized": { + "query_block": { + "select_id": 2, + "filesort": { + "sort_key": "t1.c1, t0.c0, t0.c1", + "temporary_table": { + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "index", + "key": "c1", + "key_length": "5", + "used_key_parts": ["c1"], + "rows": 2, + "filtered": 100, + "using_index": true + } + }, + { + "block-nl-join": { + "table": { + "table_name": "t0", + "access_type": "ALL", + "rows": 1, + "filtered": 100 + }, + "buffer_type": "flat", + "buffer_size": "65", + "join_type": "BNL", + "attached_condition": "t0.c1 not in (t1.c1,t1.c1 <> t0.c0) <> 0" + } + } + ] + } + } + } + } + } + } + ] + } +} +drop table t0, t1; # End of 10.11 tests diff --git a/mysql-test/main/having_cond_pushdown.test b/mysql-test/main/having_cond_pushdown.test index 507c5f15bfbf2..191978cfc6537 100644 --- a/mysql-test/main/having_cond_pushdown.test +++ b/mysql-test/main/having_cond_pushdown.test @@ -1657,4 +1657,37 @@ execute stmt; drop table t1, t2; +--echo # +--echo # MDEV-38819: Logic Inconsistency between Direct HAVING Query and Derived Table Relocation +--echo # + +CREATE TABLE t0(c0 INT, c1 INT UNIQUE); +CREATE TABLE t1(c0 VARCHAR(100), c1 INT UNIQUE); +INSERT INTO t1 VALUES ('C', -1833670268); +INSERT INTO t1 VALUES ('\\', 1046230419); +INSERT INTO t0 VALUES (500, -1016012686); + +let $q= +SELECT t1.c1 AS g0, t0.c0 AS g1, t0.c1 AS g2 +FROM t1 STRAIGHT_JOIN t0 +GROUP BY t1.c1, t0.c0, t0.c1 +HAVING g2 NOT IN (g0, g0 != g1); +eval $q; +eval explain format=json $q; + +let $q= +SELECT g0, g1, g2 +FROM ( + SELECT t1.c1 AS g0, t0.c0 AS g1, t0.c1 AS g2, + t0.c1 NOT IN (t1.c1, t1.c1 != t0.c0) AS ref1 + FROM t1 STRAIGHT_JOIN t0 + GROUP BY t1.c1, t0.c0, t0.c1 +) AS s +WHERE ref1; + +eval $q; +eval explain format=json $q; + +drop table t0, t1; + --echo # End of 10.11 tests diff --git a/sql/sql_lex.cc b/sql/sql_lex.cc index 4358aed10b645..749673cc45c64 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -11533,7 +11533,7 @@ Item *st_select_lex::pushdown_from_having_into_where(THD *thd, Item *having) list of all its conjuncts saved in attach_to_conds. Otherwise, the condition is put into attach_to_conds as the only its element. */ - List_iterator_fast it(attach_to_conds); + List_iterator it(attach_to_conds); Item *item; check_cond_extraction_for_grouping_fields(thd, having); if (build_pushable_cond_for_having_pushdown(thd, having)) @@ -11593,9 +11593,11 @@ Item *st_select_lex::pushdown_from_having_into_where(THD *thd, Item *having) it.rewind(); while ((item=it++)) { - item= item->transform(thd, - &Item::field_transformer_for_having_pushdown, - (uchar *)this); + Item *cloned_item= item->deep_copy_with_checks(thd); + if (!cloned_item) + cloned_item= item; + item= cloned_item->transform( + thd, &Item::field_transformer_for_having_pushdown, (uchar *) this); if (item->walk(&Item::cleanup_excluding_immutables_processor, 0, STOP_PTR) || item->fix_fields(thd, NULL)) @@ -11603,6 +11605,7 @@ Item *st_select_lex::pushdown_from_having_into_where(THD *thd, Item *having) attach_to_conds.empty(); goto exit; } + it.replace(item); } /*