diff --git a/mysql-test/suite/parts/r/partition_exchange_generated_columns.result b/mysql-test/suite/parts/r/partition_exchange_generated_columns.result new file mode 100644 index 0000000000000..636956d8e030b --- /dev/null +++ b/mysql-test/suite/parts/r/partition_exchange_generated_columns.result @@ -0,0 +1,103 @@ +# +# Test EXCHANGE PARTITION with generated columns containing AND/OR conditions +# This verifies that Item_cond::eq() correctly performs set-based comparison +# for commutative AND/OR operations, allowing partition exchange to succeed +# even when condition order differs between original and re-parsed expressions. +# verifies MDEV-38757 +# +DROP TABLE IF EXISTS t1, t1_working; +# Test 1: Generated column with OR condition +CREATE TABLE t1 ( +id INT NOT NULL AUTO_INCREMENT, +userId INT NOT NULL, +col1 INT NOT NULL, +vcol1 TINYINT(1) GENERATED ALWAYS AS (col1 > 80 OR col1 < 20) STORED, +PRIMARY KEY (id, userId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1) ENGINE = InnoDB, +PARTITION p2 VALUES IN (2) ENGINE = InnoDB); +CREATE TABLE t1_working LIKE t1; +ALTER TABLE t1_working REMOVE PARTITIONING; +INSERT INTO t1_working (userId, col1) VALUES (1, 10), (1, 50), (1, 90); +# This should succeed: EXCHANGE PARTITION with OR condition +ALTER TABLE t1 EXCHANGE PARTITION p1 WITH TABLE t1_working; +SELECT id, userId, col1, vcol1 FROM t1 ORDER BY id; +id userId col1 vcol1 +1 1 10 1 +2 1 50 0 +3 1 90 1 +DROP TABLE t1_working; +DROP TABLE t1; +# Test 2: Generated column with AND condition +CREATE TABLE t1 ( +id INT NOT NULL AUTO_INCREMENT, +userId INT NOT NULL, +col1 INT NOT NULL, +col2 INT NOT NULL, +vcol2 TINYINT(1) GENERATED ALWAYS AS (col1 > 10 AND col2 < 100) STORED, +PRIMARY KEY (id, userId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1) ENGINE = InnoDB); +CREATE TABLE t1_working LIKE t1; +ALTER TABLE t1_working REMOVE PARTITIONING; +INSERT INTO t1_working (userId, col1, col2) VALUES (1, 20, 50), (1, 5, 200); +# This should succeed: EXCHANGE PARTITION with AND condition +ALTER TABLE t1 EXCHANGE PARTITION p1 WITH TABLE t1_working; +SELECT id, userId, col1, col2, vcol2 FROM t1 ORDER BY id; +id userId col1 col2 vcol2 +1 1 20 50 1 +2 1 5 200 0 +DROP TABLE t1_working; +DROP TABLE t1; +# Test 3: Multiple generated columns with different AND/OR combinations +CREATE TABLE t1 ( +id INT NOT NULL AUTO_INCREMENT, +userId INT NOT NULL, +col1 INT NOT NULL, +col2 INT NOT NULL, +vcol1 TINYINT(1) GENERATED ALWAYS AS (col1 > 80 OR col1 < 20) STORED, +vcol2 TINYINT(1) GENERATED ALWAYS AS (col1 > 10 AND col2 < 100) STORED, +vcol3 TINYINT(1) GENERATED ALWAYS AS (col2 > 50 OR col2 < 10) STORED, +PRIMARY KEY (id, userId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1) ENGINE = InnoDB, +PARTITION p2 VALUES IN (2) ENGINE = InnoDB); +CREATE TABLE t1_working LIKE t1; +ALTER TABLE t1_working REMOVE PARTITIONING; +INSERT INTO t1_working (userId, col1, col2) VALUES (1, 15, 75), (1, 90, 5), (1, 50, 200); +# This should succeed: EXCHANGE PARTITION with multiple AND/OR conditions +ALTER TABLE t1 EXCHANGE PARTITION p1 WITH TABLE t1_working; +SELECT id, userId, col1, col2, vcol1, vcol2, vcol3 FROM t1 ORDER BY id; +id userId col1 col2 vcol1 vcol2 vcol3 +1 1 15 75 1 1 1 +2 1 90 5 1 1 1 +3 1 50 200 0 0 1 +DROP TABLE t1_working; +DROP TABLE t1; +# Test 4: Nested AND/OR conditions +CREATE TABLE t1 ( +id INT NOT NULL AUTO_INCREMENT, +userId INT NOT NULL, +col1 INT NOT NULL, +col2 INT NOT NULL, +col3 INT NOT NULL, +vcol4 TINYINT(1) GENERATED ALWAYS AS ((col1 > 10 AND col2 < 100) OR col3 > 50) STORED, +PRIMARY KEY (id, userId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1) ENGINE = InnoDB); +CREATE TABLE t1_working LIKE t1; +ALTER TABLE t1_working REMOVE PARTITIONING; +INSERT INTO t1_working (userId, col1, col2, col3) VALUES (1, 20, 50, 30), (1, 5, 200, 60); +# This should succeed: EXCHANGE PARTITION with nested AND/OR conditions +ALTER TABLE t1 EXCHANGE PARTITION p1 WITH TABLE t1_working; +SELECT id, userId, col1, col2, col3, vcol4 FROM t1 ORDER BY id; +id userId col1 col2 col3 vcol4 +1 1 20 50 30 1 +2 1 5 200 60 1 +DROP TABLE t1_working; +DROP TABLE t1; +# End of test diff --git a/mysql-test/suite/parts/t/partition_exchange_generated_columns.test b/mysql-test/suite/parts/t/partition_exchange_generated_columns.test new file mode 100644 index 0000000000000..f2724a3931135 --- /dev/null +++ b/mysql-test/suite/parts/t/partition_exchange_generated_columns.test @@ -0,0 +1,117 @@ +--source include/have_innodb.inc +--source include/have_partition.inc + +--echo # +--echo # Test EXCHANGE PARTITION with generated columns containing AND/OR conditions +--echo # This verifies that Item_cond::eq() correctly performs set-based comparison +--echo # for commutative AND/OR operations, allowing partition exchange to succeed +--echo # even when condition order differs between original and re-parsed expressions. +--echo # verifies MDEV-38757 +--echo # + +--disable_warnings +DROP TABLE IF EXISTS t1, t1_working; +--enable_warnings + +--echo # Test 1: Generated column with OR condition +CREATE TABLE t1 ( + id INT NOT NULL AUTO_INCREMENT, + userId INT NOT NULL, + col1 INT NOT NULL, + vcol1 TINYINT(1) GENERATED ALWAYS AS (col1 > 80 OR col1 < 20) STORED, + PRIMARY KEY (id, userId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1) ENGINE = InnoDB, + PARTITION p2 VALUES IN (2) ENGINE = InnoDB); + +CREATE TABLE t1_working LIKE t1; +ALTER TABLE t1_working REMOVE PARTITIONING; + +INSERT INTO t1_working (userId, col1) VALUES (1, 10), (1, 50), (1, 90); + +--echo # This should succeed: EXCHANGE PARTITION with OR condition +ALTER TABLE t1 EXCHANGE PARTITION p1 WITH TABLE t1_working; + +SELECT id, userId, col1, vcol1 FROM t1 ORDER BY id; +DROP TABLE t1_working; +DROP TABLE t1; + +--echo # Test 2: Generated column with AND condition +CREATE TABLE t1 ( + id INT NOT NULL AUTO_INCREMENT, + userId INT NOT NULL, + col1 INT NOT NULL, + col2 INT NOT NULL, + vcol2 TINYINT(1) GENERATED ALWAYS AS (col1 > 10 AND col2 < 100) STORED, + PRIMARY KEY (id, userId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1) ENGINE = InnoDB); + +CREATE TABLE t1_working LIKE t1; +ALTER TABLE t1_working REMOVE PARTITIONING; + +INSERT INTO t1_working (userId, col1, col2) VALUES (1, 20, 50), (1, 5, 200); + +--echo # This should succeed: EXCHANGE PARTITION with AND condition +ALTER TABLE t1 EXCHANGE PARTITION p1 WITH TABLE t1_working; + +SELECT id, userId, col1, col2, vcol2 FROM t1 ORDER BY id; +DROP TABLE t1_working; +DROP TABLE t1; + +--echo # Test 3: Multiple generated columns with different AND/OR combinations +CREATE TABLE t1 ( + id INT NOT NULL AUTO_INCREMENT, + userId INT NOT NULL, + col1 INT NOT NULL, + col2 INT NOT NULL, + vcol1 TINYINT(1) GENERATED ALWAYS AS (col1 > 80 OR col1 < 20) STORED, + vcol2 TINYINT(1) GENERATED ALWAYS AS (col1 > 10 AND col2 < 100) STORED, + vcol3 TINYINT(1) GENERATED ALWAYS AS (col2 > 50 OR col2 < 10) STORED, + PRIMARY KEY (id, userId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1) ENGINE = InnoDB, + PARTITION p2 VALUES IN (2) ENGINE = InnoDB); + +CREATE TABLE t1_working LIKE t1; +ALTER TABLE t1_working REMOVE PARTITIONING; + +INSERT INTO t1_working (userId, col1, col2) VALUES (1, 15, 75), (1, 90, 5), (1, 50, 200); + +--echo # This should succeed: EXCHANGE PARTITION with multiple AND/OR conditions +ALTER TABLE t1 EXCHANGE PARTITION p1 WITH TABLE t1_working; + +SELECT id, userId, col1, col2, vcol1, vcol2, vcol3 FROM t1 ORDER BY id; +DROP TABLE t1_working; +DROP TABLE t1; + +--echo # Test 4: Nested AND/OR conditions +CREATE TABLE t1 ( + id INT NOT NULL AUTO_INCREMENT, + userId INT NOT NULL, + col1 INT NOT NULL, + col2 INT NOT NULL, + col3 INT NOT NULL, + vcol4 TINYINT(1) GENERATED ALWAYS AS ((col1 > 10 AND col2 < 100) OR col3 > 50) STORED, + PRIMARY KEY (id, userId) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1) ENGINE = InnoDB); + +CREATE TABLE t1_working LIKE t1; +ALTER TABLE t1_working REMOVE PARTITIONING; + +INSERT INTO t1_working (userId, col1, col2, col3) VALUES (1, 20, 50, 30), (1, 5, 200, 60); + +--echo # This should succeed: EXCHANGE PARTITION with nested AND/OR conditions +ALTER TABLE t1 EXCHANGE PARTITION p1 WITH TABLE t1_working; + +SELECT id, userId, col1, col2, col3, vcol4 FROM t1 ORDER BY id; +DROP TABLE t1_working; + +DROP TABLE t1; + +--echo # End of test diff --git a/sql/item_cmpfunc.cc b/sql/item_cmpfunc.cc index 5f2c471de7fba..0d99ef7e917eb 100644 --- a/sql/item_cmpfunc.cc +++ b/sql/item_cmpfunc.cc @@ -5595,6 +5595,83 @@ bool Item_cond::excl_dep_on_grouping_fields(st_select_lex *sel) } +/** + Compare two Item_cond expressions for equality. + + For AND/OR conditions, the order of arguments doesn't matter (commutative), + so we need to do a set-based comparison rather than order-based. + + @param item The other Item to compare with + @param config Comparison configuration + + @return true if expressions are equivalent, false otherwise +*/ +bool Item_cond::eq(const Item *item, const Eq_config &config) const +{ + /* Assume we don't have rtti */ + if (this == item) + return true; + + /* Ensure we are comparing two condition items */ + if (item->type() != COND_ITEM) + return false; + + const Item_cond *item_cond= (const Item_cond *) item; + + /* Must be same type (AND vs OR) */ + if (functype() != item_cond->functype()) + return false; + + /* Must have same number of arguments */ + if (list.elements != item_cond->list.elements) + return false; + + /* For AND/OR conditions, order doesn't matter - do set-based comparison */ + /* Check if every argument in this list has an equivalent in the other list */ + List_iterator_fast li1(const_cast&>(list)); + Item *arg1; + while ((arg1= li1++)) + { + bool found_match= false; + List_iterator_fast li2(const_cast&>(item_cond->list)); + Item *arg2; + while ((arg2= li2++)) + { + if (arg1->eq(arg2, config)) + { + found_match= true; + break; + } + } + if (!found_match) + return false; + } + + /* Also check the reverse - every argument in other list has equivalent in this list */ + /* (This handles cases where there might be duplicates) */ + List_iterator_fast li2(const_cast&>(item_cond->list)); + Item *arg2; + while ((arg2= li2++)) + { + bool found_match= false; + List_iterator_fast li1(const_cast&>(list)); + Item *arg1; + while ((arg1= li1++)) + { + if (arg2->eq(arg1, config)) + { + found_match= true; + break; + } + } + if (!found_match) + return false; + } + + return true; +} + + void Item_cond_and::mark_as_condition_AND_part(TABLE_LIST *embedding) { List_iterator li(list); diff --git a/sql/item_cmpfunc.h b/sql/item_cmpfunc.h index 753d939f6560d..cf74961cf5f16 100644 --- a/sql/item_cmpfunc.h +++ b/sql/item_cmpfunc.h @@ -3339,6 +3339,7 @@ class Item_cond :public Item_bool_func Item *deep_copy(THD *thd) const override; bool excl_dep_on_table(table_map tab_map) override; bool excl_dep_on_grouping_fields(st_select_lex *sel) override; + bool eq(const Item *item, const Eq_config &config) const override; private: void merge_sub_condition(List_iterator& li);