From 26823fe344e7f36f6aa4bb93e790fb52e3da87dc Mon Sep 17 00:00:00 2001 From: Aleksey Midenkov Date: Wed, 16 Sep 2026 12:40:35 +0300 Subject: [PATCH 01/28] MDEV-39074 trans_rollback_stmt(THD *): Assertion `! thd->in_sub_stmt' failed. slave_close_thread_tables() unconditionally called trans_commit_stmt()/trans_rollback_stmt(), which assert !thd->in_sub_stmt. A BINLOG statement with malformed base64 payload executed from an AFTER INSERT trigger hits this: on decode failure, mysql_client_binlog_statement() sets thd->is_error() and calls slave_close_thread_tables() from within the trigger's sub-statement. The fix refuses BINLOG in stored routines, triggers, events and anonymous blocks with ER_SP_BADSTATEMENT at parse time, like LOCK, HANDLER etc. Dynamic SQL (EXECUTE IMMEDIATE, EXECUTE) run from a routine bypasses the parser check, so mysql_client_binlog_statement() checks spcont as well: in a stored procedure in_sub_stmt is 0, but the decoded events assert !spcont in reset_for_next_command(). 1. BINLOG statement executed from a trigger, SF or SP is disabled by the patch: Row events (Rows_log_event) open their target table via a one-shot check that only fires at the top of a fresh statement. Inside a trigger, the table is never opened this way. It may be fixed by reusing query_tables, but: - find_locked_table() matches only by table name -- it could return the TABLE instance the enclosing statement is actively writing through, not an idle one. Reusing it would require pre-saving its state: record[0]/bitmaps/handler/etc. (and would bypass ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG) - set_stmt_row_injection()/set_time() calls would mutate thd->lex, which at that point is main_lex -- shared with the enclosing statement, not something safe to touch. Statement events (Query_log_event) run the embedded query via mysql_parse(): it bundles lex_start(), reset_for_next_command() and parse_sql() as one unit meant for a genuinely new top-level statement, not a one-off nested parse. Calling parse_sql() directly instead avoids that, but then we own everything mysql_parse() was doing for us: a private LEX and a private Query_arena (or allocations land on main_lex/whatever arena is currently active, shared with the enclosing statement), plus calling mysql_execute_command() ourselves afterwards. In any case, DML for query_tables cannot be done due to ER_CANT_UPDATE_USED_TABLE_IN_SF_OR_TRG reasons explained above. 2. PS for BINLOG statement still works and needs a leak fix for statement events: A BINLOG statement decoding to a Query_log_event, executed via PREPARE/EXECUTE, leaked its nested query's allocations onto the PS's own persistent arena: thd->stmt_arena pointed at it while mysql_parse() ran the decoded query. Second EXECUTE asserted on ROOT_FLAG_READ_ONLY, since PROTECT_STATEMENT_MEMROOT marks that arena read-only after a successful execution. The fix redirects thd->stmt_arena to thd itself for the duration of the event application, so stmt_arena->is_conventional() reads true and activate_stmt_arena_if_needed() (called e.g. from save_leaf_tables()) never redirects allocations to the PS's arena in the first place. Harmless for what it's protecting: leaf_tables_exec is normally cached on the persistent arena so a repeatedly-executed statement's SELECT_LEX doesn't rebuild it every time, but our SELECT_LEX is torn down and reparsed fresh (due to mysql_parse() semantics) on every EXECUTE, so there's nothing to cache here regardless of which arena is used. --- .../suite/binlog/r/binlog_base64_flag.result | 71 ++++++++++++++ .../suite/binlog/t/binlog_base64_flag.test | 98 +++++++++++++++++++ sql/sql_binlog.cc | 14 +++ sql/sql_yacc.yy | 4 + 4 files changed, 187 insertions(+) diff --git a/mysql-test/suite/binlog/r/binlog_base64_flag.result b/mysql-test/suite/binlog/r/binlog_base64_flag.result index ef92e63eb93..ac08a3fb94d 100644 --- a/mysql-test/suite/binlog/r/binlog_base64_flag.result +++ b/mysql-test/suite/binlog/r/binlog_base64_flag.result @@ -133,3 +133,74 @@ BINLOG '-2079193929'; ERROR HY000: Decoding of base64 string failed BINLOG 'xç↓%~∙D╒ƒ╡'; ERROR HY000: Decoding of base64 string failed +# +# MDEV-39074 trans_rollback_stmt(THD *): Assertion `! thd->in_sub_stmt' failed. +# +create table t0 (a int); +create table t1 (a int); +## Case 1: Trigger (disabled) +create trigger trig_t0_insert after insert on t0 for each row binlog 'enabled'; +ERROR 0A000: BINLOG is not allowed in stored procedures +create trigger trig_t0_insert after insert on t0 for each row +binlog @a, @b; +ERROR 0A000: BINLOG is not allowed in stored procedures +## Case 2: SP (disabled) +create procedure proc_t1_insert() binlog 'AAAAAAIBAAAAPwAAAAAAAAAAAAEAAAAAAAAABAAAAAB0ZXN0AGluc2VydCBpbnRvIHQxIHZhbHVlcyAoOTkp'; +ERROR 0A000: BINLOG is not allowed in stored procedures +## Case 3: SF (disabled) +create function func_t1_insert() returns int +begin +binlog 'TFtYRxMBAAAAKQAAAH8BAAAAABAAAAAAAAAABHRlc3QAAnQxAAEDAAE=TFtYRxcBAAAAIgAAAKEBAAAQABAAAAAAAAEAAf/+AwAAAA=='; +return 0; +end| +ERROR 0A000: BINLOG is not allowed in stored procedures +create function func_t1_insert() returns int +begin +binlog @a, @b; +return 0; +end| +ERROR 0A000: BINLOG is not allowed in stored procedures +## Case 4: Event (disabled) +create event ev_t1_insert on schedule every 1 day do +binlog 'TFtYRxMBAAAAKQAAAH8BAAAAABAAAAAAAAAABHRlc3QAAnQxAAEDAAE=TFtYRxcBAAAAIgAAAKEBAAAQABAAAAAAAAEAAf/+AwAAAA=='; +ERROR 0A000: BINLOG is not allowed in stored procedures +## Case 5: Anonymous block (disabled) +begin not atomic +binlog 'TFtYRxMBAAAAKQAAAH8BAAAAABAAAAAAAAAABHRlc3QAAnQxAAEDAAE=TFtYRxcBAAAAIgAAAKEBAAAQABAAAAAAAAEAAf/+AwAAAA=='; +end| +ERROR 0A000: BINLOG is not allowed in stored procedures +## Case 6: Dynamic SQL in SP (disabled) +create procedure proc_t1_insert() +execute immediate "binlog 'TFtYRxMBAAAAKQAAAH8BAAAAABAAAAAAAAAABHRlc3QAAnQxAAEDAAE=TFtYRxcBAAAAIgAAAKEBAAAQABAAAAAAAAEAAf/+AwAAAA=='"; +call proc_t1_insert(); +ERROR 0A000: BINLOG is not allowed in stored procedures +drop procedure proc_t1_insert; +prepare stmt from "binlog 'TFtYRxMBAAAAKQAAAH8BAAAAABAAAAAAAAAABHRlc3QAAnQxAAEDAAE=TFtYRxcBAAAAIgAAAKEBAAAQABAAAAAAAAEAAf/+AwAAAA=='"; +create procedure proc_t1_insert() execute stmt; +call proc_t1_insert(); +ERROR 0A000: BINLOG is not allowed in stored procedures +drop procedure proc_t1_insert; +deallocate prepare stmt; +## Case 7: Row event in PS (works) +binlog 'ODdYRw8BAAAAZgAAAGoAAAABAAQANS4xLjIzLXJjLWRlYnVnLWxvZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4N1hHEzgNAAgAEgAEBAQEEgAAUwAEGggAAAAICAgC'; +prepare stmt from "binlog 'TFtYRxMBAAAAKQAAAH8BAAAAABAAAAAAAAAABHRlc3QAAnQxAAEDAAE=TFtYRxcBAAAAIgAAAKEBAAAQABAAAAAAAAEAAf/+AwAAAA=='"; +execute stmt; +execute stmt; +deallocate prepare stmt; +select * from t1; +a +3 +3 +## Case 8: Statement event in PS (works) +prepare stmt from "binlog 'AAAAAAIBAAAAPwAAAAAAAAAAAAEAAAAAAAAABAAAAAB0ZXN0AGluc2VydCBpbnRvIHQxIHZhbHVlcyAoOTkp'"; +execute stmt; +execute stmt; +deallocate prepare stmt; +select * from t1; +a +3 +3 +99 +99 +drop table t0, t1; +# End of 10.11 tests diff --git a/mysql-test/suite/binlog/t/binlog_base64_flag.test b/mysql-test/suite/binlog/t/binlog_base64_flag.test index c2e3d583822..c1c51cf5fe7 100644 --- a/mysql-test/suite/binlog/t/binlog_base64_flag.test +++ b/mysql-test/suite/binlog/t/binlog_base64_flag.test @@ -188,3 +188,101 @@ BINLOG '123'; BINLOG '-2079193929'; --error ER_BASE64_DECODE_ERROR BINLOG 'xç↓%~∙D╒ƒ╡'; + +--echo # +--echo # MDEV-39074 trans_rollback_stmt(THD *): Assertion `! thd->in_sub_stmt' failed. +--echo # + +create table t0 (a int); +create table t1 (a int); + +# Format_description_log_event, same as in the FD test above +--let $FORMAT_DESCR_LOG_EVENT= 'ODdYRw8BAAAAZgAAAGoAAAABAAQANS4xLjIzLXJjLWRlYnVnLWxvZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4N1hHEzgNAAgAEgAEBAQEEgAAUwAEGggAAAAICAgC' + +# table_map+write_rows_log_event corresponding to: insert into t1 values (3) +--let $ROW_INSERT_T1= 'TFtYRxMBAAAAKQAAAH8BAAAAABAAAAAAAAAABHRlc3QAAnQxAAEDAAE=TFtYRxcBAAAAIgAAAKEBAAAQABAAAAAAAAEAAf/+AwAAAA==' + +# Query_log_event (no checksum, matches the checksum-less FD event above) +# corresponding to: insert into t1 values (99) +--let $STMT_INSERT_T1= 'AAAAAAIBAAAAPwAAAAAAAAAAAAEAAAAAAAAABAAAAAB0ZXN0AGluc2VydCBpbnRvIHQxIHZhbHVlcyAoOTkp' + +--echo ## Case 1: Trigger (disabled) + +--error ER_SP_BADSTATEMENT +create trigger trig_t0_insert after insert on t0 for each row binlog 'enabled'; +--error ER_SP_BADSTATEMENT +create trigger trig_t0_insert after insert on t0 for each row +binlog @a, @b; + +--echo ## Case 2: SP (disabled) + +--error ER_SP_BADSTATEMENT +eval create procedure proc_t1_insert() binlog $STMT_INSERT_T1; + +--echo ## Case 3: SF (disabled) + +delimiter |; +--error ER_SP_BADSTATEMENT +eval create function func_t1_insert() returns int +begin + binlog $ROW_INSERT_T1; + return 0; +end| +--error ER_SP_BADSTATEMENT +create function func_t1_insert() returns int +begin + binlog @a, @b; + return 0; +end| +delimiter ;| + +--echo ## Case 4: Event (disabled) + +--error ER_SP_BADSTATEMENT +eval create event ev_t1_insert on schedule every 1 day do +binlog $ROW_INSERT_T1; + +--echo ## Case 5: Anonymous block (disabled) + +delimiter |; +--error ER_SP_BADSTATEMENT +eval begin not atomic + binlog $ROW_INSERT_T1; +end| +delimiter ;| + +--echo ## Case 6: Dynamic SQL in SP (disabled) + +eval create procedure proc_t1_insert() +execute immediate "binlog $ROW_INSERT_T1"; +--error ER_SP_BADSTATEMENT +call proc_t1_insert(); +drop procedure proc_t1_insert; + +eval prepare stmt from "binlog $ROW_INSERT_T1"; +create procedure proc_t1_insert() execute stmt; +--error ER_SP_BADSTATEMENT +call proc_t1_insert(); +drop procedure proc_t1_insert; +deallocate prepare stmt; + +--echo ## Case 7: Row event in PS (works) + +eval binlog $FORMAT_DESCR_LOG_EVENT; +eval prepare stmt from "binlog $ROW_INSERT_T1"; +execute stmt; +execute stmt; +deallocate prepare stmt; +select * from t1; + +--echo ## Case 8: Statement event in PS (works) + +eval prepare stmt from "binlog $STMT_INSERT_T1"; +execute stmt; +execute stmt; +deallocate prepare stmt; +select * from t1; + +drop table t0, t1; + +--echo # End of 10.11 tests diff --git a/sql/sql_binlog.cc b/sql/sql_binlog.cc index 118b361eb79..85e5f006c29 100644 --- a/sql/sql_binlog.cc +++ b/sql/sql_binlog.cc @@ -244,6 +244,16 @@ void mysql_client_binlog_statement(THD* thd) thd->lex->comment.length : 2048), thd->lex->comment.str)); + /* + The parser refuses BINLOG in a routine body; this catches dynamic SQL + (EXECUTE IMMEDIATE, EXECUTE) run from a routine. + */ + if (thd->spcont) + { + my_error(ER_SP_BADSTATEMENT, MYF(0), "BINLOG"); + DBUG_VOID_RETURN; + } + if (check_global_access(thd, PRIV_STMT_BINLOG)) DBUG_VOID_RETURN; @@ -421,8 +431,12 @@ void mysql_client_binlog_statement(THD* thd) applied in PS mode. So, reset the current lex temporary to point to thd->main_lex before running ev->apply_event() and restore its original value on return. + + Likewise, point thd->stmt_arena to thd: otherwise a Query event's + statement leaks its allocations onto the PS's persistent arena. */ LEX *backup_lex; + SCOPE_VALUE(thd->stmt_arena, static_cast(thd)); thd->backup_and_reset_current_lex(&backup_lex); err= save_restore_context_apply_event(ev, rgi); diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 3e96888ea68..c6631cae5d5 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -8231,6 +8231,8 @@ table_index_name: binlog_base64_event: BINLOG_SYM TEXT_STRING_sys { + if (Lex->sphead) + my_yyabort_error((ER_SP_BADSTATEMENT, MYF(0), "BINLOG")); Lex->sql_command = SQLCOM_BINLOG_BASE64_EVENT; Lex->comment= $2; Lex->ident.str= NULL; @@ -8239,6 +8241,8 @@ binlog_base64_event: | BINLOG_SYM '@' ident_or_text ',' '@' ident_or_text { + if (Lex->sphead) + my_yyabort_error((ER_SP_BADSTATEMENT, MYF(0), "BINLOG")); Lex->sql_command = SQLCOM_BINLOG_BASE64_EVENT; Lex->comment= $3; Lex->ident= $6; From 635058a3ffa05f83861a6afe7e866fc312af4c86 Mon Sep 17 00:00:00 2001 From: Sergei Petrunia Date: Mon, 12 Oct 2026 12:57:36 +0300 Subject: [PATCH 02/28] Disable -Wredundant-move warnings in storage/rocksdb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The warnings were in RocksDB (not MyRocks) and looked like this: rocksdb/db/db_impl/db_impl.cc: In member function ‘virtual rocksdb::Status rocksdb::DBImpl::FlushWAL(bool)’: rocksdb/db/db_impl/db_impl.cc:1329:23: warning: redundant move in return statement [-Wredundant-move] 1329 | return std::move(io_s); | ~~~~~~~~~^~~~~~ --- storage/rocksdb/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/storage/rocksdb/CMakeLists.txt b/storage/rocksdb/CMakeLists.txt index ddca7d156f3..30a8e82739a 100644 --- a/storage/rocksdb/CMakeLists.txt +++ b/storage/rocksdb/CMakeLists.txt @@ -11,6 +11,7 @@ at maximising storage efficiency while maintaining InnoDB-like performance." PAR MY_CHECK_AND_SET_COMPILER_FLAG(-Wno-range-loop-construct) MY_CHECK_AND_SET_COMPILER_FLAG(-Wno-invalid-offsetof) +MY_CHECK_AND_SET_COMPILER_FLAG(-Wno-redundant-move) MY_CHECK_AND_SET_COMPILER_FLAG(-Wno-effc++ DEBUG RELWITHDEBINFO) MACRO(SKIP_ROCKSDB_PLUGIN msg) From 6b02e17b1edee0cb43f0b882efbfd728ef839296 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Mon, 20 Jul 2026 11:45:09 +1000 Subject: [PATCH 03/28] MDEV-40386: DuckDB - pass through compiler and linker flags Without passing through the compile and linker flags, options like -stdlib=libc++ can create ABI differences between ducksdb extensions and what the server links too. The jemalloc has C files so CMAKE_C_FLAGS is passed through too. --- storage/duckdb/cmake/duckdb.cmake | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/storage/duckdb/cmake/duckdb.cmake b/storage/duckdb/cmake/duckdb.cmake index 3987960b4a2..22c4ba7579d 100644 --- a/storage/duckdb/cmake/duckdb.cmake +++ b/storage/duckdb/cmake/duckdb.cmake @@ -104,7 +104,10 @@ ExternalProject_Add(duckdb_build -DBUILD_TPCE=OFF -DEXTENSION_STATIC_BUILD=1 "-DDUCKDB_EXTENSION_CONFIGS=${CMAKE_CURRENT_SOURCE_DIR}/cmake/duckdb_extensions.cmake" - "-DCMAKE_CXX_FLAGS=${_DUCKDB_EXTRA_CXX_FLAGS}" + "-DCMAKE_CXX_FLAGS=${CMAKE_CXX_FLAGS} ${_DUCKDB_EXTRA_CXX_FLAGS}" + "-DCMAKE_C_FLAGS=${CMAKE_C_FLAGS}" + "-DCMAKE_EXE_LINKER_FLAGS=${CMAKE_EXE_LINKER_FLAGS}" + "-DCMAKE_MODULE_LINKER_FLAGS=${CMAKE_MODULE_LINKER_FLAGS}" -DENABLE_SANITIZER=${ADDRESS_SANITIZER} -DENABLE_UBSAN=${UB_SANITIZER} -DOVERRIDE_GIT_DESCRIBE=${_DUCKDB_GIT_DESCRIBE} From 7c77e821892ac81f2b1fe7cd5e53ba9f1628eef4 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Fri, 16 Oct 2026 18:11:53 +1100 Subject: [PATCH 04/28] MDEV-40698 Fix ROLLUP query results with empty result set (testfix) Like b3830bce9a7e12851feb7e6d6b4e0d33a66ba8af, this corrects the ROLLUP result of the duckdb.duckdb_sql_syntax test to have a NULL row when there is an empty result set. --- storage/duckdb/mysql-test/duckdb/r/duckdb_sql_syntax.result | 1 + 1 file changed, 1 insertion(+) diff --git a/storage/duckdb/mysql-test/duckdb/r/duckdb_sql_syntax.result b/storage/duckdb/mysql-test/duckdb/r/duckdb_sql_syntax.result index a46a1766f74..3bda1811423 100644 --- a/storage/duckdb/mysql-test/duckdb/r/duckdb_sql_syntax.result +++ b/storage/duckdb/mysql-test/duckdb/r/duckdb_sql_syntax.result @@ -1,6 +1,7 @@ CREATE TABLE t1 (id INT PRIMARY KEY, col1 INT, col2 INT); SELECT id, col1, col2 FROM t1 GROUP BY id, col1, col2 WITH ROLLUP; id col1 col2 +NULL NULL NULL ALTER TABLE t1 ENGINE=DuckDB; SELECT id, col1, col2 FROM t1 GROUP BY id, col1, col2 WITH ROLLUP; id col1 col2 From f5c731ffce61fcd9a348721d9afb30fe79060894 Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Fri, 16 Oct 2026 18:16:30 +1100 Subject: [PATCH 05/28] MDEV-40386 enable duckdb tests for MSAN --- storage/duckdb/mysql-test/duckdb/t/alter_default_debug.test | 1 - storage/duckdb/mysql-test/duckdb/t/alter_duckdb_column.test | 1 - storage/duckdb/mysql-test/duckdb/t/alter_duckdb_column_copy.test | 1 - storage/duckdb/mysql-test/duckdb/t/alter_duckdb_index.test | 1 - storage/duckdb/mysql-test/duckdb/t/alter_engine_duckdb.test | 1 - storage/duckdb/mysql-test/duckdb/t/batch_disconnect.test | 1 - storage/duckdb/mysql-test/duckdb/t/batch_rollback.test | 1 - .../mysql-test/duckdb/t/bugfix_crash_after_commit_error.test | 1 - .../mysql-test/duckdb/t/bugfix_temp_and_system_database.test | 1 - storage/duckdb/mysql-test/duckdb/t/charset_and_collation.test | 1 - storage/duckdb/mysql-test/duckdb/t/create_table_column.test | 1 - .../mysql-test/duckdb/t/create_table_column_timestamp.test | 1 - storage/duckdb/mysql-test/duckdb/t/create_table_constraint.test | 1 - storage/duckdb/mysql-test/duckdb/t/cross_engine_join.test | 1 - storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow.test | 1 - storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow_dup.test | 1 - storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow_limit.test | 1 - storage/duckdb/mysql-test/duckdb/t/cross_engine_union.test | 1 - storage/duckdb/mysql-test/duckdb/t/cross_engine_where.test | 1 - storage/duckdb/mysql-test/duckdb/t/decimal_high_precision.test | 1 - .../mysql-test/duckdb/t/decimal_precision_all_possibilities.test | 1 - storage/duckdb/mysql-test/duckdb/t/dml_delete.test | 1 - storage/duckdb/mysql-test/duckdb/t/dml_update.test | 1 - storage/duckdb/mysql-test/duckdb/t/drop_database.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_add_backticks.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_agg_func.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_allow_encryption.test | 1 - .../duckdb/mysql-test/duckdb/t/duckdb_alter_table_engine.test | 1 - .../duckdb/t/duckdb_appender_allocator_flush_threshold.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_auto_increment.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_bit_string.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_collate.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_cte.test | 1 - .../duckdb/mysql-test/duckdb/t/duckdb_db_table_strconvert.test | 1 - .../mysql-test/duckdb/t/duckdb_ddl_during_transaction.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_default_expr.test | 1 - .../mysql-test/duckdb/t/duckdb_default_value_injection.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_fix_sql.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_json.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_kill.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_monitor.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_numeric_func.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_refuse_xa.test | 1 - .../duckdb/mysql-test/duckdb/t/duckdb_require_primary_key.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_set_operation.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_sql_mode.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_sql_syntax.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_string_func.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_time_func.test | 1 - .../duckdb/mysql-test/duckdb/t/duckdb_time_func_unsupported.test | 1 - storage/duckdb/mysql-test/duckdb/t/duckdb_uuid.test | 1 - storage/duckdb/mysql-test/duckdb/t/feature_duckdb_data_type.test | 1 - storage/duckdb/mysql-test/duckdb/t/ha_duckdb.test | 1 - storage/duckdb/mysql-test/duckdb/t/mdev_40379.test | 1 - storage/duckdb/mysql-test/duckdb/t/mdev_40651.test | 1 - storage/duckdb/mysql-test/duckdb/t/mdev_40957.test | 1 - storage/duckdb/mysql-test/duckdb/t/mixed_dml.test | 1 - storage/duckdb/mysql-test/duckdb/t/pushdown_rewrite.test | 1 - storage/duckdb/mysql-test/duckdb/t/rename_duckdb_table.test | 1 - storage/duckdb/mysql-test/duckdb/t/run_in_duckdb_access.test | 1 - storage/duckdb/mysql-test/duckdb/t/space_reclaim.test | 1 - storage/duckdb/mysql-test/duckdb/t/streaming_select.test | 1 - storage/duckdb/mysql-test/duckdb/t/supported_copy_ddl.test | 1 - storage/duckdb/mysql-test/duckdb/t/system_timezone.test | 1 - storage/duckdb/mysql-test/duckdb/t/transaction.test | 1 - .../duckdb/t/truncate_and_maintenance_duckdb_table.test | 1 - 66 files changed, 66 deletions(-) diff --git a/storage/duckdb/mysql-test/duckdb/t/alter_default_debug.test b/storage/duckdb/mysql-test/duckdb/t/alter_default_debug.test index 7ebcc4d25ed..a1a5cec984c 100644 --- a/storage/duckdb/mysql-test/duckdb/t/alter_default_debug.test +++ b/storage/duckdb/mysql-test/duckdb/t/alter_default_debug.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc CREATE TABLE t(id INT PRIMARY KEY, a INT, b INT, c INT) ENGINE = DuckDB; INSERT INTO t VALUES(1, 1, 1, 1); diff --git a/storage/duckdb/mysql-test/duckdb/t/alter_duckdb_column.test b/storage/duckdb/mysql-test/duckdb/t/alter_duckdb_column.test index 2e5076e8b19..0ae239d698c 100644 --- a/storage/duckdb/mysql-test/duckdb/t/alter_duckdb_column.test +++ b/storage/duckdb/mysql-test/duckdb/t/alter_duckdb_column.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --echo #################### --echo # TEST FOR INSTANT # --echo #################### diff --git a/storage/duckdb/mysql-test/duckdb/t/alter_duckdb_column_copy.test b/storage/duckdb/mysql-test/duckdb/t/alter_duckdb_column_copy.test index dd862e488dc..2a88f36f9c8 100644 --- a/storage/duckdb/mysql-test/duckdb/t/alter_duckdb_column_copy.test +++ b/storage/duckdb/mysql-test/duckdb/t/alter_duckdb_column_copy.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --echo ################# --echo # TEST FOR COPY # --echo ################# diff --git a/storage/duckdb/mysql-test/duckdb/t/alter_duckdb_index.test b/storage/duckdb/mysql-test/duckdb/t/alter_duckdb_index.test index 5d08f86baa4..8df5bdd951b 100644 --- a/storage/duckdb/mysql-test/duckdb/t/alter_duckdb_index.test +++ b/storage/duckdb/mysql-test/duckdb/t/alter_duckdb_index.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --echo #################### --echo # TEST FOR INSTANT # --echo #################### diff --git a/storage/duckdb/mysql-test/duckdb/t/alter_engine_duckdb.test b/storage/duckdb/mysql-test/duckdb/t/alter_engine_duckdb.test index 6240c455bf7..7d047686055 100644 --- a/storage/duckdb/mysql-test/duckdb/t/alter_engine_duckdb.test +++ b/storage/duckdb/mysql-test/duckdb/t/alter_engine_duckdb.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc SET GLOBAL duckdb_copy_ddl_in_batch=ON; --source ../include/alter_engine_duckdb.inc diff --git a/storage/duckdb/mysql-test/duckdb/t/batch_disconnect.test b/storage/duckdb/mysql-test/duckdb/t/batch_disconnect.test index 0ef4f782ea7..6a4de6d2282 100644 --- a/storage/duckdb/mysql-test/duckdb/t/batch_disconnect.test +++ b/storage/duckdb/mysql-test/duckdb/t/batch_disconnect.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --disable_query_log SET @saved_duckdb_dml_in_batch = @@GLOBAL.duckdb_dml_in_batch; diff --git a/storage/duckdb/mysql-test/duckdb/t/batch_rollback.test b/storage/duckdb/mysql-test/duckdb/t/batch_rollback.test index 94bf421487c..60bc3e591d6 100644 --- a/storage/duckdb/mysql-test/duckdb/t/batch_rollback.test +++ b/storage/duckdb/mysql-test/duckdb/t/batch_rollback.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --disable_query_log SET @saved_duckdb_dml_in_batch = @@GLOBAL.duckdb_dml_in_batch; diff --git a/storage/duckdb/mysql-test/duckdb/t/bugfix_crash_after_commit_error.test b/storage/duckdb/mysql-test/duckdb/t/bugfix_crash_after_commit_error.test index da058f62026..bc6757d2401 100644 --- a/storage/duckdb/mysql-test/duckdb/t/bugfix_crash_after_commit_error.test +++ b/storage/duckdb/mysql-test/duckdb/t/bugfix_crash_after_commit_error.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --skip TODO create table t1(id int primary key) engine = duckdb; diff --git a/storage/duckdb/mysql-test/duckdb/t/bugfix_temp_and_system_database.test b/storage/duckdb/mysql-test/duckdb/t/bugfix_temp_and_system_database.test index 420fd8c26aa..e802f5c4c35 100644 --- a/storage/duckdb/mysql-test/duckdb/t/bugfix_temp_and_system_database.test +++ b/storage/duckdb/mysql-test/duckdb/t/bugfix_temp_and_system_database.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc # DuckDB has default databases named 'temp' and 'system', if we try to create # schema named 'temp' or 'system', we get an error. diff --git a/storage/duckdb/mysql-test/duckdb/t/charset_and_collation.test b/storage/duckdb/mysql-test/duckdb/t/charset_and_collation.test index 54ea1077c1d..38ee163154c 100644 --- a/storage/duckdb/mysql-test/duckdb/t/charset_and_collation.test +++ b/storage/duckdb/mysql-test/duckdb/t/charset_and_collation.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --echo # --echo # check collation config when execute --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/create_table_column.test b/storage/duckdb/mysql-test/duckdb/t/create_table_column.test index fadf29f6c0d..4d56714c484 100644 --- a/storage/duckdb/mysql-test/duckdb/t/create_table_column.test +++ b/storage/duckdb/mysql-test/duckdb/t/create_table_column.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc # file: duckdb_create_table.test # # Test case for creating tables with various MySQL field types in DuckDB. diff --git a/storage/duckdb/mysql-test/duckdb/t/create_table_column_timestamp.test b/storage/duckdb/mysql-test/duckdb/t/create_table_column_timestamp.test index ffdc20b657d..1b9ecfc4bcd 100644 --- a/storage/duckdb/mysql-test/duckdb/t/create_table_column_timestamp.test +++ b/storage/duckdb/mysql-test/duckdb/t/create_table_column_timestamp.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --source include/have_debug.inc --disable_query_log diff --git a/storage/duckdb/mysql-test/duckdb/t/create_table_constraint.test b/storage/duckdb/mysql-test/duckdb/t/create_table_constraint.test index 3875082690b..cccf1e06d35 100644 --- a/storage/duckdb/mysql-test/duckdb/t/create_table_constraint.test +++ b/storage/duckdb/mysql-test/duckdb/t/create_table_constraint.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --source include/have_debug.inc diff --git a/storage/duckdb/mysql-test/duckdb/t/cross_engine_join.test b/storage/duckdb/mysql-test/duckdb/t/cross_engine_join.test index 0806df98e96..facf513363f 100644 --- a/storage/duckdb/mysql-test/duckdb/t/cross_engine_join.test +++ b/storage/duckdb/mysql-test/duckdb/t/cross_engine_join.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --echo # --echo # Cross-engine join: DuckDB + InnoDB via _mdb_scan replacement scan --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow.test b/storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow.test index 5ba8e936e3e..c0ef2086739 100644 --- a/storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow.test +++ b/storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --source include/have_sequence.inc --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow_dup.test b/storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow_dup.test index d572787dfa0..b19b13d33d1 100644 --- a/storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow_dup.test +++ b/storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow_dup.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --echo # --echo # Cross-engine RYOW: the same external table referenced more than once. diff --git a/storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow_limit.test b/storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow_limit.test index 84cc677bd9c..c7daf2b5e3c 100644 --- a/storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow_limit.test +++ b/storage/duckdb/mysql-test/duckdb/t/cross_engine_ryow_limit.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --source include/have_sequence.inc --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/cross_engine_union.test b/storage/duckdb/mysql-test/duckdb/t/cross_engine_union.test index 0ae97958ccd..d303b974cb0 100644 --- a/storage/duckdb/mysql-test/duckdb/t/cross_engine_union.test +++ b/storage/duckdb/mysql-test/duckdb/t/cross_engine_union.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --echo # --echo # Cross-engine UNION: DuckDB + InnoDB via select_handler unit pushdown --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/cross_engine_where.test b/storage/duckdb/mysql-test/duckdb/t/cross_engine_where.test index 51102d6b29f..4736fb64a97 100644 --- a/storage/duckdb/mysql-test/duckdb/t/cross_engine_where.test +++ b/storage/duckdb/mysql-test/duckdb/t/cross_engine_where.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --source include/have_sequence.inc --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/decimal_high_precision.test b/storage/duckdb/mysql-test/duckdb/t/decimal_high_precision.test index 3570bf4a2db..ec7bb24c7bd 100644 --- a/storage/duckdb/mysql-test/duckdb/t/decimal_high_precision.test +++ b/storage/duckdb/mysql-test/duckdb/t/decimal_high_precision.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/decimal_precision_all_possibilities.test b/storage/duckdb/mysql-test/duckdb/t/decimal_precision_all_possibilities.test index 1ec9348e5d0..2ea44599475 100644 --- a/storage/duckdb/mysql-test/duckdb/t/decimal_precision_all_possibilities.test +++ b/storage/duckdb/mysql-test/duckdb/t/decimal_precision_all_possibilities.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc set global duckdb_dml_in_batch = OFF; --source ../include/decimal_precision_all_possibilities.inc diff --git a/storage/duckdb/mysql-test/duckdb/t/dml_delete.test b/storage/duckdb/mysql-test/duckdb/t/dml_delete.test index d822cb86b29..0c49ad96e63 100644 --- a/storage/duckdb/mysql-test/duckdb/t/dml_delete.test +++ b/storage/duckdb/mysql-test/duckdb/t/dml_delete.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --echo # --echo # DuckDB DELETE operations test --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/dml_update.test b/storage/duckdb/mysql-test/duckdb/t/dml_update.test index 681c86315a9..360dcfc1436 100644 --- a/storage/duckdb/mysql-test/duckdb/t/dml_update.test +++ b/storage/duckdb/mysql-test/duckdb/t/dml_update.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --echo # --echo # DuckDB UPDATE operations test --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/drop_database.test b/storage/duckdb/mysql-test/duckdb/t/drop_database.test index 9f3ed4955b0..75877300a67 100644 --- a/storage/duckdb/mysql-test/duckdb/t/drop_database.test +++ b/storage/duckdb/mysql-test/duckdb/t/drop_database.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --echo # --echo # DuckDB DROP DATABASE operations test --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_add_backticks.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_add_backticks.test index a38ef13a7a9..e354231ed18 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_add_backticks.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_add_backticks.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc SET GLOBAL duckdb_require_primary_key=OFF; --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_agg_func.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_agg_func.test index 8a97c539156..bc50c1bf8cd 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_agg_func.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_agg_func.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc CREATE TABLE t_innodb ( id INT PRIMARY KEY, diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_allow_encryption.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_allow_encryption.test index f0144c977ee..8f7ba7574ed 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_allow_encryption.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_allow_encryption.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --disable_warnings DROP DATABASE IF EXISTS encryption_test; diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_alter_table_engine.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_alter_table_engine.test index 52a5cdb78d6..80ef3297927 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_alter_table_engine.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_alter_table_engine.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --echo # --echo # Test DDL: alter table engine = innodb from duckdb tables diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_appender_allocator_flush_threshold.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_appender_allocator_flush_threshold.test index d3d8a8a2b77..3a154d82679 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_appender_allocator_flush_threshold.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_appender_allocator_flush_threshold.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc SHOW GLOBAL VARIABLES LIKE "duckdb_appender_allocator_flush_threshold"; SELECT run_in_duckdb("FROM duckdb_settings() WHERE name = 'allocator_flush_threshold'"); diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_auto_increment.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_auto_increment.test index 4648aa05efa..b0614b1992e 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_auto_increment.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_auto_increment.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --disable_query_log CREATE DATABASE IF NOT EXISTS db_autoinc CHARACTER SET utf8mb4; diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_bit_string.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_bit_string.test index f7c8543a54d..22e82e4c3e5 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_bit_string.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_bit_string.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc CREATE TABLE t1 (id INT PRIMARY KEY, col1 VARCHAR(100)) ENGINE=DuckDB; CREATE TABLE t2 (id INT PRIMARY KEY, col1 BLOB) ENGINE=DuckDB; diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_collate.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_collate.test index 1d29484ce32..c575cbc9201 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_collate.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_collate.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --disable_query_log --disable_warnings DROP TABLE IF EXISTS t1; diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_cte.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_cte.test index c714205c845..b0210d0b558 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_cte.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_cte.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc CREATE DATABASE IF NOT EXISTS duckdb_cte; USE duckdb_cte; diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_db_table_strconvert.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_db_table_strconvert.test index 884e10f721f..0d50b5a1c9b 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_db_table_strconvert.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_db_table_strconvert.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc # Test for escape characters in the table name or database name. --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_ddl_during_transaction.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_ddl_during_transaction.test index 1d192f2e214..b0146bf54cb 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_ddl_during_transaction.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_ddl_during_transaction.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --echo ########################################################################### --echo # Prepare diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_default_expr.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_default_expr.test index c5a8ae25ec3..15cc00024f8 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_default_expr.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_default_expr.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --disable_query_log CREATE DATABASE IF NOT EXISTS db_default_expr CHARACTER SET utf8mb4; diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_default_value_injection.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_default_value_injection.test index 170e8b809c6..8713fecb3f5 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_default_value_injection.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_default_value_injection.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --echo # --echo # MDEV-40610: an unescaped column DEFAULT string must not inject DuckDB SQL diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_fix_sql.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_fix_sql.test index 38acb142797..a005b802703 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_fix_sql.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_fix_sql.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc CREATE TABLE fake_innodb_table (id INT PRIMARY KEY) ENGINE=InnoDB; INSERT INTO fake_innodb_table VALUES (0); diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_json.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_json.test index 7bd522a04f7..f607d971e4f 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_json.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_json.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc CREATE TABLE t1_innodb (id INT PRIMARY KEY, col1 JSON) ENGINE=InnoDB; CREATE TABLE t1_duckdb (id INT PRIMARY KEY, col1 JSON) ENGINE=DuckDB; diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_kill.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_kill.test index a9344b6be5b..2915977740b 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_kill.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_kill.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --source include/have_debug.inc --source include/have_debug_sync.inc diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_monitor.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_monitor.test index f8fad1d37b1..4e9627f958a 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_monitor.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_monitor.test @@ -1,7 +1,6 @@ --source ../include/have_duckdb.inc # MDEV-40449: MSan reports use-of-uninitialized-value in # duckdb::JemallocExtension::ThreadFlush (false positive from uninstrumented jemalloc). ---source include/not_msan.inc # # DuckDB monitoring status variables test. diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_numeric_func.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_numeric_func.test index 3d0fdaf1ac8..5f992cb23b5 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_numeric_func.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_numeric_func.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc # 1. CONV(), CRC32(), MOD, DIV, TRUNCATE() is not support yet. # 2. The RAND() function in duckdb and mysql uses different random number generation algorithms, so it cannot support the seed argument. diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_refuse_xa.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_refuse_xa.test index 4489a54fd33..0e34bd12c61 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_refuse_xa.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_refuse_xa.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc CREATE TABLE t1 (c1 INT PRIMARY KEY, c2 VARCHAR(5)) ENGINE=duckdb; diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_require_primary_key.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_require_primary_key.test index f972509960a..cc8ffb15acb 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_require_primary_key.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_require_primary_key.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_set_operation.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_set_operation.test index 04db6e8cc53..e245e7c9575 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_set_operation.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_set_operation.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc CREATE TABLE t1 (id INT PRIMARY KEY, col1 INT) ENGINE=DuckDB; INSERT INTO t1 VALUES (1, 1), (2, 1), (3, 2), (4, 2); CREATE TABLE t2 (id INT PRIMARY KEY, col1 INT) ENGINE=DuckDB; diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_sql_mode.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_sql_mode.test index 772254a650e..baeca66b9db 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_sql_mode.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_sql_mode.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --echo --echo 1. ONLY_FULL_GROUP_BY diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_sql_syntax.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_sql_syntax.test index aee922d26f1..c9db95ddd25 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_sql_syntax.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_sql_syntax.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc # WITH ROLLUP CREATE TABLE t1 (id INT PRIMARY KEY, col1 INT, col2 INT); SELECT id, col1, col2 FROM t1 GROUP BY id, col1, col2 WITH ROLLUP; diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_string_func.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_string_func.test index 59489ef568b..a14478edc65 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_string_func.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_string_func.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc # 1. In Duckdb, some string function does not support blob fields as input # 2. Regex-related functions are basically incompatible, and regex functions will be processed uniformly in the future. diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_time_func.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_time_func.test index 8a57fd81aa5..bc437121db8 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_time_func.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_time_func.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc # Compared with MySQL, duckdb's date functions generally have the following incompatibilities: # 1. Conversion between integer type and date/timestamp/time type diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_time_func_unsupported.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_time_func_unsupported.test index 61b390edfd7..0c21437ed4d 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_time_func_unsupported.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_time_func_unsupported.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc # Unsupported / incompatible date-time functions for DuckDB pushdown. # This is the companion of duckdb_time_func.test which keeps only the diff --git a/storage/duckdb/mysql-test/duckdb/t/duckdb_uuid.test b/storage/duckdb/mysql-test/duckdb/t/duckdb_uuid.test index c65fca8b8a5..161d588487f 100644 --- a/storage/duckdb/mysql-test/duckdb/t/duckdb_uuid.test +++ b/storage/duckdb/mysql-test/duckdb/t/duckdb_uuid.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --source include/have_sequence.inc --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/feature_duckdb_data_type.test b/storage/duckdb/mysql-test/duckdb/t/feature_duckdb_data_type.test index 06f0163991b..e21b2cb311e 100644 --- a/storage/duckdb/mysql-test/duckdb/t/feature_duckdb_data_type.test +++ b/storage/duckdb/mysql-test/duckdb/t/feature_duckdb_data_type.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc # Feature ORC data type test # Prepare diff --git a/storage/duckdb/mysql-test/duckdb/t/ha_duckdb.test b/storage/duckdb/mysql-test/duckdb/t/ha_duckdb.test index 14c5ce546ec..ffdfff0e07e 100644 --- a/storage/duckdb/mysql-test/duckdb/t/ha_duckdb.test +++ b/storage/duckdb/mysql-test/duckdb/t/ha_duckdb.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --echo # --echo # Basic DuckDB handler test: DDL and DML operations --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/mdev_40379.test b/storage/duckdb/mysql-test/duckdb/t/mdev_40379.test index 82a48ceac59..0c21a7d0c60 100644 --- a/storage/duckdb/mysql-test/duckdb/t/mdev_40379.test +++ b/storage/duckdb/mysql-test/duckdb/t/mdev_40379.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc # # MDEV-40379: Assertion failure in CreateTableConvertor::translate # upon CREATE TEMPORARY TABLE with ENGINE=DuckDB diff --git a/storage/duckdb/mysql-test/duckdb/t/mdev_40651.test b/storage/duckdb/mysql-test/duckdb/t/mdev_40651.test index 08624224661..5be863f0401 100644 --- a/storage/duckdb/mysql-test/duckdb/t/mdev_40651.test +++ b/storage/duckdb/mysql-test/duckdb/t/mdev_40651.test @@ -1,6 +1,5 @@ --source ../include/have_duckdb.inc --source include/have_partition.inc ---source include/not_msan.inc # MDEV-40651: keep DuckDB DDL atomic and preserve implicit defaults. CREATE TABLE t (c1 INT KEY) ENGINE=DuckDB; diff --git a/storage/duckdb/mysql-test/duckdb/t/mdev_40957.test b/storage/duckdb/mysql-test/duckdb/t/mdev_40957.test index 1909a4c3c5f..0eb1da4bfa7 100644 --- a/storage/duckdb/mysql-test/duckdb/t/mdev_40957.test +++ b/storage/duckdb/mysql-test/duckdb/t/mdev_40957.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --disable_query_log --disable_warnings diff --git a/storage/duckdb/mysql-test/duckdb/t/mixed_dml.test b/storage/duckdb/mysql-test/duckdb/t/mixed_dml.test index b555c1ff9e4..d25a7879178 100644 --- a/storage/duckdb/mysql-test/duckdb/t/mixed_dml.test +++ b/storage/duckdb/mysql-test/duckdb/t/mixed_dml.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --disable_query_log --disable_warnings diff --git a/storage/duckdb/mysql-test/duckdb/t/pushdown_rewrite.test b/storage/duckdb/mysql-test/duckdb/t/pushdown_rewrite.test index d01a7e29a9e..bfc8cbf4750 100644 --- a/storage/duckdb/mysql-test/duckdb/t/pushdown_rewrite.test +++ b/storage/duckdb/mysql-test/duckdb/t/pushdown_rewrite.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --disable_warnings DROP DATABASE IF EXISTS pushdown_rewrite; diff --git a/storage/duckdb/mysql-test/duckdb/t/rename_duckdb_table.test b/storage/duckdb/mysql-test/duckdb/t/rename_duckdb_table.test index 2511b3b41c1..6ea744b0fd0 100644 --- a/storage/duckdb/mysql-test/duckdb/t/rename_duckdb_table.test +++ b/storage/duckdb/mysql-test/duckdb/t/rename_duckdb_table.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --echo # --echo # 1) Prepare diff --git a/storage/duckdb/mysql-test/duckdb/t/run_in_duckdb_access.test b/storage/duckdb/mysql-test/duckdb/t/run_in_duckdb_access.test index 1e7afdabdfa..268b9858205 100644 --- a/storage/duckdb/mysql-test/duckdb/t/run_in_duckdb_access.test +++ b/storage/duckdb/mysql-test/duckdb/t/run_in_duckdb_access.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc SET @saved_allow_run_in_duckdb = @@GLOBAL.duckdb_allow_run_in_duckdb; # The suite enables duckdb_allow_run_in_duckdb globally (suite.opt), and MTR may diff --git a/storage/duckdb/mysql-test/duckdb/t/space_reclaim.test b/storage/duckdb/mysql-test/duckdb/t/space_reclaim.test index 2748253d487..f23b82603fd 100644 --- a/storage/duckdb/mysql-test/duckdb/t/space_reclaim.test +++ b/storage/duckdb/mysql-test/duckdb/t/space_reclaim.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc --source include/have_sequence.inc --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/streaming_select.test b/storage/duckdb/mysql-test/duckdb/t/streaming_select.test index 4999fa997d5..bf4e0d8fffc 100644 --- a/storage/duckdb/mysql-test/duckdb/t/streaming_select.test +++ b/storage/duckdb/mysql-test/duckdb/t/streaming_select.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --source include/have_sequence.inc --disable_warnings diff --git a/storage/duckdb/mysql-test/duckdb/t/supported_copy_ddl.test b/storage/duckdb/mysql-test/duckdb/t/supported_copy_ddl.test index ad37d55159b..97069c1b09f 100644 --- a/storage/duckdb/mysql-test/duckdb/t/supported_copy_ddl.test +++ b/storage/duckdb/mysql-test/duckdb/t/supported_copy_ddl.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc # Test for DDL which are supported by DuckDB using COPY algorithm --echo # --echo # RENAME TABLE WITH DIFFERENT DATABASES diff --git a/storage/duckdb/mysql-test/duckdb/t/system_timezone.test b/storage/duckdb/mysql-test/duckdb/t/system_timezone.test index 1d034ce0d6e..59aa9d106a5 100644 --- a/storage/duckdb/mysql-test/duckdb/t/system_timezone.test +++ b/storage/duckdb/mysql-test/duckdb/t/system_timezone.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --source ../include/have_mysqld_safe.inc # 1) Set valiables to be used in parameters of mysqld_safe. diff --git a/storage/duckdb/mysql-test/duckdb/t/transaction.test b/storage/duckdb/mysql-test/duckdb/t/transaction.test index 84688d7a379..7f080ce7456 100644 --- a/storage/duckdb/mysql-test/duckdb/t/transaction.test +++ b/storage/duckdb/mysql-test/duckdb/t/transaction.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --echo # --echo # DuckDB transaction tests --echo # diff --git a/storage/duckdb/mysql-test/duckdb/t/truncate_and_maintenance_duckdb_table.test b/storage/duckdb/mysql-test/duckdb/t/truncate_and_maintenance_duckdb_table.test index d3f9ee37e4d..3c2108ff356 100644 --- a/storage/duckdb/mysql-test/duckdb/t/truncate_and_maintenance_duckdb_table.test +++ b/storage/duckdb/mysql-test/duckdb/t/truncate_and_maintenance_duckdb_table.test @@ -1,4 +1,3 @@ ---source include/not_msan.inc --echo # --echo # 1) PREPARE --echo # From e71bdbf3d97dce5a6c49fd8eb79407c86acfd79d Mon Sep 17 00:00:00 2001 From: Alessandro Vetere Date: Thu, 15 Oct 2026 12:10:06 +0200 Subject: [PATCH 06/28] MDEV-41557 Test encryption.innochecksum is flaky The test checks that sys.innodb_buffer_stats_by_table reports old pages (pages_old) after a server restart. InnoDB creates the old sublist of the buffer pool LRU list only when the list holds at least BUF_LRU_OLD_MIN_LEN (512) pages. After the restart, most of the pages in the list come from the buffer pool load at startup. This load runs asynchronously, and the server accepts connections before it completes. If the test runs the check first, the list can be shorter than 512 pages, no page is old, and the result shows 0 instead of 1. The option --innodb-buffer-pool-load-at-startup=0 disables the load. This is the extreme case of the race: the check always runs before any page of the dump is loaded. With this option, the test fails every time with innodb_page_size=4k and 16k. Wait for the buffer pool load to complete before the check, as other tests do. --- mysql-test/suite/encryption/t/innochecksum.test | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/mysql-test/suite/encryption/t/innochecksum.test b/mysql-test/suite/encryption/t/innochecksum.test index e440e3d3344..d4a3d78a944 100644 --- a/mysql-test/suite/encryption/t/innochecksum.test +++ b/mysql-test/suite/encryption/t/innochecksum.test @@ -300,6 +300,16 @@ if (0 && $have_debug) { # these messages sometimes fail to appear --source include/start_mysqld.inc +# The check of pages_old needs old pages in the buffer pool LRU list. +# They exist only when the list holds at least BUF_LRU_OLD_MIN_LEN pages. +# Without the asynchronous load of the buffer pool dump, the list can be +# shorter than that. +let $wait_condition = + SELECT SUBSTR(variable_value, 1, 33) = 'Buffer pool(s) load completed at ' + FROM information_schema.global_status + WHERE LOWER(variable_name) = 'innodb_buffer_pool_load_status'; +--source include/wait_condition.inc + --echo # Trigger AHI and test pages_hashed, pages_old as non zero from sys view SELECT STRAIGHT_JOIN COUNT(t6a.a) From 9993e91785ea68c72f5151273a632d7a2560ad7e Mon Sep 17 00:00:00 2001 From: Aleksey Midenkov Date: Fri, 16 Oct 2026 15:11:20 +0300 Subject: [PATCH 07/28] MDEV-17613 bitmap_range() for iteration over partition bitmap Returns bit range for partition element. --- sql/ha_partition.h | 6 +++--- sql/partition_element.h | 13 +++++++++++++ sql/partition_info.cc | 7 +++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/sql/ha_partition.h b/sql/ha_partition.h index 6dfffe235d1..3a2da8bfbe9 100644 --- a/sql/ha_partition.h +++ b/sql/ha_partition.h @@ -1642,9 +1642,9 @@ public: ha_rows part_records(partition_element *part_elem) { DBUG_ASSERT(m_part_info); - uint32 sub_factor= m_part_info->num_subparts ? m_part_info->num_subparts : 1; - uint32 part_id= part_elem->id * sub_factor; - uint32 part_id_end= part_id + sub_factor; + uint32 part_id_end; + uint32 part_id= part_elem->bitmap_range(m_part_info->num_subparts, + part_id_end); DBUG_ASSERT(part_id_end <= m_tot_parts); ha_rows part_recs= 0; for (; part_id < part_id_end; ++part_id) diff --git a/sql/partition_element.h b/sql/partition_element.h index 1abaa315218..1cc08ecb64b 100644 --- a/sql/partition_element.h +++ b/sql/partition_element.h @@ -173,6 +173,19 @@ public: DBUG_ASSERT(ev->col_val_array); return ev->col_val_array[idx]; } + + /* + Leaf-bitmap (read/lock_partitions) range covered by this top-level + partition: a single bit if not subpartitioned, num_subparts bits + otherwise. Returns the first bit; bit_no_end receives one past the last. + */ + uint32 bitmap_range(uint num_subparts, uint32 &bit_no_end) const + { + uint32 sub_factor= num_subparts ? num_subparts : 1; + uint32 first= id * sub_factor; + bit_no_end= first + sub_factor; + return first; + } }; #endif /* PARTITION_ELEMENT_INCLUDED */ diff --git a/sql/partition_info.cc b/sql/partition_info.cc index 9aa871e1418..2a54b8142d5 100644 --- a/sql/partition_info.cc +++ b/sql/partition_info.cc @@ -1035,10 +1035,9 @@ void partition_info::vers_check_limit(THD *thd) of partitions. */ #ifndef DBUG_OFF - const uint32 sub_factor= num_subparts ? num_subparts : 1; - uint32 part_id= vers_info->hist_part->id * sub_factor; - const uint32 part_id_end __attribute__((unused)) = part_id + sub_factor; - DBUG_ASSERT(part_id_end <= num_parts * sub_factor); + uint32 part_id_end; + vers_info->hist_part->bitmap_range(num_subparts, part_id_end); + DBUG_ASSERT(part_id_end <= get_tot_partitions()); #endif ha_partition *hp= (ha_partition*)(table->file); From b55aca57f26256ce2ef42fd0c2d67ebad26c55b7 Mon Sep 17 00:00:00 2001 From: Aleksey Midenkov Date: Fri, 16 Oct 2026 15:11:21 +0300 Subject: [PATCH 08/28] MDEV-17613 Refactor prune_partitions() to build into a dedicated bitmap prune_partitions() built the used-partition set directly in read_partitions: it cleared it, let find_used_partitions*() fill it, and in the index-merge case used it as scratch too - both buffer and result. The patch adds PART_PRUNE_PARAM::parts_bitmap and builds the return into it instead. read_partitions is produced from it only at the end: copied when the condition was analyzed (IMPOSSIBLE included, parts_bitmap left empty), or set to all locked partitions via mark_all_partitions_as_used() when nothing could be pruned. Pure refactoring: read_partitions ends up identical on every path. It decouples the pruning scratch from read_partitions, so a later commit can switch the final bitmap_copy() to bitmap_intersect() - letting pruning narrow a pre-set read_partitions (e.g. a versioned table selecting only its current partition) instead of overwriting it. --- sql/opt_range.cc | 48 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/sql/opt_range.cc b/sql/opt_range.cc index f45317d422a..86a95fdbb39 100644 --- a/sql/opt_range.cc +++ b/sql/opt_range.cc @@ -4019,7 +4019,7 @@ void store_key_image_to_rec(Field *field, uchar *ptr, uint len) struct st_part_prune_param; struct st_part_opt_info; -typedef void (*mark_full_part_func)(partition_info*, uint32); +typedef void (*mark_full_part_func)(struct st_part_prune_param*, uint32); /* Partition pruning operation context @@ -4080,6 +4080,12 @@ typedef struct st_part_prune_param /* Iterator to be used to obtain the "current" set of used partitions */ PARTITION_ITERATOR part_iter; + /* + Initialized bitmap of read_partitions size. + find_used_partitions() will save the set of used partitions here. + */ + MY_BITMAP parts_bitmap; + /* Initialized bitmap of num_subparts size */ MY_BITMAP subparts_bitmap; @@ -4181,7 +4187,7 @@ bool prune_partitions(THD *thd, TABLE *table, Item *pprune_cond) thd->no_errors=1; // Don't warn about NULL thd->mem_root=&alloc; - bitmap_clear_all(&part_info->read_partitions); + bitmap_clear_all(&prune_param.parts_bitmap); prune_param.key= prune_param.range_param.key_parts; SEL_TREE *tree; @@ -4259,7 +4265,11 @@ bool prune_partitions(THD *thd, TABLE *table, Item *pprune_cond) all_used: retval= FALSE; // some partitions are used mark_all_partitions_as_used(prune_param.part_info); + goto all_used_end; end: + bitmap_copy(&prune_param.part_info->read_partitions, + &prune_param.parts_bitmap); +all_used_end: dbug_tmp_restore_column_maps(&table->read_set, &table->write_set, old_sets); thd->no_errors=0; thd->mem_root= range_par->old_root; @@ -4337,20 +4347,21 @@ static void store_selargs_to_rec(PART_PRUNE_PARAM *ppar, SEL_ARG **start, /* Mark a partition as used in the case when there are no subpartitions */ -static void mark_full_partition_used_no_parts(partition_info* part_info, +static void mark_full_partition_used_no_parts(PART_PRUNE_PARAM *ppar, uint32 part_id) { DBUG_ENTER("mark_full_partition_used_no_parts"); DBUG_PRINT("enter", ("Mark partition %u as used", part_id)); - bitmap_set_bit(&part_info->read_partitions, part_id); + bitmap_set_bit(&ppar->parts_bitmap, part_id); DBUG_VOID_RETURN; } /* Mark a partition as used in the case when there are subpartitions */ -static void mark_full_partition_used_with_parts(partition_info *part_info, +static void mark_full_partition_used_with_parts(PART_PRUNE_PARAM *ppar, uint32 part_id) { + partition_info *part_info= ppar->part_info; uint32 start= part_id * part_info->num_subparts; uint32 end= start + part_info->num_subparts; DBUG_ENTER("mark_full_partition_used_with_parts"); @@ -4358,7 +4369,7 @@ static void mark_full_partition_used_with_parts(partition_info *part_info, for (; start != end; start++) { DBUG_PRINT("info", ("1:Mark subpartition %u as used", start)); - bitmap_set_bit(&part_info->read_partitions, start); + bitmap_set_bit(&ppar->parts_bitmap, start); } DBUG_VOID_RETURN; } @@ -4386,7 +4397,7 @@ static int find_used_partitions_imerge_list(PART_PRUNE_PARAM *ppar, MY_BITMAP all_merges; uint bitmap_bytes; my_bitmap_map *bitmap_buf; - uint n_bits= ppar->part_info->read_partitions.n_bits; + uint n_bits= ppar->parts_bitmap.n_bits; bitmap_bytes= bitmap_buffer_size(n_bits); if (!(bitmap_buf= (my_bitmap_map*) alloc_root(ppar->range_param.mem_root, bitmap_bytes))) @@ -4412,15 +4423,15 @@ static int find_used_partitions_imerge_list(PART_PRUNE_PARAM *ppar, } if (res != -1) - bitmap_intersect(&all_merges, &ppar->part_info->read_partitions); + bitmap_intersect(&all_merges, &ppar->parts_bitmap); if (bitmap_is_clear_all(&all_merges)) return 0; - bitmap_clear_all(&ppar->part_info->read_partitions); + bitmap_clear_all(&ppar->parts_bitmap); } - memcpy(ppar->part_info->read_partitions.bitmap, all_merges.bitmap, + memcpy(ppar->parts_bitmap.bitmap, all_merges.bitmap, bitmap_bytes); return 1; } @@ -4573,6 +4584,8 @@ int find_used_partitions_imerge(PART_PRUNE_PARAM *ppar, SEL_IMERGE *imerge) -1 Couldn't infer any partition pruning "intervals" from the passed SEL_ARG* tree (which means that all partitions should be marked as used) Marking partitions as used is the responsibility of the caller. + + The set of used partitions is saved in PART_PRUNE_PARAM::parts_bitmap. */ static @@ -4791,7 +4804,7 @@ int find_used_partitions(PART_PRUNE_PARAM *ppar, SEL_ARG *key_tree) { for (uint i= 0; i < ppar->part_info->num_subparts; i++) if (bitmap_is_set(&ppar->subparts_bitmap, i)) - bitmap_set_bit(&ppar->part_info->read_partitions, + bitmap_set_bit(&ppar->parts_bitmap, part_id * ppar->part_info->num_subparts + i); } goto pop_and_go_right; @@ -4853,7 +4866,7 @@ int find_used_partitions(PART_PRUNE_PARAM *ppar, SEL_ARG *key_tree) while ((part_id= ppar->part_iter.get_next(&ppar->part_iter)) != NOT_A_PARTITION_ID) { - bitmap_set_bit(&part_info->read_partitions, + bitmap_set_bit(&ppar->parts_bitmap, part_id * part_info->num_subparts + subpart_id); } res= 1; /* Some partitions were marked as used */ @@ -4908,7 +4921,7 @@ process_next_key_part: while ((part_id= ppar->part_iter.get_next(&ppar->part_iter)) != NOT_A_PARTITION_ID) { - ppar->mark_full_partition_used(ppar->part_info, part_id); + ppar->mark_full_partition_used(ppar, part_id); found= TRUE; } res= MY_TEST(found); @@ -5048,6 +5061,15 @@ static bool create_partition_index_description(PART_PRUNE_PARAM *ppar) !(ppar->is_subpart_keypart= (my_bool*)alloc_root(alloc, sizeof(my_bool)* total_parts))) return TRUE; + + { + my_bitmap_map *buf; + uint32 bufsize= bitmap_buffer_size(part_info->read_partitions.n_bits); + if (!(buf= (my_bitmap_map*) alloc_root(alloc, bufsize))) + return TRUE; + my_bitmap_init(&ppar->parts_bitmap, buf, + part_info->read_partitions.n_bits); + } if (ppar->subpart_fields) { From 6743ca8ce431ce57f0973a3f6633d5753ff1ab75 Mon Sep 17 00:00:00 2001 From: Aleksey Midenkov Date: Fri, 16 Oct 2026 15:11:21 +0300 Subject: [PATCH 09/28] MDEV-17613 Refactor prune_partitions() to narrow read_partitions instead of full rewrite read_partitions is reset to all partitions (or the PARTITION() set) once per statement in open_table() (set_partition_bitmaps()), and prune_partitions() only intersects it with lock_partitions and the pruned set instead of rebuilding it. mark_all_partitions_as_used() is dropped. Pure refactoring, results unchanged. It lets a restriction set on read_partitions before pruning survive it - needed when read_partitions is pre-restricted, e.g. a versioned table whose history is partitioned reads only the current partition. --- .../main/partition_explicit_prune.result | 23 ++++++++ mysql-test/main/partition_explicit_prune.test | 23 ++++++++ sql/opt_range.cc | 58 ++++++++++--------- 3 files changed, 77 insertions(+), 27 deletions(-) diff --git a/mysql-test/main/partition_explicit_prune.result b/mysql-test/main/partition_explicit_prune.result index d31790e4433..f01816a26e8 100644 --- a/mysql-test/main/partition_explicit_prune.result +++ b/mysql-test/main/partition_explicit_prune.result @@ -2006,3 +2006,26 @@ drop table t1; # # End of 10.4 tests # +# +# MDEV-17613 Read sole now-partition for non-historical versioned query +# +# INSERT ... SELECT from explicit PARTITION() without WHERE must not +# read partitions outside of it (read_partitions widened by auto-increment) +# +create table t1 (id int auto_increment primary key, x int) +partition by range (id) ( +partition p0 values less than (10), +partition p1 values less than maxvalue); +insert into t1 values (1, 1), (2, 2), (11, 11), (12, 12); +create table t2 (id int, x int); +# Fresh share: auto-increment is not initialized yet +flush tables; +insert into t2 select * from t1 partition (p0); +select * from t2; +id x +1 1 +2 2 +drop tables t1, t2; +# +# End of 10.11 tests +# diff --git a/mysql-test/main/partition_explicit_prune.test b/mysql-test/main/partition_explicit_prune.test index 859dcba6ddc..a66ee0db29f 100644 --- a/mysql-test/main/partition_explicit_prune.test +++ b/mysql-test/main/partition_explicit_prune.test @@ -942,3 +942,26 @@ drop table t1; --echo # End of 10.4 tests --echo # +--echo # +--echo # MDEV-17613 Read sole now-partition for non-historical versioned query +--echo # +--echo # INSERT ... SELECT from explicit PARTITION() without WHERE must not +--echo # read partitions outside of it (read_partitions widened by auto-increment) +--echo # +create table t1 (id int auto_increment primary key, x int) +partition by range (id) ( + partition p0 values less than (10), + partition p1 values less than maxvalue); +insert into t1 values (1, 1), (2, 2), (11, 11), (12, 12); +create table t2 (id int, x int); +--echo # Fresh share: auto-increment is not initialized yet +flush tables; +insert into t2 select * from t1 partition (p0); +--sorted_result +select * from t2; +drop tables t1, t2; + +--echo # +--echo # End of 10.11 tests +--echo # + diff --git a/sql/opt_range.cc b/sql/opt_range.cc index 86a95fdbb39..67801e416ee 100644 --- a/sql/opt_range.cc +++ b/sql/opt_range.cc @@ -4101,7 +4101,6 @@ static int find_used_partitions_imerge(PART_PRUNE_PARAM *ppar, SEL_IMERGE *imerge); static int find_used_partitions_imerge_list(PART_PRUNE_PARAM *ppar, List &merges); -static void mark_all_partitions_as_used(partition_info *part_info); #ifndef DBUG_OFF static void print_partitioning_index(KEY_PART *parts, KEY_PART *parts_end); @@ -4118,13 +4117,28 @@ static void dbug_print_singlepoint_range(SEL_ARG **start, uint num); @param table Table to perform partition pruning for @param pprune_cond Condition to use for partition pruning + @param[in] part_info->read_partitions + Candidate partitions to scan; normally all partitions (or the + PARTITION() set), possibly already restricted (e.g. to the + current partition of a versioned table) + + @param[out] part_info->read_partitions + Narrowed to the partitions that may hold records matching the + condition; never widened + @note This function assumes that lock_partitions are setup when it is invoked. The function analyzes the condition, finds partitions that need to be used to retrieve the records that match the condition, and - marks them as used by setting appropriate bit in part_info->read_partitions - In the worst case all partitions are marked as used. If the table is not - yet locked, it will also unset bits in part_info->lock_partitions that is - not set in read_partitions. + narrows part_info->read_partitions to them. + If the table is not yet locked, it will also unset bits in + part_info->lock_partitions that is not set in read_partitions. + + The matching set is computed independently of the incoming + read_partitions/lock_partitions: the analysis walks the whole partition + list (or only the value interval derived from the condition for + range-capable partitioning), and the result is then intersected into + read_partitions. A restriction already present in read_partitions + therefore narrows the outcome but does not reduce the pruning work. This function returns promptly if called for non-partitioned table. @@ -4142,11 +4156,18 @@ bool prune_partitions(THD *thd, TABLE *table, Item *pprune_cond) if (!part_info) DBUG_RETURN(FALSE); /* not a partitioned table */ + /* + Must be a subset of the locked partitions. + lock_partitions contains the partitions marked by explicit partition + selection (... t PARTITION (pX) ...) and we must only use partitions + within that set. read_partitions may be wider on entry: e.g. + check_insert_or_replace_autoincrement() sets all bits for + INSERT/REPLACE ... SELECT. Done before the early returns below. + */ + bitmap_intersect(&part_info->read_partitions, &part_info->lock_partitions); + if (!pprune_cond) - { - mark_all_partitions_as_used(part_info); DBUG_RETURN(FALSE); - } PART_PRUNE_PARAM prune_param; MEM_ROOT alloc; @@ -4162,7 +4183,6 @@ bool prune_partitions(THD *thd, TABLE *table, Item *pprune_cond) if (create_partition_index_description(&prune_param)) { - mark_all_partitions_as_used(part_info); free_root(&alloc,MYF(0)); // Return memory & allocator DBUG_RETURN(FALSE); } @@ -4264,24 +4284,15 @@ bool prune_partitions(THD *thd, TABLE *table, Item *pprune_cond) all_used: retval= FALSE; // some partitions are used - mark_all_partitions_as_used(prune_param.part_info); goto all_used_end; end: - bitmap_copy(&prune_param.part_info->read_partitions, - &prune_param.parts_bitmap); + bitmap_intersect(&prune_param.part_info->read_partitions, + &prune_param.parts_bitmap); all_used_end: dbug_tmp_restore_column_maps(&table->read_set, &table->write_set, old_sets); thd->no_errors=0; thd->mem_root= range_par->old_root; free_root(&alloc,MYF(0)); // Return memory & allocator - /* - Must be a subset of the locked partitions. - lock_partitions contains the partitions marked by explicit partition - selection (... t PARTITION (pX) ...) and we must only use partitions - within that set. - */ - bitmap_intersect(&prune_param.part_info->read_partitions, - &prune_param.part_info->lock_partitions); /* If not yet locked, also prune partitions to lock if not UPDATEing partition key fields. This will also prune lock_partitions if we are under @@ -4950,13 +4961,6 @@ pop_and_go_right: } -static void mark_all_partitions_as_used(partition_info *part_info) -{ - bitmap_copy(&(part_info->read_partitions), - &(part_info->lock_partitions)); -} - - /* Check if field types allow to construct partitioning index description From c441e4df1459b8ec1256f7aca75772c869ded55d Mon Sep 17 00:00:00 2001 From: Aleksey Midenkov Date: Fri, 16 Oct 2026 15:11:21 +0300 Subject: [PATCH 10/28] MDEV-17613 Read sole now-partition for non-historical versioned query When a versioned table stores its history in partitions, an implicit query (no FOR SYSTEM_TIME) needs only current rows, which all live in the current partition. vers_setup_conds() no longer adds a row_end condition for such a table, and prune_partitions() restricts read_partitions to the current partition instead. read_partitions is reset to all partitions every statement in open_table(), so the restriction must be applied on each execution. prune_partitions() runs on each execution, also for merged derived tables and views, where vers_setup_conds() is not called again. Both functions guard with !vers_conditions.was_set(). In prune_partitions() !is_set() would not do: set_all() sets type (flips is_set()) but not orig_type (was_set()), so is_set() would be true on prepared-statement / stored-procedure re-executions and skip the restriction, leaving history partitions readable - rows matched then grow on each execute/call. A query whose time comes from @@system_versioning_asof must still read history, so the restriction is also skipped when from_sysvar is set. init_from_sysvar() sets this new flag rather than orig_type, so was_set(), print() and the write lock check still see only an explicit FOR SYSTEM_TIME. Like orig_type, the flag persists to prepared-statement re-execution. EXPLAIN no longer shows "Using where" for such a query: correctness now comes from partition selection rather than a row_end filter. With sql_safe_updates, DELETE without WHERE on such a table is now rejected even with LIMIT, as for a non-versioned table. With an index on row_end, UPDATE or DELETE without WHERE (UPDATE also without LIMIT) is now rejected too: it was accepted as a range scan on row_end. A versioned table without SYSTEM_TIME partitions keeps the old behavior (MDEV-41393). --- .../suite/versioning/r/partition,heap.rdiff | 16 ++ .../suite/versioning/r/partition.result | 194 +++++++++++++++++- mysql-test/suite/versioning/r/sysvars.result | 21 ++ mysql-test/suite/versioning/t/partition.test | 146 +++++++++++++ mysql-test/suite/versioning/t/sysvars.test | 18 ++ sql/opt_range.cc | 30 ++- sql/sql_select.cc | 16 ++ sql/table.h | 38 ++++ 8 files changed, 475 insertions(+), 4 deletions(-) diff --git a/mysql-test/suite/versioning/r/partition,heap.rdiff b/mysql-test/suite/versioning/r/partition,heap.rdiff index 6e611ad5733..e055a2c6047 100644 --- a/mysql-test/suite/versioning/r/partition,heap.rdiff +++ b/mysql-test/suite/versioning/r/partition,heap.rdiff @@ -221,3 +221,19 @@ # INSERT .. ON DUPLICATE KEY UPDATE (ODKU) set timestamp= unix_timestamp('2000-01-01 00:00:00'); create or replace table t1 (x int primary key) with system versioning +@@ -3657,13 +3450,13 @@ + delete from t1 where id = 1; + select * from t1; + id +-0 + 2 ++0 + # Below there should be: Select tables optimized away + # (except heap combination) + explain extended select max(id) from t1; + id select_type table type possible_keys key key_len ref rows filtered Extra +-1 SIMPLE NULL NULL NULL NULL NULL NULL NULL NULL Select tables optimized away ++1 SIMPLE t1 ALL NULL NULL NULL NULL 2 100.00 + Warnings: + Note 1003 select max(`test`.`t1`.`id`) AS `max(id)` from `test`.`t1` + drop tables t1; diff --git a/mysql-test/suite/versioning/r/partition.result b/mysql-test/suite/versioning/r/partition.result index 79544c00fc6..293131fca0c 100644 --- a/mysql-test/suite/versioning/r/partition.result +++ b/mysql-test/suite/versioning/r/partition.result @@ -409,7 +409,7 @@ i 6 explain partitions select * from t1; id select_type table partitions type possible_keys key key_len ref rows Extra -1 SIMPLE t1 pn_pnsp0,pn_pnsp1 ALL NULL NULL NULL NULL # Using where +1 SIMPLE t1 pn_pnsp0,pn_pnsp1 ALL NULL NULL NULL NULL # explain partitions select * from t1 for system_time as of '2001-02-04 10:20:30'; id select_type table partitions type possible_keys key key_len ref rows Extra 1 SIMPLE t1 p0_p0sp0,p0_p0sp1,p2_p2sp0,p2_p2sp1,pn_pnsp0,pn_pnsp1 ALL NULL NULL NULL NULL # Using where @@ -1124,6 +1124,23 @@ update v1 set f= ''; ERROR HY000: Table 't1' was locked with a READ lock and can't be updated delete from v1; ERROR HY000: Table 't1' was locked with a READ lock and can't be updated +# system_versioning_asof is ignored by DML, so it does not cause ER_TABLE_NOT_LOCKED_FOR_WRITE +create or replace view v1 as select * from t1; +set system_versioning_asof= '2000-01-01 00:00:00'; +select * from v1; +f +select * from v1 for update; +f +update v1 set f= ''; +affected rows: 1 +info: Rows matched: 1 Changed: 1 Inserted: 1 Warnings: 0 +Warnings: +Warning 4114 Versioned table `test`.`t1`: last HISTORY partition (`p2`) is out of LIMIT, need more HISTORY partitions +delete from v1; +affected rows: 1 +Warnings: +Warning 4114 Versioned table `test`.`t1`: last HISTORY partition (`p2`) is out of LIMIT, need more HISTORY partitions +set system_versioning_asof= default; drop view v1; drop table t1; # @@ -3750,4 +3767,179 @@ set timestamp= default; set time_zone= default; drop table t1; # End of 10.11 tests +# +# MDEV-17613 Read sole now-partition for non-historical versioned query +# +create table t1 ( +id int, +ts timestamp(6) generated always as row start invisible, +te timestamp(6) generated always as row end invisible, +primary key (id, te), +period for system_time (ts, te) +) with system versioning +partition by system_time interval 1 month ( +partition p0 history, partition p1 history, partition pcur current); +insert t1 values (1), (2), (0); +delete from t1 where id = 1; +select * from t1; +id +0 +2 +# Below there should be: Select tables optimized away +# (except heap combination) +explain extended select max(id) from t1; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE NULL NULL NULL NULL NULL NULL NULL NULL Select tables optimized away +Warnings: +Note 1003 select max(`test`.`t1`.`id`) AS `max(id)` from `test`.`t1` +# Exact COUNT(*) (MyISAM, heap) must not count history rows +select count(*) from t1; +count(*) +2 +# UPDATE without WHERE changes only current rows +update t1 set id= id + 10; +select * from t1; +id +10 +12 +select count(*) from t1 for system_time all; +count(*) +5 +# sql_safe_updates rejects DELETE without WHERE even with LIMIT, +# as for a non-versioned table +set sql_safe_updates= 1; +delete from t1 limit 1; +ERROR HY000: You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column +set sql_safe_updates= default; +# DELETE without WHERE removes only current rows +delete from t1; +select count(*) from t1; +count(*) +0 +select count(*) from t1 for system_time all; +count(*) +5 +drop tables t1; +# Multi-table DELETE/UPDATE under sql_safe_updates: checks use the +# written WHERE and the join access types, as before +create table t1 (x int) with system versioning +partition by system_time (partition p0 history, partition pn current); +create table t2 (y int); +insert into t1 values (1), (2); +insert into t2 values (1), (2); +set sql_safe_updates= 1; +delete t1 from t1, t2; +ERROR HY000: You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column +update t1, t2 set t1.x= t2.y; +ERROR HY000: You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column +# LIMIT does not exempt multi-table UPDATE from the full join check +update t1, t2 set t1.x= t2.y order by t1.x limit 1; +ERROR HY000: You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column +set sql_safe_updates= default; +drop tables t1, t2; +# As for a non-versioned table, an index on row_end does not exempt +# UPDATE/DELETE without WHERE: UPDATE needs LIMIT, DELETE is rejected +create table t1 ( +x int, +ts timestamp(6) as row start invisible, +te timestamp(6) as row end invisible, +period for system_time (ts, te), +key (te) +) with system versioning +partition by system_time (partition p0 history, partition pn current); +insert into t1 values (1), (2); +set sql_safe_updates= 1; +update t1 set x= x + 1; +ERROR HY000: You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column +delete from t1; +ERROR HY000: You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column +update t1 set x= x + 1 limit 1; +delete from t1 limit 1; +ERROR HY000: You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column +set sql_safe_updates= default; +drop table t1; +# Corner cases: outer and sysvar-set FOR SYSTEM_TIME +set timestamp= unix_timestamp('2000-01-01 00:00:00'); +create or replace table t1 (id int) +with system versioning +partition by system_time interval 1 hour +starts timestamp'2000-01-01 00:00:00' ( +partition p0 history, +partition p1 history, +partition pn current); +insert into t1 values (1); +set timestamp= unix_timestamp('2000-01-01 01:30:00'); +update t1 set id= 2; +# implicit query reads only the current partition (id=2): +select id from t1; +id +2 +# outer FOR SYSTEM_TIME must propagate into derived t1 and read history (id=1): +select * from (select id from t1) for system_time as of timestamp'2000-01-01 01:00:00' as dt; +id +1 +# system_versioning_asof must be honored and read history (id=1): +set system_versioning_asof= '2000-01-01 01:00:00'; +select id from t1; +id +1 +set system_versioning_asof= default; +# re-execution reads only the current partition (id=2): +prepare s from 'select id from t1'; +execute s; +id +2 +execute s; +id +2 +deallocate prepare s; +create procedure sp() select id from t1; +call sp; +id +2 +call sp; +id +2 +drop procedure sp; +# the same through merged derived table and view (id=2): +create view v1 as select id from t1; +prepare s from 'select id from (select id from t1) dt'; +execute s; +id +2 +execute s; +id +2 +deallocate prepare s; +prepare s from 'select id from v1'; +execute s; +id +2 +execute s; +id +2 +deallocate prepare s; +create procedure sp() select id from (select id from t1) dt; +call sp; +id +2 +call sp; +id +2 +drop procedure sp; +drop view v1; +# re-execution with system_versioning_asof reads history (id=1): +set system_versioning_asof= '2000-01-01 01:00:00'; +prepare s from 'select id from t1'; +execute s; +id +1 +execute s; +id +1 +deallocate prepare s; +set system_versioning_asof= default; +set timestamp= default; +drop table t1; +# End of 11.4 tests set global innodb_stats_persistent= @save_persistent; diff --git a/mysql-test/suite/versioning/r/sysvars.result b/mysql-test/suite/versioning/r/sysvars.result index a5a3f79990c..1766cdb2725 100644 --- a/mysql-test/suite/versioning/r/sysvars.result +++ b/mysql-test/suite/versioning/r/sysvars.result @@ -254,3 +254,24 @@ a DROP TABLE t1; SET system_versioning_asof= DEFAULT; # End of 10.4 tests +# +# MDEV-17613 Read sole now-partition for non-historical versioned query +# +# system_versioning_asof does not act as explicit FOR SYSTEM_TIME AS OF +create table t1 (x int) with system versioning; +insert into t1 values (1); +set system_versioning_asof= '2000-01-01 00:00:00'; +# Allowed, unlike explicit FOR SYSTEM_TIME AS OF: +select * from t1 for update; +x +select * from t1 for system_time as of timestamp'2000-01-01 00:00:00' for update; +ERROR HY000: Table 't1' was locked with a READ lock and can't be updated +# Not printed as FOR SYSTEM_TIME AS OF: +explain extended select * from t1; +id select_type table type possible_keys key key_len ref rows filtered Extra +1 SIMPLE NULL NULL NULL NULL NULL NULL NULL NULL Impossible WHERE noticed after reading const tables +Warnings: +Note 1003 select 1 AS `x` from dual where 0 +set system_versioning_asof= default; +drop table t1; +# End of 10.11 tests diff --git a/mysql-test/suite/versioning/t/partition.test b/mysql-test/suite/versioning/t/partition.test index 9604f779779..e705cc84679 100644 --- a/mysql-test/suite/versioning/t/partition.test +++ b/mysql-test/suite/versioning/t/partition.test @@ -969,6 +969,17 @@ update v1 set f= ''; --error ER_TABLE_NOT_LOCKED_FOR_WRITE delete from v1; +--echo # system_versioning_asof is ignored by DML, so it does not cause ER_TABLE_NOT_LOCKED_FOR_WRITE +create or replace view v1 as select * from t1; +set system_versioning_asof= '2000-01-01 00:00:00'; +select * from v1; +select * from v1 for update; +--enable_info +update v1 set f= ''; +delete from v1; +--disable_info +set system_versioning_asof= default; + # cleanup drop view v1; drop table t1; @@ -2957,5 +2968,140 @@ drop table t1; --echo # End of 10.11 tests +--echo # +--echo # MDEV-17613 Read sole now-partition for non-historical versioned query +--echo # +create table t1 ( + id int, + ts timestamp(6) generated always as row start invisible, + te timestamp(6) generated always as row end invisible, + primary key (id, te), + period for system_time (ts, te) +) with system versioning +partition by system_time interval 1 month ( + partition p0 history, partition p1 history, partition pcur current); + +insert t1 values (1), (2), (0); +delete from t1 where id = 1; +select * from t1; +--echo # Below there should be: Select tables optimized away +--echo # (except heap combination) +explain extended select max(id) from t1; +--echo # Exact COUNT(*) (MyISAM, heap) must not count history rows +select count(*) from t1; +--echo # UPDATE without WHERE changes only current rows +update t1 set id= id + 10; +--sorted_result +select * from t1; +select count(*) from t1 for system_time all; +--echo # sql_safe_updates rejects DELETE without WHERE even with LIMIT, +--echo # as for a non-versioned table +set sql_safe_updates= 1; +--error ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE +delete from t1 limit 1; +set sql_safe_updates= default; +--echo # DELETE without WHERE removes only current rows +delete from t1; +select count(*) from t1; +select count(*) from t1 for system_time all; + +drop tables t1; + +--echo # Multi-table DELETE/UPDATE under sql_safe_updates: checks use the +--echo # written WHERE and the join access types, as before +create table t1 (x int) with system versioning +partition by system_time (partition p0 history, partition pn current); +create table t2 (y int); +insert into t1 values (1), (2); +insert into t2 values (1), (2); +set sql_safe_updates= 1; +--error ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE +delete t1 from t1, t2; +--error ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE +update t1, t2 set t1.x= t2.y; +--echo # LIMIT does not exempt multi-table UPDATE from the full join check +--error ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE +update t1, t2 set t1.x= t2.y order by t1.x limit 1; +set sql_safe_updates= default; +drop tables t1, t2; + +--echo # As for a non-versioned table, an index on row_end does not exempt +--echo # UPDATE/DELETE without WHERE: UPDATE needs LIMIT, DELETE is rejected +create table t1 ( + x int, + ts timestamp(6) as row start invisible, + te timestamp(6) as row end invisible, + period for system_time (ts, te), + key (te) +) with system versioning +partition by system_time (partition p0 history, partition pn current); +insert into t1 values (1), (2); +set sql_safe_updates= 1; +--error ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE +update t1 set x= x + 1; +--error ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE +delete from t1; +update t1 set x= x + 1 limit 1; +--error ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE +delete from t1 limit 1; +set sql_safe_updates= default; +drop table t1; + +--echo # Corner cases: outer and sysvar-set FOR SYSTEM_TIME +set timestamp= unix_timestamp('2000-01-01 00:00:00'); +create or replace table t1 (id int) +with system versioning +partition by system_time interval 1 hour +starts timestamp'2000-01-01 00:00:00' ( + partition p0 history, + partition p1 history, + partition pn current); +insert into t1 values (1); +set timestamp= unix_timestamp('2000-01-01 01:30:00'); +update t1 set id= 2; +--echo # implicit query reads only the current partition (id=2): +select id from t1; +--echo # outer FOR SYSTEM_TIME must propagate into derived t1 and read history (id=1): +select * from (select id from t1) for system_time as of timestamp'2000-01-01 01:00:00' as dt; +--echo # system_versioning_asof must be honored and read history (id=1): +set system_versioning_asof= '2000-01-01 01:00:00'; +select id from t1; +set system_versioning_asof= default; +--echo # re-execution reads only the current partition (id=2): +prepare s from 'select id from t1'; +execute s; +execute s; +deallocate prepare s; +create procedure sp() select id from t1; +call sp; +call sp; +drop procedure sp; +--echo # the same through merged derived table and view (id=2): +create view v1 as select id from t1; +prepare s from 'select id from (select id from t1) dt'; +execute s; +execute s; +deallocate prepare s; +prepare s from 'select id from v1'; +execute s; +execute s; +deallocate prepare s; +create procedure sp() select id from (select id from t1) dt; +call sp; +call sp; +drop procedure sp; +drop view v1; +--echo # re-execution with system_versioning_asof reads history (id=1): +set system_versioning_asof= '2000-01-01 01:00:00'; +prepare s from 'select id from t1'; +execute s; +execute s; +deallocate prepare s; +set system_versioning_asof= default; +set timestamp= default; +drop table t1; + +--echo # End of 11.4 tests + set global innodb_stats_persistent= @save_persistent; --source suite/versioning/common_finish.inc diff --git a/mysql-test/suite/versioning/t/sysvars.test b/mysql-test/suite/versioning/t/sysvars.test index 91dd278a2e1..de3ddf758a5 100644 --- a/mysql-test/suite/versioning/t/sysvars.test +++ b/mysql-test/suite/versioning/t/sysvars.test @@ -204,3 +204,21 @@ DROP TABLE t1; SET system_versioning_asof= DEFAULT; --echo # End of 10.4 tests + +--echo # +--echo # MDEV-17613 Read sole now-partition for non-historical versioned query +--echo # +--echo # system_versioning_asof does not act as explicit FOR SYSTEM_TIME AS OF +create table t1 (x int) with system versioning; +insert into t1 values (1); +set system_versioning_asof= '2000-01-01 00:00:00'; +--echo # Allowed, unlike explicit FOR SYSTEM_TIME AS OF: +select * from t1 for update; +--error ER_TABLE_NOT_LOCKED_FOR_WRITE +select * from t1 for system_time as of timestamp'2000-01-01 00:00:00' for update; +--echo # Not printed as FOR SYSTEM_TIME AS OF: +explain extended select * from t1; +set system_versioning_asof= default; +drop table t1; + +--echo # End of 10.11 tests diff --git a/sql/opt_range.cc b/sql/opt_range.cc index 67801e416ee..c88d17037b2 100644 --- a/sql/opt_range.cc +++ b/sql/opt_range.cc @@ -4119,12 +4119,14 @@ static void dbug_print_singlepoint_range(SEL_ARG **start, uint num); @param[in] part_info->read_partitions Candidate partitions to scan; normally all partitions (or the - PARTITION() set), possibly already restricted (e.g. to the - current partition of a versioned table) + PARTITION() set) @param[out] part_info->read_partitions Narrowed to the partitions that may hold records matching the - condition; never widened + condition. For a versioned table with history partitions + accessed without FOR SYSTEM_TIME, PARTITION() and + @@system_versioning_asof, also narrowed to the now-partition, + with or without a condition. Never widened. @note This function assumes that lock_partitions are setup when it is invoked. The function analyzes the condition, finds partitions that @@ -4156,6 +4158,28 @@ bool prune_partitions(THD *thd, TABLE *table, Item *pprune_cond) if (!part_info) DBUG_RETURN(FALSE); /* not a partitioned table */ + /* + Read only now_part, as vers_setup_conds() did not add the row_end condition. + Done here as prune_partitions() runs on each execution, also for merged + derived tables and views, for which vers_setup_conds() is not called + again. + */ + if (TABLE_LIST *tl= table->pos_in_table_list) + { + if (part_info->vers_info && !tl->partition_names && + !tl->vers_conditions.was_set() && !tl->vers_conditions.from_sysvar) + { + DBUG_ASSERT(part_info->vers_info->initialized()); + uint32 end; + const uint32 start= + part_info->vers_info->now_part->bitmap_range(part_info->num_subparts, + end); + for (uint32 i= 0; i < part_info->read_partitions.n_bits; i++) + if (i < start || i >= end) + bitmap_clear_bit(&part_info->read_partitions, i); + } + } + /* Must be a subset of the locked partitions. lock_partitions contains the partitions marked by explicit partition diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 14ea4a545be..97ed619a584 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -989,6 +989,7 @@ bool vers_select_conds_t::init_from_sysvar(THD *thd) { vers_asof_timestamp_t &in= thd->variables.vers_asof_timestamp; type= (vers_system_time_t) in.type; + from_sysvar= (type != SYSTEM_TIME_UNSPECIFIED); delete_history= false; start.unit= VERS_TIMESTAMP; if (type != SYSTEM_TIME_UNSPECIFIED && type != SYSTEM_TIME_ALL) @@ -1311,6 +1312,21 @@ int SELECT_LEX::vers_setup_conds(THD *thd, TABLE_LIST *tables) DBUG_RETURN(-1); } +#ifdef WITH_PARTITION_STORAGE_ENGINE + partition_info *part_info= table->table->part_info; + if (part_info && part_info->vers_info && + !table->partition_names && + !vers_conditions.was_set() && + !vers_conditions.from_sysvar) + { + /* + No row_end condition: prune_partitions() restricts read_partitions + to now_part on each execution, with the same conditions. + */ + vers_conditions.set_all(); + } +#endif + if (vers_conditions.is_set()) { if (vers_conditions.was_set() && diff --git a/sql/table.h b/sql/table.h index 0675682646c..88aa9096d66 100644 --- a/sql/table.h +++ b/sql/table.h @@ -2285,11 +2285,47 @@ public: struct vers_select_conds_t { + /* + Effective system-time selection used to build the actual row condition + (row_start/row_end predicate). is_set() tests it against SYSTEM_TIME_UNSPECIFIED. + set_all() forces it to SYSTEM_TIME_ALL in three situations: + + 1) once the row_start/row_end predicate for the requested time has been + generated and attached to the query - marking it applied so a later pass + or the next prepared-statement execution neither rebuilds nor re-applies + it (guarded by "if (type == SYSTEM_TIME_ALL) continue"); + + 2) a versioned table whose history is stored in partitions is queried with + an explicit PARTITION(...) clause - the named partitions are read as-is, + without a versioning predicate; + + 3) the same kind of table accessed without FOR SYSTEM_TIME, PARTITION(...) + and @@system_versioning_asof - + read_partitions is narrowed to the now-partition, which takes the place + of the row_end predicate. + */ vers_system_time_t type; + /* + Originally requested system-time selection, set by init() from an explicit + FOR SYSTEM_TIME or DELETE HISTORY clause, or copied with the conditions of + an outer derived table or view. Unlike 'type' it is NOT overwritten by + set_all(), so it still reflects the request after the predicate has been + applied. + Tested by was_set(); used by print() to reconstruct the FOR SYSTEM_TIME + clause. + */ vers_system_time_t orig_type; bool used:1; bool delete_history:1; bool has_param:1; + /* + Set by init_from_sysvar() when @@system_versioning_asof supplied the + selection. Tested to keep the now-partition narrowing off, so the query + still reads history. Unlike orig_type it affects neither print() nor + the write lock check, and it persists to prepared-statement + re-execution, where init_from_sysvar() is skipped. + */ + bool from_sysvar:1; Vers_history_point start; Vers_history_point end; Lex_ident name; @@ -2306,6 +2342,7 @@ struct vers_select_conds_t used= false; delete_history= false; has_param= false; + from_sysvar= false; start.empty(); end.empty(); } @@ -2321,6 +2358,7 @@ struct vers_select_conds_t delete_history= (type == SYSTEM_TIME_HISTORY || type == SYSTEM_TIME_BEFORE); has_param= false; + from_sysvar= false; start= _start; end= _end; name= _name; From cfc6a0e7333ccce58dbb68feac51675a32d33049 Mon Sep 17 00:00:00 2001 From: Aleksey Midenkov Date: Fri, 16 Oct 2026 15:11:21 +0300 Subject: [PATCH 11/28] MDEV-17613 Test cleanup: sorted result for versioning.partition Avoid result fluctuations with different storage engines. --- .../suite/versioning/r/partition,heap.rdiff | 9 +---- .../suite/versioning/r/partition.result | 10 +++--- mysql-test/suite/versioning/t/partition.test | 34 +++++++++++++++++++ 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/mysql-test/suite/versioning/r/partition,heap.rdiff b/mysql-test/suite/versioning/r/partition,heap.rdiff index e055a2c6047..b57ae891704 100644 --- a/mysql-test/suite/versioning/r/partition,heap.rdiff +++ b/mysql-test/suite/versioning/r/partition,heap.rdiff @@ -221,14 +221,7 @@ # INSERT .. ON DUPLICATE KEY UPDATE (ODKU) set timestamp= unix_timestamp('2000-01-01 00:00:00'); create or replace table t1 (x int primary key) with system versioning -@@ -3657,13 +3450,13 @@ - delete from t1 where id = 1; - select * from t1; - id --0 - 2 -+0 - # Below there should be: Select tables optimized away +@@ -3663,7 +3456,7 @@ # (except heap combination) explain extended select max(id) from t1; id select_type table type possible_keys key key_len ref rows filtered Extra diff --git a/mysql-test/suite/versioning/r/partition.result b/mysql-test/suite/versioning/r/partition.result index 293131fca0c..52db2d45a6d 100644 --- a/mysql-test/suite/versioning/r/partition.result +++ b/mysql-test/suite/versioning/r/partition.result @@ -373,10 +373,10 @@ set timestamp=unix_timestamp('2001-02-04 10:30:10'); update t1 set i=6 where i=5; select subpartition_name, partition_description, table_rows from information_schema.partitions where table_schema='test' and table_name='t1'; subpartition_name partition_description table_rows -p1sp0 2001-02-04 00:00:00 1 -p1sp1 2001-02-04 00:00:00 0 p0sp0 2001-02-05 00:00:00 1 p0sp1 2001-02-05 00:00:00 1 +p1sp0 2001-02-04 00:00:00 1 +p1sp1 2001-02-04 00:00:00 0 p2sp0 2001-02-06 00:00:00 0 p2sp1 2001-02-06 00:00:00 0 pnsp0 CURRENT 0 @@ -386,8 +386,8 @@ i 1 select * from t1 partition (p0); i -5 2 +5 select * from t1 partition (p2); i alter table t1 rebuild partition p0, p1, p2; @@ -396,8 +396,8 @@ i 1 select * from t1 partition (p0); i -5 2 +5 select * from t1 partition (p2); i ## pruning check @@ -2217,9 +2217,9 @@ t1 CREATE TABLE `t1` ( PARTITION `pn` CURRENT ENGINE = X) select * from t1; x -3 13 23 +3 select * from t1 for system_time all order by x; x 3 diff --git a/mysql-test/suite/versioning/t/partition.test b/mysql-test/suite/versioning/t/partition.test index e705cc84679..f58695e76f8 100644 --- a/mysql-test/suite/versioning/t/partition.test +++ b/mysql-test/suite/versioning/t/partition.test @@ -26,14 +26,17 @@ partition by range columns (x) ( partition p1 values less than (1000)); insert into t1 values (3), (300); +--sorted_result select * from t1; select * from t1 partition (p0); select * from t1 partition (p1); delete from t1; +--sorted_result select * from t1; select * from t1 partition (p0); select * from t1 partition (p1); +--sorted_result select * from t1 for system_time all; select * from t1 partition (p0) for system_time all; select * from t1 partition (p1) for system_time all; @@ -140,6 +143,7 @@ alter table t1 drop partition p1; --error ER_VERS_WRONG_PARTS alter table t1 drop partition p0; +--sorted_result select x from t1; --echo # rename works @@ -216,6 +220,7 @@ update t1 set x = x + 1; --source suite/versioning/wait_system_clock.inc +--sorted_result execute select_p0; execute select_pn; @@ -251,11 +256,14 @@ show create table t1; alter table t1 drop partition non_existent; insert into t1 values (1), (2), (3), (4), (5), (6); +--sorted_result select * from t1 partition (pn); delete from t1 where x < 4; delete from t1; --echo # You see warning above ^ +--sorted_result select * from t1 partition (p0); +--sorted_result select * from t1 partition (p1); insert into t1 values (7), (8); @@ -288,13 +296,17 @@ set @ts=(select partition_description from information_schema.partitions where table_schema='test' and table_name='t1' and partition_name='p0'); alter table t1 add column b int; +--sorted_result select partition_name,partition_ordinal_position,partition_method,timediff(partition_description, @ts) from information_schema.partitions where table_schema='test' and table_name='t1'; alter table t1 add partition (partition p1 history, partition p2 history); +--sorted_result select partition_name,partition_ordinal_position,partition_method,timediff(partition_description, @ts) from information_schema.partitions where table_schema='test' and table_name='t1'; alter table t1 drop partition p0; +--sorted_result select partition_name,partition_ordinal_position,partition_method,timediff(partition_description, @ts) from information_schema.partitions where table_schema='test' and table_name='t1'; --error ER_VERS_DROP_PARTITION_INTERVAL alter table t1 drop partition p2; +--sorted_result select partition_name,partition_ordinal_position,partition_method,timediff(partition_description, @ts) from information_schema.partitions where table_schema='test' and table_name='t1'; # @@ -310,7 +322,9 @@ insert t1 values (1); delete from t1; set timestamp=unix_timestamp('2001-02-04 10:20:50'); insert t1 values (2); delete from t1; +--sorted_result select subpartition_name, partition_description, table_rows from information_schema.partitions where table_schema='test' and table_name='t1'; +--sorted_result select * from t1 partition (p1); set timestamp=unix_timestamp('2001-02-04 10:20:55'); @@ -320,13 +334,16 @@ insert t1 values (4),(5); set timestamp=unix_timestamp('2001-02-04 10:30:10'); update t1 set i=6 where i=5; +--sorted_result select subpartition_name, partition_description, table_rows from information_schema.partitions where table_schema='test' and table_name='t1'; select * from t1 partition (p1); +--sorted_result select * from t1 partition (p0); select * from t1 partition (p2); alter table t1 rebuild partition p0, p1, p2; select * from t1 partition (p1); +--sorted_result select * from t1 partition (p0); select * from t1 partition (p2); @@ -466,6 +483,7 @@ set timestamp= unix_timestamp('2001-01-01 00:00:02'); update t1 set i= i + 1; select *, row_end from t1 partition (p0); +--sorted_result select *, row_end from t1 partition (p1); set timestamp= unix_timestamp('2000-01-01 00:00:00'); @@ -484,6 +502,7 @@ update t1 set i= i + 1; set timestamp= unix_timestamp('2000-01-04 00:00:01'); update t1 set i= i + 1; +--sorted_result select *, row_end from t1 partition (p0); select *, row_end from t1 partition (p1); @@ -519,7 +538,9 @@ subpartition by key (x) subpartitions 2; insert into t1 (x) values (1), (2), (3), (4), (5); +--sorted_result select * from t1 partition (pnsp0); +--sorted_result select * from t1 partition (pnsp1); delete from t1 where x < 3; @@ -529,6 +550,7 @@ delete from t1; --echo # You see warning above ^ (no matter if nothing was deleted) select * from t1 partition (p0sp0); select * from t1 partition (p0sp1); +--sorted_result select * from t1 partition (p1sp0); select * from t1 partition (p1sp1); @@ -873,6 +895,7 @@ create or replace table t1 ( insert into t1 (x) values (1), (2), (3), (4); update t1 set a= 'foo' limit 3; update t1 set a= 'bar' limit 4; +--sorted_result select * from t1; drop table t1; @@ -930,6 +953,7 @@ create or replace table t1 (pk int primary key, f int) engine=innodb partition by key() partitions 2; insert into t1 values (1,10),(2,20); --echo # expected to hit same partition +--sorted_result select * from t1 partition (p0); alter table t1 drop system versioning; @@ -1141,17 +1165,20 @@ replace into t1 select seq from seq_1_to_70; # Puts 60 rows into p1 replace into t1 select seq from seq_1_to_60; +--sorted_result select partition_name, table_rows from information_schema.partitions where table_name = 't1'; rollback; +--sorted_result select partition_name, table_rows from information_schema.partitions where table_name = 't1'; # Should put 10 rows into the empty partition p0 replace into t1 select seq from seq_1_to_10; +--sorted_result select partition_name, table_rows from information_schema.partitions where table_name = 't1'; @@ -1633,6 +1660,7 @@ starts '2000-01-01 00:00:00'; --replace_result $default_engine DEFAULT_ENGINE show create table t1; select * from t1 partition (p0); +--sorted_result select * from t1 partition (p1); select * from t1 partition (p2); select * from t1 partition (pn); @@ -1648,6 +1676,7 @@ update t1 set x= x + 1; update t1 set x= x + 1; --echo # You see warning above ^ select * from t1 partition (p0); +--sorted_result select * from t1 partition (p1); select * from t1 partition (p2); select * from t1 partition (p3); @@ -1802,6 +1831,7 @@ select * from tp1 for system_time all; --replace_result $default_engine X ' PAGE_CHECKSUM=1' '' show create table t1; +--sorted_result select * from t1; select * from t1 for system_time all order by x; @@ -2656,6 +2686,7 @@ insert into t1 (a,row_start,row_end) values ('p1', '2021-09-30', '2021-10-01 10:00:00'), ('overflows, so also p1','2021-09-30', '2021-10-10 10:00:00'), ('pn, current', '2021-09-30', '2038-01-19 03:14:07.999999'); +--sorted_result select table_name,partition_name,partition_ordinal_position,partition_method,partition_description,table_rows from information_schema.partitions where table_schema='test'; drop table t1; @@ -2798,6 +2829,7 @@ update t1 set x= 3; --echo # CREATE result: row 1 got into p1 select *, row_start, row_end from t1 partition (p0); +--sorted_result select *, row_start, row_end from t1 partition (p1); flush tables; --echo # For CREATE pruning is affected by current timestamp, but SELECT works @@ -2817,6 +2849,7 @@ partition by system_time interval 1 hour ( --echo # ALTER result: row 1 got into p1 Select *, row_start, row_end from t1 partition (p0); +--sorted_result Select *, row_start, row_end from t1 partition (p1); flush tables; Explain partitions select * from t1 for system_time as of '2000-01-01 00:59:59'; @@ -2983,6 +3016,7 @@ partition by system_time interval 1 month ( insert t1 values (1), (2), (0); delete from t1 where id = 1; +--sorted_result select * from t1; --echo # Below there should be: Select tables optimized away --echo # (except heap combination) From 26af7ea811b896ec25901f0c1dc780df631f60f2 Mon Sep 17 00:00:00 2001 From: Dave Gosselin Date: Thu, 8 Oct 2026 15:07:54 -0400 Subject: [PATCH 12/28] MDEV-41146: Merge pseudo_bits_cond into outer_ref_cond A join of only const tables stored the WHERE conjuncts that refer to a column of an outer query or call rand() in pseudo_bits_cond. The executor read pseudo_bits_cond only when the query needed no temporary table, and read outer_ref_cond otherwise. A window function or DISTINCT with ROLLUP needs a temporary table, so those conjuncts were never checked, and the query returned rows that the WHERE clause rejects. pseudo_bits_cond came from the fix for LP bug 908269 in 2012. For a join of only const tables, that fix also collected conjuncts that call rand(), and it stored them in a new field without stating a reason. The code that filled the two fields and the code that read them chose between them by the same test. The tests have differed since MDEV-8646 in 2016, which added the temporary table condition to the executor's test. Later changes only display the field, in EXPLAIN FORMAT=JSON (MDEV-19714) and in the optimizer trace (MDEV-21095). In MDEV-19714, its author wrote that it was not clear why pseudo_bits_cond was needed in addition to outer_ref_cond. pseudo_bits_cond is removed, and outer_ref_cond holds these conjuncts for every join. The executor checks it on both paths, so where the conjuncts are stored no longer depends on the executor's test. Conjuncts that call rand() are still collected only for a join of only const tables. EXPLAIN FORMAT=JSON shows these conjuncts as outer_ref_condition instead of pseudo_bits_condition. Co-Authored-By: Claude Opus 5.5 (1M context) --- mysql-test/main/group_by.result | 4 +- mysql-test/main/olap.result | 15 ++++ mysql-test/main/olap.test | 16 +++++ mysql-test/main/subselect_no_semijoin.result | 2 +- mysql-test/main/win.result | 24 +++++++ mysql-test/main/win.test | 25 +++++++ .../encryption/r/tempfiles_encrypted.result | 24 +++++++ sql/sql_explain.cc | 5 -- sql/sql_explain.h | 1 - sql/sql_select.cc | 69 +++++++++---------- sql/sql_select.h | 11 ++- 11 files changed, 148 insertions(+), 48 deletions(-) diff --git a/mysql-test/main/group_by.result b/mysql-test/main/group_by.result index 26e1ba14e95..e221744fc47 100644 --- a/mysql-test/main/group_by.result +++ b/mysql-test/main/group_by.result @@ -3038,7 +3038,7 @@ EXPLAIN { "query_block": { "select_id": 1, - "pseudo_bits_condition": "if(uuid_short(),NULL,1)", + "outer_ref_condition": "if(uuid_short(),NULL,1)", "nested_loop": [ { "table": { @@ -3064,7 +3064,7 @@ EXPLAIN { "query_block": { "select_id": 1, - "pseudo_bits_condition": "10 = rand()", + "outer_ref_condition": "10 = rand()", "nested_loop": [ { "table": { diff --git a/mysql-test/main/olap.result b/mysql-test/main/olap.result index 22186ee085c..5d9060ec733 100644 --- a/mysql-test/main/olap.result +++ b/mysql-test/main/olap.result @@ -905,3 +905,18 @@ NULL NULL DROP TABLE t1; # End of 10.3 Tests +# +# MDEV-41146: WHERE is not checked when every table is const and +# the query needs a temporary table +# +create table t1 (a int primary key); +insert into t1 values (1),(2),(3); +# rand() < 0 is never true, so the result is empty. +select distinct a from t1 where a=1 and rand() < 0 group by a with rollup; +a +select distinct a from t1 where a=1 and rand() >= 0 group by a with rollup; +a +1 +NULL +drop table t1; +# End of 11.4 tests diff --git a/mysql-test/main/olap.test b/mysql-test/main/olap.test index 3b4852ebbdc..67018ecf8be 100644 --- a/mysql-test/main/olap.test +++ b/mysql-test/main/olap.test @@ -533,3 +533,19 @@ SELECT NULLIF( CAST( 'foo' AS DATE ), NULL & 'bar' ) AS f FROM t1 GROUP BY f WIT DROP TABLE t1; --echo # End of 10.3 Tests + +--echo # +--echo # MDEV-41146: WHERE is not checked when every table is const and +--echo # the query needs a temporary table +--echo # + +create table t1 (a int primary key); +insert into t1 values (1),(2),(3); + +--echo # rand() < 0 is never true, so the result is empty. +select distinct a from t1 where a=1 and rand() < 0 group by a with rollup; +select distinct a from t1 where a=1 and rand() >= 0 group by a with rollup; + +drop table t1; + +--echo # End of 11.4 tests diff --git a/mysql-test/main/subselect_no_semijoin.result b/mysql-test/main/subselect_no_semijoin.result index 069bc7f9f9b..65eb2d81735 100644 --- a/mysql-test/main/subselect_no_semijoin.result +++ b/mysql-test/main/subselect_no_semijoin.result @@ -7668,7 +7668,7 @@ EXPLAIN { "query_block": { "select_id": 2, - "pseudo_bits_condition": "1 = t1.a or (1,(subquery#3))", + "outer_ref_condition": "1 = t1.a or (1,(subquery#3))", "nested_loop": [ { "table": { diff --git a/mysql-test/main/win.result b/mysql-test/main/win.result index 7af4a87d3eb..718e772a560 100644 --- a/mysql-test/main/win.result +++ b/mysql-test/main/win.result @@ -4866,3 +4866,27 @@ DROP TABLE t1; # # End of 10.11 tests # +# +# MDEV-41146: WHERE is not checked when every table is const and +# the query needs a temporary table +# +create table t1 (a int primary key); +insert into t1 values (1),(2),(3); +create table t2 (b int); +insert into t2 values (1),(10); +# rand() < 0 is never true, so the result is empty. +select a, row_number() over () from t1 where a=1 and rand() < 0; +a row_number() over () +select a, row_number() over () from t1 where a=1 and rand() >= 0; +a row_number() over () +1 1 +# t2.b > 5 is true only for the row where t2.b is 10. +select b, (select row_number() over () from t1 where t1.a=1 and t2.b > 5) x +from t2; +b x +1 NULL +10 1 +drop table t1, t2; +# +# End of 11.4 tests +# diff --git a/mysql-test/main/win.test b/mysql-test/main/win.test index dd40e61d312..1da3dfa3598 100644 --- a/mysql-test/main/win.test +++ b/mysql-test/main/win.test @@ -3150,3 +3150,28 @@ DROP TABLE t1; --echo # --echo # End of 10.11 tests --echo # + +--echo # +--echo # MDEV-41146: WHERE is not checked when every table is const and +--echo # the query needs a temporary table +--echo # + +create table t1 (a int primary key); +insert into t1 values (1),(2),(3); +create table t2 (b int); +insert into t2 values (1),(10); + +--echo # rand() < 0 is never true, so the result is empty. +select a, row_number() over () from t1 where a=1 and rand() < 0; +select a, row_number() over () from t1 where a=1 and rand() >= 0; + +--echo # t2.b > 5 is true only for the row where t2.b is 10. +--sorted_result +select b, (select row_number() over () from t1 where t1.a=1 and t2.b > 5) x +from t2; + +drop table t1, t2; + +--echo # +--echo # End of 11.4 tests +--echo # diff --git a/mysql-test/suite/encryption/r/tempfiles_encrypted.result b/mysql-test/suite/encryption/r/tempfiles_encrypted.result index f6180fcfa28..60bf65d3cad 100644 --- a/mysql-test/suite/encryption/r/tempfiles_encrypted.result +++ b/mysql-test/suite/encryption/r/tempfiles_encrypted.result @@ -4873,6 +4873,30 @@ DROP TABLE t1; # End of 10.11 tests # # +# MDEV-41146: WHERE is not checked when every table is const and +# the query needs a temporary table +# +create table t1 (a int primary key); +insert into t1 values (1),(2),(3); +create table t2 (b int); +insert into t2 values (1),(10); +# rand() < 0 is never true, so the result is empty. +select a, row_number() over () from t1 where a=1 and rand() < 0; +a row_number() over () +select a, row_number() over () from t1 where a=1 and rand() >= 0; +a row_number() over () +1 1 +# t2.b > 5 is true only for the row where t2.b is 10. +select b, (select row_number() over () from t1 where t1.a=1 and t2.b > 5) x +from t2; +b x +1 NULL +10 1 +drop table t1, t2; +# +# End of 11.4 tests +# +# # MDEV-23867: select crash in compute_window_func # set @save_sort_buffer_size=@@sort_buffer_size; diff --git a/sql/sql_explain.cc b/sql/sql_explain.cc index 4e67f1a2343..6b1eb02f2c9 100644 --- a/sql/sql_explain.cc +++ b/sql/sql_explain.cc @@ -1200,11 +1200,6 @@ void Explain_select::print_explain_json(Explain_query *query, writer->add_member("outer_ref_condition"); write_item(writer, outer_ref_cond); } - if (pseudo_bits_cond) - { - writer->add_member("pseudo_bits_condition"); - write_item(writer, pseudo_bits_cond); - } /* we do not print HAVING which always evaluates to TRUE */ if (having || (having_value == Item::COND_FALSE)) diff --git a/sql/sql_explain.h b/sql/sql_explain.h index 1aff4c05e60..18e2499acbd 100644 --- a/sql/sql_explain.h +++ b/sql/sql_explain.h @@ -248,7 +248,6 @@ public: /* Expensive constant condition */ Item *exec_const_cond; Item *outer_ref_cond; - Item *pseudo_bits_cond; /* HAVING condition */ Item *having; diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 97ed619a584..6a07e34be0c 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -570,7 +570,7 @@ void JOIN::init(THD *thd_arg, List &fields_arg, first_select= sub_select; group_sent= 0; - outer_ref_cond= pseudo_bits_cond= NULL; + outer_ref_cond= NULL; in_to_exists_where= NULL; in_to_exists_having= NULL; emb_sjm_nest= NULL; @@ -14588,40 +14588,37 @@ make_join_select(JOIN *join,SQL_SELECT *select,COND *cond) join->exec_const_cond= const_cond; } - if (join->table_count != join->const_tables) { - COND *outer_ref_cond= make_cond_for_table(thd, cond, - join->const_table_map | - OUTER_REF_TABLE_BIT, - OUTER_REF_TABLE_BIT, - -1, FALSE, FALSE); - if (outer_ref_cond) + /* + Collect the conjuncts that are checked once for each execution + of the query, before the join reads a table that is not const. + Each refers to at least one pseudo table in pseudo_bits and to + no table that is not const. + + A conjunct that refers to a column of an outer query is always + collected, because that column has one value for each + execution. rand() takes a new value on each call, and a + conjunct that calls it is checked for each row of the last top + level table that is neither const nor a materialized semijoin + nest. A join of only const tables has no such table and + returns at most one row, and its rand() conjuncts are collected + here. + */ + table_map pseudo_bits= OUTER_REF_TABLE_BIT; + if (join->only_const_tables()) + pseudo_bits= PSEUDO_TABLE_BITS; + COND *pseudo_cond= make_cond_for_table(thd, cond, + join->const_table_map | + pseudo_bits, + pseudo_bits, + -1, FALSE, FALSE); + if (pseudo_cond) { - add_cond_and_fix(thd, &outer_ref_cond, join->outer_ref_cond); - join->outer_ref_cond= outer_ref_cond; + add_cond_and_fix(thd, &pseudo_cond, join->outer_ref_cond); + join->outer_ref_cond= pseudo_cond; Json_writer_object trace(thd); - trace.add("outer_ref_cond", outer_ref_cond); - } - else if (thd->is_error()) - DBUG_RETURN(1); - } - else - { - COND *pseudo_bits_cond= - make_cond_for_table(thd, cond, - join->const_table_map | - PSEUDO_TABLE_BITS, - PSEUDO_TABLE_BITS, - -1, FALSE, FALSE); - if (pseudo_bits_cond) - { - add_cond_and_fix(thd, &pseudo_bits_cond, - join->pseudo_bits_cond); - join->pseudo_bits_cond= pseudo_bits_cond; - - Json_writer_object trace(thd); - trace.add("pseudo_bits_cond", pseudo_bits_cond); + trace.add("outer_ref_cond", pseudo_cond); } else if (thd->is_error()) DBUG_RETURN(1); @@ -24056,12 +24053,11 @@ do_select(JOIN *join, Procedure *procedure) HAVING will be checked after processing aggregate functions, But WHERE should checked here (we alredy have read tables). Notice that make_join_select() splits all conditions in this case - into two groups exec_const_cond and outer_ref_cond. - If join->table_count == join->const_tables then it is - sufficient to check only the condition pseudo_bits_cond. + into two groups exec_const_cond and outer_ref_cond. exec_const_cond + is checked before the join starts, and it is sufficient to check + only the condition outer_ref_cond. */ - DBUG_ASSERT(join->outer_ref_cond == NULL); - if (!join->pseudo_bits_cond || join->pseudo_bits_cond->val_bool()) + if (!join->outer_ref_cond || join->outer_ref_cond->val_bool()) { // HAVING will be checked by end_select error= (*end_select)(join, 0, 0); @@ -31667,7 +31663,6 @@ int JOIN::save_explain_data_intern(Explain_query *output, xpl_sel->exec_const_cond= exec_const_cond; xpl_sel->outer_ref_cond= outer_ref_cond; - xpl_sel->pseudo_bits_cond= pseudo_bits_cond; if (tmp_having) xpl_sel->having= tmp_having; else diff --git a/sql/sql_select.h b/sql/sql_select.h index 2711b1138b1..f7563577f0f 100644 --- a/sql/sql_select.h +++ b/sql/sql_select.h @@ -1648,8 +1648,15 @@ public: ORDER *order, *group_list, *proc_param; //hold parameters of mysql_select COND *conds; // ---"--- Item *conds_history; // store WHERE for explain - COND *outer_ref_cond; /// *join_list; ///< list of joined tables in reverse order COND_EQUAL *cond_equal; From 18c80e48d6718db1bdd4c029c873a89409dfba5f Mon Sep 17 00:00:00 2001 From: Dave Gosselin Date: Thu, 8 Oct 2026 15:09:25 -0400 Subject: [PATCH 13/28] MDEV-41146: Check rand() in WHERE when top level base tables are const A WHERE conjunct that calls rand() but doesn't refer to a column, such as rand() < 0, is checked on the last non-const top level base table in the join order. A join of only const tables has no such table and returns at most one row, so the conjunct is checked once before the join starts, together with the conditions on columns of an outer query. When every top level base table is const and materialized semi-joins follow them, the join order also has no non-const top level base table, but only_const_tables() is false. The conjunct was never checked, and such a query gave wrong results. The check before the join now applies whenever the join order has no non-const top level base table, which is the same condition under which no table in the join order receives the conjunct. Such a join also returns at most one row. Co-Authored-By: Claude Opus 5.5 (1M context) --- mysql-test/main/olap.result | 3 +- mysql-test/main/olap.test | 2 +- mysql-test/main/subselect_sj_mat_rand.opt | 1 + mysql-test/main/subselect_sj_mat_rand.result | 421 +++++++++++++++++++ mysql-test/main/subselect_sj_mat_rand.test | 214 ++++++++++ sql/sql_select.cc | 38 +- sql/sql_select.h | 4 +- 7 files changed, 668 insertions(+), 15 deletions(-) create mode 100644 mysql-test/main/subselect_sj_mat_rand.opt create mode 100644 mysql-test/main/subselect_sj_mat_rand.result create mode 100644 mysql-test/main/subselect_sj_mat_rand.test diff --git a/mysql-test/main/olap.result b/mysql-test/main/olap.result index 5d9060ec733..514bad04f63 100644 --- a/mysql-test/main/olap.result +++ b/mysql-test/main/olap.result @@ -911,9 +911,10 @@ DROP TABLE t1; # create table t1 (a int primary key); insert into t1 values (1),(2),(3); -# rand() < 0 is never true, so the result is empty. +# rand() < 0 is never true, so only the ROLLUP total row is returned. select distinct a from t1 where a=1 and rand() < 0 group by a with rollup; a +NULL select distinct a from t1 where a=1 and rand() >= 0 group by a with rollup; a 1 diff --git a/mysql-test/main/olap.test b/mysql-test/main/olap.test index 67018ecf8be..cdf553c0b58 100644 --- a/mysql-test/main/olap.test +++ b/mysql-test/main/olap.test @@ -542,7 +542,7 @@ DROP TABLE t1; create table t1 (a int primary key); insert into t1 values (1),(2),(3); ---echo # rand() < 0 is never true, so the result is empty. +--echo # rand() < 0 is never true, so only the ROLLUP total row is returned. select distinct a from t1 where a=1 and rand() < 0 group by a with rollup; select distinct a from t1 where a=1 and rand() >= 0 group by a with rollup; diff --git a/mysql-test/main/subselect_sj_mat_rand.opt b/mysql-test/main/subselect_sj_mat_rand.opt new file mode 100644 index 00000000000..1af9e30741b --- /dev/null +++ b/mysql-test/main/subselect_sj_mat_rand.opt @@ -0,0 +1 @@ +--memory.optimizer_row_copy_cost=0 diff --git a/mysql-test/main/subselect_sj_mat_rand.result b/mysql-test/main/subselect_sj_mat_rand.result new file mode 100644 index 00000000000..692d522e4c9 --- /dev/null +++ b/mysql-test/main/subselect_sj_mat_rand.result @@ -0,0 +1,421 @@ +# +# MDEV-41146: rand() in WHERE is not checked when every base table +# is const and a materialized semi-join terminates the join order +# +# A WHERE conjunct that calls rand() and names no field is checked on +# the last top level base table that is not const. When every base +# table is const, the conjunct is checked once in outer_ref_condition. +# +create table t1 (a int primary key); +insert into t1 values (1),(2),(3); +create table t2 (b int primary key); +insert into t2 values (1),(2),(3),(4); +create table t3 (c int, d int); +insert into t3 values (1,1),(2,2),(3,3),(4,4),(5,5), +(6,6),(7,7),(8,8),(9,9),(10,10); +insert into t3 select c+10, d+10 from t3; +insert into t3 select c+20, d+20 from t3; +insert into t3 select c+40, d+40 from t3; +create table t4 (e int, f int); +insert into t4 select c, d from t3; +create table t5 (g int); +insert into t5 select c+1000 from t3; +set @save_optimizer_switch= @@optimizer_switch; +set @save_optimizer_where_cost= @@optimizer_where_cost; +set optimizer_switch='firstmatch=off,loosescan=off'; +set optimizer_where_cost=0; +# One const table. rand() < 0 is never true, so the result is empty. +explain +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) and rand() < 0; +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t1 const PRIMARY PRIMARY 4 const 1 Using index +1 PRIMARY eq_ref distinct_key distinct_key 4 func 1 +2 MATERIALIZED t3 ALL NULL NULL NULL NULL 80 Using where +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) and rand() < 0; +a +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) and rand() >= 0; +a +1 +# The IN-equality is still checked. t5 holds no value below 1001. +select t1.a from t1 where t1.a=1 and t1.a in (select g from t5) and rand() >= 0; +a +select t1.a from t1 where t1.a=1 and t1.a in (select g from t5) and rand() < 0; +a +# Two rand() conjuncts. outer_ref_condition shows both. +explain format=json +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) +and rand() < 0 and rand() < 2; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": "COST_REPLACED", + "outer_ref_condition": "rand() < 0 and rand() < 2", + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "const", + "possible_keys": ["PRIMARY"], + "key": "PRIMARY", + "key_length": "4", + "used_key_parts": ["a"], + "ref": ["const"], + "rows": 1, + "filtered": 100, + "using_index": true + } + }, + { + "table": { + "table_name": "", + "access_type": "eq_ref", + "possible_keys": ["distinct_key"], + "key": "distinct_key", + "key_length": "4", + "used_key_parts": ["c"], + "ref": ["func"], + "rows": 1, + "filtered": 100, + "materialized": { + "unique": 1, + "query_block": { + "select_id": 2, + "nested_loop": [ + { + "table": { + "table_name": "t3", + "access_type": "ALL", + "loops": 1, + "rows": 80, + "cost": "COST_REPLACED", + "filtered": 100, + "attached_condition": "t3.c = 1" + } + } + ] + } + } + } + } + ] + } +} +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) +and rand() < 0 and rand() < 2; +a +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) +and rand() >= 0 and rand() < 2; +a +1 +# Two const tables joined, as in the original report. +explain +select t1.a x, t2.b y from t1 left join t2 on t1.a=t2.b +where t1.a=1 and t2.b=1 and t1.a in (select c from t3) and rand() < 0; +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t1 const PRIMARY PRIMARY 4 const 1 Using index +1 PRIMARY t2 const PRIMARY PRIMARY 4 const 1 Using index +1 PRIMARY eq_ref distinct_key distinct_key 4 func 1 +2 MATERIALIZED t3 ALL NULL NULL NULL NULL 80 Using where +select t1.a x, t2.b y from t1 left join t2 on t1.a=t2.b +where t1.a=1 and t2.b=1 and t1.a in (select c from t3) and rand() < 0; +x y +select t1.a x, t2.b y from t1 left join t2 on t1.a=t2.b +where t1.a=1 and t2.b=1 and t1.a in (select c from t3) and rand() >= 0; +x y +1 1 +# Two materialized nests terminate the join order. +explain +select t1.a from t1 +where t1.a=1 and t1.a in (select c from t3) and t1.a in (select e from t4) +and rand() < 0; +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t1 const PRIMARY PRIMARY 4 const 1 Using index +1 PRIMARY eq_ref distinct_key distinct_key 4 func 1 +1 PRIMARY eq_ref distinct_key distinct_key 4 func 1 +2 MATERIALIZED t3 ALL NULL NULL NULL NULL 80 Using where +3 MATERIALIZED t4 ALL NULL NULL NULL NULL 80 Using where +select t1.a from t1 +where t1.a=1 and t1.a in (select c from t3) and t1.a in (select e from t4) +and rand() < 0; +a +select t1.a from t1 +where t1.a=1 and t1.a in (select c from t3) and t1.a in (select e from t4) +and rand() >= 0; +a +1 +# The const table column is bigint and the subquery column is int. +create table t6 (a bigint primary key); +insert into t6 values (1),(2),(3); +explain +select t6.a from t6 where t6.a=1 and t6.a in (select c from t3) and rand() < 0; +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t6 const PRIMARY PRIMARY 8 const 1 Using index +1 PRIMARY eq_ref distinct_key distinct_key 4 func 1 +2 MATERIALIZED t3 ALL NULL NULL NULL NULL 80 +select t6.a from t6 where t6.a=1 and t6.a in (select c from t3) and rand() < 0; +a +select t6.a from t6 where t6.a=1 and t6.a in (select c from t3) and rand() >= 0; +a +1 +# An outer table that is not const precedes the nest. The conjunct +# is checked on that table. +create table t7 (a int); +insert into t7 values (1),(2),(3),(100); +explain +select t7.a from t7 where t7.a in (select c from t3) and rand() < 0; +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t7 ALL NULL NULL NULL NULL 4 Using where +1 PRIMARY eq_ref distinct_key distinct_key 4 func 1 +2 MATERIALIZED t3 ALL NULL NULL NULL NULL 80 +select t7.a from t7 where t7.a in (select c from t3) and rand() < 0; +a +select t7.a from t7 where t7.a in (select c from t3) and rand() >= 0; +a +1 +2 +3 +# The conjunct also reads a column of the enclosing query. It is true +# when t7.a is 4 or more, which holds only for the row where t7.a is +# 100. +explain +select t7.a, +(select t1.a from t1 +where t1.a=1 and t1.a in (select c from t3) and rand() < t7.a - 3) x +from t7; +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t7 ALL NULL NULL NULL NULL 4 +2 DEPENDENT SUBQUERY t1 const PRIMARY PRIMARY 4 const 1 Using index +2 DEPENDENT SUBQUERY eq_ref distinct_key distinct_key 4 func 1 +3 MATERIALIZED t3 ALL NULL NULL NULL NULL 80 Using where +select t7.a, +(select t1.a from t1 +where t1.a=1 and t1.a in (select c from t3) and rand() < t7.a - 3) x +from t7; +a x +1 NULL +100 1 +2 NULL +3 NULL +# rand(7) continues one sequence across the executions of the +# subquery. The conjunct takes one value for each row of t8, +# including the rows where t8.a is 100 and the lookup finds no match. +# A plan where t1 is not const checks the conjunct on t1 before the +# lookup and returns the same rows. +create table t8 (id int, a int); +insert into t8 values (1,1),(2,100),(3,2),(4,100),(5,3),(6,100),(7,4), +(8,100),(9,5),(10,100),(11,6),(12,7),(13,8),(14,9); +explain +select t8.id, +(select t1.a from t1 +where t1.a=1 and t8.a in (select c from t3) and rand(7) < 0.5) x +from t8; +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t8 ALL NULL NULL NULL NULL 14 +2 DEPENDENT SUBQUERY t1 const PRIMARY PRIMARY 4 const 1 Using index +2 DEPENDENT SUBQUERY eq_ref distinct_key distinct_key 4 func 1 +3 MATERIALIZED t3 ALL NULL NULL NULL NULL 80 +select t8.id, +(select t1.a from t1 +where t1.a=1 and t8.a in (select c from t3) and rand(7) < 0.5) x +from t8 order by t8.id; +id x +1 NULL +2 NULL +3 1 +4 NULL +5 NULL +6 NULL +7 1 +8 NULL +9 1 +10 NULL +11 NULL +12 1 +13 1 +14 NULL +# rand() in the WHERE of the subquery instead of the outer query. +# Every row of t9 has c=1. The first four values of rand(1) are not +# below 0.09 and the fifth is, so checking the condition on each row +# of t9 matches t1.a=1. A subquery that calls rand() is not converted +# to a semi-join, so the condition is not moved to the outer query and +# checked once there. Adding 0*t9.d makes the condition read a column +# of t9, which gives the same result in any plan. +create table t9 (c int, d int); +insert into t9 select 1, 1 from seq_1_to_80; +explain +select count(*) from t1 +where t1.a=1 and t1.a in (select c from t9 where rand(1) < 0.09); +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t1 const PRIMARY PRIMARY 4 const 1 Using index +1 PRIMARY const distinct_key distinct_key 4 const 1 +2 MATERIALIZED t9 ALL NULL NULL NULL NULL 80 Using where +select count(*) from t1 +where t1.a=1 and t1.a in (select c from t9 where rand(1) < 0.09); +count(*) +1 +select count(*) from t1 +where t1.a=1 and t1.a in (select c from t9 where rand(1) < 0.09 + 0*t9.d); +count(*) +1 +# The conjunct is checked once, in outer_ref_condition, only when +# the join returns at most one row. f1() counts its calls. With +# the const t1 and a lookup nest, f1() is called once. Adding t10, +# which holds two rows, makes the join return two rows, and f1() is +# called once for each. +create table t10 (b int); +insert into t10 values (1),(2); +create function f1() returns int not deterministic +begin +set @calls= @calls + 1; +return 1; +end| +explain +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) and f1() > 0; +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t1 const PRIMARY PRIMARY 4 const 1 Using index +1 PRIMARY eq_ref distinct_key distinct_key 4 func 1 +2 MATERIALIZED t3 ALL NULL NULL NULL NULL 80 Using where +set @calls= 0; +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) and f1() > 0; +a +1 +select @calls; +@calls +1 +explain +select t1.a, t10.b from t1, t10 +where t1.a=1 and t1.a in (select c from t3) and f1() > 0; +id select_type table type possible_keys key key_len ref rows Extra +1 PRIMARY t1 const PRIMARY PRIMARY 4 const 1 Using index +1 PRIMARY t10 ALL NULL NULL NULL NULL 2 Using where +1 PRIMARY eq_ref distinct_key distinct_key 4 func 1 +2 MATERIALIZED t3 ALL NULL NULL NULL NULL 80 Using where +set @calls= 0; +select t1.a, t10.b from t1, t10 +where t1.a=1 and t1.a in (select c from t3) and f1() > 0; +a b +1 1 +1 2 +select @calls; +@calls +2 +drop function f1; +# A materialized nest read by a scan precedes t11, which is not +# const, so the conjunct is checked on t11. The optimizer chooses +# the scan only with the statistics from ANALYZE, which give 200 +# rows of t11 for each value of a. +create table t11 (a int, key(a)); +insert into t11 select seq mod 10 from seq_1_to_2000; +create table t12 (b int); +insert into t12 select seq mod 10 from seq_1_to_20; +analyze table t11, t12 persistent for all; +Table Op Msg_type Msg_text +test.t11 analyze status Engine-independent statistics collected +test.t11 analyze status Table is already up to date +test.t12 analyze status Engine-independent statistics collected +test.t12 analyze status OK +explain format=json +select count(*) from t1, t11 +where t1.a=1 and t11.a in (select b from t12) and rand() < 0; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "cost": "COST_REPLACED", + "nested_loop": [ + { + "table": { + "table_name": "t1", + "access_type": "const", + "possible_keys": ["PRIMARY"], + "key": "PRIMARY", + "key_length": "4", + "used_key_parts": ["a"], + "ref": ["const"], + "rows": 1, + "filtered": 100, + "using_index": true + } + }, + { + "table": { + "table_name": "", + "access_type": "ALL", + "possible_keys": ["distinct_key"], + "rows": 20, + "filtered": 100, + "materialized": { + "unique": 1, + "query_block": { + "select_id": 2, + "nested_loop": [ + { + "table": { + "table_name": "t12", + "access_type": "ALL", + "loops": 1, + "rows": 20, + "cost": "COST_REPLACED", + "filtered": 100, + "attached_condition": "t12.b is not null" + } + } + ] + } + } + } + }, + { + "table": { + "table_name": "t11", + "access_type": "ref", + "possible_keys": ["a"], + "key": "a", + "key_length": "5", + "used_key_parts": ["a"], + "ref": ["test.t12.b"], + "loops": 20, + "rows": 200, + "cost": "COST_REPLACED", + "filtered": 100, + "attached_condition": "rand() < 0", + "using_index": true + } + } + ] + } +} +select count(*) from t1, t11 +where t1.a=1 and t11.a in (select b from t12) and rand() < 0; +count(*) +0 +select count(*) from t1, t11 +where t1.a=1 and t11.a in (select b from t12) and rand() >= 0; +count(*) +2000 +# A prepared statement executed twice, with the bound for rand() +# passed as a parameter. The first execution returns no row and +# the second returns t1.a=1. +prepare stmt from "select t1.a from t1 +where t1.a=1 and t1.a in (select c from t3) and rand() < ?"; +execute stmt using 0; +a +execute stmt using 2; +a +1 +deallocate prepare stmt; +# The same query in a stored procedure called twice. +create procedure p1(x int) +select t1.a from t1 +where t1.a=1 and t1.a in (select c from t3) and rand() < x; +call p1(0); +a +call p1(2); +a +1 +drop procedure p1; +set optimizer_switch= @save_optimizer_switch; +set optimizer_where_cost= @save_optimizer_where_cost; +drop table t1,t2,t3,t4,t5,t6,t7,t8,t9,t10,t11,t12; +# End of 11.4 tests diff --git a/mysql-test/main/subselect_sj_mat_rand.test b/mysql-test/main/subselect_sj_mat_rand.test new file mode 100644 index 00000000000..be6c85739b2 --- /dev/null +++ b/mysql-test/main/subselect_sj_mat_rand.test @@ -0,0 +1,214 @@ +--source include/have_sequence.inc + +--echo # +--echo # MDEV-41146: rand() in WHERE is not checked when every base table +--echo # is const and a materialized semi-join terminates the join order +--echo # +--echo # A WHERE conjunct that calls rand() and names no field is checked on +--echo # the last top level base table that is not const. When every base +--echo # table is const, the conjunct is checked once in outer_ref_condition. +--echo # + +create table t1 (a int primary key); +insert into t1 values (1),(2),(3); +create table t2 (b int primary key); +insert into t2 values (1),(2),(3),(4); +create table t3 (c int, d int); +insert into t3 values (1,1),(2,2),(3,3),(4,4),(5,5), +(6,6),(7,7),(8,8),(9,9),(10,10); +insert into t3 select c+10, d+10 from t3; +insert into t3 select c+20, d+20 from t3; +insert into t3 select c+40, d+40 from t3; +create table t4 (e int, f int); +insert into t4 select c, d from t3; +create table t5 (g int); +insert into t5 select c+1000 from t3; + +set @save_optimizer_switch= @@optimizer_switch; +set @save_optimizer_where_cost= @@optimizer_where_cost; +set optimizer_switch='firstmatch=off,loosescan=off'; +set optimizer_where_cost=0; + +--echo # One const table. rand() < 0 is never true, so the result is empty. +explain +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) and rand() < 0; +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) and rand() < 0; +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) and rand() >= 0; + +--echo # The IN-equality is still checked. t5 holds no value below 1001. +select t1.a from t1 where t1.a=1 and t1.a in (select g from t5) and rand() >= 0; +select t1.a from t1 where t1.a=1 and t1.a in (select g from t5) and rand() < 0; + +--echo # Two rand() conjuncts. outer_ref_condition shows both. +--source include/explain-no-costs.inc +explain format=json +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) +and rand() < 0 and rand() < 2; +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) +and rand() < 0 and rand() < 2; +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) +and rand() >= 0 and rand() < 2; + +--echo # Two const tables joined, as in the original report. +explain +select t1.a x, t2.b y from t1 left join t2 on t1.a=t2.b +where t1.a=1 and t2.b=1 and t1.a in (select c from t3) and rand() < 0; +select t1.a x, t2.b y from t1 left join t2 on t1.a=t2.b +where t1.a=1 and t2.b=1 and t1.a in (select c from t3) and rand() < 0; +select t1.a x, t2.b y from t1 left join t2 on t1.a=t2.b +where t1.a=1 and t2.b=1 and t1.a in (select c from t3) and rand() >= 0; + +--echo # Two materialized nests terminate the join order. +explain +select t1.a from t1 +where t1.a=1 and t1.a in (select c from t3) and t1.a in (select e from t4) + and rand() < 0; +select t1.a from t1 +where t1.a=1 and t1.a in (select c from t3) and t1.a in (select e from t4) + and rand() < 0; +select t1.a from t1 +where t1.a=1 and t1.a in (select c from t3) and t1.a in (select e from t4) + and rand() >= 0; + +--echo # The const table column is bigint and the subquery column is int. +create table t6 (a bigint primary key); +insert into t6 values (1),(2),(3); +explain +select t6.a from t6 where t6.a=1 and t6.a in (select c from t3) and rand() < 0; +select t6.a from t6 where t6.a=1 and t6.a in (select c from t3) and rand() < 0; +select t6.a from t6 where t6.a=1 and t6.a in (select c from t3) and rand() >= 0; + +--echo # An outer table that is not const precedes the nest. The conjunct +--echo # is checked on that table. +create table t7 (a int); +insert into t7 values (1),(2),(3),(100); +explain +select t7.a from t7 where t7.a in (select c from t3) and rand() < 0; +select t7.a from t7 where t7.a in (select c from t3) and rand() < 0; +--sorted_result +select t7.a from t7 where t7.a in (select c from t3) and rand() >= 0; + +--echo # The conjunct also reads a column of the enclosing query. It is true +--echo # when t7.a is 4 or more, which holds only for the row where t7.a is +--echo # 100. +explain +select t7.a, + (select t1.a from t1 + where t1.a=1 and t1.a in (select c from t3) and rand() < t7.a - 3) x +from t7; +--sorted_result +select t7.a, + (select t1.a from t1 + where t1.a=1 and t1.a in (select c from t3) and rand() < t7.a - 3) x +from t7; + +--echo # rand(7) continues one sequence across the executions of the +--echo # subquery. The conjunct takes one value for each row of t8, +--echo # including the rows where t8.a is 100 and the lookup finds no match. +--echo # A plan where t1 is not const checks the conjunct on t1 before the +--echo # lookup and returns the same rows. +create table t8 (id int, a int); +insert into t8 values (1,1),(2,100),(3,2),(4,100),(5,3),(6,100),(7,4), +(8,100),(9,5),(10,100),(11,6),(12,7),(13,8),(14,9); +explain +select t8.id, + (select t1.a from t1 + where t1.a=1 and t8.a in (select c from t3) and rand(7) < 0.5) x +from t8; +select t8.id, + (select t1.a from t1 + where t1.a=1 and t8.a in (select c from t3) and rand(7) < 0.5) x +from t8 order by t8.id; + +--echo # rand() in the WHERE of the subquery instead of the outer query. +--echo # Every row of t9 has c=1. The first four values of rand(1) are not +--echo # below 0.09 and the fifth is, so checking the condition on each row +--echo # of t9 matches t1.a=1. A subquery that calls rand() is not converted +--echo # to a semi-join, so the condition is not moved to the outer query and +--echo # checked once there. Adding 0*t9.d makes the condition read a column +--echo # of t9, which gives the same result in any plan. +create table t9 (c int, d int); +insert into t9 select 1, 1 from seq_1_to_80; +explain +select count(*) from t1 +where t1.a=1 and t1.a in (select c from t9 where rand(1) < 0.09); +select count(*) from t1 +where t1.a=1 and t1.a in (select c from t9 where rand(1) < 0.09); +select count(*) from t1 +where t1.a=1 and t1.a in (select c from t9 where rand(1) < 0.09 + 0*t9.d); + +--echo # The conjunct is checked once, in outer_ref_condition, only when +--echo # the join returns at most one row. f1() counts its calls. With +--echo # the const t1 and a lookup nest, f1() is called once. Adding t10, +--echo # which holds two rows, makes the join return two rows, and f1() is +--echo # called once for each. +create table t10 (b int); +insert into t10 values (1),(2); +delimiter |; +create function f1() returns int not deterministic +begin + set @calls= @calls + 1; + return 1; +end| +delimiter ;| +explain +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) and f1() > 0; +set @calls= 0; +# A second execution under --ps-protocol would double @calls. +--disable_ps2_protocol +select t1.a from t1 where t1.a=1 and t1.a in (select c from t3) and f1() > 0; +--enable_ps2_protocol +select @calls; +explain +select t1.a, t10.b from t1, t10 +where t1.a=1 and t1.a in (select c from t3) and f1() > 0; +set @calls= 0; +# A second execution under --ps-protocol would double @calls. +--disable_ps2_protocol +--sorted_result +select t1.a, t10.b from t1, t10 +where t1.a=1 and t1.a in (select c from t3) and f1() > 0; +--enable_ps2_protocol +select @calls; +drop function f1; + +--echo # A materialized nest read by a scan precedes t11, which is not +--echo # const, so the conjunct is checked on t11. The optimizer chooses +--echo # the scan only with the statistics from ANALYZE, which give 200 +--echo # rows of t11 for each value of a. +create table t11 (a int, key(a)); +insert into t11 select seq mod 10 from seq_1_to_2000; +create table t12 (b int); +insert into t12 select seq mod 10 from seq_1_to_20; +analyze table t11, t12 persistent for all; +--source include/explain-no-costs.inc +explain format=json +select count(*) from t1, t11 +where t1.a=1 and t11.a in (select b from t12) and rand() < 0; +select count(*) from t1, t11 +where t1.a=1 and t11.a in (select b from t12) and rand() < 0; +select count(*) from t1, t11 +where t1.a=1 and t11.a in (select b from t12) and rand() >= 0; + +--echo # A prepared statement executed twice, with the bound for rand() +--echo # passed as a parameter. The first execution returns no row and +--echo # the second returns t1.a=1. +prepare stmt from "select t1.a from t1 +where t1.a=1 and t1.a in (select c from t3) and rand() < ?"; +execute stmt using 0; +execute stmt using 2; +deallocate prepare stmt; + +--echo # The same query in a stored procedure called twice. +create procedure p1(x int) +select t1.a from t1 +where t1.a=1 and t1.a in (select c from t3) and rand() < x; +call p1(0); +call p1(2); +drop procedure p1; + +set optimizer_switch= @save_optimizer_switch; +set optimizer_where_cost= @save_optimizer_where_cost; +drop table t1,t2,t3,t4,t5,t6,t7,t8,t9,t10,t11,t12; + +--echo # End of 11.4 tests diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 6a07e34be0c..62821a88d18 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -14510,6 +14510,26 @@ bool build_tmp_join_prefix_cond(JOIN *join, JOIN_TAB *last_tab, Item **ret) } +/* + Find the last top level table in the join order that is neither + const nor a materialized semijoin nest. Return its index in + join->join_tab, or a value below join->const_tables when no such + table exists. +*/ + +static uint +get_last_top_base_tab_idx(const JOIN *join) +{ + uint i; + for (i= join->top_join_tab_count - 1; i >= join->const_tables; i--) + { + if (!join->join_tab[i].bush_children) + break; + } + return i; +} + + static bool make_join_select(JOIN *join,SQL_SELECT *select,COND *cond) { @@ -14523,6 +14543,10 @@ make_join_select(JOIN *join,SQL_SELECT *select,COND *cond) "attached_conditions_computation"); add_not_null_conds(join); table_map used_tables; + + /* Step #1 and Step #2 both decide from this index. */ + const uint last_top_base_tab_idx= get_last_top_base_tab_idx(join); + /* Step #1: Extract constant condition - Extract and check the constant part of the WHERE @@ -14600,12 +14624,11 @@ make_join_select(JOIN *join,SQL_SELECT *select,COND *cond) execution. rand() takes a new value on each call, and a conjunct that calls it is checked for each row of the last top level table that is neither const nor a materialized semijoin - nest. A join of only const tables has no such table and - returns at most one row, and its rand() conjuncts are collected - here. + nest. When no such table exists, the join returns at most one + row, and its rand() conjuncts are collected here. */ table_map pseudo_bits= OUTER_REF_TABLE_BIT; - if (join->only_const_tables()) + if (last_top_base_tab_idx < join->const_tables) pseudo_bits= PSEUDO_TABLE_BITS; COND *pseudo_cond= make_cond_for_table(thd, cond, join->const_table_map | @@ -14631,13 +14654,6 @@ make_join_select(JOIN *join,SQL_SELECT *select,COND *cond) */ uint i; - for (i= join->top_join_tab_count - 1; i >= join->const_tables; i--) - { - if (!join->join_tab[i].bush_children) - break; - } - uint last_top_base_tab_idx= i; - table_map save_used_tables= 0; used_tables=((select->const_tables=join->const_table_map) | OUTER_REF_TABLE_BIT | RAND_TABLE_BIT); diff --git a/sql/sql_select.h b/sql/sql_select.h index f7563577f0f..bd5df87031b 100644 --- a/sql/sql_select.h +++ b/sql/sql_select.h @@ -1653,8 +1653,8 @@ public: non-const tables. (Do not confuse this with exec_const_cond which is constant across all executions). Normally, this is condition that refers to outer tables only. - For degenerate joins that only have constant tables, this includes - conditions with RAND_TABLE_BIT. + For degenerate joins that only have constant tables and semi-join nests, + this includes conditions with RAND_TABLE_BIT. */ Item *outer_ref_cond; TABLE_LIST *tables_list; /// Date: Wed, 14 Oct 2026 11:27:07 +0200 Subject: [PATCH 14/28] MDEV-40845: DuckDB: reject SELECT ... FOR UPDATE / LOCK IN SHARE MODE DuckDB has no row locks and its parser rejects locking clauses, so SELECT ... FOR UPDATE on a DuckDB table failed with ER_GET_ERRMSG ("Parser Error: SELECT locking clause is not supported!") coming from the embedded DuckDB. Check for locking reads in ha_duckdb::external_lock() and raise ER_NOT_SUPPORTED_YET from the storage engine instead. The check only applies to SQLCOM_SELECT, so INSERT ... SELECT, UPDATE and DELETE are not affected. --- storage/duckdb/ha_duckdb.cc | 9 +++++++++ .../mysql-test/duckdb/r/select_for_update.result | 7 +++++++ .../mysql-test/duckdb/t/select_for_update.test | 12 ++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 storage/duckdb/mysql-test/duckdb/r/select_for_update.result create mode 100644 storage/duckdb/mysql-test/duckdb/t/select_for_update.test diff --git a/storage/duckdb/ha_duckdb.cc b/storage/duckdb/ha_duckdb.cc index 96442413cb0..4242378ecc0 100644 --- a/storage/duckdb/ha_duckdb.cc +++ b/storage/duckdb/ha_duckdb.cc @@ -1165,6 +1165,15 @@ int ha_duckdb::external_lock(THD *thd, int lock_type) DBUG_ENTER("ha_duckdb::external_lock"); if (lock_type != F_UNLCK) { + /* DuckDB has no row locks: reject SELECT ... FOR UPDATE / LOCK IN SHARE MODE. */ + if (thd_sql_command(thd) == SQLCOM_SELECT && + (table->reginfo.lock_type == TL_READ_WITH_SHARED_LOCKS || + table->reginfo.lock_type >= TL_FIRST_WRITE)) + { + my_error(ER_NOT_SUPPORTED_YET, MYF(0), "locking SELECT"); + DBUG_RETURN(HA_ERR_WRONG_COMMAND); + } + /* DuckDB does not support XA transactions. Reject DML early. */ if (myduck::reject_xa_if_active(thd)) DBUG_RETURN(HA_ERR_WRONG_COMMAND); diff --git a/storage/duckdb/mysql-test/duckdb/r/select_for_update.result b/storage/duckdb/mysql-test/duckdb/r/select_for_update.result new file mode 100644 index 00000000000..dc41c28fb88 --- /dev/null +++ b/storage/duckdb/mysql-test/duckdb/r/select_for_update.result @@ -0,0 +1,7 @@ +CREATE TABLE t (id INT PRIMARY KEY, v INT) ENGINE=DuckDB; +INSERT INTO t VALUES (1, 1),(2, 2); +SELECT v FROM t WHERE id=1 FOR UPDATE; +ERROR 42000: This version of MariaDB doesn't yet support 'locking SELECT' +SELECT v FROM t WHERE id=1 LOCK IN SHARE MODE; +ERROR 42000: This version of MariaDB doesn't yet support 'locking SELECT' +DROP TABLE t; diff --git a/storage/duckdb/mysql-test/duckdb/t/select_for_update.test b/storage/duckdb/mysql-test/duckdb/t/select_for_update.test new file mode 100644 index 00000000000..cb0852cc29c --- /dev/null +++ b/storage/duckdb/mysql-test/duckdb/t/select_for_update.test @@ -0,0 +1,12 @@ +--source ../include/have_duckdb.inc +--source include/not_msan.inc + +CREATE TABLE t (id INT PRIMARY KEY, v INT) ENGINE=DuckDB; +INSERT INTO t VALUES (1, 1),(2, 2); + +--error ER_NOT_SUPPORTED_YET +SELECT v FROM t WHERE id=1 FOR UPDATE; + +--error ER_NOT_SUPPORTED_YET +SELECT v FROM t WHERE id=1 LOCK IN SHARE MODE; +DROP TABLE t; From 40cae5d6ed5f6419f11989a0b3df571fe73fba18 Mon Sep 17 00:00:00 2001 From: ParadoxV5 Date: Sun, 11 Oct 2026 14:59:35 -0600 Subject: [PATCH 15/28] MDEV-32947 Async conflicts with semi-sync in multi-source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `@@rpl_semi_sync_slave_enabled` can be changed without stopping replication, because it takes effect when a connection is established. But when it takes effect, it applies to *all* replication connections. This means that starting an async connection will switch any ongoing semi-sync connections to async as well, and vice versa, which will cause those connections to fail to replicate in the wrong mode. This fix resolves this oversight by changing the value application to be specific to only the establishing connection (technically, moving the singleton’s field `Repl_semi_sync_slave::m_slave_enabled` to the instance variable `Master_info::semi_sync_enabled`). This design builds toward MDEV-40941, which proposes migrating `@@rpl_semi_sync_slave_enabled` to a proper per-connection config. Note, `Master_info::semi_sync_reply_enabled` must remain separate, because it only disables ACK replying in async slave fallback, whereas the semi-sync slave setting also controls whether the IO thread should expect additional semi-sync flags in event packets. To maintain backward compatibility until MDEV-40941, the status variable `Rpl_semi_sync_slave_status` is kept showing the applied `@@rpl_semi_sync_slave_enabled` for the most recent START SLAVE in any connection; that is, not yet made per-connection. Reviewed-by: Brandon Nesterenko --- .../suite/multi_source/mix_semisync_async.inc | 84 ++++++++++++++ .../multi_source/mix_semisync_async.result | 109 ++++++++++++++++++ .../multi_source/mix_semisync_async.test | 63 ++++++++++ sql/rpl_mi.h | 1 + sql/semisync_slave.cc | 29 ++--- sql/semisync_slave.h | 19 +-- sql/slave.cc | 8 +- 7 files changed, 287 insertions(+), 26 deletions(-) create mode 100644 mysql-test/suite/multi_source/mix_semisync_async.inc create mode 100644 mysql-test/suite/multi_source/mix_semisync_async.result create mode 100644 mysql-test/suite/multi_source/mix_semisync_async.test diff --git a/mysql-test/suite/multi_source/mix_semisync_async.inc b/mysql-test/suite/multi_source/mix_semisync_async.inc new file mode 100644 index 00000000000..c9fad636e11 --- /dev/null +++ b/mysql-test/suite/multi_source/mix_semisync_async.inc @@ -0,0 +1,84 @@ +# Test replicating from an additional connection of semi-sync mode `$mode2` +# does not interfere with the active connection of mode `$mode1`. +# +# Pre-Conditions: +# * On connection `slave`. +# * CHANGE MASTER `master1` and `master2` set up to +# replicate from the connection with the same name. +# * None of the replication threads are running. +# * User variables `$mode1` and `$mode2` contain +# corresponding `@@rpl_semi_sync_slave_enabled` values. +# +# Post-Conditions: +# * On connection `slave`. +# * None of the replication threads are running. +# * @@default_master_connection = 'master1', +# @@rpl_semi_sync_slave_enabled = $mode2 + +--let $status_var= Rpl_semi_sync_master_clients +--let $status_var_value= 1 + + +# Start `master1` +SET @@SESSION.default_master_connection= 'master1'; +--eval SET @@GLOBAL.rpl_semi_sync_slave_enabled= $mode1 +--source include/start_slave.inc + +# Start `master2` +SET @@SESSION.default_master_connection= 'master2'; +--eval SET @@GLOBAL.rpl_semi_sync_slave_enabled= $mode2 +--source include/start_slave.inc + + +# TODO MDEV-40941: +# `Rpl_semi_sync_slave_status` does not refer to `@@default_master_connection`, +# but rather the most recent START SLAVE (`$mode2` in this case). +SHOW STATUS LIKE 'Rpl_semi_sync_slave_status'; +SET @@SESSION.default_master_connection= 'master1'; +SHOW STATUS LIKE 'Rpl_semi_sync_slave_status'; + + +# Verify `master2` +--connection master2 + if ($mode2) + { + --source include/wait_for_status_var.inc + } + CREATE TABLE t2 (a INT); + DROP TABLE t2; + --save_master_pos +--connection slave +--sync_with_master 0, master2 + +# Verify `master1` +--connection master1 + if ($mode1) + { + --source include/wait_for_status_var.inc + } + CREATE TABLE t1 (a INT); + --save_master_pos +--connection slave +--sync_with_master 0, master1 +SHOW STATUS LIKE 'Rpl_semi_sync_slave_send_ack'; + + +# Stop `master2` +SET @@SESSION.default_master_connection= 'master2'; +--source include/stop_slave.inc +# TODO MDEV-40941: +# `Rpl_semi_sync_slave_status` shows OFF after STOP SLAVE on any connection. +SHOW STATUS LIKE 'Rpl_semi_sync_slave_status'; + +# Verify `master1` +--connection master1 + DROP TABLE t1; + SHOW STATUS LIKE 'Rpl_semi_sync_master_clients'; + --save_master_pos +--connection slave +--sync_with_master 0, master1 +SHOW STATUS LIKE 'Rpl_semi_sync_slave_send_ack'; + +# Stop `master1` +SET @@SESSION.default_master_connection= 'master1'; +--source include/stop_slave.inc diff --git a/mysql-test/suite/multi_source/mix_semisync_async.result b/mysql-test/suite/multi_source/mix_semisync_async.result new file mode 100644 index 00000000000..29b33ea0331 --- /dev/null +++ b/mysql-test/suite/multi_source/mix_semisync_async.result @@ -0,0 +1,109 @@ +# Setup +connect master1, 127.0.0.1, root, , , $SERVER_MYPORT_1; +SET @@GLOBAL.rpl_semi_sync_master_wait_no_slave= 0; +SET @@GLOBAL.rpl_semi_sync_master_enabled= 1; +SET @@SESSION.gtid_domain_id= 1; +RESET MASTER; +connect master2, 127.0.0.1, root, , , $SERVER_MYPORT_2; +SET @@GLOBAL.rpl_semi_sync_master_wait_no_slave= 0; +SET @@GLOBAL.rpl_semi_sync_master_enabled= 1; +SET @@SESSION.gtid_domain_id= 2; +RESET MASTER; +connect slave, 127.0.0.1, root, , , $SERVER_MYPORT_3; +SET @@GLOBAL.gtid_slave_pos= ''; +CHANGE MASTER 'master1' TO +master_host='127.0.0.1', master_port=SERVER_MYPORT_1, master_user='root'; +CHANGE MASTER 'master2' TO +master_host='127.0.0.1', master_port=SERVER_MYPORT_2, master_user='root'; +# Test semi-sync replication during async replication +SET @@SESSION.default_master_connection= 'master1'; +SET @@GLOBAL.rpl_semi_sync_slave_enabled= 0; +include/start_slave.inc +SET @@SESSION.default_master_connection= 'master2'; +SET @@GLOBAL.rpl_semi_sync_slave_enabled= 1; +include/start_slave.inc +SHOW STATUS LIKE 'Rpl_semi_sync_slave_status'; +Variable_name Value +Rpl_semi_sync_slave_status ON +SET @@SESSION.default_master_connection= 'master1'; +SHOW STATUS LIKE 'Rpl_semi_sync_slave_status'; +Variable_name Value +Rpl_semi_sync_slave_status ON +connection master2; +CREATE TABLE t2 (a INT); +DROP TABLE t2; +connection slave; +connection master1; +CREATE TABLE t1 (a INT); +connection slave; +SHOW STATUS LIKE 'Rpl_semi_sync_slave_send_ack'; +Variable_name Value +Rpl_semi_sync_slave_send_ack 2 +SET @@SESSION.default_master_connection= 'master2'; +include/stop_slave.inc +SHOW STATUS LIKE 'Rpl_semi_sync_slave_status'; +Variable_name Value +Rpl_semi_sync_slave_status OFF +connection master1; +DROP TABLE t1; +SHOW STATUS LIKE 'Rpl_semi_sync_master_clients'; +Variable_name Value +Rpl_semi_sync_master_clients 0 +connection slave; +SHOW STATUS LIKE 'Rpl_semi_sync_slave_send_ack'; +Variable_name Value +Rpl_semi_sync_slave_send_ack 2 +SET @@SESSION.default_master_connection= 'master1'; +include/stop_slave.inc +# Test the other order +SET @@SESSION.default_master_connection= 'master1'; +SET @@GLOBAL.rpl_semi_sync_slave_enabled= 1; +include/start_slave.inc +SET @@SESSION.default_master_connection= 'master2'; +SET @@GLOBAL.rpl_semi_sync_slave_enabled= 0; +include/start_slave.inc +SHOW STATUS LIKE 'Rpl_semi_sync_slave_status'; +Variable_name Value +Rpl_semi_sync_slave_status OFF +SET @@SESSION.default_master_connection= 'master1'; +SHOW STATUS LIKE 'Rpl_semi_sync_slave_status'; +Variable_name Value +Rpl_semi_sync_slave_status OFF +connection master2; +CREATE TABLE t2 (a INT); +DROP TABLE t2; +connection slave; +connection master1; +CREATE TABLE t1 (a INT); +connection slave; +SHOW STATUS LIKE 'Rpl_semi_sync_slave_send_ack'; +Variable_name Value +Rpl_semi_sync_slave_send_ack 1 +SET @@SESSION.default_master_connection= 'master2'; +include/stop_slave.inc +SHOW STATUS LIKE 'Rpl_semi_sync_slave_status'; +Variable_name Value +Rpl_semi_sync_slave_status OFF +connection master1; +DROP TABLE t1; +SHOW STATUS LIKE 'Rpl_semi_sync_master_clients'; +Variable_name Value +Rpl_semi_sync_master_clients 1 +connection slave; +SHOW STATUS LIKE 'Rpl_semi_sync_slave_send_ack'; +Variable_name Value +Rpl_semi_sync_slave_send_ack 2 +SET @@SESSION.default_master_connection= 'master1'; +include/stop_slave.inc +# Cleanup +RESET SLAVE 'master1' ALL; +RESET SLAVE 'master2' ALL; +disconnect slave; +connection master1; +SET @@GLOBAL.rpl_semi_sync_master_enabled= 0; +SET @@GLOBAL.rpl_semi_sync_master_wait_no_slave= 1; +disconnect master1; +connection master2; +SET @@GLOBAL.rpl_semi_sync_master_enabled= 0; +SET @@GLOBAL.rpl_semi_sync_master_wait_no_slave= 1; +disconnect master2; diff --git a/mysql-test/suite/multi_source/mix_semisync_async.test b/mysql-test/suite/multi_source/mix_semisync_async.test new file mode 100644 index 00000000000..8e3b035e95f --- /dev/null +++ b/mysql-test/suite/multi_source/mix_semisync_async.test @@ -0,0 +1,63 @@ +# MDEV-32947 Async conflicts with semi-sync in multi-source +# +# With a async master and a semi-sync master, +# test replication does not mix async and semi-sync regardless of order + +--source include/have_binlog_format_mixed.inc # format-agnostic +--let $orig_wait_no_slave= `SELECT @@GLOBAL.rpl_semi_sync_master_wait_no_slave` + +--echo # Setup +# Enable semi-sync on both masters; +# the slave will disable semi-sync on one of its connections. + +--connect (master1, 127.0.0.1, root, , , $SERVER_MYPORT_1) +SET @@GLOBAL.rpl_semi_sync_master_wait_no_slave= 0; +SET @@GLOBAL.rpl_semi_sync_master_enabled= 1; +SET @@SESSION.gtid_domain_id= 1; +RESET MASTER; + +--connect (master2, 127.0.0.1, root, , , $SERVER_MYPORT_2) +SET @@GLOBAL.rpl_semi_sync_master_wait_no_slave= 0; +SET @@GLOBAL.rpl_semi_sync_master_enabled= 1; +SET @@SESSION.gtid_domain_id= 2; +RESET MASTER; + +--connect (slave, 127.0.0.1, root, , , $SERVER_MYPORT_3) +--disable_warnings + SET @@GLOBAL.gtid_slave_pos= ''; +--enable_warnings + +--replace_result $SERVER_MYPORT_1 SERVER_MYPORT_1 +eval CHANGE MASTER 'master1' TO + master_host='127.0.0.1', master_port=$SERVER_MYPORT_1, master_user='root'; +--replace_result $SERVER_MYPORT_2 SERVER_MYPORT_2 +eval CHANGE MASTER 'master2' TO + master_host='127.0.0.1', master_port=$SERVER_MYPORT_2, master_user='root'; + + +--echo # Test semi-sync replication during async replication +--let $mode1= 0 +--let $mode2= 1 +--source mix_semisync_async.inc + +--echo # Test the other order +--let $mode1= 1 +--let $mode2= 0 +--source mix_semisync_async.inc + + +--echo # Cleanup + +RESET SLAVE 'master1' ALL; +RESET SLAVE 'master2' ALL; +--disconnect slave + +--connection master1 + SET @@GLOBAL.rpl_semi_sync_master_enabled= 0; + --eval SET @@GLOBAL.rpl_semi_sync_master_wait_no_slave= $orig_wait_no_slave +--disconnect master1 + +--connection master2 + SET @@GLOBAL.rpl_semi_sync_master_enabled= 0; + --eval SET @@GLOBAL.rpl_semi_sync_master_wait_no_slave= $orig_wait_no_slave +--disconnect master2 diff --git a/sql/rpl_mi.h b/sql/rpl_mi.h index b471e2165c5..60de29036b8 100644 --- a/sql/rpl_mi.h +++ b/sql/rpl_mi.h @@ -376,6 +376,7 @@ class Master_info : public Slave_reporting_capability it must be ignored similarly to the replicate-same-server-id rule. */ bool do_accept_own_server_id= false; + bool semi_sync_enabled= false; /* Set to 1 when semi_sync is enabled. Set to 0 if there is any transmit problems to the slave, in which case any furter semi-sync reply is diff --git a/sql/semisync_slave.cc b/sql/semisync_slave.cc index 32e53dab758..45e03fa6f4c 100644 --- a/sql/semisync_slave.cc +++ b/sql/semisync_slave.cc @@ -46,7 +46,8 @@ int rpl_semi_sync_enabled(THD *thd, SHOW_VAR *var, void *buff, system_status_var *status_var, enum_var_type scope) { - local_semi_sync_enabled= repl_semisync_slave.get_slave_enabled(); + //TODO MDEV-40941 + local_semi_sync_enabled= repl_semisync_slave.m_slave_enabled; var->type= SHOW_BOOL; var->value= (char*) &local_semi_sync_enabled; return 0; @@ -55,14 +56,14 @@ int rpl_semi_sync_enabled(THD *thd, SHOW_VAR *var, void *buff, int Repl_semi_sync_slave::slave_read_sync_header(const uchar *header, unsigned long total_len, - int *semi_flags, + Master_info *mi, const uchar **payload, unsigned long *payload_len) { int read_res = 0; DBUG_ENTER("Repl_semi_sync_slave::slave_read_sync_header"); - if (get_slave_enabled()) + if (get_slave_enabled(mi)) { if (!DBUG_IF("semislave_corrupt_log") && header[0] == k_packet_magic_num) @@ -76,9 +77,9 @@ int Repl_semi_sync_slave::slave_read_sync_header(const uchar *header, semi_sync_need_reply)); if (semi_sync_need_reply) - *semi_flags |= SEMI_SYNC_NEED_ACK; + mi->semi_ack |= SEMI_SYNC_NEED_ACK; if (is_delay_master()) - *semi_flags |= SEMI_SYNC_SLAVE_DELAY_SYNC; + mi->semi_ack |= SEMI_SYNC_SLAVE_DELAY_SYNC; } else { @@ -111,7 +112,7 @@ void Repl_semi_sync_slave::slave_start(Master_info *mi) */ bool semi_sync= global_rpl_semi_sync_slave_enabled; - set_slave_enabled(semi_sync); + set_slave_enabled(mi, semi_sync); mi->semi_sync_reply_enabled= 0; sql_print_information("Slave I/O thread: Start %s replication to\ @@ -127,7 +128,7 @@ void Repl_semi_sync_slave::slave_start(Master_info *mi) void Repl_semi_sync_slave::slave_stop(Master_info *mi) { - if (get_slave_enabled()) + if (get_slave_enabled(mi)) { #ifdef ENABLED_DEBUG_SYNC /* @@ -144,7 +145,7 @@ void Repl_semi_sync_slave::slave_stop(Master_info *mi) kill_connection(mi); } - set_slave_enabled(0); + set_slave_enabled(mi, 0); } void Repl_semi_sync_slave::slave_reconnect(Master_info *mi) @@ -153,7 +154,7 @@ void Repl_semi_sync_slave::slave_reconnect(Master_info *mi) Start semi-sync either if it globally enabled or if was enabled before the reconnect. */ - if (global_rpl_semi_sync_slave_enabled || get_slave_enabled()) + if (global_rpl_semi_sync_slave_enabled || get_slave_enabled(mi)) slave_start(mi); } @@ -215,7 +216,7 @@ int Repl_semi_sync_slave::request_transmit(Master_info *mi) MYSQL_ROW row; const char *query; - if (!get_slave_enabled()) + if (!get_slave_enabled(mi)) return 0; query= "SHOW VARIABLES LIKE 'rpl_semi_sync_master_enabled'"; @@ -223,7 +224,7 @@ int Repl_semi_sync_slave::request_transmit(Master_info *mi) !(res= mysql_store_result(mysql))) { sql_print_error("Execution failed on master: %s, error :%s", query, mysql_error(mysql)); - set_slave_enabled(0); + set_slave_enabled(mi, 0); return 1; } @@ -234,7 +235,7 @@ int Repl_semi_sync_slave::request_transmit(Master_info *mi) if (!row) sql_print_warning("Master server does not support semi-sync, " "fallback to asynchronous replication"); - set_slave_enabled(0); + set_slave_enabled(mi, 0); mysql_free_result(res); return 0; } @@ -253,7 +254,7 @@ int Repl_semi_sync_slave::request_transmit(Master_info *mi) if (mysql_real_query(mysql, query, (ulong)strlen(query))) { sql_print_error("%s on master failed", query); - set_slave_enabled(0); + set_slave_enabled(mi, 0); return 1; } mi->semi_sync_reply_enabled= 1; @@ -276,7 +277,7 @@ int Repl_semi_sync_slave::slave_reply(Master_info *mi) int reply_res = 0; size_t name_len = strlen(binlog_filename); DBUG_ENTER("Repl_semi_sync_slave::slave_reply"); - DBUG_ASSERT(get_slave_enabled() && mi->semi_sync_reply_enabled); + DBUG_ASSERT(get_slave_enabled(mi) && mi->semi_sync_reply_enabled); /* Prepare the buffer of the reply. */ reply_buffer[REPLY_MAGIC_NUM_OFFSET] = k_packet_magic_num; diff --git a/sql/semisync_slave.h b/sql/semisync_slave.h index 79949067de8..776891e1e09 100644 --- a/sql/semisync_slave.h +++ b/sql/semisync_slave.h @@ -17,6 +17,7 @@ #ifndef SEMISYNC_SLAVE_H #define SEMISYNC_SLAVE_H +#ifdef HAVE_REPLICATION #include "semisync.h" #include "my_global.h" @@ -45,12 +46,12 @@ public: */ int init_object(); - inline bool get_slave_enabled() { - return m_slave_enabled; + inline bool get_slave_enabled(Master_info *mi) { + return mi->semi_sync_enabled; } - void set_slave_enabled(bool enabled) { - m_slave_enabled = enabled; + void set_slave_enabled(Master_info *mi, bool enabled) { + m_slave_enabled= mi->semi_sync_enabled= enabled; } inline bool is_delay_master(){ @@ -71,8 +72,8 @@ public: * Input: * header - (IN) packet header pointer * total_len - (IN) total packet length: metadata + payload - * semi_flags - (IN) store flags: SEMI_SYNC_SLAVE_DELAY_SYNC and - SEMI_SYNC_NEED_ACK + * mi - (IN) store flags: SEMI_SYNC_SLAVE_DELAY_SYNC and + SEMI_SYNC_NEED_ACK, to Master_info::semi_ack * payload - (IN) payload: the replication event * payload_len - (IN) payload length * @@ -80,7 +81,7 @@ public: * 0: success; non-zero: error */ int slave_read_sync_header(const uchar *header, unsigned long total_len, - int *semi_flags, + Master_info *mi, const uchar **payload, unsigned long *payload_len); /* A slave replies to the master indicating its replication process. It @@ -94,10 +95,11 @@ public: int request_transmit(Master_info *mi); void kill_connection(Master_info *mi); + /// @deprecated For `Rpl_semi_sync_slave_status` compatibility only + bool m_slave_enabled; private: /* True when init_object has been called */ bool m_init_done; - bool m_slave_enabled; /* semi-sync is enabled on the slave */ bool m_delay_master; unsigned int m_kill_conn_timeout; }; @@ -115,4 +117,5 @@ extern unsigned long long rpl_semi_sync_slave_send_ack; extern int rpl_semi_sync_enabled(THD *thd, SHOW_VAR *var, void *buff, system_status_var *status_var, enum_var_type scope); +#endif /* HAVE_REPLICATION */ #endif /* SEMISYNC_SLAVE_H */ diff --git a/sql/slave.cc b/sql/slave.cc index 54550a42eff..9b844f0f371 100644 --- a/sql/slave.cc +++ b/sql/slave.cc @@ -5293,7 +5293,7 @@ Stopping slave I/O thread due to out-of-memory error from master"); if (repl_semisync_slave. slave_read_sync_header((const uchar*) mysql->net.read_pos + 1, event_len, - &(mi->semi_ack), &event_buf, &event_len)) + mi, &event_buf, &event_len)) { mi->report(ERROR_LEVEL, ER_SLAVE_FATAL_ERROR, NULL, ER_THD(thd, ER_SLAVE_FATAL_ERROR), @@ -5350,7 +5350,7 @@ Stopping slave I/O thread due to out-of-memory error from master"); goto err; } - if (repl_semisync_slave.get_slave_enabled() && + if (repl_semisync_slave.get_slave_enabled(mi) && mi->semi_sync_reply_enabled && (mi->semi_ack & SEMI_SYNC_NEED_ACK)) { @@ -5394,7 +5394,7 @@ Stopping slave I/O thread due to out-of-memory error from master"); master info only when ack is needed. This may lead to at least one group transaction delay but affords better performance improvement. */ - (!repl_semisync_slave.get_slave_enabled() || + (!repl_semisync_slave.get_slave_enabled(mi) || (!(mi->semi_ack & SEMI_SYNC_SLAVE_DELAY_SYNC) || (mi->semi_ack & (SEMI_SYNC_NEED_ACK)))) && (DBUG_IF("failed_flush_master_info") || @@ -7072,7 +7072,7 @@ dbug_gtid_accept: */ mi->do_accept_own_server_id= (s_id == global_system_variables.server_id && - repl_semisync_slave.get_slave_enabled() && opt_gtid_strict_mode && + repl_semisync_slave.get_slave_enabled(mi) && opt_gtid_strict_mode && mi->using_gtid != Master_info::USE_GTID_NO && !mysql_bin_log.check_strict_gtid_sequence(event_gtid.domain_id, event_gtid.server_id, From 5dabf1c52dc6c479c8748a78f67fde42991b700c Mon Sep 17 00:00:00 2001 From: Daniel Black Date: Mon, 19 Oct 2026 09:58:40 +1100 Subject: [PATCH 16/28] MDEV-40845: DuckDB: reject SELECT ... FOR UPDATE / LOCK IN SHARE MODE (msan) Works under MSAN just fine. --- storage/duckdb/mysql-test/duckdb/t/select_for_update.test | 1 - 1 file changed, 1 deletion(-) diff --git a/storage/duckdb/mysql-test/duckdb/t/select_for_update.test b/storage/duckdb/mysql-test/duckdb/t/select_for_update.test index cb0852cc29c..7d845e40e14 100644 --- a/storage/duckdb/mysql-test/duckdb/t/select_for_update.test +++ b/storage/duckdb/mysql-test/duckdb/t/select_for_update.test @@ -1,5 +1,4 @@ --source ../include/have_duckdb.inc ---source include/not_msan.inc CREATE TABLE t (id INT PRIMARY KEY, v INT) ENGINE=DuckDB; INSERT INTO t VALUES (1, 1),(2, 2); From f6869f90684309ef487da856b2f1a746d224cd2c Mon Sep 17 00:00:00 2001 From: Oleksandr Byelkin Date: Wed, 14 Oct 2026 17:16:24 +0200 Subject: [PATCH 17/28] MDEV-41405 ASAN stack-buffer-overflow TDBXML::Initialize/TDBXML::OpenDB after insert into CONNECT table XmlDB was copied with strcpy/strcat into a 64-byte stack buffer to build the XPath. Tabname used the same buffer with snprintf, silently truncating the XPath. Build the XPath in a buffer allocated by its real length. Co-Authored-By: Claude Opus 5.5 --- .../connect/mysql-test/connect/r/xml2.result | 53 +++++++++++++++++++ .../connect/mysql-test/connect/t/xml2.test | 49 +++++++++++++++++ storage/connect/tabxml.cpp | 38 ++++++------- 3 files changed, 122 insertions(+), 18 deletions(-) diff --git a/storage/connect/mysql-test/connect/r/xml2.result b/storage/connect/mysql-test/connect/r/xml2.result index 603e5584e1a..e06e3db7691 100644 --- a/storage/connect/mysql-test/connect/r/xml2.result +++ b/storage/connect/mysql-test/connect/r/xml2.result @@ -396,3 +396,56 @@ CAST(@a AS CHAR CHARACTER SET latin1) +# +# MDEV-41405 ASAN stack-buffer-overflow TDBXML::Initialize/TDBXML::OpenDB +# after insert into CONNECT table +# +CREATE TABLE t1 (id INT) +ENGINE=CONNECT TABLE_TYPE=XML FILE_NAME='x.xml' + OPTION_LIST='XmlDB=root89,xmlsup=libxml2'; +INSERT INTO t1 VALUES (1); +DROP TABLE t1; +CREATE TABLE t2 (id INT) +ENGINE=CONNECT TABLE_TYPE=XML FILE_NAME='x.xml' + OPTION_LIST='XmlDB=XMLDB,xmlsup=libxml2'; +INSERT INTO t2 VALUES (2); +ERROR HY000: Got error 174 'Missing XMLDB' from CONNECT +DROP TABLE t2; +CREATE TABLE t3 (id INT) +ENGINE=CONNECT TABLE_TYPE=XML FILE_NAME='y.xml' + OPTION_LIST='XmlDB=XMLDB,xmlsup=libxml2'; +CREATE TABLE t4 (id INT) +ENGINE=CONNECT TABLE_TYPE=XML FILE_NAME='y.xml' + OPTION_LIST='XmlDB=XMLDB,xmlsup=libxml2'; +CREATE TABLE t5 (id INT) +ENGINE=CONNECT TABLE_TYPE=XML FILE_NAME='y.xml' + OPTION_LIST='XmlDB=/XMLDB,xmlsup=libxml2'; +INSERT INTO t3 VALUES (3); +INSERT INTO t4 VALUES (4); +INSERT INTO t5 VALUES (5); +DROP TABLE t3, t4, t5; +SET @a=LOAD_FILE('MYSQLD_DATADIR/test/y.xml'); +SELECT REPLACE(@a, REPEAT('A', 240), 'XMLDB') AS y; +y + + + + + 3 + + + + + 4 + + + + + 5 + + + + +# +# End of 10.11 tests +# diff --git a/storage/connect/mysql-test/connect/t/xml2.test b/storage/connect/mysql-test/connect/t/xml2.test index 9c5f685d399..bf2e2216886 100644 --- a/storage/connect/mysql-test/connect/t/xml2.test +++ b/storage/connect/mysql-test/connect/t/xml2.test @@ -318,3 +318,52 @@ SELECT CAST(@a AS CHAR CHARACTER SET latin1); --remove_file $MYSQLD_DATADIR/test/xsample.xml --remove_file $MYSQLD_DATADIR/test/latin1.xml --remove_file $MYSQLD_DATADIR/test/cp1251.xml + +--echo # +--echo # MDEV-41405 ASAN stack-buffer-overflow TDBXML::Initialize/TDBXML::OpenDB +--echo # after insert into CONNECT table +--echo # +# Create x.xml so that the inserts below go into an existing file +CREATE TABLE t1 (id INT) + ENGINE=CONNECT TABLE_TYPE=XML FILE_NAME='x.xml' + OPTION_LIST='XmlDB=root89,xmlsup=libxml2'; +INSERT INTO t1 VALUES (1); +DROP TABLE t1; + +let $xmldb= `SELECT REPEAT('A', 240)`; +--replace_result $xmldb XMLDB +eval CREATE TABLE t2 (id INT) + ENGINE=CONNECT TABLE_TYPE=XML FILE_NAME='x.xml' + OPTION_LIST='XmlDB=$xmldb,xmlsup=libxml2'; +--replace_regex /A{60,}/XMLDB/ +--error ER_GET_ERRMSG +INSERT INTO t2 VALUES (2); +DROP TABLE t2; +--remove_file $MYSQLD_DATADIR/test/x.xml + +# Long XmlDB node created by t3 must be found by t4 and t5 +--replace_result $xmldb XMLDB +eval CREATE TABLE t3 (id INT) + ENGINE=CONNECT TABLE_TYPE=XML FILE_NAME='y.xml' + OPTION_LIST='XmlDB=$xmldb,xmlsup=libxml2'; +--replace_result $xmldb XMLDB +eval CREATE TABLE t4 (id INT) + ENGINE=CONNECT TABLE_TYPE=XML FILE_NAME='y.xml' + OPTION_LIST='XmlDB=$xmldb,xmlsup=libxml2'; +--replace_result $xmldb XMLDB +eval CREATE TABLE t5 (id INT) + ENGINE=CONNECT TABLE_TYPE=XML FILE_NAME='y.xml' + OPTION_LIST='XmlDB=/$xmldb,xmlsup=libxml2'; +INSERT INTO t3 VALUES (3); +INSERT INTO t4 VALUES (4); +INSERT INTO t5 VALUES (5); +DROP TABLE t3, t4, t5; +--chmod 0777 $MYSQLD_DATADIR/test/y.xml +--replace_result $MYSQLD_DATADIR MYSQLD_DATADIR +--eval SET @a=LOAD_FILE('$MYSQLD_DATADIR/test/y.xml') +SELECT REPLACE(@a, REPEAT('A', 240), 'XMLDB') AS y; +--remove_file $MYSQLD_DATADIR/test/y.xml + +--echo # +--echo # End of 10.11 tests +--echo # diff --git a/storage/connect/tabxml.cpp b/storage/connect/tabxml.cpp index 188dbcdde02..ed952988adc 100644 --- a/storage/connect/tabxml.cpp +++ b/storage/connect/tabxml.cpp @@ -819,6 +819,23 @@ int TDBXML::LoadTableFile(PGLOBAL g, char *filename) return rc; } // end of LoadTableFile +/***********************************************************************/ +/* Return name as an Xpath. If name is not an Xpath, construct one */ +/* that will find it anywhere. */ +/***********************************************************************/ +static char *MakeXpath(PGLOBAL g, PCSZ name) + { + size_t len = strlen(name) + 3; + char *xpath = (char*)PlugSubAlloc(g, NULL, len); + + if (!strchr(name, '/')) + snprintf(xpath, len, "//%s", name); + else + snprintf(xpath, len, "%s", name); + + return xpath; + } // end of MakeXpath + /***********************************************************************/ /* Initialize the processing of the XML file. */ /* Note: this function can be called several times, eventally before */ @@ -850,7 +867,7 @@ bool TDBXML::Initialize(PGLOBAL g) #else if (!Root) { #endif - char tabpath[64], filename[_MAX_PATH]; + char filename[_MAX_PATH]; // We used the file name relative to recorded datapath PlugSetPath(filename, Xfile, GetPath()); @@ -866,15 +883,8 @@ bool TDBXML::Initialize(PGLOBAL g) goto error; } // endif Root - // If tabname is not an Xpath, - // construct one that will find it anywhere - if (!strchr(Tabname, '/')) - snprintf(tabpath, sizeof(tabpath), "//%s", Tabname); - else - snprintf(tabpath, sizeof(tabpath), "%s", Tabname); - // Evaluate table xpath - if ((TabNode = Root->SelectSingleNode(g, tabpath))) { + if ((TabNode = Root->SelectSingleNode(g, MakeXpath(g, Tabname)))) { if (TabNode->GetType() != XML_ELEMENT_NODE) { snprintf(g->Message, sizeof(g->Message), MSG(BAD_NODE_TYPE), TabNode->GetType()); goto error; @@ -882,15 +892,7 @@ bool TDBXML::Initialize(PGLOBAL g) } else if (Mode == MODE_INSERT && XmlDB) { // We are adding a new table to a multi-table file - - // If XmlDB is not an Xpath, - // construct one that will find it anywhere - if (!strchr(XmlDB, '/')) - strcat(strcpy(tabpath, "//"), XmlDB); - else - strcpy(tabpath, XmlDB); - - if (!(DBnode = Root->SelectSingleNode(g, tabpath))) { + if (!(DBnode = Root->SelectSingleNode(g, MakeXpath(g, XmlDB)))) { // DB node does not exist yet; we cannot create it // because we don't know where it should be placed snprintf(g->Message, sizeof(g->Message), MSG(MISSING_NODE), XmlDB, Xfile); From 412d8fa525eb2b14a0239c8ece994b6b11872b0a Mon Sep 17 00:00:00 2001 From: Alessandro Vetere Date: Wed, 14 Oct 2026 11:26:00 +0200 Subject: [PATCH 18/28] MDEV-40123 Assertion failure after lock wait is unblocked in SKIP LOCKED A locking read with SKIP LOCKED through a secondary index calls row_sel_get_clust_rec_for_mysql() to lock the clustered index record. On a lock conflict, that function returned DB_LOCK_WAIT without setting the output record, and row_search_mvcc() then called lock_trx_handle_wait() to cancel the wait. If the conflicting transaction had committed in the meantime, the waiting lock was already granted and lock_trx_handle_wait() returned DB_SUCCESS. row_search_mvcc() then found clust_rec == NULL for a locking read: debug builds failed an assertion, and release builds skipped a row whose lock the transaction now held. Row_sel_get_clust_rec_for_mysql::operator()(): Call lock_trx_handle_wait() here instead of in row_search_mvcc(). When the lock was granted meanwhile, return the clustered index record and store the cursor position as for any granted lock, like row_search_mvcc() does with "goto locks_ok" for the record that it is positioned on. --- .../innodb/r/skip_locked_wait_granted.result | 116 ++++++++++++++++ .../innodb/t/skip_locked_wait_granted.test | 128 ++++++++++++++++++ storage/innobase/row/row0sel.cc | 10 +- 3 files changed, 251 insertions(+), 3 deletions(-) create mode 100644 mysql-test/suite/innodb/r/skip_locked_wait_granted.result create mode 100644 mysql-test/suite/innodb/t/skip_locked_wait_granted.test diff --git a/mysql-test/suite/innodb/r/skip_locked_wait_granted.result b/mysql-test/suite/innodb/r/skip_locked_wait_granted.result new file mode 100644 index 00000000000..c4c5877e5ba --- /dev/null +++ b/mysql-test/suite/innodb/r/skip_locked_wait_granted.result @@ -0,0 +1,116 @@ +# +# MDEV-40123 Assertion failure after lock wait is unblocked in SKIP LOCKED +# +CREATE TABLE t1 (pk INT PRIMARY KEY, a INT, b INT, KEY(a)) ENGINE=InnoDB; +INSERT INTO t1 VALUES (1,1,1),(2,2,2),(3,3,3); +connect con1,localhost,root; +connect con2,localhost,root; +# The lock on the clustered index record is granted after the wait +connection con1; +BEGIN; +SELECT b FROM t1 WHERE pk=1 FOR UPDATE; +b +1 +connection con2; +BEGIN; +SET DEBUG_SYNC='lock_trx_handle_wait_enter SIGNAL waiting WAIT_FOR go'; +SELECT b FROM t1 FORCE INDEX (a) ORDER BY a FOR UPDATE SKIP LOCKED; +connection con1; +SET DEBUG_SYNC='now WAIT_FOR waiting'; +COMMIT; +SET DEBUG_SYNC='now SIGNAL go'; +connection con2; +b +1 +2 +3 +connection con1; +SELECT pk FROM t1 WHERE pk=1 FOR UPDATE SKIP LOCKED; +pk +connection con2; +COMMIT; +# The granted lock covers a row whose non-key column was updated +connection con1; +BEGIN; +UPDATE t1 SET b=10 WHERE pk=1; +connection con2; +BEGIN; +SET DEBUG_SYNC='lock_trx_handle_wait_enter SIGNAL waiting WAIT_FOR go'; +SELECT b FROM t1 FORCE INDEX (a) ORDER BY a FOR UPDATE SKIP LOCKED; +connection con1; +SET DEBUG_SYNC='now WAIT_FOR waiting'; +COMMIT; +SET DEBUG_SYNC='now SIGNAL go'; +connection con2; +b +10 +2 +3 +connection con1; +SELECT pk FROM t1 WHERE pk=1 FOR UPDATE SKIP LOCKED; +pk +connection con2; +COMMIT; +# The lock on the delete-marked secondary index record is granted +connection con1; +BEGIN; +DELETE FROM t1 WHERE pk=1; +connection con2; +BEGIN; +SET DEBUG_SYNC='lock_trx_handle_wait_enter SIGNAL waiting WAIT_FOR go'; +SELECT b FROM t1 FORCE INDEX (a) ORDER BY a FOR UPDATE SKIP LOCKED; +connection con1; +SET DEBUG_SYNC='now WAIT_FOR waiting'; +COMMIT; +SET DEBUG_SYNC='now SIGNAL go'; +connection con2; +b +2 +3 +COMMIT; +# The lock is still held: the record is skipped +connection con1; +BEGIN; +SELECT b FROM t1 WHERE pk=2 FOR UPDATE; +b +2 +connection con2; +BEGIN; +SELECT b FROM t1 FORCE INDEX (a) ORDER BY a FOR UPDATE SKIP LOCKED; +b +3 +COMMIT; +connection con1; +COMMIT; +# A deadlock is reported while waiting for the lock +CREATE TABLE t2 (x INT) ENGINE=InnoDB; +connection con1; +BEGIN; +INSERT INTO t2 VALUES (1),(2),(3),(4),(5); +SELECT b FROM t1 WHERE pk=2 FOR UPDATE; +b +2 +connection con2; +BEGIN; +SET DEBUG_SYNC='lock_trx_handle_wait_enter SIGNAL waiting WAIT_FOR go'; +SELECT b FROM t1 FORCE INDEX (a) ORDER BY a FOR UPDATE SKIP LOCKED; +connection con1; +SET DEBUG_SYNC='now WAIT_FOR waiting'; +SET DEBUG_SYNC='deadlock_report_before_lock_releasing SIGNAL deadlock'; +SELECT pk FROM t1 FORCE INDEX (a) WHERE a=2 FOR UPDATE; +connection default; +SET DEBUG_SYNC='now WAIT_FOR deadlock'; +SET DEBUG_SYNC='now SIGNAL go'; +connection con2; +ERROR 40001: Deadlock found when trying to get lock; try restarting transaction +connection con1; +pk +2 +disconnect con2; +connection con1; +COMMIT; +disconnect con1; +connection default; +SET DEBUG_SYNC='RESET'; +DROP TABLE t1, t2; +# End of 11.4 tests diff --git a/mysql-test/suite/innodb/t/skip_locked_wait_granted.test b/mysql-test/suite/innodb/t/skip_locked_wait_granted.test new file mode 100644 index 00000000000..ada4f5c2586 --- /dev/null +++ b/mysql-test/suite/innodb/t/skip_locked_wait_granted.test @@ -0,0 +1,128 @@ +--source include/have_innodb.inc +--source include/have_debug_sync.inc + +--echo # +--echo # MDEV-40123 Assertion failure after lock wait is unblocked in SKIP LOCKED +--echo # + +CREATE TABLE t1 (pk INT PRIMARY KEY, a INT, b INT, KEY(a)) ENGINE=InnoDB; +INSERT INTO t1 VALUES (1,1,1),(2,2,2),(3,3,3); + +connect con1,localhost,root; +connect con2,localhost,root; + +--echo # The lock on the clustered index record is granted after the wait +connection con1; +BEGIN; +SELECT b FROM t1 WHERE pk=1 FOR UPDATE; + +connection con2; +BEGIN; +SET DEBUG_SYNC='lock_trx_handle_wait_enter SIGNAL waiting WAIT_FOR go'; +send SELECT b FROM t1 FORCE INDEX (a) ORDER BY a FOR UPDATE SKIP LOCKED; + +connection con1; +SET DEBUG_SYNC='now WAIT_FOR waiting'; +COMMIT; +SET DEBUG_SYNC='now SIGNAL go'; + +connection con2; +reap; +connection con1; +SELECT pk FROM t1 WHERE pk=1 FOR UPDATE SKIP LOCKED; +connection con2; +COMMIT; + +--echo # The granted lock covers a row whose non-key column was updated +connection con1; +BEGIN; +UPDATE t1 SET b=10 WHERE pk=1; + +connection con2; +BEGIN; +SET DEBUG_SYNC='lock_trx_handle_wait_enter SIGNAL waiting WAIT_FOR go'; +send SELECT b FROM t1 FORCE INDEX (a) ORDER BY a FOR UPDATE SKIP LOCKED; + +connection con1; +SET DEBUG_SYNC='now WAIT_FOR waiting'; +COMMIT; +SET DEBUG_SYNC='now SIGNAL go'; + +connection con2; +reap; +connection con1; +SELECT pk FROM t1 WHERE pk=1 FOR UPDATE SKIP LOCKED; +connection con2; +COMMIT; + +--echo # The lock on the delete-marked secondary index record is granted +connection con1; +BEGIN; +DELETE FROM t1 WHERE pk=1; + +connection con2; +BEGIN; +SET DEBUG_SYNC='lock_trx_handle_wait_enter SIGNAL waiting WAIT_FOR go'; +send SELECT b FROM t1 FORCE INDEX (a) ORDER BY a FOR UPDATE SKIP LOCKED; + +connection con1; +SET DEBUG_SYNC='now WAIT_FOR waiting'; +COMMIT; +SET DEBUG_SYNC='now SIGNAL go'; + +connection con2; +reap; +COMMIT; + +--echo # The lock is still held: the record is skipped +connection con1; +BEGIN; +SELECT b FROM t1 WHERE pk=2 FOR UPDATE; + +connection con2; +BEGIN; +SELECT b FROM t1 FORCE INDEX (a) ORDER BY a FOR UPDATE SKIP LOCKED; +COMMIT; + +connection con1; +COMMIT; + +--echo # A deadlock is reported while waiting for the lock +CREATE TABLE t2 (x INT) ENGINE=InnoDB; +connection con1; +BEGIN; +# Make con1 heavier than con2, so that con2 is chosen as the deadlock victim +INSERT INTO t2 VALUES (1),(2),(3),(4),(5); +SELECT b FROM t1 WHERE pk=2 FOR UPDATE; + +connection con2; +BEGIN; +SET DEBUG_SYNC='lock_trx_handle_wait_enter SIGNAL waiting WAIT_FOR go'; +send SELECT b FROM t1 FORCE INDEX (a) ORDER BY a FOR UPDATE SKIP LOCKED; + +connection con1; +SET DEBUG_SYNC='now WAIT_FOR waiting'; +SET DEBUG_SYNC='deadlock_report_before_lock_releasing SIGNAL deadlock'; +send SELECT pk FROM t1 FORCE INDEX (a) WHERE a=2 FOR UPDATE; + +connection default; +SET DEBUG_SYNC='now WAIT_FOR deadlock'; +SET DEBUG_SYNC='now SIGNAL go'; + +connection con2; +--error ER_LOCK_DEADLOCK +reap; + +connection con1; +reap; + +disconnect con2; +connection con1; +COMMIT; +disconnect con1; + +connection default; +SET DEBUG_SYNC='RESET'; +DROP TABLE t1, t2; + +--echo # End of 11.4 tests diff --git a/storage/innobase/row/row0sel.cc b/storage/innobase/row/row0sel.cc index f56810e9da2..6ff76ae8961 100644 --- a/storage/innobase/row/row0sel.cc +++ b/storage/innobase/row/row0sel.cc @@ -3518,6 +3518,13 @@ Row_sel_get_clust_rec_for_mysql::operator()( LOCK_REC_NOT_GAP, thr); + if (err == DB_LOCK_WAIT && prebuilt->skip_locked) { + /* Cancel the lock wait, unless the lock was granted + meanwhile; then continue with a normal locking read + of clust_rec. */ + err = lock_trx_handle_wait(trx); + } + switch (err) { case DB_SUCCESS: case DB_SUCCESS_LOCKED_REC: @@ -5555,9 +5562,6 @@ requires_clust_rec: &offsets, &heap, need_vrow ? &vrow : NULL, &mtr); - if (err == DB_LOCK_WAIT && prebuilt->skip_locked) { - err = lock_trx_handle_wait(trx); - } switch (err) { case DB_SUCCESS: if (clust_rec == NULL) { From 744b2ca9988f19348f8e939246b415952d5d9d00 Mon Sep 17 00:00:00 2001 From: sjaakola Date: Thu, 15 Oct 2026 11:21:17 +0300 Subject: [PATCH 19/28] MDEV-41553 galera_restart_on_unknown_option test regression after MDEV-40023 The problem with the test was that it restarted node 2 by writing "try:--galera-unknown-option" into the expect file. However, MariaDB mtr has never supported a "try" command. Instead mtr skipped any expect file line not starting with "restart", and node 2 was not started with the unknown option (which was the idea in the test logic). The test only slept and then restarted node 2 normally. MDEV-40023 made mtr report unknown expect file commands as errors, so the test now fails in 13.1 and later. The fix is to start node 2 with the unknown option directly with --exec. In cluster, the unknown option is found only after the state transfer has started. The server should interrupt the state transfer and abort. Galera version 26.4.25 or later is needed for graceful shutdown, and this is now required by the test. --- .../r/galera_restart_on_unknown_option.result | 11 ++- .../t/galera_restart_on_unknown_option.test | 98 +++++++++---------- 2 files changed, 54 insertions(+), 55 deletions(-) diff --git a/mysql-test/suite/galera/r/galera_restart_on_unknown_option.result b/mysql-test/suite/galera/r/galera_restart_on_unknown_option.result index 221ccc76afd..42c0700220b 100644 --- a/mysql-test/suite/galera/r/galera_restart_on_unknown_option.result +++ b/mysql-test/suite/galera/r/galera_restart_on_unknown_option.result @@ -1,5 +1,6 @@ connection node_2; connection node_1; +# Correct Galera library found CALL mtr.add_suppression("Aborting"); CALL mtr.add_suppression("unknown option '--galera-unknown-option'"); connection node_1; @@ -22,7 +23,10 @@ f1 f2 2 b 3 c connection node_2; -Starting server ... +Starting server with an unknown option ... +FOUND 1 /unknown option '--galera-unknown-option'/ in galera_restart_on_unknown_option.err +FOUND 1 /State transfer interrupted, shutting down gracefully/ in galera_restart_on_unknown_option.err +NOT FOUND /Assertion .* failed/ in galera_restart_on_unknown_option.err Starting server ... SELECT * FROM t1; f1 f2 @@ -39,7 +43,10 @@ f1 f2 2 d 3 d connection node_2; -Starting server ... +Starting server with an unknown option ... +FOUND 1 /unknown option '--galera-unknown-option'/ in galera_restart_on_unknown_option.err +FOUND 1 /State transfer interrupted, shutting down gracefully/ in galera_restart_on_unknown_option.err +NOT FOUND /Assertion .* failed/ in galera_restart_on_unknown_option.err Starting server ... call mtr.add_suppression("WSREP: Failed to scan the last segment to the end\\. Last events may be missing\\. Last recovered event: "); SELECT * FROM t1; diff --git a/mysql-test/suite/galera/t/galera_restart_on_unknown_option.test b/mysql-test/suite/galera/t/galera_restart_on_unknown_option.test index 9a11a174dfe..647bfa83ef4 100644 --- a/mysql-test/suite/galera/t/galera_restart_on_unknown_option.test +++ b/mysql-test/suite/galera/t/galera_restart_on_unknown_option.test @@ -1,6 +1,13 @@ --source include/galera_cluster.inc --source include/have_innodb.inc +# +# Galera library version with the graceful shutdown on an interrupted state +# transfer (MDEV-31517), which this test expects +# +--let $galera_version=26.4.25 +source ../wsrep/include/check_galera_version.inc; + # Suppress expected warnings: CALL mtr.add_suppression("Aborting"); @@ -11,22 +18,6 @@ CALL mtr.add_suppression("unknown option '--galera-unknown-option'"); --let $node_2=node_2 --source include/auto_increment_offset_save.inc -# -# We should count the number of "Assertion failed" warnings -# in the log file before and after testing. To do this we need -# to save original log file before testing: -# ---let TEST_LOG=$MYSQLTEST_VARDIR/log/mysqld.2.err ---perl - use strict; - my $test_log=$ENV{'TEST_LOG'} or die "TEST_LOG not set"; - my $test_log_copy=$test_log . '.copy'; - if (-e $test_log_copy) { - unlink $test_log_copy; - } -EOF ---copy_file $TEST_LOG $TEST_LOG.copy - --connection node_2 CREATE TABLE t1 (f1 INTEGER PRIMARY KEY, f2 CHAR(1)); @@ -59,14 +50,26 @@ SELECT * FROM t1; --remove_file $MYSQLTEST_VARDIR/mysqld.2/data/grastate.dat ---let $start_mysqld_params=--galera-unknown-option +# The server must refuse to start, and exit without an assertion. +# Start it directly rather than through the expect file: mtr would treat +# a server that fails to start as a test failure. +# The unknown option is found only after the state transfer has started, +# which is then interrupted, and Galera aborts the process on purpose +# (SIGABRT, exit code 134). ---echo Starting server ... ---write_line "try:$start_mysqld_params" $_expect_file_name - -# Sleep to ensure that server exited... - ---sleep 30 +--echo Starting server with an unknown option ... +--let $unknown_option_log= $MYSQLTEST_VARDIR/tmp/galera_restart_on_unknown_option.err +--error 134 +--exec $MYSQLD --defaults-group-suffix=.2 --defaults-file=$MYSQLTEST_VARDIR/my.cnf --log-error=$unknown_option_log --galera-unknown-option > $unknown_option_log.out 2>&1 +--let SEARCH_FILE= $unknown_option_log +--let SEARCH_PATTERN= unknown option '--galera-unknown-option' +--source include/search_pattern_in_file.inc +--let SEARCH_PATTERN= State transfer interrupted, shutting down gracefully +--source include/search_pattern_in_file.inc +--let SEARCH_PATTERN= Assertion .* failed +--source include/search_pattern_in_file.inc +--remove_file $unknown_option_log +--remove_file $unknown_option_log.out # Restart node 2 without unknown option: @@ -104,14 +107,26 @@ SELECT * FROM t1; --connection node_2 ---let $start_mysqld_params=--galera-unknown-option +# The server must refuse to start, and exit without an assertion. +# Start it directly rather than through the expect file: mtr would treat +# a server that fails to start as a test failure. +# The unknown option is found only after the state transfer has started, +# which is then interrupted, and Galera aborts the process on purpose +# (SIGABRT, exit code 134). ---echo Starting server ... ---write_line "try:$start_mysqld_params" $_expect_file_name - -# Sleep to ensure that server exited... - ---sleep 30 +--echo Starting server with an unknown option ... +--let $unknown_option_log= $MYSQLTEST_VARDIR/tmp/galera_restart_on_unknown_option.err +--error 134 +--exec $MYSQLD --defaults-group-suffix=.2 --defaults-file=$MYSQLTEST_VARDIR/my.cnf --log-error=$unknown_option_log --galera-unknown-option > $unknown_option_log.out 2>&1 +--let SEARCH_FILE= $unknown_option_log +--let SEARCH_PATTERN= unknown option '--galera-unknown-option' +--source include/search_pattern_in_file.inc +--let SEARCH_PATTERN= State transfer interrupted, shutting down gracefully +--source include/search_pattern_in_file.inc +--let SEARCH_PATTERN= Assertion .* failed +--source include/search_pattern_in_file.inc +--remove_file $unknown_option_log +--remove_file $unknown_option_log.out # Restart node 2 without unknown option: @@ -133,27 +148,4 @@ SELECT * FROM t1; DROP TABLE t1; --source include/auto_increment_offset_restore.inc -# -# We should count the number of "Assertion failed" warnings -# in the log file during test phase - to print the error message -# if quantity of such warnings in log file increased at the end -# of the test: -# ---perl - use strict; - my $test_log=$ENV{'TEST_LOG'} or die "TEST_LOG not set"; - my $test_log_copy=$test_log . '.copy'; - open(FILE, $test_log_copy) or die("Unable to open $test_log_copy: $!\n"); - my $initial=grep(/Assertion * failed/gi,); - close(FILE); - open(FILE, $test_log) or die("Unable to open $test_log: $!\n"); - my $count_warnings=grep(/Assertion * failed/gi,); - close(FILE); - if ($count_warnings != $initial) { - my $diff=$count_warnings-$initial; - print "Assertion failed $diff times.\n"; - } -EOF ---remove_file $TEST_LOG.copy - --source include/auto_increment_offset_restore.inc From 85ecdbe546f37b98d23d86ee72b10c3395d44fb5 Mon Sep 17 00:00:00 2001 From: forkfun Date: Wed, 9 Sep 2026 13:32:31 +0200 Subject: [PATCH 20/28] MDEV-39146 Fix stack-use-after-return in PROCESSLIST for event threads Event_job_data::execute() pointed thd->security_ctx at a local Security_context. A concurrent PROCESSLIST scan captures that pointer without a lock and can read it after execute() returns. Make the Security_context a member of Event_job_data, which lives in Event_worker_thread::run() until after thd is deleted. Add main.events_debug test --- mysql-test/main/events_debug.result | 47 ++++++++++++++++++++ mysql-test/main/events_debug.test | 68 +++++++++++++++++++++++++++++ sql/event_data_objects.cc | 2 +- sql/event_data_objects.h | 13 +++++- sql/event_scheduler.cc | 3 ++ sql/sql_show.cc | 7 +++ 6 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 mysql-test/main/events_debug.result create mode 100644 mysql-test/main/events_debug.test diff --git a/mysql-test/main/events_debug.result b/mysql-test/main/events_debug.result new file mode 100644 index 00000000000..0d8c3a4e6dc --- /dev/null +++ b/mysql-test/main/events_debug.result @@ -0,0 +1,47 @@ +# +# MDEV-39146 Fix stack-use-after-return in PROCESSLIST for event threads +# +CREATE USER definer@localhost; +GRANT EVENT ON *.* TO definer@localhost; +GRANT SUPER ON *.* TO definer@localhost; +SET DEBUG_SYNC= 'RESET'; +# Per-session DEBUG_SYNC: must arm from inside the event body (its own +# thd), not from another connection. +CREATE DEFINER=definer@localhost EVENT ev +ON SCHEDULE AT CURRENT_TIMESTAMP +DO +BEGIN +SET DEBUG_SYNC= 'event_worker_thread_after_execute_returned SIGNAL event_execute_returned'; +SET DEBUG_SYNC= 'now SIGNAL definer_ctx_active WAIT_FOR event_may_finish'; +END| +Warnings: +Warning 1105 Event scheduler is switched off, use SET GLOBAL event_scheduler=ON to enable it. +SET GLOBAL event_scheduler= ON; +# Connect 'waiter' now: iterate() holds server_threads' rwlock for the +# whole scan; connecting after would deadlock (needs that lock's +# write side). +connect waiter, localhost, root,,; +connection default; +SET DEBUG_SYNC= 'now WAIT_FOR definer_ctx_active'; +# Pause right after processlist_callback() captures the +# DEFINER-switched security_ctx, before USER/HOST are read. +SET @saved_dbug= @@SESSION.debug_dbug; +SET SESSION debug_dbug= '+d,processlist_callback_pause_after_sctx_capture'; +SET DEBUG_SYNC= 'processlist_callback_captured_sctx SIGNAL scanner_captured WAIT_FOR let_scanner_continue'; +# Filter by name: checks the value read through the captured pointer. +SELECT user, host FROM information_schema.processlist WHERE user = 'definer'; +connection waiter; +SET DEBUG_SYNC= 'now WAIT_FOR scanner_captured'; +# Let the event finish and execute() return. +SET DEBUG_SYNC= 'now SIGNAL event_may_finish WAIT_FOR event_execute_returned TIMEOUT 60'; +# Release the scanner to dereference the pointer it captured. +SET DEBUG_SYNC= 'now SIGNAL let_scanner_continue'; +disconnect waiter; +connection default; +user host +definer localhost +SET SESSION debug_dbug= @saved_dbug; +SET DEBUG_SYNC= 'RESET'; +SET GLOBAL event_scheduler= OFF; +DROP USER definer@localhost; +# End of 10.11 tests diff --git a/mysql-test/main/events_debug.test b/mysql-test/main/events_debug.test new file mode 100644 index 00000000000..66dfec1a293 --- /dev/null +++ b/mysql-test/main/events_debug.test @@ -0,0 +1,68 @@ +--source include/have_debug.inc +--source include/have_debug_sync.inc +--source include/not_embedded.inc + +--echo # +--echo # MDEV-39146 Fix stack-use-after-return in PROCESSLIST for event threads +--echo # + +CREATE USER definer@localhost; +GRANT EVENT ON *.* TO definer@localhost; +GRANT SUPER ON *.* TO definer@localhost; + +SET DEBUG_SYNC= 'RESET'; + +--echo # Per-session DEBUG_SYNC: must arm from inside the event body (its own +--echo # thd), not from another connection. +--delimiter | +CREATE DEFINER=definer@localhost EVENT ev +ON SCHEDULE AT CURRENT_TIMESTAMP +DO +BEGIN + SET DEBUG_SYNC= 'event_worker_thread_after_execute_returned SIGNAL event_execute_returned'; + SET DEBUG_SYNC= 'now SIGNAL definer_ctx_active WAIT_FOR event_may_finish'; +END| +--delimiter ; + +SET GLOBAL event_scheduler= ON; + +--echo # Connect 'waiter' now: iterate() holds server_threads' rwlock for the +--echo # whole scan; connecting after would deadlock (needs that lock's +--echo # write side). +connect (waiter, localhost, root,,); +connection default; + +SET DEBUG_SYNC= 'now WAIT_FOR definer_ctx_active'; + +--echo # Pause right after processlist_callback() captures the +--echo # DEFINER-switched security_ctx, before USER/HOST are read. +SET @saved_dbug= @@SESSION.debug_dbug; +SET SESSION debug_dbug= '+d,processlist_callback_pause_after_sctx_capture'; +SET DEBUG_SYNC= 'processlist_callback_captured_sctx SIGNAL scanner_captured WAIT_FOR let_scanner_continue'; + +--echo # Filter by name: checks the value read through the captured pointer. +--send SELECT user, host FROM information_schema.processlist WHERE user = 'definer' + +connection waiter; +SET DEBUG_SYNC= 'now WAIT_FOR scanner_captured'; + +--echo # Let the event finish and execute() return. +SET DEBUG_SYNC= 'now SIGNAL event_may_finish WAIT_FOR event_execute_returned TIMEOUT 60'; + +--echo # Release the scanner to dereference the pointer it captured. +SET DEBUG_SYNC= 'now SIGNAL let_scanner_continue'; + +--disconnect waiter +--connection default +--reap + +SET SESSION debug_dbug= @saved_dbug; +SET DEBUG_SYNC= 'RESET'; + +SET GLOBAL event_scheduler= OFF; +--source include/check_events_off.inc +let $count_sessions= 1; +--source include/wait_until_count_sessions.inc +DROP USER definer@localhost; + +--echo # End of 10.11 tests diff --git a/sql/event_data_objects.cc b/sql/event_data_objects.cc index 4fdfad925a7..57232b91091 100644 --- a/sql/event_data_objects.cc +++ b/sql/event_data_objects.cc @@ -1358,7 +1358,7 @@ Event_job_data::execute(THD *thd, bool drop) { String sp_sql; #ifndef NO_EMBEDDED_ACCESS_CHECKS - Security_context event_sctx, *save_sctx= NULL; + Security_context *save_sctx= NULL; #endif List empty_item_list; bool ret= TRUE; diff --git a/sql/event_data_objects.h b/sql/event_data_objects.h index d66707c0eaa..e6db04e4125 100644 --- a/sql/event_data_objects.h +++ b/sql/event_data_objects.h @@ -24,9 +24,9 @@ #include "event_parse_data.h" #include "thr_lock.h" /* thr_lock_type */ +#include "sql_class.h" /* Security_context */ class Field; -class THD; class Time_zone; struct TABLE; @@ -173,6 +173,17 @@ public: class Stored_program_creation_ctx *creation_ctx; + /* + DEFINER security context, active in thd->security_ctx during execute(). + Kept here, not in execute(), to outlive it: a concurrent PROCESSLIST + scan may still read it. See Event_worker_thread::run(). + + acl_getroot() aliases user/host/ip to definer_user/definer_host + instead of copying them, so they must live in this object. + Never call event_sctx.destroy(): it would free strings not owned here. + */ + Security_context event_sctx; + Event_job_data(); virtual bool diff --git a/sql/event_scheduler.cc b/sql/event_scheduler.cc index d2e58460134..ffd68e22d01 100644 --- a/sql/event_scheduler.cc +++ b/sql/event_scheduler.cc @@ -317,6 +317,9 @@ Event_worker_thread::run(THD *thd, Event_queue_element_for_exec *event) res= job_data.execute(thd, event->dropped); + /* Test hook: execute() has returned, job_data is still alive. */ + DEBUG_SYNC(thd, "event_worker_thread_after_execute_returned"); + print_warnings(thd, &job_data); if (res && global_system_variables.log_warnings > 2) diff --git a/sql/sql_show.cc b/sql/sql_show.cc index 7c7a023500f..b0b6e8fda07 100644 --- a/sql/sql_show.cc +++ b/sql/sql_show.cc @@ -3334,6 +3334,13 @@ static my_bool processlist_callback(THD *tmp, processlist_callback_arg *arg) ulonglong max_counter; bool got_thd_data; + /* Test hook: pause after tmp_sctx is captured, before it is read. */ + DBUG_EXECUTE_IF("processlist_callback_pause_after_sctx_capture", + if (tmp->system_thread == SYSTEM_THREAD_EVENT_WORKER && + tmp_sctx != &tmp->main_security_ctx) + DEBUG_SYNC(arg->thd, + "processlist_callback_captured_sctx");); + if (!thd_visible_in_processlist(arg->thd->security_ctx, tmp)) return 0; From d5815e38435fc1ae0a8c741e2078ef5f29517685 Mon Sep 17 00:00:00 2001 From: Aleksey Midenkov Date: Mon, 19 Oct 2026 12:39:35 +0300 Subject: [PATCH 21/28] MDEV-41583 Tests fail on platform-dependent max TIMESTAMP of current row versioning.partition (MDEV-41543) and main.grant2 (MDEV-41099) print row_end of the current row. Since MDEV-32188 in 11.5 the max TIMESTAMP is 2106-02-07 06:28:15.999999 on 64-bit and 2038-01-19 03:14:07.999999 on 32-bit, so the results recorded on 64-bit fail on 32-bit. The fix prints CURRENT instead of row_end of the current row in versioning.partition (using current_row_ts() from common.inc) and replaces row_end with # in main.grant2, where only the privilege to read it is tested. Affected since 11.8. The fix is committed to the originating 10.11 to minimize conflicts. --- mysql-test/main/grant2.result | 4 ++-- mysql-test/main/grant2.test | 3 ++- mysql-test/suite/versioning/r/partition.result | 10 ++++++---- mysql-test/suite/versioning/t/partition.test | 6 ++++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/mysql-test/main/grant2.result b/mysql-test/main/grant2.result index 27cff87cbd2..e2639885544 100644 --- a/mysql-test/main/grant2.result +++ b/mysql-test/main/grant2.result @@ -1145,11 +1145,11 @@ connection con1; set time_zone= '+00:00'; select row_start, row_end from t; row_start row_end -2001-01-01 00:00:00.000000 2038-01-19 03:14:07.999999 +2001-01-01 00:00:00.000000 # handler t open; handler t read first; id a b row_start row_end DB_ROW_HASH_1 -1 10 x 2001-01-01 00:00:00.000000 2038-01-19 03:14:07.999999 # +1 10 x 2001-01-01 00:00:00.000000 # # handler t close; disconnect con1; connection default; diff --git a/mysql-test/main/grant2.test b/mysql-test/main/grant2.test index 30a67169b6b..70a1611a537 100644 --- a/mysql-test/main/grant2.test +++ b/mysql-test/main/grant2.test @@ -1179,9 +1179,10 @@ connection default; grant select (a) on db.t to u@localhost; connection con1; set time_zone= '+00:00'; +--replace_column 2 # select row_start, row_end from t; handler t open; ---replace_column 6 # +--replace_column 5 # 6 # handler t read first; handler t close; disconnect con1; diff --git a/mysql-test/suite/versioning/r/partition.result b/mysql-test/suite/versioning/r/partition.result index 30c8ed16f98..ef2f23d608e 100644 --- a/mysql-test/suite/versioning/r/partition.result +++ b/mysql-test/suite/versioning/r/partition.result @@ -3748,20 +3748,22 @@ alter table t1 add y int partition by system_time interval 1 hour starts '2000-01-01 00:00:00'; affected rows: 3 info: Records: 3 Duplicates: 0 Warnings: 0 -select x, y, row_end from t1 for system_time all order by x; +select x, y, if(current_row_ts(row_end), 'CURRENT', row_end) as row_end +from t1 for system_time all order by x; x y row_end 0 NULL 2000-01-01 01:30:00.000000 1 NULL 2000-01-01 02:30:00.000000 -2 NULL 2038-01-19 03:14:07.999999 +2 NULL CURRENT # LIMIT with DROP COLUMN: the table is rebuilt alter table t1 drop y partition by system_time limit 100; affected rows: 3 info: Records: 3 Duplicates: 0 Warnings: 0 -select x, row_end from t1 for system_time all order by x; +select x, if(current_row_ts(row_end), 'CURRENT', row_end) as row_end +from t1 for system_time all order by x; x row_end 0 2000-01-01 01:30:00.000000 1 2000-01-01 02:30:00.000000 -2 2038-01-19 03:14:07.999999 +2 CURRENT set timestamp= default; set time_zone= default; drop table t1; diff --git a/mysql-test/suite/versioning/t/partition.test b/mysql-test/suite/versioning/t/partition.test index 9604f779779..3a68a2dc303 100644 --- a/mysql-test/suite/versioning/t/partition.test +++ b/mysql-test/suite/versioning/t/partition.test @@ -2945,12 +2945,14 @@ select x, row_end from t1 partition (p2); alter table t1 add y int partition by system_time interval 1 hour starts '2000-01-01 00:00:00'; --disable_info -select x, y, row_end from t1 for system_time all order by x; +select x, y, if(current_row_ts(row_end), 'CURRENT', row_end) as row_end +from t1 for system_time all order by x; --echo # LIMIT with DROP COLUMN: the table is rebuilt --enable_info alter table t1 drop y partition by system_time limit 100; --disable_info -select x, row_end from t1 for system_time all order by x; +select x, if(current_row_ts(row_end), 'CURRENT', row_end) as row_end +from t1 for system_time all order by x; set timestamp= default; set time_zone= default; drop table t1; From e63f94313a95c9b1a650202eeeaa6cde205d8023 Mon Sep 17 00:00:00 2001 From: Aleksey Midenkov Date: Mon, 12 Oct 2026 16:21:04 +0300 Subject: [PATCH 22/28] MDEV-40380 vcol.races sporadic timeout After MDEV-39261 purge opens TABLE via open_purge_table() for any undo record of a table with indexed virtual columns, including the insert undo record. If purge does that after FLUSH TABLES, the TABLE is put back into the table cache. Then DELETE from con1 reuses the cached TABLE, skips open_table_from_share() and never hits the TABLE_after_field_clone sync point. DELETE completes immediately, wait_condition times out and SELECT returns an empty set. The fix waits for purge to complete before FLUSH TABLES. --- mysql-test/suite/vcol/t/races.test | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mysql-test/suite/vcol/t/races.test b/mysql-test/suite/vcol/t/races.test index b6b42b1771d..9df6d6997e3 100644 --- a/mysql-test/suite/vcol/t/races.test +++ b/mysql-test/suite/vcol/t/races.test @@ -5,6 +5,10 @@ source include/have_innodb.inc; source include/have_debug_sync.inc; create table t1 (f text, vf tinytext as (f), key (vf(64))) engine=innodb; insert t1 (f) values ('foo'); +# Purge may open t1 into the table cache after FLUSH TABLES +--disable_query_log +set global innodb_max_purge_lag_wait= 0; +--enable_query_log flush tables; connect con1,localhost,root,,test; set debug_sync='TABLE_after_field_clone WAIT_FOR go'; From 9660806dc965244d67eb28707d5a19ed4e84852c Mon Sep 17 00:00:00 2001 From: Vladislav Vaintroub Date: Thu, 15 Oct 2026 21:39:03 +0200 Subject: [PATCH 23/28] MDEV-41508 Off-by-One Uninitialized Stack Read in Proxy Protocol V2 Port Parsing parse_v2_header() required only 25 (TCPv4) / 49 (TCPv6) bytes, but then reads the source port at hdr[25] / hdr[49]. A truncated header could make it read uninitialized stack. Require the whole address block: 28 bytes for TCPv4 and 52 for TCPv6. Also, the declared header length was trusted without checking how many bytes vio_read() returned. Reject short reads. The spec requires the sender to send the header at once, and allows the receiver to drop the connection on a partial header, so a single vio_read() is enough. The v1 parser had the same problem on EOF: vio_read() returning 0 was taken for a byte read, so the uninitialized buffer was parsed. Require exactly one byte. Extend test_proxy_header_tcp with a header cut short before the port, and add test_proxy_header_short for a header shorter than it declares. Light refactoring, to avoid repetition: added connect_tcp() and init_v2_header(), which are now shared by several proxy header tests. Assisted-by: Claude:claude-sonnet-5-5 --- sql/proxy_protocol.cc | 22 +++-- tests/mysql_client_test.c | 197 +++++++++++++++++++++++++++----------- 2 files changed, 158 insertions(+), 61 deletions(-) diff --git a/sql/proxy_protocol.cc b/sql/proxy_protocol.cc index 117d9b9a107..a0dabd0a66a 100644 --- a/sql/proxy_protocol.cc +++ b/sql/proxy_protocol.cc @@ -92,6 +92,15 @@ static int parse_v1_header(char *hdr, size_t len, proxy_peer_info *peer_info) } +/* + PROXY v2 header is 16 bytes, followed by the address block with source and + destination address and port. Whole header length for TCP over IPv4, IPv6. +*/ +#define PROXY_V2_HEADER_LEN 16 +#define PROXY_V2_TCP4_LEN (PROXY_V2_HEADER_LEN + 2 * 4 + 2 * 2) +#define PROXY_V2_TCP6_LEN (PROXY_V2_HEADER_LEN + 2 * 16 + 2 * 2) + + /* Parse proxy protocol V2 (binary) header */ @@ -129,14 +138,14 @@ static int parse_v2_header(uchar *hdr, size_t len,proxy_peer_info *peer_info) switch (fam) { case 0x11: /* TCPv4 */ - if (len < 25) + if (len < PROXY_V2_TCP4_LEN) return -1; sin->sin_family= AF_INET; memcpy(&(sin->sin_addr), hdr + 16, 4); peer_info->port= (hdr[24] << 8) + hdr[25]; break; case 0x21: /* TCPv6 */ - if (len < 49) + if (len < PROXY_V2_TCP6_LEN) return -1; sin6->sin6_family= AF_INET6; memcpy(&(sin6->sin6_addr), hdr + 16, 16); @@ -201,8 +210,8 @@ int parse_proxy_protocol_header(NET *net, proxy_peer_info *peer_info) /* Read until end of header (newline character)*/ while(pos < sizeof(hdr) - 1) { - long len= (long)vio_read(vio, hdr + pos, 1); - if (len < 0) + ssize_t len= vio_read(vio, hdr + pos, 1); + if (len != 1) return -1; pos++; if (hdr[pos-1] == '\n') @@ -216,10 +225,9 @@ int parse_proxy_protocol_header(NET *net, proxy_peer_info *peer_info) } else // if (have_v2_header) { -#define PROXY_V2_HEADER_LEN 16 /* read off 16 bytes of the header.*/ ssize_t len= vio_read(vio, hdr + pos, PROXY_V2_HEADER_LEN - pos); - if (len < 0) + if (len != PROXY_V2_HEADER_LEN - ssize_t(pos)) return -1; // 2 last bytes are the length in network byte order of the part following header ushort trail_len= ((ushort)hdr[PROXY_V2_HEADER_LEN-2] << 8) + hdr[PROXY_V2_HEADER_LEN-1]; @@ -228,7 +236,7 @@ int parse_proxy_protocol_header(NET *net, proxy_peer_info *peer_info) if (trail_len > 0) { len= vio_read(vio, hdr + PROXY_V2_HEADER_LEN, trail_len); - if (len < 0) + if (len != trail_len) return -1; } pos= PROXY_V2_HEADER_LEN + trail_len; diff --git a/tests/mysql_client_test.c b/tests/mysql_client_test.c index 115b5b7f358..2daba652058 100644 --- a/tests/mysql_client_test.c +++ b/tests/mysql_client_test.c @@ -20576,6 +20576,22 @@ typedef struct { } v2_proxy_header; #ifndef EMBEDDED_LIBRARY +/* + Zero the header, and set the part that is common to all PROXY v2 headers: + the signature, version 2 with the command (0x1 PROXY, 0x0 LOCAL), the + address family and protocol, and the length of the address block. +*/ +static void init_v2_header(v2_proxy_header *header, int command, int fam, + size_t len) +{ + memset(header, 0, sizeof(*header)); + memcpy(header->sig, "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A", + sizeof(header->sig)); + header->ver_cmd= (0x2 << 4) | command; + header->fam= fam; + header->len= htons(len); +} + static void test_proxy_header_tcp(const char *ipaddr, int port) { int rc; @@ -20584,26 +20600,22 @@ static void test_proxy_header_tcp(const char *ipaddr, int port) char query[256]; char text_header[256], bad_text_header[256]; char addr_bin[16]; - v2_proxy_header v2_header, bad_v2_header; - void *header_data[4]; - size_t header_lengths[4]; + v2_proxy_header v2_header, bad_v2_header, short_v2_header; + void *header_data[5]; + size_t header_lengths[5]; size_t i; // normalize IPv4-mapped IPv6 addresses, e.g ::ffff:127.0.0.2 to 127.0.0.2 const char *normalized_addr= strncmp(ipaddr, "::ffff:", 7)?ipaddr : ipaddr + 7; myheader("test_proxy_header_tcp"); - memset(&v2_header, 0, sizeof(v2_header)); snprintf(text_header, sizeof(text_header), "PROXY %s %s %s %d 3306\r\n",family == AF_INET?"TCP4":"TCP6", ipaddr, ipaddr, port); - + inet_pton(family,ipaddr,addr_bin); - memcpy(v2_header.sig, "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A", 12); - v2_header.ver_cmd = (0x2 << 4) | 0x1; /* Version (0x2) , Command = PROXY (0x1) */ if(family == AF_INET) { - v2_header.fam= 0x11; - v2_header.len= htons(12); + init_v2_header(&v2_header, 0x1, 0x11, sizeof(v2_header.addr.ip4)); v2_header.addr.ip4.src_port= htons(port); v2_header.addr.ip4.dst_port= htons(3306); memcpy(&v2_header.addr.ip4.src_addr,addr_bin, sizeof (v2_header.addr.ip4.src_addr)); @@ -20611,8 +20623,7 @@ static void test_proxy_header_tcp(const char *ipaddr, int port) } else { - v2_header.fam= 0x21; - v2_header.len= htons(36); + init_v2_header(&v2_header, 0x1, 0x21, sizeof(v2_header.addr.ip6)); v2_header.addr.ip6.src_port= htons(port); v2_header.addr.ip6.dst_port= htons(3306); memcpy(v2_header.addr.ip6.src_addr,addr_bin, sizeof (v2_header.addr.ip6.src_addr)); @@ -20627,11 +20638,14 @@ static void test_proxy_header_tcp(const char *ipaddr, int port) header_data[1]= &v2_header; header_data[2]= bad_text_header; header_data[3]= &bad_v2_header; + header_data[4]= &short_v2_header; header_lengths[0]= strlen(text_header); header_lengths[1]= family == AF_INET ? 28 : 52; header_lengths[2]= sizeof(text_header)-4; header_lengths[3]= header_lengths[1]; + /* MDEV-41508: address block ends before the source port's low byte */ + header_lengths[4]= family == AF_INET ? 25 : 49; memset(bad_text_header, ' ', sizeof(bad_text_header)); memcpy(bad_text_header, text_header, header_lengths[1]-6); @@ -20639,6 +20653,9 @@ static void test_proxy_header_tcp(const char *ipaddr, int port) bad_v2_header= v2_header; bad_v2_header.len= 0; + short_v2_header= v2_header; + short_v2_header.len= htons(header_lengths[4] - 16); + for (i = 0; i < 2; i++) { MYSQL *m; @@ -20679,8 +20696,7 @@ static void test_proxy_header_tcp(const char *ipaddr, int port) DIE(0); printf("pass %zu error %i - %s\n", i, mysql_errno(m), mysql_error(m)); - DIE_IF(i == 2 && mysql_errno(m) != ER_UNKNOWN_ERROR); - DIE_IF(i == 3 && mysql_errno(m) != ER_UNKNOWN_ERROR); + DIE_IF(mysql_errno(m) != ER_UNKNOWN_ERROR); mysql_close(m); } snprintf(query, sizeof(query), "DROP USER 'u'@'%s'",normalized_addr); @@ -20701,11 +20717,7 @@ static void test_proxy_header_localhost() int rc; myheader("test_proxy_header_localhost"); - memset(&v2_header, 0, sizeof(v2_header)); - memcpy(v2_header.sig, "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A", 12); - v2_header.ver_cmd = (0x2 << 4) | 0x1; /* Version (0x2) , Command = PROXY (0x1) */ - v2_header.fam= 0x31; - v2_header.len= htons(216); + init_v2_header(&v2_header, 0x1, 0x31, sizeof(v2_header.addr.unx)); strcpy(v2_header.addr.unx.src_addr,"/tmp/mysql.sock"); strcpy(v2_header.addr.unx.dst_addr,"/tmp/mysql.sock"); rc = mysql_query(mysql, "CREATE USER 'u'@'localhost' IDENTIFIED BY 'password'"); @@ -20738,10 +20750,8 @@ static void test_proxy_header_ignore() DIE_UNLESS(mysql_real_connect(m, opt_host, "root", "", NULL, opt_port, opt_unix_socket, 0) == m); mysql_close(m); - memcpy(v2_header.sig, "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A", 12); - v2_header.ver_cmd = (0x2 << 4) | 0x0; /* Version (0x2) , Command = LOCAL (0x0) */ - v2_header.fam= 0x0; /* AF_UNSPEC*/ - v2_header.len= htons(0); + /* Command LOCAL (0x0), AF_UNSPEC, no address block */ + init_v2_header(&v2_header, 0x0, 0x0, 0); m = mysql_client_init(NULL); mysql_optionsv(m, MARIADB_OPT_PROXY_HEADER, &v2_header,16); DIE_UNLESS(mysql_real_connect(m, opt_host, "root", "", NULL, opt_port, opt_unix_socket, 0) == m); @@ -20760,6 +20770,113 @@ static void test_proxy_header_ignore() myquery(rc); } +/* Open a raw TCP connection to opt_host:opt_port */ +static my_socket connect_tcp(void) +{ + struct addrinfo hints, *ai= NULL, *p; + char portbuf[MY_INT32_NUM_DECIMAL_DIGITS]; + int connected= 0; + my_socket s= INVALID_SOCKET; + + snprintf(portbuf, sizeof(portbuf), "%u", opt_port); + memset(&hints, 0, sizeof(hints)); + hints.ai_family= AF_UNSPEC; + hints.ai_socktype= SOCK_STREAM; + DIE_UNLESS(getaddrinfo(opt_host ? opt_host : "localhost", portbuf, + &hints, &ai) == 0); + + for (p= ai; p; p= p->ai_next) + { + s= socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (s == INVALID_SOCKET) + continue; + if (connect(s, p->ai_addr, (int) p->ai_addrlen) == 0) + { + connected= 1; + break; + } + closesocket(s); + s= INVALID_SOCKET; + } + freeaddrinfo(ai); + DIE_UNLESS(connected); + return s; +} + + +/* + MDEV-41508 PROXY v2 header, shorter than it declares, must be rejected. + Raw socket is used, as the client library would follow the header with + its login packet, which would be taken for the missing header bytes. +*/ +static void test_proxy_header_short() +{ + v2_proxy_header v2_header; + /* 1024 is enough for the greeting packet plus the error packet */ + char buf[1024]; + /* Error packet payload: 0xff, error code, '#' and SQLSTATE, then message */ + const size_t error_payload_header_len= 1 + 2 + 1 + SQLSTATE_LENGTH; + size_t total= 0, error_packet_pos, error_packet_len, err_msg_len; + const uchar *error_packet, *error_payload; + const char *err_msg; + static const LEX_CSTRING expected_msg= + { STRING_WITH_LEN("Failed to parse proxy header") }; + uint err_code; + my_socket s; + + myheader("test_proxy_header_short"); + + /* Command PROXY, TCP over IPv4, declares the whole IPv4 address block */ + init_v2_header(&v2_header, 0x1, 0x11, sizeof(v2_header.addr.ip4)); + + s= connect_tcp(); + /* + Send an incomplete PROXY v2 header, 25 bytes: the signature, 2 IPv4 + addresses, and the first byte of the source port (16 + 4 + 4 + 1). + The complete header is 28 bytes. + */ + DIE_UNLESS(send(s, (const char *) &v2_header, 25, 0) == 25); + + /* + The server sends its greeting first. Then it rejects the header with an + error packet and closes the connection, so read until EOF. + */ + for (;;) + { + int n= recv(s, buf + total, (int) (sizeof(buf) - total), 0); + if (n <= 0) + break; + total+= n; + } + closesocket(s); + + /* + Every packet is: 3 bytes of payload length, 1 byte of sequence number, + and the payload. Skip the greeting packet. + */ + DIE_UNLESS(total >= NET_HEADER_SIZE); + error_packet_pos= NET_HEADER_SIZE + uint3korr(buf); + + /* The error packet, after its header: 0xff, error code, '#', SQLSTATE */ + DIE_UNLESS(total >= error_packet_pos + NET_HEADER_SIZE); + error_packet= (const uchar *) buf + error_packet_pos; + error_packet_len= uint3korr(error_packet); + error_payload= error_packet + NET_HEADER_SIZE; + DIE_UNLESS(error_packet_len >= error_payload_header_len); + DIE_UNLESS(total >= error_packet_pos + NET_HEADER_SIZE + error_packet_len); + DIE_UNLESS(error_payload[0] == 0xff); + DIE_UNLESS(error_payload[3] == '#'); + err_code= uint2korr(error_payload + 1); + err_msg= (const char *) error_payload + error_payload_header_len; + err_msg_len= error_packet_len - error_payload_header_len; + printf("error %u: %.*s\n", err_code, (int) err_msg_len, err_msg); + + DIE_UNLESS(err_code == ER_UNKNOWN_ERROR); + DIE_UNLESS(err_msg_len == expected_msg.length); + DIE_UNLESS(!memcmp(err_msg, expected_msg.str, expected_msg.length)); +} + + static void test_proxy_header_limits() { MYSQL *m; @@ -20823,11 +20940,7 @@ static void test_proxy_header_dbug_remote_connection() myquery(rc); /* Build a v2 proxy header announcing proxy_ipv6 as the client address. */ - memset(&v2_header, 0, sizeof(v2_header)); - memcpy(v2_header.sig, "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A", 12); - v2_header.ver_cmd = (0x2 << 4) | 0x1; /* PROXY command */ - v2_header.fam = 0x21; /* TCPv6 */ - v2_header.len = htons(36); + init_v2_header(&v2_header, 0x1, 0x21, sizeof(v2_header.addr.ip6)); inet_pton(AF_INET6, proxy_ipv6, addr_bin); memcpy(v2_header.addr.ip6.src_addr, addr_bin, 16); memcpy(v2_header.addr.ip6.dst_addr, addr_bin, 16); @@ -20872,33 +20985,8 @@ static void test_proxy_header_dbug_remote_connection() */ static void proxy_send_handshake_error(const char *client_ip) { - struct addrinfo hints, *ai= NULL, *p; - char portbuf[20], header[128]; - int connected= 0; - my_socket s= INVALID_SOCKET; - - snprintf(portbuf, sizeof(portbuf), "%u", opt_port); - memset(&hints, 0, sizeof(hints)); - hints.ai_family= AF_UNSPEC; - hints.ai_socktype= SOCK_STREAM; - DIE_UNLESS(getaddrinfo(opt_host ? opt_host : "localhost", portbuf, - &hints, &ai) == 0); - - for (p= ai; p; p= p->ai_next) - { - s= socket(p->ai_family, p->ai_socktype, p->ai_protocol); - if (s == INVALID_SOCKET) - continue; - if (connect(s, p->ai_addr, (int) p->ai_addrlen) == 0) - { - connected= 1; - break; - } - closesocket(s); - s= INVALID_SOCKET; - } - freeaddrinfo(ai); - DIE_UNLESS(connected); + char header[128]; + my_socket s= connect_tcp(); if (client_ip) { @@ -21198,6 +21286,7 @@ static void test_proxy_header() test_proxy_header_localhost(); test_proxy_header_ignore(); test_proxy_header_limits(); + test_proxy_header_short(); test_proxy_header_dbug_remote_connection(); test_proxy_header_connect_errors_reset(); test_proxy_header_proxy_host_connect_errors_reset(); From fb641b0cd1810e7d148d6b4d70f915c06c2c0f9e Mon Sep 17 00:00:00 2001 From: Vladislav Vaintroub Date: Tue, 20 Oct 2026 01:06:46 +0200 Subject: [PATCH 24/28] MDEV-40287 AES_ENCRYPT() and KDF() return NULL with OpenSSL 4.0 MyCTX used an EVP_CIPHER_CTX in a stack buffer instead of allocating it with EVP_CIPHER_CTX_new(). OpenSSL 4.0 rejects a context that was not created that way for ciphers that use an IV: EVP_CipherInit_ex() fails with "invalid iv length", so AES_ENCRYPT() and KDF() in CBC/CTR/GCM modes return NULL. ECB has no IV and still works, which is why only the non-ECB modes broke. Allocate the context with EVP_CIPHER_CTX_new()/EVP_CIPHER_CTX_free(), and on an init error free and null it, so a later update()/finish() from a caller that ignored the error is a safe no-op, not a double free. Remove check_openssl_compatibility() and the EVP_CIPHER_CTX_SIZE and EVP_CIPHER_CTX_init macros. my_md5() used the same stack-buffer-sized-by-EVP_MD_CTX_SIZE hack, whose only guard was check_openssl_compatibility(). MD5_CTX is a concrete struct in both OpenSSL and WolfSSL, so use it directly with MD5_Init/Update/Final on a caller-owned context, like my_sha.inl does for SHA*_CTX; this removes the EVP_MD_CTX hack and EVP_MD_CTX_SIZE. The exception is OpenSSL 1.0.x, which aborts on the low-level MD5 API in FIPS mode (MDEV-7788): there, and only there, keep the EVP path with the non-FIPS flag. 1.1.0 removed the FIPS Object Module and 3.0+ low-level MD5 bypasses the provider, so the concrete MD5_CTX is correct on every other version. SHA is unaffected, it already used concrete SHA*_CTX structs. Verified against OpenSSL 4.0.1: the mysys aes-t test fails on the CBC/CTR/GCM cases with the stack buffer and passes with EVP_CIPHER_CTX_new(). The 1.0.x MD5 path verified against a FIPS build of OpenSSL 1.0.2u: the EVP path computes MD5, the low-level path aborts. No visible performance degradation: the extra allocation costs ~12 ns/call (WolfSSL) and ~30 ns (OpenSSL) on Windows at a 30-byte payload, nothing at 16 KB, and nothing on Linux/glibc; sysbench OLTP over encrypted tables and redo log is unchanged. Tests main.tlsv13 and main.ssl_crl: OpenSSL 4.0.3 reports TLS alerts as "tls alert ..." (was "ssl/tls alert ..."), accept both. Assisted-by: Claude:claude-opus-4-8 --- include/ssl_compat.h | 24 --------- mysql-test/main/ssl_crl.test | 2 +- mysql-test/main/tlsv13.test | 4 +- mysys_ssl/CMakeLists.txt | 1 - mysys_ssl/my_crypt.cc | 45 +++++++++-------- mysys_ssl/my_md5.cc | 96 ++++++++++++++++------------------- mysys_ssl/openssl.c | 98 ------------------------------------ sql/mysqld.cc | 8 --- 8 files changed, 70 insertions(+), 208 deletions(-) delete mode 100644 mysys_ssl/openssl.c diff --git a/include/ssl_compat.h b/include/ssl_compat.h index 6bec4f48285..200c4cbcf3d 100644 --- a/include/ssl_compat.h +++ b/include/ssl_compat.h @@ -24,12 +24,6 @@ #define HAVE_OPENSSL11 1 #define SSL_LIBRARY OpenSSL_version(OPENSSL_VERSION) #define ERR_remove_state(X) ERR_clear_error() -#define EVP_CIPHER_CTX_SIZE 200 -#define EVP_MD_CTX_SIZE 80 -#undef EVP_MD_CTX_init -#define EVP_MD_CTX_init(X) do { memset((X), 0, EVP_MD_CTX_SIZE); EVP_MD_CTX_reset(X); } while(0) -#undef EVP_CIPHER_CTX_init -#define EVP_CIPHER_CTX_init(X) do { memset((X), 0, EVP_CIPHER_CTX_SIZE); EVP_CIPHER_CTX_reset(X); } while(0) /* Macros below are deprecated. OpenSSL 1.1 may define them or not, @@ -66,24 +60,16 @@ #endif /* HAVE_OPENSSL11 */ #endif -#ifdef HAVE_WOLFSSL -#define EVP_MD_CTX_SIZE sizeof(wc_Md5) -#endif - #ifndef HAVE_OPENSSL11 #ifndef ASN1_STRING_get0_data #define ASN1_STRING_get0_data(X) ASN1_STRING_data(X) #endif -#ifndef EVP_MD_CTX_SIZE -#define EVP_MD_CTX_SIZE sizeof(EVP_MD_CTX) -#endif #ifndef DH_set0_pqg #define DH_set0_pqg(D,P,Q,G) ((D)->p= (P), (D)->g= (G)) #endif #define EVP_CIPHER_CTX_encrypting(ctx) ((ctx)->encrypt) -#define EVP_CIPHER_CTX_SIZE sizeof(EVP_CIPHER_CTX) #ifndef HAVE_WOLFSSL #define OPENSSL_init_ssl(X,Y) SSL_library_init() @@ -97,13 +83,3 @@ #ifndef TLS1_3_VERSION #define SSL_CTX_set_ciphersuites(X,Y) 0 #endif - -#ifdef __cplusplus -extern "C" { -#endif /* __cplusplus */ - -int check_openssl_compatibility(); - -#ifdef __cplusplus -} -#endif diff --git a/mysql-test/main/ssl_crl.test b/mysql-test/main/ssl_crl.test index 6da116f4ebe..13958e33c1b 100644 --- a/mysql-test/main/ssl_crl.test +++ b/mysql-test/main/ssl_crl.test @@ -9,6 +9,6 @@ --echo # try logging in with a certificate in the server's --ssl-crl : should fail # OpenSSL 1.1.1a and later releases correctly rejects the certificate, but the error message is different ---replace_regex /(ERROR 2013 \(HY000\): Lost connection to server at '.*', system error: [0-9]+|ERROR 2026 \(HY000\): TLS\/SSL error: sslv3 alert certificate revoked)/ERROR 2026 (HY000): TLS\/SSL error: ssl\/tls alert certificate revoked/ +--replace_regex /(ERROR 2013 \(HY000\): Lost connection to server at '.*', system error: [0-9]+|ERROR 2026 \(HY000\): TLS\/SSL error: (sslv3|tls) alert certificate revoked)/ERROR 2026 (HY000): TLS\/SSL error: ssl\/tls alert certificate revoked/ --error 1 --exec $MYSQL --ssl-ca=$MYSQL_TEST_DIR/std_data/cacert.pem --ssl-key=$MYSQL_TEST_DIR/std_data/client-key.pem --ssl-cert=$MYSQL_TEST_DIR/std_data/client-cert.pem test -e "SHOW STATUS LIKE 'Ssl_version'" 2>&1 diff --git a/mysql-test/main/tlsv13.test b/mysql-test/main/tlsv13.test index f90b6849a86..2c80b4c2089 100644 --- a/mysql-test/main/tlsv13.test +++ b/mysql-test/main/tlsv13.test @@ -19,11 +19,11 @@ source include/restart_mysqld.inc; --exec $MYSQL --host=localhost --ssl-cipher=ECDHE-RSA-AES256-GCM-SHA384 --tls-version=TLSv1.2 -e "SHOW STATUS LIKE 'Ssl_cipher%';" # Check that other ciphers are correctly not supported by the server ---replace_regex /sslv3 alert handshake failure/ssl\/tls alert handshake failure/ +--replace_regex /error: (sslv3|tls) alert/error: ssl\/tls alert/ --error 1 --exec $MYSQL --host=localhost --ssl-cipher=TLS_AES_256_GCM_SHA384 --tls-version=TLSv1.3 -e "SHOW STATUS LIKE 'Ssl_cipher';" 2>&1 ---replace_regex /sslv3 alert handshake failure/ssl\/tls alert handshake failure/ +--replace_regex /error: (sslv3|tls) alert/error: ssl\/tls alert/ --error 1 --exec $MYSQL --host=localhost --ssl-cipher=ECDHE-RSA-AES128-GCM-SHA256 --tls-version=TLSv1.2 -e "SHOW STATUS LIKE 'Ssl_cipher';" 2>&1 diff --git a/mysys_ssl/CMakeLists.txt b/mysys_ssl/CMakeLists.txt index 1c3f60b5bb0..6a9c29449c6 100644 --- a/mysys_ssl/CMakeLists.txt +++ b/mysys_ssl/CMakeLists.txt @@ -28,7 +28,6 @@ SET(MYSYS_SSL_HIDDEN_SOURCES my_sha384.cc my_sha512.cc my_md5.cc - openssl.c ) SET(MYSYS_SSL_SOURCES diff --git a/mysys_ssl/my_crypt.cc b/mysys_ssl/my_crypt.cc index 0356b64ccfb..792d8367875 100644 --- a/mysys_ssl/my_crypt.cc +++ b/mysys_ssl/my_crypt.cc @@ -27,29 +27,18 @@ #include #include -#include - -#define CTX_ALIGN 16 class MyCTX { public: - char ctx_buf[EVP_CIPHER_CTX_SIZE + CTX_ALIGN]; EVP_CIPHER_CTX* ctx; MyCTX() { -#if CTX_ALIGN > 0 - uintptr_t p= ((uintptr_t)ctx_buf + (CTX_ALIGN - 1)) & ~(CTX_ALIGN - 1); - ctx = reinterpret_cast(p); -#else - ctx = (EVP_CIPHER_CTX*)ctx_buf; -#endif - - EVP_CIPHER_CTX_init(ctx); + ctx= EVP_CIPHER_CTX_new(); } virtual ~MyCTX() { - EVP_CIPHER_CTX_reset(ctx); + EVP_CIPHER_CTX_free(ctx); ERR_remove_state(0); } @@ -57,6 +46,8 @@ public: uint klen, const uchar *iv, uint ivlen) { compile_time_assert(MY_AES_CTX_SIZE >= sizeof(MyCTX)); + if (unlikely(!ctx)) + return MY_AES_OPENSSL_ERROR; if (unlikely(!cipher)) return MY_AES_BAD_KEYSIZE; @@ -115,9 +106,11 @@ public: DBUG_ASSERT(ivlen == 0 || ivlen == sizeof(oiv)); int res= MyCTX::init(cipher, encrypt, key, klen, iv, ivlen); + if (res) + return res; EVP_CIPHER_CTX_set_padding(ctx, 0); - return res; + return MY_AES_OK; } /** Update last partial source block, stored in source_tail array. */ @@ -213,10 +206,12 @@ public: { compile_time_assert(MY_AES_CTX_SIZE >= sizeof(MyCTX_gcm)); int res= MyCTX::init(cipher, encrypt, key, klen, iv, ivlen); + if (res) + return res; int real_ivlen= EVP_CIPHER_CTX_iv_length(ctx); aad= iv + real_ivlen; aadlen= ivlen - real_ivlen; - return res; + return MY_AES_OK; } int update(const uchar *src, uint slen, uchar *dst, uint *dlen) override @@ -298,20 +293,30 @@ int my_aes_crypt_init(void *ctx, enum my_aes_mode mode, int flags, new (ctx) MyCTX_nopad(); else new (ctx) MyCTX(); - return ((MyCTX*)ctx)->init(ciphers[mode](klen), flags & 1, - key, klen, iv, ivlen); + MyCTX *c= (MyCTX*)ctx; + int res= c->init(ciphers[mode](klen), flags & 1, key, klen, iv, ivlen); + if (res) + { + /* release ctx but keep the object: a later update()/finish() from a + caller that ignored the error is then a safe no-op, not a double free */ + EVP_CIPHER_CTX_free(c->ctx); + c->ctx= NULL; + } + return res; } int my_aes_crypt_update(void *ctx, const uchar *src, uint slen, uchar *dst, uint *dlen) { - return ((MyCTX*)ctx)->update(src, slen, dst, dlen); + MyCTX *c= (MyCTX*)ctx; + return c->ctx ? c->update(src, slen, dst, dlen) : MY_AES_OPENSSL_ERROR; } int my_aes_crypt_finish(void *ctx, uchar *dst, uint *dlen) { - int res= ((MyCTX*)ctx)->finish(dst, dlen); - ((MyCTX*)ctx)->~MyCTX(); + MyCTX *c= (MyCTX*)ctx; + int res= c->ctx ? c->finish(dst, dlen) : MY_AES_OPENSSL_ERROR; + c->~MyCTX(); return res; } diff --git a/mysys_ssl/my_md5.cc b/mysys_ssl/my_md5.cc index 75995bccf90..22682ddf705 100644 --- a/mysys_ssl/my_md5.cc +++ b/mysys_ssl/my_md5.cc @@ -19,70 +19,60 @@ @file @brief - Wrapper functions for OpenSSL and YaSSL. Also provides a Compatibility layer - to make available YaSSL's MD5 implementation. + Wrapper functions for the OpenSSL/WolfSSL MD5 implementation. */ #include #include #include - -#if defined(HAVE_WOLFSSL) -#include #include -typedef wc_Md5 EVP_MD_CTX; -static void md5_init(EVP_MD_CTX *context) -{ - wc_InitMd5(context);; -} -static void md5_input(EVP_MD_CTX *context, const uchar *buf, unsigned len) -{ - wc_Md5Update(context, buf, len); -} - -static void md5_result(EVP_MD_CTX *context, uchar digest[MD5_HASH_SIZE]) -{ - wc_Md5Final(context,digest); -} - -#elif defined(HAVE_OPENSSL) +#if defined(HAVE_OPENSSL10) && !defined(HAVE_WOLFSSL) && \ + !defined(LIBRESSL_VERSION_NUMBER) +/* + OpenSSL 1.0.x in FIPS mode aborts on the low-level MD5 API (md5 is not used + for crypto here), so go through EVP with the non-FIPS flag. MDEV-7788. +*/ #include -#include - -static void md5_init(EVP_MD_CTX *context) +typedef EVP_MD_CTX MY_MD5_CTX; +static void md5_init(MY_MD5_CTX *context) { -#if OPENSSL_VERSION_NUMBER >= 0x30000000L - EVP_MD *md5; - EVP_MD_CTX_init(context); - /* Ok to ignore FIPS: MD5 is not used for crypto here */ - /* In OpenSSL 3.0.0+ it is a different EVP_MD provider */ - md5 = EVP_MD_fetch(NULL, "MD5", "fips=no"); - EVP_DigestInit_ex(context, md5, NULL); - EVP_MD_free(md5); -#else EVP_MD_CTX_init(context); #ifdef EVP_MD_CTX_FLAG_NON_FIPS_ALLOW - /* Ok to ignore FIPS: MD5 is not used for crypto here */ - /* In OpenSSL 1.1.1 the non FIPS allowed flag is context specific */ EVP_MD_CTX_set_flags(context, EVP_MD_CTX_FLAG_NON_FIPS_ALLOW); #endif EVP_DigestInit_ex(context, EVP_md5(), NULL); -#endif } -static void md5_input(EVP_MD_CTX *context, const uchar *buf, unsigned len) +static void md5_input(MY_MD5_CTX *context, const uchar *buf, unsigned len) { EVP_DigestUpdate(context, buf, len); } -static void md5_result(EVP_MD_CTX *context, uchar digest[MD5_HASH_SIZE]) +static void md5_result(MY_MD5_CTX *context, uchar digest[MD5_HASH_SIZE]) { EVP_DigestFinal_ex(context, digest, NULL); - EVP_MD_CTX_reset(context); + EVP_MD_CTX_cleanup(context); } -#endif /* HAVE_WOLFSSL */ +#else +#include +typedef MD5_CTX MY_MD5_CTX; +static void md5_init(MY_MD5_CTX *context) +{ + MD5_Init(context); +} + +static void md5_input(MY_MD5_CTX *context, const uchar *buf, unsigned len) +{ + MD5_Update(context, buf, len); +} + +static void md5_result(MY_MD5_CTX *context, uchar digest[MD5_HASH_SIZE]) +{ + MD5_Final(digest, context); +} +#endif /** Wrapper function to compute MD5 message digest. @@ -95,11 +85,10 @@ static void md5_result(EVP_MD_CTX *context, uchar digest[MD5_HASH_SIZE]) */ void my_md5(uchar *digest, const char *buf, size_t len) { - char ctx_buf[EVP_MD_CTX_SIZE]; - EVP_MD_CTX * const ctx= (EVP_MD_CTX*)ctx_buf; - md5_init(ctx); - md5_input(ctx, (const uchar *)buf, (uint) len); - md5_result(ctx, digest); + MY_MD5_CTX context; + md5_init(&context); + md5_input(&context, (const uchar *)buf, (uint) len); + md5_result(&context, digest); } @@ -119,34 +108,33 @@ void my_md5_multi(uchar *digest, ...) { va_list args; const uchar *str; - char ctx_buf[EVP_MD_CTX_SIZE]; - EVP_MD_CTX * const ctx= (EVP_MD_CTX*)ctx_buf; + MY_MD5_CTX context; va_start(args, digest); - md5_init(ctx); + md5_init(&context); for (str= va_arg(args, const uchar*); str; str= va_arg(args, const uchar*)) - md5_input(ctx, str, (uint) va_arg(args, size_t)); + md5_input(&context, str, (uint) va_arg(args, size_t)); - md5_result(ctx, digest); + md5_result(&context, digest); va_end(args); } size_t my_md5_context_size() { - return EVP_MD_CTX_SIZE; + return sizeof(MY_MD5_CTX); } void my_md5_init(void *context) { - md5_init((EVP_MD_CTX *)context); + md5_init((MY_MD5_CTX *)context); } void my_md5_input(void *context, const uchar *buf, size_t len) { - md5_input((EVP_MD_CTX *)context, buf, (uint) len); + md5_input((MY_MD5_CTX *)context, buf, (uint) len); } void my_md5_result(void *context, uchar *digest) { - md5_result((EVP_MD_CTX *)context, digest); + md5_result((MY_MD5_CTX *)context, digest); } diff --git a/mysys_ssl/openssl.c b/mysys_ssl/openssl.c deleted file mode 100644 index 3890e3524be..00000000000 --- a/mysys_ssl/openssl.c +++ /dev/null @@ -1,98 +0,0 @@ -/* - Copyright (c) 2017, MariaDB Corporation. - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; version 2 of the License. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ - -#include -#include -#include - -/* - The check is only done for OpenSSL 1.1.x. - It could run for OpenSSL 1.0.x but it doesn't make much sense - and it hits this bug: - https://bugs.launchpad.net/ubuntu/+source/openssl/+bug/1594748 -*/ - -#ifndef HAVE_OPENSSL11 -int check_openssl_compatibility() -{ - return 0; -} -#else -#include - -static uint testing; -static size_t alloc_size, alloc_count; - -static void *coc_malloc(size_t size -#ifndef LIBRESSL_VERSION_NUMBER - , const char *f __attribute__((unused)), - int l __attribute__((unused)) -#endif -) -{ - if (unlikely(testing)) - { - alloc_size+= size; - alloc_count++; - } - return malloc(size); -} - -static void *coc_realloc(void *addr, size_t num -#ifndef LIBRESSL_VERSION_NUMBER - , const char *file __attribute__((unused)), - int line __attribute__((unused)) -#endif -) -{ - return realloc(addr, num); -} - -static void coc_free(void *addr -#ifndef LIBRESSL_VERSION_NUMBER - , const char *file __attribute__((unused)), - int line __attribute__((unused)) -#endif -) -{ - free(addr); -} - -int check_openssl_compatibility() -{ - EVP_CIPHER_CTX *evp_ctx; - EVP_MD_CTX *md5_ctx; - - if (!CRYPTO_set_mem_functions(coc_malloc, coc_realloc, coc_free)) - return 0; - - testing= 1; - alloc_size= alloc_count= 0; - evp_ctx= EVP_CIPHER_CTX_new(); - EVP_CIPHER_CTX_free(evp_ctx); - if (alloc_count != 1 || !alloc_size || alloc_size > EVP_CIPHER_CTX_SIZE) - return 1; - - alloc_size= alloc_count= 0; - md5_ctx= EVP_MD_CTX_new(); - EVP_MD_CTX_free(md5_ctx); - if (alloc_count != 1 || !alloc_size || alloc_size > EVP_MD_CTX_SIZE) - return 1; - - testing= 0; - return 0; -} -#endif diff --git a/sql/mysqld.cc b/sql/mysqld.cc index 52938056f0b..d9e3cc4b609 100644 --- a/sql/mysqld.cc +++ b/sql/mysqld.cc @@ -4089,14 +4089,6 @@ static int init_common_variables() exit(1); } -#ifdef HAVE_OPENSSL - if (check_openssl_compatibility()) - { - sql_print_error("Incompatible OpenSSL version. Cannot continue..."); - exit(1); - } -#endif - if (init_thread_environment() || mysql_init_variables()) exit(1); From c0eea2b8c5e2b45594af2b6cb4c6b185eb0d30e2 Mon Sep 17 00:00:00 2001 From: Alessandro Vetere Date: Wed, 14 Oct 2026 18:05:47 +0200 Subject: [PATCH 25/28] MDEV-41398 Stale optimistic btr_pcur_t restore after buffer pool resize Problem: btr_pcur_t::restore_position() accepts a stored position optimistically when the block at the stored descriptor address has the same page id and the same buf_block_t::modify_clock. When buf_pool_t::resize() shrinks the buffer pool, the withdrawn block descriptors are freed. When it grows the buffer pool again, the new descriptors at the same addresses start with modify_clock=0. A cursor that was stored before the shrink can then find a different instance of the page with an equal modify_clock, and it restores to a record that is no longer correct. In a release build, an UPDATE can change a row that does not match its WHERE condition and skip the rows that match. Fix: Add buf_pool_t::withdrawn_modify_clock. buf_pool_t::shrink() sets it to the maximum modify_clock of the blocks that it withdraws. buf_pool_t::resize() sets the initial modify_clock of each block that it adds to one more than this. A relocation in shrink() increments only the modify_clock of the destination block, so a withdrawn block can keep the value that a cursor stored. Thus the modify_clock at a block descriptor address never repeats an earlier value, and a stale optimistic restore fails. The new member uses a hole before buf_pool_t::flush_list_mutex, so the layout of the other members does not change. The test innodb.buf_pool_resize_pcur_restore reads a page into the first new block after a grow, and makes a stored cursor for a read and for an UPDATE restore its position across a page reorganization, a shrink and a grow. --- .../r/buf_pool_resize_pcur_restore.result | 122 ++++++++++++ .../innodb/t/buf_pool_resize_pcur_restore.opt | 3 + .../t/buf_pool_resize_pcur_restore.test | 173 ++++++++++++++++++ storage/innobase/buf/buf0buf.cc | 6 + storage/innobase/include/buf0buf.h | 7 + 5 files changed, 311 insertions(+) create mode 100644 mysql-test/suite/innodb/r/buf_pool_resize_pcur_restore.result create mode 100644 mysql-test/suite/innodb/t/buf_pool_resize_pcur_restore.opt create mode 100644 mysql-test/suite/innodb/t/buf_pool_resize_pcur_restore.test diff --git a/mysql-test/suite/innodb/r/buf_pool_resize_pcur_restore.result b/mysql-test/suite/innodb/r/buf_pool_resize_pcur_restore.result new file mode 100644 index 00000000000..289f7504ed3 --- /dev/null +++ b/mysql-test/suite/innodb/r/buf_pool_resize_pcur_restore.result @@ -0,0 +1,122 @@ +create table t1 (pk int primary key, val varchar(255) not null) +engine=innodb stats_persistent=0 charset=latin1; +insert into t1 select seq, repeat('a', 100) from seq_1_to_20 order by seq desc; +create table t2 like t1; +insert into t2 select seq, repeat('a', 100) from seq_1_to_20 order by seq desc; +create table t3 (pk int primary key) engine=innodb stats_persistent=0 +partition by hash(pk) partitions 256; +insert into t3 select seq from seq_0_to_255; +create table t4 (a int primary key, b varchar(7000)) engine=innodb +stats_persistent=0; +insert into t4 select seq, repeat('x', 7000) from seq_1_to_3000; +create function hold(x int) returns int +begin +if x = 1 then +set debug_sync= 'now SIGNAL paused WAIT_FOR go'; +end if; +return 1; +end| +create procedure read_into_first_new_block(tbl varchar(64)) +begin +declare attempts int default 5; +declare blk bigint default -1; +declare p int; +set @read_tbl= concat('select count(*) into @n from ', tbl); +prepare read_tbl from @read_tbl; +while blk != @first_new do +if attempts = 0 then +signal sqlstate '45000' set message_text= 'Wrong placement'; +end if; +set attempts= attempts - 1; +if blk != -1 then +set global innodb_buffer_pool_size= @old_size; +end if; +# Evict all pages of t1, t2 and t3. +select sum(length(b)) into @n from t4; +if @first_new = 0 then +set @first_new= (select pool_size +from information_schema.innodb_buffer_pool_stats); +end if; +set global innodb_buffer_pool_size= @new_size; +# A grow appends the new blocks to the end of the free list. Read pages +# of t3 until only the new blocks are free. +set p= 0; +while (select free_buffers > pool_size - @first_new +from information_schema.innodb_buffer_pool_stats) do +if p = 256 then +signal sqlstate '45000' set message_text= 'Out of partitions'; +end if; +select pk into @n from t3 where pk= p; +set p= p + 1; +end while; +execute read_tbl; +select block_id into blk from information_schema.innodb_buffer_page +where table_name = concat('`test`.`', tbl, '`'); +end while; +deallocate prepare read_tbl; +end| +set global innodb_max_purge_lag_wait= 0; +set @old_size= @@global.innodb_buffer_pool_size; +set @new_size= 12582912; +set @first_new= 0; +set @save_scan_depth= @@global.innodb_lru_scan_depth; +set global innodb_lru_scan_depth= 100; +# +# A read that continues from a stale position +# +# Read the page of t1 into the first new block. +call read_into_first_new_block('t1'); +connect con1,localhost,root; +select group_concat(pk) from t1 where hold(pk); +connection default; +set debug_sync= 'now WAIT_FOR paused'; +# Reorganize the page while the read holds a stored position on pk=1. +set @save_dbug= @@debug_dbug; +set debug_dbug= '+d,do_page_reorganize'; +insert into t1 values (21, repeat('a', 100)); +set debug_dbug= @save_dbug; +# Shrink, which moves the page out of the block. +set global innodb_buffer_pool_size= @old_size; +# Read the page of t1 into the same block again. +call read_into_first_new_block('t1'); +# Resume the read, which must return pk=2 to pk=20. +set debug_sync= 'now SIGNAL go'; +connection con1; +group_concat(pk) +1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 +connection default; +set global innodb_buffer_pool_size= @old_size; +set global innodb_max_purge_lag_wait= 0; +# +# An UPDATE that continues from a stale position +# +# Read the page of t2 into the first new block. +call read_into_first_new_block('t2'); +connection con1; +update t2 set val= 'b' where hold(pk); +connection default; +set debug_sync= 'now WAIT_FOR paused'; +# Reorganize the page while the UPDATE holds a stored position on pk=1. +set debug_dbug= '+d,do_page_reorganize'; +insert into t2 values (21, repeat('a', 100)); +set debug_dbug= @save_dbug; +# Shrink, which moves the page out of the block. +set global innodb_buffer_pool_size= @old_size; +# Read the page of t2 into the same block again. +call read_into_first_new_block('t2'); +# Resume the UPDATE, which must change pk=1, not pk=20. +set debug_sync= 'now SIGNAL go'; +connection con1; +affected rows: 21 +info: Rows matched: 21 Changed: 21 Warnings: 0 +disconnect con1; +connection default; +select group_concat(pk) from t2 where val = 'b'; +group_concat(pk) +1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21 +set debug_sync= 'reset'; +drop procedure read_into_first_new_block; +drop function hold; +drop table t1, t2, t3, t4; +set global innodb_buffer_pool_size= @old_size; +set global innodb_lru_scan_depth= @save_scan_depth; diff --git a/mysql-test/suite/innodb/t/buf_pool_resize_pcur_restore.opt b/mysql-test/suite/innodb/t/buf_pool_resize_pcur_restore.opt new file mode 100644 index 00000000000..57fa706fbd4 --- /dev/null +++ b/mysql-test/suite/innodb/t/buf_pool_resize_pcur_restore.opt @@ -0,0 +1,3 @@ +--innodb-buffer-pool-size=8M +--innodb-buffer-pool-size-max=16M +--innodb-read-ahead-threshold=0 diff --git a/mysql-test/suite/innodb/t/buf_pool_resize_pcur_restore.test b/mysql-test/suite/innodb/t/buf_pool_resize_pcur_restore.test new file mode 100644 index 00000000000..3c18dedb28b --- /dev/null +++ b/mysql-test/suite/innodb/t/buf_pool_resize_pcur_restore.test @@ -0,0 +1,173 @@ +# +# A persistent cursor that is restored after the buffer pool shrank and +# grew again +# +# btr_pcur_t::restore_position() accepts the stored position without a +# search when the block descriptor that it remembers holds the same page +# id and the same modify_clock. The descriptors are at fixed addresses, +# and a grow initializes the descriptors that an earlier shrink withdrew. +# If that reset modify_clock, a page that is read into such a descriptor +# after the grow could have the modify_clock that a cursor stored before +# the shrink, when the page had been read into the same descriptor after +# an earlier grow. The page can have changed in between, and the cursor +# would continue from a stale record pointer. +# +--source include/have_innodb.inc +--source include/have_innodb_16k.inc +--source include/have_partition.inc +--source include/have_sequence.inc +--source include/have_debug.inc +--source include/have_debug_sync.inc +--source include/not_embedded.inc +--source include/count_sessions.inc + +# One leaf page each, with the records in descending key order in the page. +# A reorganization writes them in key order, so pk=20 moves to the offset +# of pk=1. +create table t1 (pk int primary key, val varchar(255) not null) +engine=innodb stats_persistent=0 charset=latin1; +insert into t1 select seq, repeat('a', 100) from seq_1_to_20 order by seq desc; +create table t2 like t1; +insert into t2 select seq, repeat('a', 100) from seq_1_to_20 order by seq desc; + +# One page per partition. A lookup of an evicted partition reads one page. +# The page cleaner keeps about innodb_lru_scan_depth blocks free, and the +# partitions must outnumber them. +create table t3 (pk int primary key) engine=innodb stats_persistent=0 +partition by hash(pk) partitions 256; +insert into t3 select seq from seq_0_to_255; + +# Three times innodb_buffer_pool_size, to evict everything else. +create table t4 (a int primary key, b varchar(7000)) engine=innodb +stats_persistent=0; +insert into t4 select seq, repeat('x', 7000) from seq_1_to_3000; + +delimiter |; +# Stop the statement after it read pk=1 and stored its position. +create function hold(x int) returns int +begin + if x = 1 then + set debug_sync= 'now SIGNAL paused WAIT_FOR go'; + end if; + return 1; +end| + +# Grow the buffer pool from @old_size to @new_size and read the only page +# of the table tbl into the first block that the grow adds. A concurrent +# page read can take that block; then shrink and try again. The first call +# sets @first_new, the block id of the first new block. Later calls require +# the same @first_new. +create procedure read_into_first_new_block(tbl varchar(64)) +begin + declare attempts int default 5; + declare blk bigint default -1; + declare p int; + set @read_tbl= concat('select count(*) into @n from ', tbl); + prepare read_tbl from @read_tbl; + while blk != @first_new do + if attempts = 0 then + signal sqlstate '45000' set message_text= 'Wrong placement'; + end if; + set attempts= attempts - 1; + if blk != -1 then + set global innodb_buffer_pool_size= @old_size; + end if; + # Evict all pages of t1, t2 and t3. + select sum(length(b)) into @n from t4; + if @first_new = 0 then + set @first_new= (select pool_size + from information_schema.innodb_buffer_pool_stats); + end if; + set global innodb_buffer_pool_size= @new_size; + # A grow appends the new blocks to the end of the free list. Read pages + # of t3 until only the new blocks are free. + set p= 0; + while (select free_buffers > pool_size - @first_new + from information_schema.innodb_buffer_pool_stats) do + if p = 256 then + signal sqlstate '45000' set message_text= 'Out of partitions'; + end if; + select pk into @n from t3 where pk= p; + set p= p + 1; + end while; + execute read_tbl; + select block_id into blk from information_schema.innodb_buffer_page + where table_name = concat('`test`.`', tbl, '`'); + end while; + deallocate prepare read_tbl; +end| +delimiter ;| + +# Purge resets DB_TRX_ID in the inserted records. Let it finish, so that it +# reads no pages into the free blocks that the procedure counts. +set global innodb_max_purge_lag_wait= 0; + +set @old_size= @@global.innodb_buffer_pool_size; +set @new_size= 12582912; +set @first_new= 0; +# The minimum, which keeps fewer blocks free than t3 has partitions. +set @save_scan_depth= @@global.innodb_lru_scan_depth; +set global innodb_lru_scan_depth= 100; + +--echo # +--echo # A read that continues from a stale position +--echo # +--echo # Read the page of t1 into the first new block. +call read_into_first_new_block('t1'); +connect (con1,localhost,root); +--send select group_concat(pk) from t1 where hold(pk) +connection default; +set debug_sync= 'now WAIT_FOR paused'; +--echo # Reorganize the page while the read holds a stored position on pk=1. +set @save_dbug= @@debug_dbug; +set debug_dbug= '+d,do_page_reorganize'; +insert into t1 values (21, repeat('a', 100)); +set debug_dbug= @save_dbug; +--echo # Shrink, which moves the page out of the block. +set global innodb_buffer_pool_size= @old_size; +--echo # Read the page of t1 into the same block again. +call read_into_first_new_block('t1'); +--echo # Resume the read, which must return pk=2 to pk=20. +set debug_sync= 'now SIGNAL go'; +connection con1; +--reap +connection default; +set global innodb_buffer_pool_size= @old_size; +# Purge was blocked by the read view of the paused read. Let it finish, so +# that it reads no pages into the new blocks of the next case. +set global innodb_max_purge_lag_wait= 0; + +--echo # +--echo # An UPDATE that continues from a stale position +--echo # +--echo # Read the page of t2 into the first new block. +call read_into_first_new_block('t2'); +connection con1; +--send update t2 set val= 'b' where hold(pk) +connection default; +set debug_sync= 'now WAIT_FOR paused'; +--echo # Reorganize the page while the UPDATE holds a stored position on pk=1. +set debug_dbug= '+d,do_page_reorganize'; +insert into t2 values (21, repeat('a', 100)); +set debug_dbug= @save_dbug; +--echo # Shrink, which moves the page out of the block. +set global innodb_buffer_pool_size= @old_size; +--echo # Read the page of t2 into the same block again. +call read_into_first_new_block('t2'); +--echo # Resume the UPDATE, which must change pk=1, not pk=20. +set debug_sync= 'now SIGNAL go'; +connection con1; +--enable_info +--reap +--disable_info +disconnect con1; +connection default; +select group_concat(pk) from t2 where val = 'b'; + +set debug_sync= 'reset'; +drop procedure read_into_first_new_block; +drop function hold; +drop table t1, t2, t3, t4; +set global innodb_buffer_pool_size= @old_size; +set global innodb_lru_scan_depth= @save_scan_depth; +--source include/wait_until_count_sessions.inc diff --git a/storage/innobase/buf/buf0buf.cc b/storage/innobase/buf/buf0buf.cc index 4b0c4062ed0..486663d24c1 100644 --- a/storage/innobase/buf/buf0buf.cc +++ b/storage/innobase/buf/buf0buf.cc @@ -1661,12 +1661,15 @@ ATTRIBUTE_COLD buf_pool_t::shrink_status buf_pool_t::shrink(size_t size) { withdraw_done: first_to_withdraw= nullptr; + uint64_t base{0}; while (buf_page_t *b= UT_LIST_GET_FIRST(withdrawn)) { UT_LIST_REMOVE(withdrawn, b); + base= std::max(base, reinterpret_cast(b)->modify_clock); /* satisfy the check in lazy_allocate() */ ut_d(memset((void*) b, 0, sizeof(buf_block_t))); } + withdrawn_modify_clock= std::max(withdrawn_modify_clock, base); return SHRINK_DONE; } @@ -1976,6 +1979,7 @@ ATTRIBUTE_COLD void buf_pool_t::resize(size_t size, THD *thd) noexcept const size_t ssize= srv_page_size_shift - UNIV_PAGE_SIZE_SHIFT_MIN; const size_t pages= pages_in_extent[ssize]; const size_t first_extent= n_blocks / pages; + const uint64_t modify_clock= withdrawn_modify_clock + 1; char *extent= memory + first_extent * innodb_buffer_pool_extent_size; @@ -1997,6 +2001,7 @@ ATTRIBUTE_COLD void buf_pool_t::resize(size_t size, THD *thd) noexcept { block->page.frame= frame; block->page.lock.init(); + block->modify_clock= modify_clock; UT_LIST_ADD_LAST(free, &block->page); ut_d(block->page.in_free_list= true); } @@ -2020,6 +2025,7 @@ ATTRIBUTE_COLD void buf_pool_t::resize(size_t size, THD *thd) noexcept { block->page.frame= frame; block->page.lock.init(); + block->modify_clock= modify_clock; UT_LIST_ADD_LAST(free, &block->page); ut_d(block->page.in_free_list= true); } diff --git a/storage/innobase/include/buf0buf.h b/storage/innobase/include/buf0buf.h index 40858279245..98ede57a0a4 100644 --- a/storage/innobase/include/buf0buf.h +++ b/storage/innobase/include/buf0buf.h @@ -1715,6 +1715,13 @@ public: /** number of index page splits */ Atomic_counter pages_split; + /** the maximum modify_clock of any block that shrink() withdrew; + resize() initializes the modify_clock of the blocks that it adds to + one more than this, so that the modify_clock at a block descriptor + address never repeats an earlier value and a stale + btr_pcur_t::restore_position() cannot succeed optimistically; + protected by mutex */ + uint64_t withdrawn_modify_clock; /** @name Page flushing algorithm fields */ /* @{ */ From 8cedfdf7e3bd0ca3a9da3fe65ba8dae6c8b8a803 Mon Sep 17 00:00:00 2001 From: Sergei Golubchik Date: Sun, 3 Aug 2025 14:54:22 +0200 Subject: [PATCH 26/28] MDEV-34817 perfschema.lowercase_fs_off fails on buildbot initialize counters when constructing a new PFS_program object The test fails because a newly created routine can get a performance schema slot freed by a routine dropped in an earlier test. The counters of a reused slot are not reset, so the new routine shows the old routine's statistics. --- storage/perfschema/pfs_program.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/storage/perfschema/pfs_program.cc b/storage/perfschema/pfs_program.cc index b10f00b7d52..2ef94c1a922 100644 --- a/storage/perfschema/pfs_program.cc +++ b/storage/perfschema/pfs_program.cc @@ -246,6 +246,8 @@ search: pfs->m_schema_name_length= schema_name_length; pfs->m_enabled= is_enabled; pfs->m_timed= is_timed; + pfs->m_stmt_stat.reset(); + pfs->m_sp_stat.reset(); /* Insert this record. */ pfs->m_lock.dirty_to_allocated(& dirty_state); From 493ebf0c3df290e8ddccf141115e58b48ed51c95 Mon Sep 17 00:00:00 2001 From: Aleksey Midenkov Date: Tue, 20 Oct 2026 11:34:03 +0300 Subject: [PATCH 27/28] innodb.innodb_buffer_pool_resize_temporary fails with (0/506) The test reads innodb_buffer_pool_resize_status while the SET GLOBAL innodb_buffer_pool_size thread is blocked at buf_pool_shrink_before_wakeup and expects nothing withdrawn yet: Withdrawing blocks. (505/505). But buf_pool_t::resize() sets n_blocks_to_withdraw and first_to_withdraw and releases buf_pool.mutex before that sync point. From then on other threads withdraw blocks: allocate() from any page allocation, buf_LRU_block_free_non_file_page() on eviction, and the page cleaner LRU batch which evicts pages from the withdrawn area while to_withdraw is nonzero. So the first counter can be anything down to 0: Withdrawing blocks. (0/506). The fix masks the first counter in replace_regex, so the result does not depend on how many blocks were already withdrawn. This does not weaken the test: it still checks that the shrink is reported while in progress. How many blocks are already withdrawn at that moment depends only on timing. --- .../suite/innodb/t/innodb_buffer_pool_resize_temporary.test | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mysql-test/suite/innodb/t/innodb_buffer_pool_resize_temporary.test b/mysql-test/suite/innodb/t/innodb_buffer_pool_resize_temporary.test index f96f89c39d5..b4064ff3646 100644 --- a/mysql-test/suite/innodb/t/innodb_buffer_pool_resize_temporary.test +++ b/mysql-test/suite/innodb/t/innodb_buffer_pool_resize_temporary.test @@ -35,8 +35,9 @@ SET DEBUG_SYNC='buf_pool_shrink_before_wakeup SIGNAL blocked WAIT_FOR go'; send SET GLOBAL innodb_buffer_pool_size=8388608; connection default; SET DEBUG_SYNC='now WAIT_FOR blocked'; -# adjust for 32-bit and SUX_LOCK_GENERIC ---replace_regex /(5..)\/\1/505\/505/ +# adjust for 32-bit and SUX_LOCK_GENERIC; other threads may already +# have withdrawn some blocks before buf_pool_shrink_before_wakeup +--replace_regex /\([0-9]+\/5[0-9][0-9]\)/(505\/505)/ SHOW STATUS LIKE 'innodb_buffer_pool_resize_status'; SET DEBUG_SYNC='now SIGNAL go'; connection con1; From 45d582be8c85d7f7cead51505a5c70ba87a6d104 Mon Sep 17 00:00:00 2001 From: iff-sal Date: Tue, 13 Oct 2026 21:07:38 +0530 Subject: [PATCH 28/28] MDEV-40297: SET changes @@default_master_connection's system variable if the explicit connection name is blank When updating replicate_do_db and slave_parallel_mode, the handlers checked !base_name->length instead of !base_name->str to determine whether to fall back to @@SESSION.default_master_connection. Because an explicit blank qualifier (``) has length 0 with a non-NULL pointer, SET statements using empty backticks mistakenly updated default_master_connection rather than the unnamed connection. Fixed by checking !base_name->str instead of !base_name->length. Also fixed SELECT @@GLOBAL.slave_parallel_mode, which had the same issue when reading the variable. Renamed the regression test to sys_vars_default_master to reflect the broader coverage and merged the test cases of MDEV-40296 and MDEV-40297. Reviewed-by: ParadoxV5 --- ....result => sys_vars_default_master.result} | 19 +++++++++++++++--- ...ster.test => sys_vars_default_master.test} | 20 +++++++++++++++---- sql/sys_vars.cc | 6 +++--- 3 files changed, 35 insertions(+), 10 deletions(-) rename mysql-test/suite/multi_source/{rpl_filter_default_master.result => sys_vars_default_master.result} (53%) rename mysql-test/suite/multi_source/{rpl_filter_default_master.test => sys_vars_default_master.test} (52%) diff --git a/mysql-test/suite/multi_source/rpl_filter_default_master.result b/mysql-test/suite/multi_source/sys_vars_default_master.result similarity index 53% rename from mysql-test/suite/multi_source/rpl_filter_default_master.result rename to mysql-test/suite/multi_source/sys_vars_default_master.result index 044e8031c8c..78d9950b23a 100644 --- a/mysql-test/suite/multi_source/rpl_filter_default_master.result +++ b/mysql-test/suite/multi_source/sys_vars_default_master.result @@ -1,9 +1,10 @@ SET @saved_replicate_do_db= @@GLOBAL.replicate_do_db; +SET @saved_slave_parallel_mode= @@GLOBAL.slave_parallel_mode; SET @saved_default_master_connection= @@SESSION.default_master_connection; CHANGE MASTER 'name' TO master_host='127.0.0.1', master_user='root'; -SET @@GLOBAL.replicate_do_db= 'unnamed'; SET @@SESSION.default_master_connection= 'name'; SET @@GLOBAL.replicate_do_db= 'named'; +SET @@GLOBAL.``.replicate_do_db= 'unnamed'; SELECT @@GLOBAL.name.replicate_do_db; @@GLOBAL.name.replicate_do_db named @@ -13,7 +14,19 @@ named SELECT @@GLOBAL.``.replicate_do_db; @@GLOBAL.``.replicate_do_db unnamed -RESET SLAVE ALL; +SET @@GLOBAL.slave_parallel_mode= 'minimal'; +SET @@GLOBAL.``.slave_parallel_mode= 'aggressive'; +SELECT @@GLOBAL.name.slave_parallel_mode; +@@GLOBAL.name.slave_parallel_mode +minimal +SELECT @@GLOBAL.slave_parallel_mode; +@@GLOBAL.slave_parallel_mode +minimal +SELECT @@GLOBAL.``.slave_parallel_mode; +@@GLOBAL.``.slave_parallel_mode +aggressive +RESET SLAVE 'name' ALL; SET @@SESSION.default_master_connection= @saved_default_master_connection; +SET @@GLOBAL.slave_parallel_mode= @saved_slave_parallel_mode; SET @@GLOBAL.replicate_do_db= @saved_replicate_do_db; -# End of 10.11 tests +# End of 11.4 tests diff --git a/mysql-test/suite/multi_source/rpl_filter_default_master.test b/mysql-test/suite/multi_source/sys_vars_default_master.test similarity index 52% rename from mysql-test/suite/multi_source/rpl_filter_default_master.test rename to mysql-test/suite/multi_source/sys_vars_default_master.test index 3693b1fd8f1..7f3e56d5ca2 100644 --- a/mysql-test/suite/multi_source/rpl_filter_default_master.test +++ b/mysql-test/suite/multi_source/sys_vars_default_master.test @@ -1,25 +1,37 @@ --source include/not_embedded.inc # MDEV-40296: SELECT @@GLOBAL.replicate_do_db ignores default_master_connection +# MDEV-40297: SET changes @@default_master_connection's system variable if the explicit connection name is blank # Setup SET @saved_replicate_do_db= @@GLOBAL.replicate_do_db; +SET @saved_slave_parallel_mode= @@GLOBAL.slave_parallel_mode; SET @saved_default_master_connection= @@SESSION.default_master_connection; CHANGE MASTER 'name' TO master_host='127.0.0.1', master_user='root'; -SET @@GLOBAL.replicate_do_db= 'unnamed'; SET @@SESSION.default_master_connection= 'name'; + +# replicate_do_db SET @@GLOBAL.replicate_do_db= 'named'; +SET @@GLOBAL.``.replicate_do_db= 'unnamed'; SELECT @@GLOBAL.name.replicate_do_db; SELECT @@GLOBAL.replicate_do_db; -# MDEV-40297: Explicit empty connection name SELECT @@GLOBAL.``.replicate_do_db; +# slave_parallel_mode +SET @@GLOBAL.slave_parallel_mode= 'minimal'; +SET @@GLOBAL.``.slave_parallel_mode= 'aggressive'; + +SELECT @@GLOBAL.name.slave_parallel_mode; +SELECT @@GLOBAL.slave_parallel_mode; +SELECT @@GLOBAL.``.slave_parallel_mode; + # Cleanup -RESET SLAVE ALL; +RESET SLAVE 'name' ALL; SET @@SESSION.default_master_connection= @saved_default_master_connection; +SET @@GLOBAL.slave_parallel_mode= @saved_slave_parallel_mode; SET @@GLOBAL.replicate_do_db= @saved_replicate_do_db; ---echo # End of 10.11 tests +--echo # End of 11.4 tests diff --git a/sql/sys_vars.cc b/sql/sys_vars.cc index 608a8a607c0..90e484e32d5 100644 --- a/sql/sys_vars.cc +++ b/sql/sys_vars.cc @@ -2478,7 +2478,7 @@ Sys_var_slave_parallel_mode::global_update(THD *thd, set_var *var) Master_info *mi; bool res= false; - if (!base_name->length) + if (!base_name->str) base_name= &thd->variables.default_master_connection; mysql_mutex_unlock(&LOCK_global_system_variables); @@ -2524,7 +2524,7 @@ Sys_var_slave_parallel_mode::global_value_ptr(THD *thd, enum_slave_parallel_mode val= (enum_slave_parallel_mode)opt_slave_parallel_mode; - if (!base_name->length) + if (!base_name->str) base_name= &thd->variables.default_master_connection; mysql_mutex_unlock(&LOCK_global_system_variables); @@ -5682,7 +5682,7 @@ bool Sys_var_rpl_filter::global_update(THD *thd, set_var *var) bool result= true; // Assume error LEX_CSTRING *base_name= &var->base; - if (!base_name->length) + if (!base_name->str) base_name= &thd->variables.default_master_connection; mysql_mutex_unlock(&LOCK_global_system_variables);