From 52289d4555737a45535c7186be3b40f0316612df Mon Sep 17 00:00:00 2001 From: ParadoxV5 Date: Tue, 1 Sep 2026 22:37:44 -0600 Subject: [PATCH 1/5] MDEV-40996 Support `--sync_with_master 0, $variable` in mysqltest `--sync_with_master` uses `get_string()`, which has `$variable` support, but it only uses the read buffer, which is written with the unexpanded string and not the variable value. Reviewed-by: KhaledR57 --- client/mysqltest.cc | 12 ++++++------ mysql-test/suite/multi_source/simple.result | 3 +++ mysql-test/suite/multi_source/simple.test | 6 ++++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/client/mysqltest.cc b/client/mysqltest.cc index d43b1c79f1f..ddc0075c381 100644 --- a/client/mysqltest.cc +++ b/client/mysqltest.cc @@ -5340,7 +5340,7 @@ void do_sync_with_master(struct st_command *command) long offset= 0; char *p= command->first_argument; const char *offset_start= p; - char *start, *buff= 0; + char *start, *buff= 0, *str; start= const_cast(""); if (*offset_start) @@ -5358,15 +5358,15 @@ void do_sync_with_master(struct st_command *command) p++; while (*p && my_isspace(charset_info, *p)) p++; - start= buff= (char*)my_malloc(PSI_NOT_INSTRUMENTED, strlen(p)+1, - MYF(MY_WME|MY_FAE)); - get_string(&buff, &p, command); + str= buff= (char*)my_malloc(PSI_NOT_INSTRUMENTED, strlen(p)+1, + MYF(MY_WME|MY_FAE)); + start= get_string(&str, &p, command); } command->last_argument= p; } do_sync_with_master2(command, offset, start); if (buff) - my_free(start); + my_free(buff); return; } @@ -8417,7 +8417,7 @@ void do_get_errcodes(struct st_command *command) /* - Get a string; Return ptr to end of string + Get a string; Return ptr to start of string Strings may be surrounded by " or ' If string is a '$variable', return the value of the variable. diff --git a/mysql-test/suite/multi_source/simple.result b/mysql-test/suite/multi_source/simple.result index 61932184b66..d9de63950f9 100644 --- a/mysql-test/suite/multi_source/simple.result +++ b/mysql-test/suite/multi_source/simple.result @@ -453,6 +453,9 @@ Executed_log_entries 7 Slave_received_heartbeats 0 Slave_heartbeat_period 60.000 Gtid_Slave_Pos +# +# MDEV-40996: the connection name may be given as a variable +# stop all slaves; Warnings: Note 1938 SLAVE 'slave2' stopped diff --git a/mysql-test/suite/multi_source/simple.test b/mysql-test/suite/multi_source/simple.test index 070d4745066..ab9e634600e 100644 --- a/mysql-test/suite/multi_source/simple.test +++ b/mysql-test/suite/multi_source/simple.test @@ -77,6 +77,12 @@ reset slave 'slave1' all; --replace_result $SERVER_MYPORT_1 MYPORT_1 $SERVER_MYPORT_2 MYPORT_2 $read_master_log_pos $relay_log_pos $relay_log_space1 $relay_log_space2 query_vertical show all slaves status; +--echo # +--echo # MDEV-40996: the connection name may be given as a variable +--echo # +--let $conn_name= slave2 +--sync_with_master 0,$conn_name + stop all slaves; --replace_result $SERVER_MYPORT_1 MYPORT_1 $SERVER_MYPORT_2 MYPORT_2 $read_master_log_pos $relay_log_pos $relay_log_space1 $relay_log_space2 query_vertical show all slaves status; From 6cdb291de4bf636b010e8a1b1ee75f99cc99a779 Mon Sep 17 00:00:00 2001 From: Brandon Nesterenko Date: Fri, 24 Jul 2026 14:57:50 -0600 Subject: [PATCH 2/5] Disable rpl_parallel_multi_domain_xa MDEV-34104 describes why this test fails. It was filed 2 years ago, but the fix is complex, and we keep this failing test around hurting all other devs. The fix is planned, once finished, we can re-enable this test. Signed-off-by: Brandon Nesterenko --- mysql-test/suite/rpl/disabled.def | 1 + 1 file changed, 1 insertion(+) diff --git a/mysql-test/suite/rpl/disabled.def b/mysql-test/suite/rpl/disabled.def index 57907210693..774750e7813 100644 --- a/mysql-test/suite/rpl/disabled.def +++ b/mysql-test/suite/rpl/disabled.def @@ -15,3 +15,4 @@ rpl_row_binlog_max_cache_size : MDEV-11092 rpl_row_index_choice : MDEV-11666 rpl_auto_increment_update_failure : disabled for now rpl_current_user : waits for MDEV-22374 fix +rpl_parallel_multi_domain_xa : (2026-07-24) MDEV-34104: waits for MDEV-32020 From 4667dddd3bfb6406b41b9676461601a284606cca Mon Sep 17 00:00:00 2001 From: sjaakola Date: Fri, 4 Sep 2026 13:37:33 +0300 Subject: [PATCH 3/5] MDEV-41012 Galera appliers hang with foreign key of types UUID, INET4, INET6 For direct row writes, the certification key for MYSQL_TYPE_STRING and MYSQL_TYPE_VARSTRING is built by collating the column value and taking the collation from Field::charset(). The data types implemented on Field_fbt - UUID, INET6 and INET4 report MYSQL_TYPE_STRING, and their charset() is my_charset_numeric, which is latin1. Their values are however plain binary and accordingly innodb maps them to DATA_FIXBINARY. Their keys were therefore run through latin1_swedish_ci, which folds them. That corrupts the key in two ways: 1. A key mismatch for the same row. The FK constraint's referenced key that is appended for the parent of a child INSERT is built from the InnoDB record and is not collated, and it does not match the primary key appended due to the parent row's direct write. Certification saw no dependency between a child INSERT and a concurrent parent UPDATE, and two appliers could apply them in parallel causing a hang or crash. 2. A key collision between distinct rows. The folding is many to one, so different values collapse onto one key, Certification compares keys byte for byte, so unrelated rows were treated as the same row. Concurrent transactions on them certified as a conflict and one was aborted with ER_LOCK_DEADLOCK. Fix is for wsrep_store_key_val_for_row() to skip the collation for fields that InnoDB stores as binary, using the same condition as get_innobase_type_from_mysql_type(). This is a no-op for the types that worked before. This change requires to bump the application protocol version to level 5. The commit has also two mtr tests for regression testing. --- .../galera/r/MDEV-41012-collision.result | 115 ++++++++++++++++++ mysql-test/suite/galera/r/MDEV-41012.result | 104 ++++++++++++++++ .../suite/galera/t/MDEV-41012-collision.inc | 67 ++++++++++ .../suite/galera/t/MDEV-41012-collision.test | 64 ++++++++++ mysql-test/suite/galera/t/MDEV-41012.inc | 67 ++++++++++ mysql-test/suite/galera/t/MDEV-41012.test | 40 ++++++ sql/wsrep_server_service.cc | 9 +- storage/innobase/handler/ha_innodb.cc | 23 +++- storage/innobase/rem/rem0rec.cc | 25 +++- 9 files changed, 511 insertions(+), 3 deletions(-) create mode 100644 mysql-test/suite/galera/r/MDEV-41012-collision.result create mode 100644 mysql-test/suite/galera/r/MDEV-41012.result create mode 100644 mysql-test/suite/galera/t/MDEV-41012-collision.inc create mode 100644 mysql-test/suite/galera/t/MDEV-41012-collision.test create mode 100644 mysql-test/suite/galera/t/MDEV-41012.inc create mode 100644 mysql-test/suite/galera/t/MDEV-41012.test diff --git a/mysql-test/suite/galera/r/MDEV-41012-collision.result b/mysql-test/suite/galera/r/MDEV-41012-collision.result new file mode 100644 index 00000000000..dc3af178d72 --- /dev/null +++ b/mysql-test/suite/galera/r/MDEV-41012-collision.result @@ -0,0 +1,115 @@ +connection node_2; +connection node_1; +# +# Each pair below is two distinct values whose bytes fold to the same +# latin1_swedish_ci weight string (0xC0->0x41, 0x6A->0x4A, 0x6B->0x4B, +# 0xD2->0x4F, 0x7A->0x5A). A UUID is stored as a byte permutation of the +# value shown, which does not affect a byte-wise folding. +# +SELECT WEIGHT_STRING(_latin1 0xC0A8016B9E4D4F7AB3D26A1E8F9C0D1B) = +WEIGHT_STRING(_latin1 0x41A8014B9E4D4F5AB34F4A1E8F9C0D1B) AS uuid_pair_collides, +WEIGHT_STRING(_latin1 0x20010DB80000000000000000C0A8016B) = +WEIGHT_STRING(_latin1 0x20010DB8000000000000000041A8014B) AS inet6_pair_collides, +WEIGHT_STRING(_latin1 0xC0A8016B) = +WEIGHT_STRING(_latin1 0x41A8014B) AS inet4_pair_collides; +uuid_pair_collides inet6_pair_collides inet4_pair_collides +1 1 1 +# +# UUID +# +connection node_1; +CREATE TABLE t (id UUID PRIMARY KEY, v INT) ENGINE=InnoDB; +INSERT INTO t VALUES ('c0a8016b-9e4d-4f7a-b3d2-6a1e8f9c0d1b', 0), ('41a8014b-9e4d-4f5a-b34f-4a1e8f9c0d1b', 0); +connection node_2; +SET SESSION wsrep_sync_wait = 0; +SET GLOBAL wsrep_slave_threads = 2; +SET GLOBAL wsrep_provider_options = 'dbug=d,apply_monitor_slave_enter_sync'; +connection node_1; +UPDATE t SET v = 1 WHERE id = 'c0a8016b-9e4d-4f7a-b3d2-6a1e8f9c0d1b'; +connection node_2; +SET SESSION wsrep_on = 0; +SET SESSION wsrep_on = 1; +SET GLOBAL wsrep_provider_options = 'dbug='; +connection node_1; +UPDATE t SET v = 2 WHERE id = '41a8014b-9e4d-4f5a-b34f-4a1e8f9c0d1b'; +connection node_2; +no_false_dependency +1 +SELECT COUNT(*) = 1 AS second_row_applied_in_parallel FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_rows_modified > 0; +second_row_applied_in_parallel +1 +SET GLOBAL wsrep_provider_options = 'signal=apply_monitor_slave_enter_sync'; +SELECT id, v FROM t ORDER BY v; +id v +c0a8016b-9e4d-4f7a-b3d2-6a1e8f9c0d1b 1 +41a8014b-9e4d-4f5a-b34f-4a1e8f9c0d1b 2 +SET GLOBAL wsrep_slave_threads = DEFAULT; +SET SESSION wsrep_sync_wait = DEFAULT; +connection node_1; +DROP TABLE t; +# +# INET6 +# +connection node_1; +CREATE TABLE t (id INET6 PRIMARY KEY, v INT) ENGINE=InnoDB; +INSERT INTO t VALUES ('2001:db8::c0a8:16b', 0), ('2001:db8::41a8:14b', 0); +connection node_2; +SET SESSION wsrep_sync_wait = 0; +SET GLOBAL wsrep_slave_threads = 2; +SET GLOBAL wsrep_provider_options = 'dbug=d,apply_monitor_slave_enter_sync'; +connection node_1; +UPDATE t SET v = 1 WHERE id = '2001:db8::c0a8:16b'; +connection node_2; +SET SESSION wsrep_on = 0; +SET SESSION wsrep_on = 1; +SET GLOBAL wsrep_provider_options = 'dbug='; +connection node_1; +UPDATE t SET v = 2 WHERE id = '2001:db8::41a8:14b'; +connection node_2; +no_false_dependency +1 +SELECT COUNT(*) = 1 AS second_row_applied_in_parallel FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_rows_modified > 0; +second_row_applied_in_parallel +1 +SET GLOBAL wsrep_provider_options = 'signal=apply_monitor_slave_enter_sync'; +SELECT id, v FROM t ORDER BY v; +id v +2001:db8::c0a8:16b 1 +2001:db8::41a8:14b 2 +SET GLOBAL wsrep_slave_threads = DEFAULT; +SET SESSION wsrep_sync_wait = DEFAULT; +connection node_1; +DROP TABLE t; +# +# INET4 +# +connection node_1; +CREATE TABLE t (id INET4 PRIMARY KEY, v INT) ENGINE=InnoDB; +INSERT INTO t VALUES ('192.168.1.107', 0), ('65.168.1.75', 0); +connection node_2; +SET SESSION wsrep_sync_wait = 0; +SET GLOBAL wsrep_slave_threads = 2; +SET GLOBAL wsrep_provider_options = 'dbug=d,apply_monitor_slave_enter_sync'; +connection node_1; +UPDATE t SET v = 1 WHERE id = '192.168.1.107'; +connection node_2; +SET SESSION wsrep_on = 0; +SET SESSION wsrep_on = 1; +SET GLOBAL wsrep_provider_options = 'dbug='; +connection node_1; +UPDATE t SET v = 2 WHERE id = '65.168.1.75'; +connection node_2; +no_false_dependency +1 +SELECT COUNT(*) = 1 AS second_row_applied_in_parallel FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_rows_modified > 0; +second_row_applied_in_parallel +1 +SET GLOBAL wsrep_provider_options = 'signal=apply_monitor_slave_enter_sync'; +SELECT id, v FROM t ORDER BY v; +id v +192.168.1.107 1 +65.168.1.75 2 +SET GLOBAL wsrep_slave_threads = DEFAULT; +SET SESSION wsrep_sync_wait = DEFAULT; +connection node_1; +DROP TABLE t; diff --git a/mysql-test/suite/galera/r/MDEV-41012.result b/mysql-test/suite/galera/r/MDEV-41012.result new file mode 100644 index 00000000000..8f1b258f64a --- /dev/null +++ b/mysql-test/suite/galera/r/MDEV-41012.result @@ -0,0 +1,104 @@ +connection node_2; +connection node_1; +# +# UUID +# +connection node_1; +CREATE TABLE p (id UUID PRIMARY KEY, v INT) ENGINE=InnoDB; +CREATE TABLE c (id INT AUTO_INCREMENT PRIMARY KEY, p_id UUID NOT NULL, KEY k (p_id), FOREIGN KEY (p_id) REFERENCES p (id)) ENGINE=InnoDB; +INSERT INTO p VALUES ('c0a8016b-9e4d-4f7a-b3d2-6a1e8f9c0d1b', 0); +connection node_2; +SET SESSION wsrep_sync_wait = 0; +SET GLOBAL wsrep_slave_threads = 2; +SET GLOBAL wsrep_provider_options = 'dbug=d,apply_monitor_slave_enter_sync'; +connection node_1; +INSERT INTO c (p_id) VALUES ('c0a8016b-9e4d-4f7a-b3d2-6a1e8f9c0d1b'); +connection node_2; +SET SESSION wsrep_on = 0; +SET SESSION wsrep_on = 1; +SET GLOBAL wsrep_provider_options = 'dbug='; +connection node_1; +UPDATE p SET v = v + 1 WHERE id = 'c0a8016b-9e4d-4f7a-b3d2-6a1e8f9c0d1b'; +connection node_2; +SELECT COUNT(*) = 0 AS no_row_modified_in_parallel FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_rows_modified > 0; +no_row_modified_in_parallel +1 +SET GLOBAL wsrep_provider_options = 'signal=apply_monitor_slave_enter_sync'; +SELECT v FROM p; +v +1 +SELECT COUNT(*) FROM c; +COUNT(*) +1 +SET GLOBAL wsrep_slave_threads = DEFAULT; +SET SESSION wsrep_sync_wait = DEFAULT; +connection node_1; +DROP TABLE c, p; +# +# INET6 +# +connection node_1; +CREATE TABLE p (id INET6 PRIMARY KEY, v INT) ENGINE=InnoDB; +CREATE TABLE c (id INT AUTO_INCREMENT PRIMARY KEY, p_id INET6 NOT NULL, KEY k (p_id), FOREIGN KEY (p_id) REFERENCES p (id)) ENGINE=InnoDB; +INSERT INTO p VALUES ('2001:db8::c0a8:16b', 0); +connection node_2; +SET SESSION wsrep_sync_wait = 0; +SET GLOBAL wsrep_slave_threads = 2; +SET GLOBAL wsrep_provider_options = 'dbug=d,apply_monitor_slave_enter_sync'; +connection node_1; +INSERT INTO c (p_id) VALUES ('2001:db8::c0a8:16b'); +connection node_2; +SET SESSION wsrep_on = 0; +SET SESSION wsrep_on = 1; +SET GLOBAL wsrep_provider_options = 'dbug='; +connection node_1; +UPDATE p SET v = v + 1 WHERE id = '2001:db8::c0a8:16b'; +connection node_2; +SELECT COUNT(*) = 0 AS no_row_modified_in_parallel FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_rows_modified > 0; +no_row_modified_in_parallel +1 +SET GLOBAL wsrep_provider_options = 'signal=apply_monitor_slave_enter_sync'; +SELECT v FROM p; +v +1 +SELECT COUNT(*) FROM c; +COUNT(*) +1 +SET GLOBAL wsrep_slave_threads = DEFAULT; +SET SESSION wsrep_sync_wait = DEFAULT; +connection node_1; +DROP TABLE c, p; +# +# INET4 +# +connection node_1; +CREATE TABLE p (id INET4 PRIMARY KEY, v INT) ENGINE=InnoDB; +CREATE TABLE c (id INT AUTO_INCREMENT PRIMARY KEY, p_id INET4 NOT NULL, KEY k (p_id), FOREIGN KEY (p_id) REFERENCES p (id)) ENGINE=InnoDB; +INSERT INTO p VALUES ('192.168.1.107', 0); +connection node_2; +SET SESSION wsrep_sync_wait = 0; +SET GLOBAL wsrep_slave_threads = 2; +SET GLOBAL wsrep_provider_options = 'dbug=d,apply_monitor_slave_enter_sync'; +connection node_1; +INSERT INTO c (p_id) VALUES ('192.168.1.107'); +connection node_2; +SET SESSION wsrep_on = 0; +SET SESSION wsrep_on = 1; +SET GLOBAL wsrep_provider_options = 'dbug='; +connection node_1; +UPDATE p SET v = v + 1 WHERE id = '192.168.1.107'; +connection node_2; +SELECT COUNT(*) = 0 AS no_row_modified_in_parallel FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_rows_modified > 0; +no_row_modified_in_parallel +1 +SET GLOBAL wsrep_provider_options = 'signal=apply_monitor_slave_enter_sync'; +SELECT v FROM p; +v +1 +SELECT COUNT(*) FROM c; +COUNT(*) +1 +SET GLOBAL wsrep_slave_threads = DEFAULT; +SET SESSION wsrep_sync_wait = DEFAULT; +connection node_1; +DROP TABLE c, p; diff --git a/mysql-test/suite/galera/t/MDEV-41012-collision.inc b/mysql-test/suite/galera/t/MDEV-41012-collision.inc new file mode 100644 index 00000000000..4922009de67 --- /dev/null +++ b/mysql-test/suite/galera/t/MDEV-41012-collision.inc @@ -0,0 +1,67 @@ +# +# Body of galera.MDEV-41012-collision, parameterized by the data type. +# +# $fbt_type - data type of the primary key column +# $fbt_value1 - a literal of that type +# $fbt_value2 - a *different* literal whose binary representation folds to +# the same latin1_swedish_ci weight string as $fbt_value1 +# + +--connection node_1 +--eval CREATE TABLE t (id $fbt_type PRIMARY KEY, v INT) ENGINE=InnoDB +--eval INSERT INTO t VALUES ($fbt_value1, 0), ($fbt_value2, 0) + +--connection node_2 +SET SESSION wsrep_sync_wait = 0; +--let $wait_condition = SELECT COUNT(*) = 2 FROM t +--source include/wait_condition.inc + +SET GLOBAL wsrep_slave_threads = 2; + +# Park the applier of the first UPDATE on entry to the apply monitor. +--let $galera_sync_point = apply_monitor_slave_enter_sync +--source include/galera_set_sync_point.inc + +--connection node_1 +--eval UPDATE t SET v = 1 WHERE id = $fbt_value1 + +--connection node_2 +--let $galera_sync_point = apply_monitor_slave_enter_sync +--source include/galera_wait_sync_point.inc + +# Disarm, so the second applier runs into the real dependency check. +--source include/galera_clear_sync_point.inc + +--let $apply_waits = `SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS WHERE VARIABLE_NAME = 'wsrep_apply_waits'` + +--connection node_1 +--eval UPDATE t SET v = 2 WHERE id = $fbt_value2 + +--connection node_2 +# The two statements modify two *different* rows, so their +# certification keys must differ, there must be no dependency between the two +# writesets, and the second applier must be free to apply its row while the +# first one is still parked. +--let $wait_condition = SELECT (SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS WHERE VARIABLE_NAME = 'wsrep_apply_waits') > $apply_waits OR EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_rows_modified > 0) +--source include/wait_condition.inc + +# While the key was collated with latin1_swedish_ci the two keys were equal, +# the second writeset was made to depend on the first, and both of these +# reported 0. +--disable_query_log +--eval SELECT VARIABLE_VALUE = $apply_waits AS no_false_dependency FROM INFORMATION_SCHEMA.GLOBAL_STATUS WHERE VARIABLE_NAME = 'wsrep_apply_waits' +--enable_query_log +SELECT COUNT(*) = 1 AS second_row_applied_in_parallel FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_rows_modified > 0; + +--let $galera_sync_point = apply_monitor_slave_enter_sync +--source include/galera_signal_sync_point.inc + +--let $wait_condition = SELECT SUM(v) = 3 FROM t +--source include/wait_condition.inc +SELECT id, v FROM t ORDER BY v; + +SET GLOBAL wsrep_slave_threads = DEFAULT; +SET SESSION wsrep_sync_wait = DEFAULT; + +--connection node_1 +DROP TABLE t; diff --git a/mysql-test/suite/galera/t/MDEV-41012-collision.test b/mysql-test/suite/galera/t/MDEV-41012-collision.test new file mode 100644 index 00000000000..f9bfba89e45 --- /dev/null +++ b/mysql-test/suite/galera/t/MDEV-41012-collision.test @@ -0,0 +1,64 @@ +# +# MDEV-41012: certification key collision between distinct values of a fixed +# binary data type (UUID, INET6, INET4). +# +# Testing key collisions with data types on Field_fbt: UUID, INET6 and INET4, +# where different keys could potentially generate same certification keys. +# i.e. a scenario where two transactions modifying two *different* +# rows certified as a conflict and the later one was aborted with a spurious +# ER_LOCK_DEADLOCK. +# +# A shared key also gives the two writesets a false dependency on each other, +# which serializes applying that should have run in parallel. The test detects +# the collision that way, because it can be driven deterministically. Whether +# the spurious ER_LOCK_DEADLOCK is observed instead depends on whether the +# first writeset has already been applied by the time the second transaction +# replicates, which is a race. +# +# This needs no foreign key: it is the row key path, +# wsrep_store_key_val_for_row(), alone. +# +# This is the second half of MDEV-41012. The other half, a key *mismatch* for +# one and the same row, is covered by galera.MDEV-41012. +# + +--source include/galera_cluster.inc +--source include/have_innodb.inc +--source include/galera_have_debug_sync.inc + +--echo # +--echo # Each pair below is two distinct values whose bytes fold to the same +--echo # latin1_swedish_ci weight string (0xC0->0x41, 0x6A->0x4A, 0x6B->0x4B, +--echo # 0xD2->0x4F, 0x7A->0x5A). A UUID is stored as a byte permutation of the +--echo # value shown, which does not affect a byte-wise folding. +--echo # +SELECT WEIGHT_STRING(_latin1 0xC0A8016B9E4D4F7AB3D26A1E8F9C0D1B) = + WEIGHT_STRING(_latin1 0x41A8014B9E4D4F5AB34F4A1E8F9C0D1B) AS uuid_pair_collides, + WEIGHT_STRING(_latin1 0x20010DB80000000000000000C0A8016B) = + WEIGHT_STRING(_latin1 0x20010DB8000000000000000041A8014B) AS inet6_pair_collides, + WEIGHT_STRING(_latin1 0xC0A8016B) = + WEIGHT_STRING(_latin1 0x41A8014B) AS inet4_pair_collides; + +--echo # +--echo # UUID +--echo # +--let $fbt_type = UUID +--let $fbt_value1 = 'c0a8016b-9e4d-4f7a-b3d2-6a1e8f9c0d1b' +--let $fbt_value2 = '41a8014b-9e4d-4f5a-b34f-4a1e8f9c0d1b' +--source MDEV-41012-collision.inc + +--echo # +--echo # INET6 +--echo # +--let $fbt_type = INET6 +--let $fbt_value1 = '2001:db8::c0a8:16b' +--let $fbt_value2 = '2001:db8::41a8:14b' +--source MDEV-41012-collision.inc + +--echo # +--echo # INET4 +--echo # +--let $fbt_type = INET4 +--let $fbt_value1 = '192.168.1.107' +--let $fbt_value2 = '65.168.1.75' +--source MDEV-41012-collision.inc diff --git a/mysql-test/suite/galera/t/MDEV-41012.inc b/mysql-test/suite/galera/t/MDEV-41012.inc new file mode 100644 index 00000000000..edb1430d74c --- /dev/null +++ b/mysql-test/suite/galera/t/MDEV-41012.inc @@ -0,0 +1,67 @@ +# +# Body of galera.MDEV-41012, parameterized by the data type of the foreign key. +# +# $fbt_type - data type of the parent PK / child FK column +# $fbt_value - a literal of that type whose binary representation contains +# bytes that latin1_swedish_ci folds (0xC0, 0x6A, 0x6B, 0xD2 ...) +# + +--connection node_1 +--eval CREATE TABLE p (id $fbt_type PRIMARY KEY, v INT) ENGINE=InnoDB +--eval CREATE TABLE c (id INT AUTO_INCREMENT PRIMARY KEY, p_id $fbt_type NOT NULL, KEY k (p_id), FOREIGN KEY (p_id) REFERENCES p (id)) ENGINE=InnoDB +--eval INSERT INTO p VALUES ($fbt_value, 0) + +--connection node_2 +SET SESSION wsrep_sync_wait = 0; +--let $wait_condition = SELECT COUNT(*) = 1 FROM p +--source include/wait_condition.inc + +# Two appliers, so that the two writesets below *can* be applied in parallel +# if certification does not make them depend on each other. +SET GLOBAL wsrep_slave_threads = 2; + +# Park the first applier on entry to the apply monitor, i.e. before the child +# INSERT is applied and before its FK check locks the parent row. +--let $galera_sync_point = apply_monitor_slave_enter_sync +--source include/galera_set_sync_point.inc + +--connection node_1 +--eval INSERT INTO c (p_id) VALUES ($fbt_value) + +--connection node_2 +--let $galera_sync_point = apply_monitor_slave_enter_sync +--source include/galera_wait_sync_point.inc + +# clear the sync point. The second applier must run into the real dependency +--source include/galera_clear_sync_point.inc + +--let $apply_waits = `SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS WHERE VARIABLE_NAME = 'wsrep_apply_waits'` + +--connection node_1 +--eval UPDATE p SET v = v + 1 WHERE id = $fbt_value + +--connection node_2 +# Wait until the second applier has blocked in the apply monitor because +--let $wait_condition = SELECT (SELECT VARIABLE_VALUE FROM INFORMATION_SCHEMA.GLOBAL_STATUS WHERE VARIABLE_NAME = 'wsrep_apply_waits') > $apply_waits +--source include/wait_condition.inc + +# the second applier waited instead of applying it in parallel. +SELECT COUNT(*) = 0 AS no_row_modified_in_parallel FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_rows_modified > 0; + +# Release the child INSERT +--let $galera_sync_point = apply_monitor_slave_enter_sync +--source include/galera_signal_sync_point.inc + +--let $wait_condition = SELECT COUNT(*) = 1 FROM c +--source include/wait_condition.inc +--let $wait_condition = SELECT v = 1 FROM p +--source include/wait_condition.inc + +SELECT v FROM p; +SELECT COUNT(*) FROM c; + +SET GLOBAL wsrep_slave_threads = DEFAULT; +SET SESSION wsrep_sync_wait = DEFAULT; + +--connection node_1 +DROP TABLE c, p; diff --git a/mysql-test/suite/galera/t/MDEV-41012.test b/mysql-test/suite/galera/t/MDEV-41012.test new file mode 100644 index 00000000000..06a8c604dd6 --- /dev/null +++ b/mysql-test/suite/galera/t/MDEV-41012.test @@ -0,0 +1,40 @@ +# +# MDEV-41012: BF-BF conflict when applying a child INSERT and a parent UPDATE +# in parallel, for a foreign key on a fixed binary data type. +# +# The test sends two write sets having access with the same primary key of fixed +# binary data type, the transactions are: +# * INSERT into the child table havinga reference key for the parent row +# * UPDATE of the parent row having the parent's primary key, +# +# The test orders appliers so that the child INSERT is parked before it +# is applied, the parent UPDATE is replicated after it, and we verify that the +# UPDATE has to wait for the INSERT instead of being applied in parallel. +# +# All three Field_fbt types (UUID, INET4 and INET6) are covered. +# + +--source include/galera_cluster.inc +--source include/have_innodb.inc +--source include/galera_have_debug_sync.inc + +--echo # +--echo # UUID +--echo # +--let $fbt_type = UUID +--let $fbt_value = 'c0a8016b-9e4d-4f7a-b3d2-6a1e8f9c0d1b' +--source MDEV-41012.inc + +--echo # +--echo # INET6 +--echo # +--let $fbt_type = INET6 +--let $fbt_value = '2001:db8::c0a8:16b' +--source MDEV-41012.inc + +--echo # +--echo # INET4 +--echo # +--let $fbt_type = INET4 +--let $fbt_value = '192.168.1.107' +--source MDEV-41012.inc diff --git a/sql/wsrep_server_service.cc b/sql/wsrep_server_service.cc index 0ede84e4cf9..ca8c01e71d2 100644 --- a/sql/wsrep_server_service.cc +++ b/sql/wsrep_server_service.cc @@ -228,11 +228,18 @@ void Wsrep_server_service::log_view( static_cast(high_priority_service); /* Update global system variables */ mysql_mutex_lock(&LOCK_global_system_variables); + /* The negotiated protocol version gates the writeset format, so it must be + picked up regardless of wsrep_auto_increment_control. Leaving it at this + node's own maximum would make a node in a cluster that settled on an older + version keep emitting the newer format. */ + if (view.own_index() >= 0) + { + wsrep_protocol_version= view.protocol_version(); + } if (wsrep_auto_increment_control && view.own_index() >= 0) { global_system_variables.auto_increment_offset= view.own_index() + 1; global_system_variables.auto_increment_increment= view.members().size(); - wsrep_protocol_version= view.protocol_version(); } mysql_mutex_unlock(&LOCK_global_system_variables); diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc index 92c4b51a2ce..f26c30896b5 100644 --- a/storage/innobase/handler/ha_innodb.cc +++ b/storage/innobase/handler/ha_innodb.cc @@ -6993,10 +6993,31 @@ wsrep_store_key_val_for_row( /* Character set for the field is defined only to fields whose type is string and real field type is not enum or set. For these fields check - if character set is multi byte. */ + if character set is multi byte. + Fields that InnoDB stores as DATA_BINARY, + DATA_FIXBINARY or DATA_BLOB must not be collated + here: their bytes are opaque. Note that data types + implemented on top of Field_fbt (UUID, INET6, INET4) + report type() == MYSQL_TYPE_STRING and charset() == + my_charset_numeric (i.e. latin1), while their values + are plain binary and InnoDB maps them to + DATA_FIXBINARY. The binary test below mirrors + get_innobase_type_from_mysql_type(). + + Skipping the collation changes the key bytes on the + wire, so it is gated on protocol version 5. Below + that version the value is still collated, and + wsrep_rec_get_foreign_key() collates the matching + reference key the same way, so the two agree in a + cluster that has not fully upgraded yet. + See MDEV-41012. */ if (real_type != MYSQL_TYPE_ENUM && real_type != MYSQL_TYPE_SET + && (wsrep_protocol_version < 5 + || (!field->binary() + && field->key_type() + != HA_KEYTYPE_BINARY)) && ( mysql_type == MYSQL_TYPE_VAR_STRING || mysql_type == MYSQL_TYPE_STRING)) { diff --git a/storage/innobase/rem/rem0rec.cc b/storage/innobase/rem/rem0rec.cc index e876e5a6720..8bf6fbb8d62 100644 --- a/storage/innobase/rem/rem0rec.cc +++ b/storage/innobase/rem/rem0rec.cc @@ -2670,6 +2670,7 @@ rec_offs_make_nth_extern( } #ifdef WITH_WSREP # include "ha_prototypes.h" +# include /* wsrep_protocol_version */ int wsrep_rec_get_foreign_key( @@ -2784,9 +2785,31 @@ wsrep_rec_get_foreign_key( data, len, buf, *buf_len - key_len, false); break; + case DATA_FIXBINARY: + if (wsrep_protocol_version < 5 + && !(col_f->prtype + & DATA_BINARY_TYPE)) { + /* A UUID, INET6 or INET4 column is + stored as fixed length binary, but + below protocol version 5 + wsrep_store_key_val_for_row() still + collates the row key of the parent + with the character set of the field. + Collate the reference key the same + way, so that the two match in a + cluster that has not fully upgraded + yet. See MDEV-41012. */ + len = wsrep_normalize_string( + (int)(col_f->prtype + & DATA_MYSQL_TYPE_MASK), + dtype_get_charset_coll( + col_f->prtype), + data, buf, len, *buf_len); + break; + } + /* fall through */ case DATA_BLOB: case DATA_BINARY: - case DATA_FIXBINARY: case DATA_GEOMETRY: memcpy(buf, data, len); break; From c1b4d3edb9be059dbbf92e108d00450ff4f6dde6 Mon Sep 17 00:00:00 2001 From: Hemant Dangi Date: Tue, 8 Sep 2026 11:28:27 +0530 Subject: [PATCH 4/5] MDEV-40501: Assertion `info->type == READ_CACHE || info->type == WRITE_CACHE' failed in reinit_io_cache upon CHANGE MASTER Issue: CHANGE MASTER ... FOR CHANNEL with a channel name within MAX_CONNECTION_NAME can still overflow the OS file name limit once escaped into the relay log file name. Relay_log_info::init() then fails to open the relay log, leaving its index file unopened, but Master_info_index::remove_master_info() unconditionally calls reset_logs() on it during CHANGE MASTER's error cleanup, which hits the assertion in reinit_io_cache(). Solution: Guard the reset_logs() call in remove_master_info() with is_open(), so a relay log that was never opened is never passed to it. Also use MY_SAFE_PATH in open_index_file() so an over-length name fails deterministically instead of silently falling back to a mangled one. --- ...rpl_change_master_long_channel_name.result | 17 ++++++++ .../rpl_change_master_long_channel_name.test | 40 +++++++++++++++++++ sql/log.cc | 10 ++--- sql/rpl_mi.cc | 3 +- 4 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 mysql-test/suite/rpl/r/rpl_change_master_long_channel_name.result create mode 100644 mysql-test/suite/rpl/t/rpl_change_master_long_channel_name.test diff --git a/mysql-test/suite/rpl/r/rpl_change_master_long_channel_name.result b/mysql-test/suite/rpl/r/rpl_change_master_long_channel_name.result new file mode 100644 index 00000000000..9859c92f0f2 --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_change_master_long_channel_name.result @@ -0,0 +1,17 @@ +include/master-slave.inc +[connection master] +connection slave; +CHANGE MASTER TO MASTER_DELAY=1 FOR CHANNEL '################################################'; +ERROR HY000: Could not initialize master info structure for '################################################'; more error messages can be found in the MariaDB error log +connection master; +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1); +connection slave; +connection slave; +include/diff_tables.inc [master:t1, slave:t1] +CHANGE MASTER 'new_channel' TO MASTER_HOST='127.0.0.1', MASTER_PORT=1, MASTER_USER='root'; +RESET SLAVE 'new_channel' ALL; +connection master; +DROP TABLE t1; +connection slave; +include/rpl_end.inc diff --git a/mysql-test/suite/rpl/t/rpl_change_master_long_channel_name.test b/mysql-test/suite/rpl/t/rpl_change_master_long_channel_name.test new file mode 100644 index 00000000000..1cb94c87f5a --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_change_master_long_channel_name.test @@ -0,0 +1,40 @@ +# +# CHANGE MASTER ... FOR CHANNEL with a name that overflows the relay +# log file name once escaped should return an error, not crash. +# +# STEPS: +# 1. Issue CHANGE MASTER ... FOR CHANNEL with a channel name that is +# within MAX_CONNECTION_NAME, but expands past the file name +# limit once escaped into the relay log file name. +# 2. Verify the statement fails with a clean error, not a crash. +# 3. Verify the server is still usable: a normal CHANGE MASTER for a +# new, short-named channel succeeds. +# +--source include/master-slave.inc + +--connection slave +--let $channel= `SELECT REPEAT('#', 48)` +--error ER_MASTER_INFO +eval CHANGE MASTER TO MASTER_DELAY=1 FOR CHANNEL '$channel'; + +--connection master +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1); +--sync_slave_with_master + +--connection slave +--let $diff_tables= master:t1, slave:t1 +--source include/diff_tables.inc + +CHANGE MASTER 'new_channel' TO MASTER_HOST='127.0.0.1', MASTER_PORT=1, MASTER_USER='root'; +RESET SLAVE 'new_channel' ALL; + +--disable_query_log +call mtr.add_suppression("Failed when trying to open logs"); +--enable_query_log + +--connection master +DROP TABLE t1; +--sync_slave_with_master + +--source include/rpl_end.inc diff --git a/sql/log.cc b/sql/log.cc index 00544f71b17..a4254256cfd 100644 --- a/sql/log.cc +++ b/sql/log.cc @@ -3871,15 +3871,15 @@ bool MYSQL_BIN_LOG::open_index_file(const char *index_file_name_arg, Create an index file that will hold all file names uses for logging. Add new entries to the end of it. */ - myf opt= MY_UNPACK_FILENAME; + myf opt= MY_UNPACK_FILENAME | MY_SAFE_PATH; if (!index_file_name_arg) { index_file_name_arg= log_name; // Use same basename for index file - opt= MY_UNPACK_FILENAME | MY_REPLACE_EXT; + opt= MY_UNPACK_FILENAME | MY_REPLACE_EXT | MY_SAFE_PATH; } - fn_format(index_file_name, index_file_name_arg, mysql_data_home, - ".index", opt); - if ((index_file_nr= mysql_file_open(m_key_file_log_index, + if (!fn_format(index_file_name, index_file_name_arg, mysql_data_home, + ".index", opt) || + (index_file_nr= mysql_file_open(m_key_file_log_index, index_file_name, O_RDWR | O_CREAT | O_BINARY | O_CLOEXEC, MYF(MY_WME))) < 0 || diff --git a/sql/rpl_mi.cc b/sql/rpl_mi.cc index 106c9047cc1..8061b643af5 100644 --- a/sql/rpl_mi.cc +++ b/sql/rpl_mi.cc @@ -1483,7 +1483,8 @@ bool Master_info_index::remove_master_info(Master_info *mi, bool clear_log_files /* This code is only executed when change_master() failes to create a new master info */ // Delete any temporary relay log files that could have been created by change_master() - mi->rli.relay_log.reset_logs(current_thd, 0, (rpl_gtid*) 0, 0, 0); + if (mi->rli.relay_log.is_open()) + mi->rli.relay_log.reset_logs(current_thd, 0, (rpl_gtid*) 0, 0, 0); /* Delete master-'connection'.info */ create_logfile_name_with_suffix(tmp_name, sizeof(tmp_name), From 3d549f65f829ac08a1f3d8c3731f46eee2937b92 Mon Sep 17 00:00:00 2001 From: Jacob Williams Date: Sun, 13 Sep 2026 23:15:56 +0300 Subject: [PATCH 5/5] MDEV-38757 Fix EXCHANGE PARTITION with generated columns containing AND/OR conditions EXCHANGE PARTITION fails with ERROR 1736 (Tables have different definitions) when tables contain generated columns with AND/OR conditions, even when the expressions are logically equivalent. This occurs because when expressions are re-parsed (e.g., via CREATE TABLE ... LIKE), the order of arguments in AND/OR conditions may change, but the comparison was order-sensitive. The Item_cond::eq() method was implemented to perform set-based comparison for commutative AND/OR operations. The set-based comparison algorithm ensures that two Item_cond expressions are considered equal if they contain the same set of equivalent arguments, regardless of order. Added comprehensive test case covering: - Generated columns with OR conditions - Generated columns with AND conditions - Multiple generated columns with different AND/OR combinations - Nested AND/OR conditions The fix allows EXCHANGE PARTITION to succeed when expressions are logically equivalent but have different argument ordering, which is correct behavior since AND/OR operations are commutative. MDEV-38757 Limit unordered vcol condition comparison to EXCHANGE PARTITION Review follow-up to the previous commit, which made Item_cond::eq() compare AND/OR argument lists as sets for every caller. That changed equality semantics globally and broke main.derived_cond_pushdown, where conditions that eq() started reporting as equal were dropped from attached_condition. Reordering AND/OR operands also changes evaluation order, which is observable when operands are functions, so the relaxed comparison must not be the default. Item::Eq_config gains an unordered_conditions flag, defaulting to false, next to the existing binary_cmp and omit_table_names flags. Item_cond::eq() compares its argument lists as sets only when that flag is set, and otherwise reports two distinct Item_cond objects as unequal, as it did before this patch series. The flag is threaded through Virtual_column_info::is_equal() and a new mysql_compare_tables() parameter, and only Sql_cmd_alter_table_exchange_partition passes it as true, so the relaxed comparison stays confined to the EXCHANGE PARTITION metadata check. The set comparison tests containment in both directions rather than comparing element counts, so an expression also matches a form that repeats one of its terms, for example col1 > 10 and col2 < 100 or col3 > 50 col3 > 50 or col1 > 10 and col2 < 100 or col3 > 50 Test 5 of parts.partition_exchange_generated_columns covers that case. --- .../suite/parts/inc/partition_exchange.inc | 146 ++++++++++++++++++ .../parts/r/partition_exchange_innodb.result | 133 ++++++++++++++++ .../parts/r/partition_exchange_memory.result | 133 ++++++++++++++++ .../parts/r/partition_exchange_myisam.result | 133 ++++++++++++++++ sql/field.h | 3 +- sql/item.h | 13 +- sql/item_cmpfunc.cc | 74 +++++++++ sql/item_cmpfunc.h | 1 + sql/sql_partition_admin.cc | 2 +- sql/sql_table.cc | 9 +- sql/sql_table.h | 3 +- 11 files changed, 641 insertions(+), 9 deletions(-) diff --git a/mysql-test/suite/parts/inc/partition_exchange.inc b/mysql-test/suite/parts/inc/partition_exchange.inc index 95a67a339d0..63ccdf61c66 100644 --- a/mysql-test/suite/parts/inc/partition_exchange.inc +++ b/mysql-test/suite/parts/inc/partition_exchange.inc @@ -352,3 +352,149 @@ SELECT * FROM t; SELECT * FROM tp; DROP TABLE t, tp, tsp; + + +--echo # +--echo # MDEV-38757 Exchange partition fails with compound expressions +--echo # + +--echo # Test EXCHANGE PARTITION with generated columns containing AND/OR conditions +--echo # This verifies set comparison of generated column conditions during +--echo # partition exchange. + +--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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1), + PARTITION p2 VALUES IN (2)); + +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1)); + +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1), + PARTITION p2 VALUES IN (2)); + +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1)); + +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 # Test 5: Reordered condition with a duplicate term +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1)); + +CREATE TABLE t1_working ( + 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 + (col3 > 50 OR col1 > 10 AND col2 < 100 OR col3 > 50) STORED, + PRIMARY KEY (id, userId) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT INTO t1_working (userId, col1, col2, col3) +VALUES (1, 20, 50, 30), (1, 5, 200, 60); + +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 10.11 tests diff --git a/mysql-test/suite/parts/r/partition_exchange_innodb.result b/mysql-test/suite/parts/r/partition_exchange_innodb.result index ac3c3831183..7da790a8b41 100644 --- a/mysql-test/suite/parts/r/partition_exchange_innodb.result +++ b/mysql-test/suite/parts/r/partition_exchange_innodb.result @@ -373,3 +373,136 @@ a b 63 Sixty three, new 99 End of values DROP TABLE t, tp, tsp; +# +# MDEV-38757 Exchange partition fails with compound expressions +# +# Test EXCHANGE PARTITION with generated columns containing AND/OR conditions +# This verifies set comparison of generated column conditions during +# partition exchange. +# 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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1), +PARTITION p2 VALUES IN (2)); +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1)); +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1), +PARTITION p2 VALUES IN (2)); +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1)); +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; +# Test 5: Reordered condition with a duplicate term +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1)); +CREATE TABLE t1_working ( +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 +(col3 > 50 OR col1 > 10 AND col2 < 100 OR col3 > 50) STORED, +PRIMARY KEY (id, userId) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +INSERT INTO t1_working (userId, col1, col2, col3) +VALUES (1, 20, 50, 30), (1, 5, 200, 60); +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 10.11 tests diff --git a/mysql-test/suite/parts/r/partition_exchange_memory.result b/mysql-test/suite/parts/r/partition_exchange_memory.result index 106d7201e09..2321310d3e8 100644 --- a/mysql-test/suite/parts/r/partition_exchange_memory.result +++ b/mysql-test/suite/parts/r/partition_exchange_memory.result @@ -373,3 +373,136 @@ a b 63 Sixty three, new 99 End of values DROP TABLE t, tp, tsp; +# +# MDEV-38757 Exchange partition fails with compound expressions +# +# Test EXCHANGE PARTITION with generated columns containing AND/OR conditions +# This verifies set comparison of generated column conditions during +# partition exchange. +# 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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1), +PARTITION p2 VALUES IN (2)); +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1)); +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1), +PARTITION p2 VALUES IN (2)); +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1)); +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; +# Test 5: Reordered condition with a duplicate term +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1)); +CREATE TABLE t1_working ( +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 +(col3 > 50 OR col1 > 10 AND col2 < 100 OR col3 > 50) STORED, +PRIMARY KEY (id, userId) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +INSERT INTO t1_working (userId, col1, col2, col3) +VALUES (1, 20, 50, 30), (1, 5, 200, 60); +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 10.11 tests diff --git a/mysql-test/suite/parts/r/partition_exchange_myisam.result b/mysql-test/suite/parts/r/partition_exchange_myisam.result index 7df0b47584b..d44d4bfaeec 100644 --- a/mysql-test/suite/parts/r/partition_exchange_myisam.result +++ b/mysql-test/suite/parts/r/partition_exchange_myisam.result @@ -373,3 +373,136 @@ a b 63 Sixty three, new 99 End of values DROP TABLE t, tp, tsp; +# +# MDEV-38757 Exchange partition fails with compound expressions +# +# Test EXCHANGE PARTITION with generated columns containing AND/OR conditions +# This verifies set comparison of generated column conditions during +# partition exchange. +# 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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1), +PARTITION p2 VALUES IN (2)); +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1)); +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1), +PARTITION p2 VALUES IN (2)); +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1)); +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; +# Test 5: Reordered condition with a duplicate term +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) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +PARTITION BY LIST (userId) +(PARTITION p1 VALUES IN (1)); +CREATE TABLE t1_working ( +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 +(col3 > 50 OR col1 > 10 AND col2 < 100 OR col3 > 50) STORED, +PRIMARY KEY (id, userId) +) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +INSERT INTO t1_working (userId, col1, col2, col3) +VALUES (1, 20, 50, 30), (1, 5, 200, 60); +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 10.11 tests diff --git a/sql/field.h b/sql/field.h index 8ff4d90f38a..ae407e47ef2 100644 --- a/sql/field.h +++ b/sql/field.h @@ -659,7 +659,8 @@ public: bool cleanup_session_expr(); bool fix_and_check_expr(THD *thd, TABLE *table); bool check_access(THD *thd); - inline bool is_equal(const Virtual_column_info* vcol, bool cmp_names) const; + inline bool is_equal(const Virtual_column_info* vcol, bool cmp_names, + bool unordered_conditions= false) const; inline void print(String*); }; diff --git a/sql/item.h b/sql/item.h index e8ea7d205ae..ed017c383ee 100644 --- a/sql/item.h +++ b/sql/item.h @@ -1269,8 +1269,11 @@ public: { bool binary_cmp; /**< Make binary comparison */ bool omit_table_names; /**< Skip table and db names comparison */ - Eq_config(bool binary_cmp, bool omit_table_names= false) - : binary_cmp(binary_cmp), omit_table_names(omit_table_names) {} + bool unordered_conditions; /**< Ignore AND/OR operand order and duplicates */ + Eq_config(bool binary_cmp, bool omit_table_names= false, + bool unordered_conditions= false) + : binary_cmp(binary_cmp), omit_table_names(omit_table_names), + unordered_conditions(unordered_conditions) {} }; virtual bool eq(const Item *, const Eq_config &config) const; enum_field_types field_type() const @@ -8385,11 +8388,13 @@ bool fix_escape_item(THD *thd, Item *escape_item, String *tmp_str, int *escape); inline bool Virtual_column_info::is_equal(const Virtual_column_info* vcol, - bool omit_table_names) const + bool omit_table_names, + bool unordered_conditions) const { return type_handler() == vcol->type_handler() && stored_in_db == vcol->is_stored() - && expr->eq(vcol->expr, {true, omit_table_names}); + && expr->eq(vcol->expr, + {true, omit_table_names, unordered_conditions}); } inline void Virtual_column_info::print(String* str) diff --git a/sql/item_cmpfunc.cc b/sql/item_cmpfunc.cc index cfae1bae1da..0deee4d885c 100644 --- a/sql/item_cmpfunc.cc +++ b/sql/item_cmpfunc.cc @@ -5595,6 +5595,80 @@ bool Item_cond::excl_dep_on_grouping_fields(st_select_lex *sel) } +/** + Compare two Item_cond expressions for equality. + + Distinct AND/OR items are normally not considered equal, preserving their + execution order. Some metadata comparisons can explicitly request a set + comparison because table definitions may be serialized with their + arguments reordered. + + @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 +{ + if (this == item) + return true; + + if (!config.unordered_conditions) + return false; + + if (item->type() != COND_ITEM || (used_tables() & RAND_TABLE_BIT)) + return false; + + const Item_cond *item_cond= (const Item_cond *) item; + if (functype() != item_cond->functype()) + return false; + + /* + Treat the argument lists as sets. Checking both directions deliberately + ignores duplicate terms: A OR B and A OR B OR A are equivalent here. + */ + 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; + } + + 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 a75b1eef510..8d3a6f7d51e 100644 --- a/sql/item_cmpfunc.h +++ b/sql/item_cmpfunc.h @@ -3341,6 +3341,7 @@ public: 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); diff --git a/sql/sql_partition_admin.cc b/sql/sql_partition_admin.cc index 5abe69e7d12..15fbcf7ed72 100644 --- a/sql/sql_partition_admin.cc +++ b/sql/sql_partition_admin.cc @@ -248,7 +248,7 @@ bool compare_table_with_partition(THD *thd, TABLE *table, TABLE *part_table, the same, so any table using data/index_file_name will fail. */ if (mysql_compare_tables(table, &part_alter_info, &part_create_info, - &metadata_equal)) + &metadata_equal, true)) { my_error(ER_TABLES_DIFFERENT_METADATA, MYF(0)); DBUG_RETURN(TRUE); diff --git a/sql/sql_table.cc b/sql/sql_table.cc index 021dca8b1e9..25bb37a9644 100644 --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -7531,13 +7531,17 @@ static void update_altered_table(const Alter_inplace_info &ha_alter_info, second table. @param[in] create_info Create options for the second table. @param[out] metadata_equal Result of comparison. + @param[in] compare_vcol_expr_as_sets + Ignore AND/OR order and duplicate terms in + generated column expressions. @retval true error @retval false success */ bool mysql_compare_tables(TABLE *table, Alter_info *alter_info, - HA_CREATE_INFO *create_info, bool *metadata_equal) + HA_CREATE_INFO *create_info, bool *metadata_equal, + bool compare_vcol_expr_as_sets) { DBUG_ENTER("mysql_compare_tables"); @@ -7606,7 +7610,8 @@ bool mysql_compare_tables(TABLE *table, Alter_info *alter_info, { if (!tmp_new_field->field->vcol_info) DBUG_RETURN(false); - if (!field->vcol_info->is_equal(tmp_new_field->field->vcol_info, true)) + if (!field->vcol_info->is_equal(tmp_new_field->field->vcol_info, + true, compare_vcol_expr_as_sets)) DBUG_RETURN(false); } diff --git a/sql/sql_table.h b/sql/sql_table.h index 6fde3066892..384a5274962 100644 --- a/sql/sql_table.h +++ b/sql/sql_table.h @@ -165,7 +165,8 @@ bool mysql_alter_table(THD *thd, const LEX_CSTRING *new_db, bool mysql_compare_tables(TABLE *table, Alter_info *alter_info, HA_CREATE_INFO *create_info, - bool *metadata_equal); + bool *metadata_equal, + bool compare_vcol_expr_as_sets); bool mysql_recreate_table(THD *thd, TABLE_LIST *table_list, class Recreate_info *recreate_info, bool table_copy); bool mysql_rename_table(handlerton *base, const LEX_CSTRING *old_db,