mirror of
https://github.com/MariaDB/server.git
synced 2025-08-08 11:22:35 +03:00
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.
This commit is contained in:
@@ -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 }
|
||||
};
|
||||
|
Reference in New Issue
Block a user