1
0
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:
Dmitry Shulga
2024-08-16 12:43:35 +07:00
parent f41a120298
commit ba5482ffc2
7 changed files with 534 additions and 14 deletions

View File

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