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 } };