From f41a120298aa17ca80eecaa68614a6addf91ebc6 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Tue, 13 Aug 2024 18:28:23 +0200 Subject: [PATCH 1/6] Fix typo in xtrabackup --help output --- extra/mariabackup/xtrabackup.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/mariabackup/xtrabackup.cc b/extra/mariabackup/xtrabackup.cc index c126d0b5b7c..4a6a4ad9afc 100644 --- a/extra/mariabackup/xtrabackup.cc +++ b/extra/mariabackup/xtrabackup.cc @@ -1704,7 +1704,7 @@ struct my_option xb_server_options[] = 0, GET_BOOL, NO_ARG, 1, 0, 0, 0, 0, 0 }, {"check-privileges", OPT_XTRA_CHECK_PRIVILEGES, "Check database user " - "privileges fro the backup user", + "privileges for the backup user", &opt_check_privileges, &opt_check_privileges, 0, GET_BOOL, NO_ARG, 1, 0, 0, 0, 0, 0 }, From ba5482ffc293f162382a7f4296fd296b59cf0acb Mon Sep 17 00:00:00 2001 From: Dmitry Shulga Date: Fri, 16 Aug 2024 12:43:35 +0700 Subject: [PATCH 2/6] MDEV-34718: Trigger doesn't work correctly with bulk update Running an UPDATE statement in PS mode and having positional parameter(s) bound with an array of actual values (that is prepared to be run in bulk mode) results in incorrect behaviour in presence of on update trigger that also executes an UPDATE statement. The same is true for handling a DELETE statement in presence of on delete trigger. Typically, the visible effect of such incorrect behaviour is expressed in a wrong number of updated/deleted rows of a target table. Additionally, in case UPDATE statement, a number of modified rows and a state message returned by a statement contains wrong information about a number of modified rows. The reason for incorrect number of updated/deleted rows is that a data structure used for binding positional argument with its actual values is stored in THD (this is thd->bulk_param) and reused on processing every INSERT/UPDATE/DELETE statement. It leads to consuming actual values bound with top-level UPDATE/DELETE statement by other DML statements used by triggers' body. To fix the issue, reset the thd->bulk_param temporary to the value nullptr before invoking triggers and restore its value on finishing its execution. The second part of the problem relating with wrong value of affected rows reported by Connector/C API is caused by the fact that diagnostics area is reused by an original DML statement and a statement invoked by a trigger. This fact should be take into account on finalizing a state of diagnostics area on completion running of a statement. Important remark: in case the macros DBUG_OFF is on, call of the method Diagnostics_area::reset_diagnostics_area() results in reset of the data members m_affected_rows, m_statement_warn_count. Values of these data members of the class Diagnostics_area are used on sending OK and EOF messages. In case DML statement is executed in PS bulk mode such resetting results in sending wrong result values to a client for affected rows in case the DML statement fires a triggers. So, reset these data members only in case the current statement being processed is not run in bulk mode. --- sql/sql_base.cc | 12 + sql/sql_delete.cc | 7 + sql/sql_error.cc | 19 +- sql/sql_error.h | 5 + sql/sql_insert.cc | 35 ++- sql/sql_update.cc | 20 ++ tests/mysql_client_test.c | 450 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 534 insertions(+), 14 deletions(-) diff --git a/sql/sql_base.cc b/sql/sql_base.cc index 8738f181d77..29daca40acb 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -8784,10 +8784,22 @@ fill_record_n_invoke_before_triggers(THD *thd, TABLE *table, if (!result && triggers) { + void *save_bulk_param= thd->bulk_param; + /* + Reset the sentinel thd->bulk_param in order not to consume the next + values of a bound array in case one of statement executed by + the trigger's body is INSERT statement. + */ + thd->bulk_param= nullptr; + if (triggers->process_triggers(thd, event, TRG_ACTION_BEFORE, TRUE) || not_null_fields_have_null_values(table)) + { + thd->bulk_param= save_bulk_param; return TRUE; + } + thd->bulk_param= save_bulk_param; /* Re-calculate virtual fields to cater for cases when base columns are diff --git a/sql/sql_delete.cc b/sql/sql_delete.cc index 945385e114d..99da79c599b 100644 --- a/sql/sql_delete.cc +++ b/sql/sql_delete.cc @@ -796,13 +796,17 @@ bool mysql_delete(THD *thd, TABLE_LIST *table_list, COND *conds, delete_history); if (delete_record) { + void *save_bulk_param= thd->bulk_param; + thd->bulk_param= nullptr; if (!delete_history && table->triggers && table->triggers->process_triggers(thd, TRG_EVENT_DELETE, TRG_ACTION_BEFORE, FALSE)) { error= 1; + thd->bulk_param= save_bulk_param; break; } + thd->bulk_param= save_bulk_param; // no LIMIT / OFFSET if (returning && result->send_data(returning->item_list) < 0) @@ -833,13 +837,16 @@ bool mysql_delete(THD *thd, TABLE_LIST *table_list, COND *conds, if (likely(!error)) { deleted++; + thd->bulk_param= nullptr; if (!delete_history && table->triggers && table->triggers->process_triggers(thd, TRG_EVENT_DELETE, TRG_ACTION_AFTER, FALSE)) { error= 1; + thd->bulk_param= save_bulk_param; break; } + thd->bulk_param= save_bulk_param; if (!--limit && using_limit) { error= -1; diff --git a/sql/sql_error.cc b/sql/sql_error.cc index 2b4ae7149c5..6a3730ed3df 100644 --- a/sql/sql_error.cc +++ b/sql/sql_error.cc @@ -309,14 +309,27 @@ Diagnostics_area::reset_diagnostics_area() m_message[0]= '\0'; Sql_state_errno::clear(); Sql_user_condition_identity::clear(); - m_affected_rows= 0; m_last_insert_id= 0; - m_statement_warn_count= 0; + if (!is_bulk_op()) + { + m_affected_rows= 0; + m_statement_warn_count= 0; + } #endif get_warning_info()->clear_error_condition(); set_is_sent(false); /** Tiny reset in debug mode to see garbage right away */ - m_status= DA_EMPTY; + if (!is_bulk_op()) + /* + For BULK DML operations (e.g. UPDATE) the data member m_status + has the value DA_OK_BULK. Keep this value in order to handle + m_affected_rows, m_statement_warn_count in correct way. Else, + the number of rows and the number of warnings affected by + the last statement executed as part of a trigger fired by the dml + (e.g. UPDATE statement fires a trigger on AFTER UPDATE) would counts + rows modified by trigger's statement. + */ + m_status= DA_EMPTY; DBUG_VOID_RETURN; } diff --git a/sql/sql_error.h b/sql/sql_error.h index 92bc0b821a9..78c29fc4a2d 100644 --- a/sql/sql_error.h +++ b/sql/sql_error.h @@ -1066,6 +1066,11 @@ public: return m_affected_rows; } + void set_message(const char *msg) + { + strmake_buf(m_message, msg); + } + ulonglong last_insert_id() const { DBUG_ASSERT(m_status == DA_OK || m_status == DA_OK_BULK); diff --git a/sql/sql_insert.cc b/sql/sql_insert.cc index 35eec93f540..cf3ec929d09 100644 --- a/sql/sql_insert.cc +++ b/sql/sql_insert.cc @@ -1021,19 +1021,11 @@ bool mysql_insert(THD *thd, TABLE_LIST *table_list, */ restore_record(table,s->default_values); // Get empty record table->reset_default_fields(); - /* - Reset the sentinel thd->bulk_param in order not to consume the next - values of a bound array in case one of statement executed by - the trigger's body is INSERT statement. - */ - void *save_bulk_param= thd->bulk_param; - thd->bulk_param= nullptr; if (unlikely(fill_record_n_invoke_before_triggers(thd, table, fields, *values, 0, TRG_EVENT_INSERT))) { - thd->bulk_param= save_bulk_param; if (values_list.elements != 1 && ! thd->is_error()) { info.records++; @@ -1047,7 +1039,6 @@ bool mysql_insert(THD *thd, TABLE_LIST *table_list, error=1; break; } - thd->bulk_param= save_bulk_param; } else { @@ -1334,7 +1325,18 @@ values_loop_end: */ if (returning) result->send_eof(); - else + else if (!(thd->in_sub_stmt & SUB_STMT_TRIGGER)) + /* + Set the status and the number of affected rows in Diagnostics_area + only in case the INSERT statement is not processed as part of a trigger + invoked by some other DML statement. Else we would result in incorrect + number of affected rows for bulk DML operations, e.g. the UPDATE + statement (called via PS protocol). It would happen since the data + member Diagnostics_area::m_affected_rows modified twice per DML + statement - first time at the end of handling the INSERT statement + invoking by a trigger fired on handling the original DML statement, + and the second time at the end of handling the original DML statement. + */ my_ok(thd, info.copied + info.deleted + ((thd->client_capabilities & CLIENT_FOUND_ROWS) ? info.touched : info.updated), id); @@ -1356,7 +1358,18 @@ values_loop_end: (long) thd->get_stmt_da()->current_statement_warn_count()); if (returning) result->send_eof(); - else + else if (!(thd->in_sub_stmt & SUB_STMT_TRIGGER)) + /* + Set the status and the number of affected rows in Diagnostics_area + only in case the INSERT statement is not processed as part of a trigger + invoked by some other DML statement. Else we would result in incorrect + number of affected rows for bulk DML operations, e.g. the UPDATE + statement (called via PS protocol). It would happen since the data + member Diagnostics_area::m_affected_rows modified twice per DML + statement - first time at the end of handling the INSERT statement + invoking by a trigger fired on handling the original DML statement, + and the second time at the end of handling the original DML statement. + */ ::my_ok(thd, info.copied + info.deleted + updated, id, buff); } thd->abort_on_warning= 0; diff --git a/sql/sql_update.cc b/sql/sql_update.cc index 92cb4191fc6..adb209b7b56 100644 --- a/sql/sql_update.cc +++ b/sql/sql_update.cc @@ -1146,13 +1146,19 @@ error: rows_inserted++; } + void *save_bulk_param= thd->bulk_param; + thd->bulk_param= nullptr; + if (table->triggers && unlikely(table->triggers->process_triggers(thd, TRG_EVENT_UPDATE, TRG_ACTION_AFTER, TRUE))) { error= 1; + thd->bulk_param= save_bulk_param; + break; } + thd->bulk_param= save_bulk_param; if (!--limit && using_limit) { @@ -1342,6 +1348,20 @@ update_end: (ulong) thd->get_stmt_da()->current_statement_warn_count()); my_ok(thd, (thd->client_capabilities & CLIENT_FOUND_ROWS) ? found : updated, id, buff); + if (thd->get_stmt_da()->is_bulk_op()) + { + /* + Update the diagnostics message sent to a client with number of actual + rows update by the statement. For bulk UPDATE operation it should be + done after returning from my_ok() since the final number of updated + rows be knows on finishing the entire bulk update statement. + */ + my_snprintf(buff, sizeof(buff), ER_THD(thd, ER_UPDATE_INFO), + (ulong) thd->get_stmt_da()->affected_rows(), + (ulong) thd->get_stmt_da()->affected_rows(), + (ulong) thd->get_stmt_da()->current_statement_warn_count()); + thd->get_stmt_da()->set_message(buff); + } DBUG_PRINT("info",("%ld records updated", (long) updated)); } thd->count_cuted_fields= CHECK_FIELD_IGNORE; /* calc cuted fields */ diff --git a/tests/mysql_client_test.c b/tests/mysql_client_test.c index 66281641b08..138eae10ab1 100644 --- a/tests/mysql_client_test.c +++ b/tests/mysql_client_test.c @@ -22059,6 +22059,452 @@ static void test_mdev_30159() myquery(rc); } + +#ifndef EMBEDDED_LIBRARY +/** + Test case for bulk UPDATE against a table with an active AFTER UPDATE + trigger. +*/ + +static void test_mdev_34718_au() +{ + int rc; + MYSQL_STMT *stmt_update; + MYSQL_BIND bind[2]; + unsigned int vals[]= { 1, 2, 3}; + unsigned int new_vals[]= { 5, 6, 7}; + unsigned int vals_array_len= 3; + my_ulonglong row_count; + MYSQL_RES *result; + MYSQL_ROW row; + const char *update_stmt= "UPDATE t1 SET a = ? WHERE a = ?"; + const char *update_stmt_state_info; + + myheader("test_mdev_34718_au"); + + /* Set up test's environment */ + rc= mysql_query(mysql, "CREATE TABLE t1 (a INT)"); + myquery(rc); + + rc= mysql_query(mysql, "CREATE TABLE t2 (a INT)"); + myquery(rc); + + rc= mysql_query(mysql, "INSERT INTO t1 VALUES (1), (2), (3)"); + myquery(rc); + + rc= mysql_query(mysql, "CREATE TRIGGER t1_au AFTER UPDATE ON t1 " + "FOR EACH ROW BEGIN INSERT INTO t2 (a) VALUES (NEW.a); END;"); + + stmt_update= mysql_stmt_init(mysql); + check_stmt(stmt_update); + + rc= mysql_stmt_prepare(stmt_update, update_stmt, strlen(update_stmt)); + check_execute(stmt_update, rc); + + memset(&bind[0], 0, sizeof(MYSQL_BIND)); + memset(&bind[1], 0, sizeof(MYSQL_BIND)); + + bind[0].buffer_type= MYSQL_TYPE_LONG; + bind[0].buffer= new_vals; + + bind[1].buffer_type= MYSQL_TYPE_LONG; + bind[1].buffer= vals; + + /* + Every input positional parameter is bound with array of 3 elements + containing actual values for positional parameters + */ + rc= mysql_stmt_attr_set(stmt_update, STMT_ATTR_ARRAY_SIZE, &vals_array_len); + check_execute(stmt_update, rc); + + rc= mysql_stmt_bind_param(stmt_update, bind); + check_execute(stmt_update, rc); + + /* + Execution of this prepared statement replaces the table rows (1), (2), (3) + with values (5), (6), (7) + */ + rc= mysql_stmt_execute(stmt_update); + check_execute(stmt_update, rc); + + /* + Check that the BULK UPDATE statement affects exactly 3 rows + */ + row_count = mysql_stmt_affected_rows(stmt_update); + DIE_UNLESS(row_count == 3); + + update_stmt_state_info= mysql_info(mysql); + + /* + Check that information about executed operation is matched with + the expected result + */ + DIE_UNLESS(!strcmp("Rows matched: 3 Changed: 3 Warnings: 0", + update_stmt_state_info)); + + /* + * Check that the AFTER UPDATE trigger of the table t1 does work correctly + * and inserts the rows (5), (6), (7) into the table t2. + */ + rc= mysql_query(mysql, "SELECT 't1' tname, a FROM t1 " + "UNION SELECT 't2' tname, a FROM t2 ORDER BY tname, a"); + myquery(rc); + + result= mysql_store_result(mysql); + + row = mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t1") == 0 && atoi(row[1]) == 5); + + row = mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t1") == 0 && atoi(row[1]) == 6); + + row = mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t1") == 0 && atoi(row[1]) == 7); + + row = mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t2") == 0 && atoi(row[1]) == 5); + + row = mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t2") == 0 && atoi(row[1]) == 6); + + row = mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t2") == 0 && atoi(row[1]) == 7); + + row= mysql_fetch_row(result); + DIE_UNLESS(row == NULL); + + mysql_free_result(result); + + mysql_stmt_close(stmt_update); + + /* Clean up */ + rc= mysql_query(mysql, "DROP TABLE t1, t2"); + myquery(rc); +} + + +/** + Test case for bulk UPDATE against a table with an active BEFORE UPDATE + trigger. +*/ + +static void test_mdev_34718_bu() +{ + int rc; + MYSQL_STMT *stmt_update; + MYSQL_BIND bind[2]; + unsigned int vals[]= { 1, 2, 3}; + unsigned int new_vals[]= { 5, 6, 7}; + unsigned int vals_array_len= 3; + my_ulonglong row_count; + MYSQL_RES *result; + MYSQL_ROW row; + const char *update_stmt= "UPDATE t1 SET a = ? WHERE a = ?"; + const char *update_stmt_state_info; + + myheader("test_mdev_34718_bu"); + + /* Set up test's environment */ + rc= mysql_query(mysql, "CREATE TABLE t1 (a INT)"); + myquery(rc); + + rc= mysql_query(mysql, "CREATE TABLE t2 (a INT)"); + myquery(rc); + + rc= mysql_query(mysql, "INSERT INTO t1 VALUES (1), (2), (3)"); + myquery(rc); + + rc= mysql_query(mysql, "CREATE TRIGGER t1_au BEFORE UPDATE ON t1 " + "FOR EACH ROW BEGIN INSERT INTO t2 (a) VALUES (NEW.a); END;"); + + /* Initialize the prepared statement and set it up for bulk operations */ + stmt_update= mysql_stmt_init(mysql); + check_stmt(stmt_update); + + rc= mysql_stmt_prepare(stmt_update, update_stmt, strlen(update_stmt)); + check_execute(stmt_update, rc); + + memset(&bind[0], 0, sizeof(MYSQL_BIND)); + memset(&bind[1], 0, sizeof(MYSQL_BIND)); + + bind[0].buffer_type= MYSQL_TYPE_LONG; + bind[0].buffer= new_vals; + + bind[1].buffer_type= MYSQL_TYPE_LONG; + bind[1].buffer= vals; + + /* + Every input positional parameter is bound with array of 3 elements + containing actual values for positional parameters + */ + rc= mysql_stmt_attr_set(stmt_update, STMT_ATTR_ARRAY_SIZE, &vals_array_len); + check_execute(stmt_update, rc); + + rc= mysql_stmt_bind_param(stmt_update, bind); + check_execute(stmt_update, rc); + + /* + Execution of this prepared statement replaces the table rows (1), (2), (3) + with values (5), (6), (7) + */ + rc= mysql_stmt_execute(stmt_update); + check_execute(stmt_update, rc); + + /* + Check that the BULK UPDATE statement affects exactly 3 rows + */ + row_count= mysql_stmt_affected_rows(stmt_update); + DIE_UNLESS(row_count == 3); + + update_stmt_state_info= mysql_info(mysql); + + /* + Check that information about executed operation is matched with + the expected result + */ + DIE_UNLESS(!strcmp("Rows matched: 3 Changed: 3 Warnings: 0", + update_stmt_state_info)); + + /* + * Check that the BEFORE UPDATE trigger of the table t1 does work correctly + * and inserts the rows (5), (6), (7) into the table t2. + */ + rc= mysql_query(mysql, "SELECT 't1' tname, a FROM t1 " + "UNION SELECT 't2' tname, a FROM t2 ORDER BY tname, a"); + myquery(rc); + + result= mysql_store_result(mysql); + + row= mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t1") == 0 && atoi(row[1]) == 5); + + row= mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t1") == 0 && atoi(row[1]) == 6); + + row= mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t1") == 0 && atoi(row[1]) == 7); + + row= mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t2") == 0 && atoi(row[1]) == 5); + + row= mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t2") == 0 && atoi(row[1]) == 6); + + row= mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t2") == 0 && atoi(row[1]) == 7); + + row= mysql_fetch_row(result); + DIE_UNLESS(row == NULL); + + mysql_free_result(result); + + mysql_stmt_close(stmt_update); + + /* Clean up */ + rc= mysql_query(mysql, "DROP TABLE t1, t2"); + myquery(rc); +} + + +/** + Test case for bulk DELETE against a table with an active BEFORE DELETE + trigger. +*/ + +static void test_mdev_34718_bd() +{ + int rc; + MYSQL_STMT *stmt_delete; + MYSQL_BIND bind[1]; + unsigned int vals[]= { 1, 2, 3}; + unsigned int vals_array_len= 3; + my_ulonglong row_count; + MYSQL_RES *result; + MYSQL_ROW row; + const char *delete_stmt= "DELETE FROM t1 WHERE a = ?"; + + myheader("test_mdev_34718_bd"); + + /* Set up test's environment */ + rc= mysql_query(mysql, "CREATE TABLE t1 (a INT)"); + myquery(rc); + + rc= mysql_query(mysql, "CREATE TABLE t2 (a INT)"); + myquery(rc); + + rc= mysql_query(mysql, "INSERT INTO t1 VALUES (1), (2), (3)"); + myquery(rc); + + rc= mysql_query(mysql, "CREATE TRIGGER t1_bd BEFORE DELETE ON t1 " + "FOR EACH ROW BEGIN INSERT INTO t2 (a) VALUES (OLD.a); END;"); + + /* Initialize the prepared statement and set it up for bulk operations */ + stmt_delete= mysql_stmt_init(mysql); + check_stmt(stmt_delete); + + rc= mysql_stmt_prepare(stmt_delete, delete_stmt, strlen(delete_stmt)); + check_execute(stmt_delete, rc); + + memset(&bind[0], 0, sizeof(MYSQL_BIND)); + + bind[0].buffer_type= MYSQL_TYPE_LONG; + bind[0].buffer= vals; + + /* + Input positional parameter is bound with array of 3 elements + containing actual values for the positional parameter + */ + rc= mysql_stmt_attr_set(stmt_delete, STMT_ATTR_ARRAY_SIZE, &vals_array_len); + check_execute(stmt_delete, rc); + + rc= mysql_stmt_bind_param(stmt_delete, bind); + check_execute(stmt_delete, rc); + + /* + Execution of this prepared statement deletes the rows (1), (2), (3) + from the table t1 and inserts the rows (1), (2), (3) into the table t2 + in result of firing the BEFORE DELETE trigger + */ + rc= mysql_stmt_execute(stmt_delete); + check_execute(stmt_delete, rc); + + /* + Check that the BULK DELETE statement affects exactly 3 rows + */ + row_count= mysql_stmt_affected_rows(stmt_delete); + DIE_UNLESS(row_count == 3); + + /* + * Check that the BEFORE DELETE trigger of the table t1 does work correctly + * and inserts the rows (1), (2), (3) into the table t2. + */ + rc= mysql_query(mysql, "SELECT 't1' tname, a FROM t1 " + "UNION SELECT 't2' tname, a FROM t2 ORDER BY tname, a"); + myquery(rc); + + result= mysql_store_result(mysql); + + row= mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t2") == 0 && atoi(row[1]) == 1); + + row= mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t2") == 0 && atoi(row[1]) == 2); + + row= mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t2") == 0 && atoi(row[1]) == 3); + + row= mysql_fetch_row(result); + DIE_UNLESS(row == NULL); + + mysql_free_result(result); + + mysql_stmt_close(stmt_delete); + + /* Clean up */ + rc= mysql_query(mysql, "DROP TABLE t1, t2"); + myquery(rc); +} + + +/** + Test case for bulk DELETE against a table with an active AFTER DELETE + trigger. +*/ +static void test_mdev_34718_ad() +{ + int rc; + MYSQL_STMT *stmt_delete; + MYSQL_BIND bind[1]; + unsigned int vals[]= { 1, 2, 3}; + unsigned int vals_array_len= 3; + my_ulonglong row_count; + MYSQL_RES *result; + MYSQL_ROW row; + const char *delete_stmt= "DELETE FROM t1 WHERE a = ?"; + + myheader("test_mdev_34718_bd"); + + /* Set up test's environment */ + rc= mysql_query(mysql, "CREATE TABLE t1 (a INT)"); + myquery(rc); + + rc= mysql_query(mysql, "CREATE TABLE t2 (a INT)"); + myquery(rc); + + rc= mysql_query(mysql, "INSERT INTO t1 VALUES (1), (2), (3)"); + myquery(rc); + + rc= mysql_query(mysql, "CREATE TRIGGER t1_bd AFTER DELETE ON t1 " + "FOR EACH ROW BEGIN INSERT INTO t2 (a) VALUES (OLD.a); END;"); + + /* Initialize the prepared statement and set it up for bulk operations */ + stmt_delete= mysql_stmt_init(mysql); + check_stmt(stmt_delete); + + rc= mysql_stmt_prepare(stmt_delete, delete_stmt, strlen(delete_stmt)); + check_execute(stmt_delete, rc); + + memset(&bind[0], 0, sizeof(MYSQL_BIND)); + + bind[0].buffer_type= MYSQL_TYPE_LONG; + bind[0].buffer= vals; + + /* + Input positional parameter is bound with array of 3 elements + containing actual values for the positional parameter + */ + rc= mysql_stmt_attr_set(stmt_delete, STMT_ATTR_ARRAY_SIZE, &vals_array_len); + check_execute(stmt_delete, rc); + + rc= mysql_stmt_bind_param(stmt_delete, bind); + check_execute(stmt_delete, rc); + + /* + Execution of this prepared statement deletes the rows (1), (2), (3) + from the table t1 and inserts the rows (1), (2), (3) into the table t2 + in result of firing the BEFORE DELETE trigger + */ + rc= mysql_stmt_execute(stmt_delete); + check_execute(stmt_delete, rc); + + /* + Check that the BULK DELETE statement affects exactly 3 rows + */ + row_count= mysql_stmt_affected_rows(stmt_delete); + DIE_UNLESS(row_count == 3); + + /* + * Check that the AFTER DELETE trigger of the table t1 does work correctly + * and inserts the rows (1), (2), (3) into the table t2. + */ + rc= mysql_query(mysql, "SELECT 't1' tname, a FROM t1 " + "UNION SELECT 't2' tname, a FROM t2 ORDER BY tname, a"); + myquery(rc); + + result= mysql_store_result(mysql); + + row= mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t2") == 0 && atoi(row[1]) == 1); + + row= mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t2") == 0 && atoi(row[1]) == 2); + + row= mysql_fetch_row(result); + DIE_UNLESS(strcmp(row[0], "t2") == 0 && atoi(row[1]) == 3); + + row= mysql_fetch_row(result); + DIE_UNLESS(row == NULL); + + mysql_free_result(result); + + mysql_stmt_close(stmt_delete); + + /* Clean up */ + rc= mysql_query(mysql, "DROP TABLE t1, t2"); + myquery(rc); +} +#endif // EMBEDDED_LIBRARY + /* Check that server_status returned after connecting to server is consistent with the value of autocommit variable. @@ -22406,6 +22852,10 @@ static struct my_tests_st my_tests[]= { { "test_connect_autocommit", test_connect_autocommit}, #ifndef EMBEDDED_LIBRARY { "test_mdev_24411", test_mdev_24411}, + { "test_mdev_34718_bu", test_mdev_34718_bu }, + { "test_mdev_34718_au", test_mdev_34718_au }, + { "test_mdev_34718_bd", test_mdev_34718_bd }, + { "test_mdev_34718_ad", test_mdev_34718_ad }, #endif { 0, 0 } }; From e51d55a63ff98bdb3b44681434d619c51adb3863 Mon Sep 17 00:00:00 2001 From: Monty Date: Mon, 19 Aug 2024 10:59:57 +0300 Subject: [PATCH 3/6] Revert "mtr: remove not_valgrind_build" The original code is correct. valgrind and asan binaries should be built with a specialiced version of mem_root that makes it easier to find memory overwrites. This is what the BUILD scripts is doing. The specialiced mem_root code allocates a new block for every allocation which is visiable for any test that depenmds on the default original malloc size and usage. --- mysql-test/include/not_valgrind_build.inc | 4 ++++ mysql-test/main/sp-no-valgrind.test | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 mysql-test/include/not_valgrind_build.inc diff --git a/mysql-test/include/not_valgrind_build.inc b/mysql-test/include/not_valgrind_build.inc new file mode 100644 index 00000000000..b62a1bc953b --- /dev/null +++ b/mysql-test/include/not_valgrind_build.inc @@ -0,0 +1,4 @@ +if (`select version() like '%valgrind%' || version() like '%asan%'`) +{ + skip Does not run with binaries built with valgrind or asan; +} diff --git a/mysql-test/main/sp-no-valgrind.test b/mysql-test/main/sp-no-valgrind.test index 1b4a0f84f1e..6bacc7b150c 100644 --- a/mysql-test/main/sp-no-valgrind.test +++ b/mysql-test/main/sp-no-valgrind.test @@ -1,5 +1,5 @@ --source include/not_msan.inc ---source include/not_valgrind.inc +--source include/not_valgrind_build.inc --echo # MDEV-20699 do not cache SP in SHOW CREATE --echo # Warmup round, this might allocate some memory for session variable From db8ab4aca27de0c02b9e4b4f367d23c2d6c27c03 Mon Sep 17 00:00:00 2001 From: Monty Date: Mon, 19 Aug 2024 11:14:11 +0300 Subject: [PATCH 4/6] Sort result from table_statistics and index_statistics This is needed as the order of rows are not deterministic, especially in future versions of table statistics. --- mysql-test/main/subselect.test | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mysql-test/main/subselect.test b/mysql-test/main/subselect.test index d010f01be56..3f01e984125 100644 --- a/mysql-test/main/subselect.test +++ b/mysql-test/main/subselect.test @@ -5932,7 +5932,9 @@ WHERE SLEEP(0.1) OR c < 'p' OR b = ( SELECT MIN(b) FROM t2 ); --enable_ps2_protocol --echo # The following shows that t2 was indeed scanned with a full scan. +--sorted_result show table_statistics; +--sorted_result show index_statistics; set global userstat=@tmp_mdev410; From fccfdc28b8b3aa42378d7433febbf31d6af7a732 Mon Sep 17 00:00:00 2001 From: Oleksandr Byelkin Date: Mon, 19 Aug 2024 17:15:46 +0200 Subject: [PATCH 5/6] MDEV-34771 Types mismatch when cloning items causes debug assertion Missing methods added to Item_bin_string --- mysql-test/main/item_types.result | 9 +++++++++ mysql-test/main/item_types.test | 10 ++++++++++ sql/item.h | 3 +++ 3 files changed, 22 insertions(+) diff --git a/mysql-test/main/item_types.result b/mysql-test/main/item_types.result index 40db01f2609..6a73549a886 100644 --- a/mysql-test/main/item_types.result +++ b/mysql-test/main/item_types.result @@ -27,4 +27,13 @@ a DROP TABLE t1; DROP VIEW v1, v2; +# +# MDEV-34771: Types mismatch when cloning items causes debug assertion +# +CREATE VIEW t AS SELECT 1 AS a; +SELECT * FROM t WHERE a=b''; +a +drop view t; +# # End of 10.5 tests +# diff --git a/mysql-test/main/item_types.test b/mysql-test/main/item_types.test index 6f10d6bf71a..56af975df9d 100644 --- a/mysql-test/main/item_types.test +++ b/mysql-test/main/item_types.test @@ -30,4 +30,14 @@ SELECT * FROM v2 WHERE a='' AND CASE '' WHEN '' THEN '' ELSE a END=''; DROP TABLE t1; DROP VIEW v1, v2; +--echo # +--echo # MDEV-34771: Types mismatch when cloning items causes debug assertion +--echo # + +CREATE VIEW t AS SELECT 1 AS a; +SELECT * FROM t WHERE a=b''; +drop view t; + +--echo # --echo # End of 10.5 tests +--echo # diff --git a/sql/item.h b/sql/item.h index 9af0a077f75..f5ef54382cb 100644 --- a/sql/item.h +++ b/sql/item.h @@ -4946,6 +4946,9 @@ class Item_bin_string: public Item_hex_hybrid public: Item_bin_string(THD *thd, const char *str, size_t str_length); void print(String *str, enum_query_type query_type) override; + Item *do_get_copy(THD *thd) const override + { return get_item_copy(thd, this); } + Item *do_build_clone(THD *thd) const override { return get_copy(thd); } }; From ae02999cdbea0ebb57cafc8c6b09878b8ea8a3be Mon Sep 17 00:00:00 2001 From: Oleksandr Byelkin Date: Mon, 19 Aug 2024 18:49:04 +0200 Subject: [PATCH 6/6] MDEV-34776 Assertion failure in Item_string::do_build_clone Added missed methods to Item_string child. --- mysql-test/main/item_types.result | 7 +++++++ mysql-test/main/item_types.test | 8 ++++++++ sql/item.h | 12 ++++++++++++ 3 files changed, 27 insertions(+) diff --git a/mysql-test/main/item_types.result b/mysql-test/main/item_types.result index 6a73549a886..0193d33be6d 100644 --- a/mysql-test/main/item_types.result +++ b/mysql-test/main/item_types.result @@ -35,5 +35,12 @@ SELECT * FROM t WHERE a=b''; a drop view t; # +# MDEV-34776: Assertion failure in Item_string::do_build_clone +# +CREATE VIEW v AS SELECT version() AS f; +SELECT * FROM v WHERE f = '10.5.20'; +f +drop view v; +# # End of 10.5 tests # diff --git a/mysql-test/main/item_types.test b/mysql-test/main/item_types.test index 56af975df9d..2818ae582af 100644 --- a/mysql-test/main/item_types.test +++ b/mysql-test/main/item_types.test @@ -38,6 +38,14 @@ CREATE VIEW t AS SELECT 1 AS a; SELECT * FROM t WHERE a=b''; drop view t; +--echo # +--echo # MDEV-34776: Assertion failure in Item_string::do_build_clone +--echo # + +CREATE VIEW v AS SELECT version() AS f; +SELECT * FROM v WHERE f = '10.5.20'; +drop view v; + --echo # --echo # End of 10.5 tests --echo # diff --git a/sql/item.h b/sql/item.h index f5ef54382cb..cddef65f4fd 100644 --- a/sql/item.h +++ b/sql/item.h @@ -4719,6 +4719,9 @@ public: Item_string_sys(THD *thd, const char *str): Item_string(thd, str, (uint) strlen(str), system_charset_info) { } + Item *do_get_copy(THD *thd) const override + { return get_item_copy(thd, this); } + Item *do_build_clone(THD *thd) const override { return get_copy(thd); } }; @@ -4733,6 +4736,9 @@ public: Item_string(thd, str, (uint) strlen(str), &my_charset_latin1, DERIVATION_COERCIBLE, MY_REPERTOIRE_ASCII) { } + Item *do_get_copy(THD *thd) const override + { return get_item_copy(thd, this); } + Item *do_build_clone(THD *thd) const override { return get_copy(thd); } }; @@ -4769,6 +4775,9 @@ public: // require fix_fields() to be re-run for every statement. return mark_unsupported_function(func_name.str, arg, VCOL_TIME_FUNC); } + Item *do_get_copy(THD *thd) const override + { return get_item_copy(thd, this); } + Item *do_build_clone(THD *thd) const override { return get_copy(thd); } }; @@ -4786,6 +4795,9 @@ public: { return mark_unsupported_function("safe_string", arg, VCOL_IMPOSSIBLE); } + Item *do_get_copy(THD *thd) const override + { return get_item_copy(thd, this); } + Item *do_build_clone(THD *thd) const override { return get_copy(thd); } };