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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)");
|
||||
|
||||
Reference in New Issue
Block a user