1
0
mirror of https://github.com/MariaDB/server.git synced 2026-10-23 18:49:29 +03:00

MDEV-23864 Wrong bulk affected rows with triggers and stored functions

In bulk operation reset_diagnostics_area() set DA_OK_BULK before the
first OK, so the affected rows of the previous statement were added,
and OK of trigger and stored function statements were added too.

Remember DA_OK_BULK/DA_EOF_BULK set by the bulk operation and restore
it when the diagnostics area is reset (also after an error handled in
a trigger or stored function, or a reprepare), and do not set OK status
in sub-statements of bulk operation (my_ok(), GET DIAGNOSTICS).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Oleksandr Byelkin
2026-10-22 13:05:55 +02:00
parent 9a55be1243
commit dee9ef432b
7 changed files with 137 additions and 35 deletions

View File

@@ -4668,6 +4668,14 @@ public:
const Diagnostics_area *get_stmt_da() const
{ return m_stmt_da; }
/*
Bulk operation sums affected rows and warnings of all executions in
the diagnostics area, so statements of triggers and stored functions
called by it must not set the status
*/
bool in_bulk_sub_stmt() const
{ return in_sub_stmt && m_stmt_da->is_bulk_op(); }
/// Sets Diagnostics-area for the current statement.
void set_stmt_da(Diagnostics_area *da)
{ m_stmt_da= da; }
@@ -6010,6 +6018,8 @@ my_ok(THD *thd, ulonglong affected_rows_arg= 0, ulonglong id= 0,
{
thd->set_row_count_func(affected_rows_arg);
thd->set_affected_rows(affected_rows_arg);
if (thd->in_bulk_sub_stmt())
return;
thd->get_stmt_da()->set_ok_status(affected_rows_arg, id, message);
}

View File

@@ -275,7 +275,7 @@ void Sql_condition::assign_defaults(THD *thd, const Sql_state_errno *from)
Diagnostics_area::Diagnostics_area(bool initialize)
: is_bulk_execution(0), m_main_wi(0, false, initialize)
: m_bulk_status(DA_EMPTY), is_bulk_execution(0), m_main_wi(0, false, initialize)
{
push_warning_info(&m_main_wi);
@@ -285,7 +285,7 @@ Diagnostics_area::Diagnostics_area(bool initialize)
Diagnostics_area::Diagnostics_area(ulonglong warning_info_id,
bool allow_unlimited_warnings,
bool initialize)
: is_bulk_execution(0),
: m_bulk_status(DA_EMPTY), is_bulk_execution(0),
m_main_wi(warning_info_id, allow_unlimited_warnings, initialize)
{
push_warning_info(&m_main_wi);
@@ -319,15 +319,13 @@ Diagnostics_area::reset_diagnostics_area()
get_warning_info()->clear_error_condition();
set_is_sent(false);
/*
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.
A bulk operation sums m_affected_rows and m_statement_warn_count of
all its executions, so the status stays DA_OK_BULK/DA_EOF_BULK once
set, also after an error handled in a trigger or stored function or
a reprepare. For RETURNING of non-optimized array binding the status
also shows that result set metadata was already sent.
*/
m_status= is_bulk_op() ? DA_OK_BULK : DA_EMPTY;
m_status= is_bulk_op() ? m_bulk_status : DA_EMPTY;
DBUG_VOID_RETURN;
}
@@ -350,10 +348,6 @@ Diagnostics_area::set_ok_status(ulonglong affected_rows,
*/
if (unlikely(is_error() || is_disabled()))
DBUG_VOID_RETURN;
/*
When running a bulk operation, m_status will be DA_OK for the first
operation and set to DA_OK_BULK for all following operations.
*/
if (m_status == DA_OK_BULK)
{
m_statement_warn_count+= current_statement_warn_count();
@@ -364,6 +358,8 @@ Diagnostics_area::set_ok_status(ulonglong affected_rows,
m_statement_warn_count= current_statement_warn_count();
m_affected_rows= affected_rows;
m_status= (is_bulk_op() ? DA_OK_BULK : DA_OK);
if (is_bulk_op())
m_bulk_status= DA_OK_BULK;
}
m_last_insert_id= last_insert_id;
if (message)
@@ -411,6 +407,8 @@ Diagnostics_area::set_eof_status(THD *thd)
else
m_statement_warn_count= current_statement_warn_count();
m_status= (is_bulk_op() ? DA_EOF_BULK : DA_EOF);
if (is_bulk_op())
m_bulk_status= DA_EOF_BULK;
}
DBUG_VOID_RETURN;

View File

@@ -1049,19 +1049,15 @@ public:
bool is_disabled() const { return m_status == DA_DISABLED; }
void set_bulk_execution(bool bulk) { is_bulk_execution= bulk; }
void set_bulk_execution(bool bulk)
{
is_bulk_execution= bulk;
m_bulk_status= DA_EMPTY;
}
bool is_bulk_op() const { return is_bulk_execution; }
enum_diagnostics_status status() const { return m_status; }
void restore_bulk_status(enum_diagnostics_status status)
{
DBUG_ASSERT(is_bulk_op());
DBUG_ASSERT(m_status == DA_OK_BULK);
DBUG_ASSERT(status == DA_EMPTY || status == DA_OK_BULK ||
status == DA_EOF_BULK);
m_status= status;
}
const char *message() const
{
@@ -1320,6 +1316,12 @@ private:
enum_diagnostics_status m_status;
/*
DA_EMPTY until the bulk operation sets m_status to DA_OK_BULK or
DA_EOF_BULK, reset by set_bulk_execution()
*/
enum_diagnostics_status m_bulk_status;
my_bool is_bulk_execution;
Warning_info m_main_wi;

View File

@@ -59,7 +59,8 @@ Sql_cmd_get_diagnostics::execute(THD *thd)
/* Bail out early if statement succeeded. */
if (! rv)
{
thd->get_stmt_da()->set_ok_status(0, 0, NULL);
if (!thd->in_bulk_sub_stmt())
thd->get_stmt_da()->set_ok_status(0, 0, NULL);
DBUG_RETURN(false);
}
@@ -81,7 +82,7 @@ Sql_cmd_get_diagnostics::execute(THD *thd)
message);
/* Appending might have failed. */
if (unlikely(!(rv= thd->is_error())))
if (unlikely(!(rv= thd->is_error())) && !thd->in_bulk_sub_stmt())
thd->get_stmt_da()->set_ok_status(0, 0, NULL);
DBUG_RETURN(rv);

View File

@@ -4962,8 +4962,6 @@ reexecute:
/* items must not cache values of the parameters of the first row */
if (bulk_optimized)
set_params_changing_value(true);
Diagnostics_area::enum_diagnostics_status da_status=
thd->get_stmt_da()->status();
error= execute(expanded_query, open_cursor) || thd->is_error();
if (!bulk_optimized)
thd->get_stmt_da()->set_bulk_warn_count(warn_count);
@@ -4994,11 +4992,6 @@ reexecute:
{
DBUG_ASSERT(thd->get_stmt_da()->sql_errno() == ER_NEED_REPREPARE);
thd->clear_error();
/*
The failed attempt must not change the status, because it shows
whether result set metadata was already sent
*/
thd->get_stmt_da()->restore_bulk_status(da_status);
error= reprepare();

View File

@@ -1395,7 +1395,7 @@ 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())
if (thd->get_stmt_da()->is_bulk_op() && !thd->in_bulk_sub_stmt())
{
/*
Update the diagnostics message sent to a client with number of actual

View File

@@ -23805,13 +23805,111 @@ static void test_mdev_23864()
myquery(rc);
test_mdev_23864_query("UPDATE t1 SET val= f1(10 * ?) WHERE id= ? / 10",
"INSERT INTO t1 VALUES (1, 0), (2, 0), (3, 0)",
same, 0, FALSE);
same, 3, FALSE);
test_mdev_23864_query("INSERT INTO t1 (id, val) VALUES "
"(?, f1((SELECT b FROM t2 WHERE a = ?)))",
"DELETE FROM t4", looked_up, 0, FALSE);
"DELETE FROM t4", looked_up, 3, FALSE);
rc= mysql_query(mysql, "DROP FUNCTION f1");
myquery(rc);
/* rows changed by a trigger are not counted as affected */
rc= mysql_query(mysql, "CREATE TRIGGER t1_bi BEFORE INSERT ON t1 "
"FOR EACH ROW INSERT INTO t4 (v) VALUES (NEW.val)");
myquery(rc);
test_mdev_23864_query("INSERT INTO t1 (id, val) VALUES (?, ?)", NULL,
same, 3, FALSE);
test_mdev_23864_query("INSERT INTO t1 (id, val) VALUES "
"(?, (SELECT b FROM t2 WHERE a = ?))", NULL,
looked_up, 3, FALSE);
rc= mysql_query(mysql, "DROP TRIGGER t1_bi");
myquery(rc);
rc= mysql_query(mysql, "CREATE TRIGGER t1_ai AFTER INSERT ON t1 "
"FOR EACH ROW UPDATE t4 SET v= v + 1");
myquery(rc);
rc= mysql_query(mysql, "CREATE TRIGGER t1_au AFTER UPDATE ON t1 "
"FOR EACH ROW UPDATE t4 SET v= v + 1");
myquery(rc);
test_mdev_23864_query("INSERT INTO t1 (id, val) VALUES (?, ?)", NULL,
same, 3, FALSE);
test_mdev_23864_query("INSERT INTO t1 (id, val) VALUES "
"(?, (SELECT b FROM t2 WHERE a = ?))", NULL,
looked_up, 3, FALSE);
test_mdev_23864_query("UPDATE t1 SET val= 10 * ? WHERE id= ? / 10",
"INSERT INTO t1 VALUES (1, 0), (2, 0), (3, 0)",
same, 3, FALSE);
rc= mysql_query(mysql, "DROP TRIGGER t1_ai");
myquery(rc);
rc= mysql_query(mysql, "DROP TRIGGER t1_au");
myquery(rc);
/* an error handled in a trigger does not reset the affected rows */
rc= mysql_query(mysql, "CREATE TABLE t7 (id INT PRIMARY KEY)");
myquery(rc);
rc= mysql_query(mysql, "CREATE TRIGGER t1_ai AFTER INSERT ON t1 "
"FOR EACH ROW BEGIN "
"DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END; "
"INSERT INTO t7 VALUES (NEW.id); END");
myquery(rc);
rc= mysql_query(mysql, "CREATE TRIGGER t1_au AFTER UPDATE ON t1 "
"FOR EACH ROW BEGIN "
"DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN END; "
"INSERT INTO t7 VALUES (NEW.id); END");
myquery(rc);
test_mdev_23864_query("INSERT INTO t1 (id, val) VALUES (?, ?)",
"REPLACE INTO t7 VALUES (2)", same, 3, FALSE);
test_mdev_23864_query("INSERT INTO t1 (id, val) VALUES "
"(?, (SELECT b FROM t2 WHERE a = ?))",
"REPLACE INTO t7 VALUES (2)", looked_up, 3, FALSE);
test_mdev_23864_query("UPDATE t1 SET val= 10 * ? WHERE id= ? / 10",
"INSERT INTO t1 VALUES (1, 0), (2, 0), (3, 0)",
same, 3, FALSE);
rc= mysql_query(mysql, "DROP TRIGGER t1_ai");
myquery(rc);
rc= mysql_query(mysql, "DROP TRIGGER t1_au");
myquery(rc);
rc= mysql_query(mysql, "DROP TABLE t7");
myquery(rc);
/* statements of a trigger which set the diagnostics area status */
rc= mysql_query(mysql, "CREATE TRIGGER t1_ai AFTER INSERT ON t1 "
"FOR EACH ROW BEGIN DECLARE n INT; "
"GET DIAGNOSTICS n= NUMBER; "
"SELECT COUNT(*) INTO @a FROM t2; END");
myquery(rc);
rc= mysql_query(mysql, "CREATE TRIGGER t1_au AFTER UPDATE ON t1 "
"FOR EACH ROW BEGIN DECLARE n INT; "
"GET DIAGNOSTICS n= NUMBER; "
"SELECT COUNT(*) INTO @a FROM t2; END");
myquery(rc);
test_mdev_23864_query("INSERT INTO t1 (id, val) VALUES (?, ?)", NULL,
same, 3, FALSE);
test_mdev_23864_query("INSERT INTO t1 (id, val) VALUES (?, ?) "
"RETURNING id, val", NULL, same, 3, TRUE);
test_mdev_23864_query("INSERT INTO t1 (id, val) VALUES "
"(?, (SELECT b FROM t2 WHERE a = ?))", NULL,
looked_up, 3, FALSE);
test_mdev_23864_query("INSERT INTO t1 (id, val) VALUES "
"(?, (SELECT b FROM t2 WHERE a = ?)) "
"RETURNING id, val", NULL, looked_up, 3, TRUE);
test_mdev_23864_query("UPDATE t1 SET val= 10 * ? WHERE id= ? / 10",
"INSERT INTO t1 VALUES (1, 0), (2, 0), (3, 0)",
same, 3, FALSE);
rc= mysql_query(mysql, "DROP TRIGGER t1_ai");
myquery(rc);
rc= mysql_query(mysql, "DROP TRIGGER t1_au");
myquery(rc);
/* the statement is killed in not the first execution */
test_mdev_23864_exec("UPDATE t1 SET val= ? + SLEEP(id = 3) "
"WHERE id= ? / 10",
(const char *[]) {"DELETE FROM t1",
"INSERT INTO t1 VALUES "
"(1, 0), (2, 0), (3, 0)",
"SET max_statement_time= 0.5",
NULL}, ER_STATEMENT_TIMEOUT, 0, 0);
rc= mysql_query(mysql, "SET max_statement_time= DEFAULT");
myquery(rc);
/* The first generated id is reported, as for the optimized execution */
rc= mysql_query(mysql, "CREATE TABLE t3 (id INT AUTO_INCREMENT PRIMARY KEY,"
" val INT)");