From 502cf821cd008867a79e87a463fdb8585faa4b90 Mon Sep 17 00:00:00 2001 From: Alexander Barkov Date: Mon, 17 Aug 2026 16:27:38 +0400 Subject: [PATCH 1/2] Cherry-pick from 12.3: MDEV-40790 SELECT INTO row_type_of.field crashes the server The server crashed on DBUG_ASSERT on a SELECT into: - a `ROW TYPE OF table1` field variable - a `ROW TYPE OF cursor1` field variable Fix: - Adding a class my_var_sp_row_field_by_name - Adding a method sp_rcontext::set_variable_row_field_by_name() - Fixing the DBUG_ASSERT --- mysql-test/main/select_into_row.result | 140 ++++++++++++++++++++ mysql-test/main/select_into_row.test | 174 +++++++++++++++++++++++++ sql/sp_rcontext.cc | 14 ++ sql/sp_rcontext.h | 3 + sql/sql_type_row.cc | 38 +++++- 5 files changed, 367 insertions(+), 2 deletions(-) create mode 100644 mysql-test/main/select_into_row.result create mode 100644 mysql-test/main/select_into_row.test diff --git a/mysql-test/main/select_into_row.result b/mysql-test/main/select_into_row.result new file mode 100644 index 0000000000000..98315c114d124 --- /dev/null +++ b/mysql-test/main/select_into_row.result @@ -0,0 +1,140 @@ +# +# MDEV-40790 SELECT INTO row_type_of.field crashes the server +# +# Into an explicit ROW variable +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +CREATE PROCEDURE p1() +BEGIN +DECLARE r0 ROW (a INT, b VARCHAR(10)); +SELECT a,b INTO r0 FROM t1; +SELECT r0.a, r0.b; +END; +$$ +CALL p1; +r0.a r0.b +10 b10 +DROP PROCEDURE p1; +DROP TABLE t1; +# Into an explicit ROW field variable +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +CREATE PROCEDURE p1() +BEGIN +DECLARE r0 ROW (a INT, b VARCHAR(10)); +SELECT a,b INTO r0.a,r0.b FROM t1; +SELECT r0.a, r0.b; +END; +$$ +CALL p1; +r0.a r0.b +10 b10 +DROP PROCEDURE p1; +DROP TABLE t1; +# Into an explicit ROW field variable - non-existing field +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +CREATE PROCEDURE p1() +BEGIN +DECLARE r0 ROW (a INT, b VARCHAR(10)); +SELECT a,b INTO r0.a,r0.b1_non_existing FROM t1; +END; +$$ +ERROR HY000: Row variable 'r0' does not have a field 'b1_non_existing' +DROP TABLE t1; +# Into a `ROW TYPE OF table1` variable +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +CREATE PROCEDURE p1() +BEGIN +DECLARE r0 ROW TYPE OF t1; +SELECT a,b INTO r0 FROM t1; +SELECT r0.a, r0.b; +END; +$$ +CALL p1; +r0.a r0.b +10 b10 +DROP PROCEDURE p1; +DROP TABLE t1; +# Into a `ROW TYPE OF table1` field variable +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +CREATE PROCEDURE p1() +BEGIN +DECLARE r0 ROW TYPE OF t1; +SELECT a,b INTO r0.a,r0.b FROM t1; +SELECT r0.a, r0.b; +END; +$$ +CALL p1; +r0.a r0.b +10 b10 +DROP PROCEDURE p1; +DROP TABLE t1; +# Into a `ROW TYPE OF table1` field variable - non-existing field +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +CREATE PROCEDURE p1() +BEGIN +DECLARE r0 ROW TYPE OF t1; +SELECT a,b INTO r0.a,r0.b1_non_existing FROM t1; +END; +$$ +CALL p1; +ERROR HY000: Row variable 'r0' does not have a field 'b1_non_existing' +DROP PROCEDURE p1; +DROP TABLE t1; +# Into a `ROW TYPE OF cursor1` variable +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +CREATE PROCEDURE p1() +BEGIN +DECLARE c1 CURSOR FOR SELECT * FROM t1; +BEGIN +DECLARE r0 ROW TYPE OF c1; +SELECT a,b INTO r0 FROM t1; +SELECT r0.a, r0.b; +END; +END; +$$ +CALL p1; +r0.a r0.b +10 b10 +DROP PROCEDURE p1; +DROP TABLE t1; +# Into a `ROW TYPE OF cursor1` field variable +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +CREATE PROCEDURE p1() +BEGIN +DECLARE c1 CURSOR FOR SELECT * FROM t1; +BEGIN +DECLARE r0 ROW TYPE OF c1; +SELECT a,b INTO r0.a,r0.b FROM t1; +SELECT r0.a, r0.b; +END; +END; +$$ +CALL p1; +r0.a r0.b +10 b10 +DROP PROCEDURE p1; +DROP TABLE t1; +# Into a `ROW TYPE OF cursor1` field variable - non-existing field +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +CREATE PROCEDURE p1() +BEGIN +DECLARE c1 CURSOR FOR SELECT * FROM t1; +BEGIN +DECLARE r0 ROW TYPE OF c1; +SELECT a,b INTO r0.a,r0.b1_non_existing FROM t1; +END; +END; +$$ +CALL p1; +ERROR HY000: Row variable 'r0' does not have a field 'b1_non_existing' +DROP PROCEDURE p1; +DROP TABLE t1; +# End of 12.3 tests diff --git a/mysql-test/main/select_into_row.test b/mysql-test/main/select_into_row.test new file mode 100644 index 0000000000000..79127eca21b6b --- /dev/null +++ b/mysql-test/main/select_into_row.test @@ -0,0 +1,174 @@ +--echo # +--echo # MDEV-40790 SELECT INTO row_type_of.field crashes the server +--echo # + + +--echo # Into an explicit ROW variable + +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +DELIMITER $$; +CREATE PROCEDURE p1() +BEGIN + DECLARE r0 ROW (a INT, b VARCHAR(10)); + SELECT a,b INTO r0 FROM t1; + SELECT r0.a, r0.b; +END; +$$ +DELIMITER ;$$ +CALL p1; +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # Into an explicit ROW field variable + +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +DELIMITER $$; +CREATE PROCEDURE p1() +BEGIN + DECLARE r0 ROW (a INT, b VARCHAR(10)); + SELECT a,b INTO r0.a,r0.b FROM t1; + SELECT r0.a, r0.b; +END; +$$ +DELIMITER ;$$ +CALL p1; +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # Into an explicit ROW field variable - non-existing field + +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +DELIMITER $$; +--error ER_ROW_VARIABLE_DOES_NOT_HAVE_FIELD +CREATE PROCEDURE p1() +BEGIN + DECLARE r0 ROW (a INT, b VARCHAR(10)); + SELECT a,b INTO r0.a,r0.b1_non_existing FROM t1; +END; +$$ +DELIMITER ;$$ +DROP TABLE t1; + + +--echo # Into a `ROW TYPE OF table1` variable + +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +DELIMITER $$; +CREATE PROCEDURE p1() +BEGIN + DECLARE r0 ROW TYPE OF t1; + SELECT a,b INTO r0 FROM t1; + SELECT r0.a, r0.b; +END; +$$ +DELIMITER ;$$ +CALL p1; +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # Into a `ROW TYPE OF table1` field variable + +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +DELIMITER $$; +CREATE PROCEDURE p1() +BEGIN + DECLARE r0 ROW TYPE OF t1; + SELECT a,b INTO r0.a,r0.b FROM t1; + SELECT r0.a, r0.b; +END; +$$ +DELIMITER ;$$ +CALL p1; +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # Into a `ROW TYPE OF table1` field variable - non-existing field + +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +DELIMITER $$; +CREATE PROCEDURE p1() +BEGIN + DECLARE r0 ROW TYPE OF t1; + SELECT a,b INTO r0.a,r0.b1_non_existing FROM t1; +END; +$$ +DELIMITER ;$$ +--error ER_ROW_VARIABLE_DOES_NOT_HAVE_FIELD +CALL p1; +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # Into a `ROW TYPE OF cursor1` variable + +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +DELIMITER $$; +CREATE PROCEDURE p1() +BEGIN + DECLARE c1 CURSOR FOR SELECT * FROM t1; + BEGIN + DECLARE r0 ROW TYPE OF c1; + SELECT a,b INTO r0 FROM t1; + SELECT r0.a, r0.b; + END; +END; +$$ +DELIMITER ;$$ +CALL p1; +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # Into a `ROW TYPE OF cursor1` field variable + +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +DELIMITER $$; +CREATE PROCEDURE p1() +BEGIN + DECLARE c1 CURSOR FOR SELECT * FROM t1; + BEGIN + DECLARE r0 ROW TYPE OF c1; + SELECT a,b INTO r0.a,r0.b FROM t1; + SELECT r0.a, r0.b; + END; +END; +$$ +DELIMITER ;$$ +CALL p1; +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # Into a `ROW TYPE OF cursor1` field variable - non-existing field + +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +DELIMITER $$; +CREATE PROCEDURE p1() +BEGIN + DECLARE c1 CURSOR FOR SELECT * FROM t1; + BEGIN + DECLARE r0 ROW TYPE OF c1; + SELECT a,b INTO r0.a,r0.b1_non_existing FROM t1; + END; +END; +$$ +DELIMITER ;$$ +--error ER_ROW_VARIABLE_DOES_NOT_HAVE_FIELD +CALL p1; +DROP PROCEDURE p1; +DROP TABLE t1; + +--echo # End of 12.3 tests diff --git a/sql/sp_rcontext.cc b/sql/sp_rcontext.cc index 5dec16c308157..ee386b9395613 100644 --- a/sql/sp_rcontext.cc +++ b/sql/sp_rcontext.cc @@ -688,6 +688,20 @@ int sp_rcontext::set_variable_row_field(THD *thd, uint var_idx, uint field_idx, } +int sp_rcontext::set_variable_row_field_by_name(THD *thd, uint var_idx, + const Lex_ident_sys_st &field_name, + Item **value) +{ + DBUG_ENTER("sp_rcontext::set_variable_row_field"); + DBUG_ASSERT(value); + uint field_idx= 0; + if (find_row_field_by_name_or_error(&field_idx, var_idx, field_name)) + DBUG_RETURN(true); + Virtual_tmp_table *vtable= virtual_tmp_table_for_row(var_idx); + DBUG_RETURN(thd->sp_eval_expr(vtable->field[field_idx], value)); +} + + int sp_rcontext::set_variable_row(THD *thd, uint var_idx, List &items) { DBUG_ENTER("sp_rcontext::set_variable_row"); diff --git a/sql/sp_rcontext.h b/sql/sp_rcontext.h index 7cf68d7c94935..2927311d55ddc 100644 --- a/sql/sp_rcontext.h +++ b/sql/sp_rcontext.h @@ -209,6 +209,9 @@ class sp_rcontext : public Sql_alloc int set_variable(THD *thd, uint var_idx, Item **value); int set_variable_row_field(THD *thd, uint var_idx, uint field_idx, Item **value); + int set_variable_row_field_by_name(THD *thd, uint var_idx, + const Lex_ident_sys_st &field_name, + Item **value); int set_variable_row(THD *thd, uint var_idx, List &items); int set_variable_composite_field_by_key(THD *thd, diff --git a/sql/sql_type_row.cc b/sql/sql_type_row.cc index f09d0fd36f32a..e747f15d6f629 100644 --- a/sql/sql_type_row.cc +++ b/sql/sql_type_row.cc @@ -136,6 +136,32 @@ class my_var_sp_row_field: public my_var_sp }; +class my_var_sp_row_field_by_name: public my_var_sp +{ + const Lex_ident_sys_st m_field_name; +public: + my_var_sp_row_field_by_name(const Lex_ident_sys_st &varname, + const sp_rcontext_addr &varaddr, + const Lex_ident_sys_st &field_name, + sp_head *s) + :my_var_sp(varname, varaddr, + &type_handler_double/*Not really used*/, s), + m_field_name(field_name) + { } + bool check_assignability(THD *thd, const List &select_list, + bool *assign_as_row) const override + { + *assign_as_row= false; + return select_list.elements == 1; + } + bool set(THD *thd, Item *item) override + { + return get_rcontext(thd->spcont)-> + set_variable_row_field_by_name(thd, offset(), m_field_name, &item); + } +}; + + my_var *Type_handler_row::make_outvar(THD *thd, const Lex_ident_sys_st &name, const sp_rcontext_addr &addr, @@ -160,11 +186,19 @@ my_var *Type_handler_row::make_outvar_field(THD *thd, DBUG_ASSERT(t); DBUG_ASSERT(t->type_handler() == this); + if (t->field_def.is_table_rowtype_ref() || + t->field_def.is_cursor_rowtype_ref()) + { + if (validate_only) + return nullptr; + return new (thd->mem_root) my_var_sp_row_field_by_name(name, addr, + field, sphead); + + } uint row_field_offset; if (!t->find_row_field(&name, &field, &row_field_offset)) { - DBUG_ASSERT(0); - my_error(ER_ROW_VARIABLE_DOES_NOT_HAVE_FIELD, MYF(0), name.str, field.str); + DBUG_ASSERT(thd->is_error()); return NULL; } if (validate_only) // e.g. EXPLAIN SELECT .. INTO spvar_row.field; From e3b6b7840b16f2b01ed4fd5da82aee712d653638 Mon Sep 17 00:00:00 2001 From: Alexander Barkov Date: Mon, 17 Aug 2026 09:36:56 +0400 Subject: [PATCH 2/2] MDEV-39563 Implement UPDATE ... RETURNING ... INTO Adding support for UPDATE .. RETURNING .. INTO queries. For example: UPDATE t1 SET a=10,b=20 RETURNING a,b INTO va,vb; UPDATE t1 SET a=10,b=20 RETURNING a,b INTO @a,@b; Limitations: 1. These types of queries: - REPLACE .. RETURNING .. INTO - DELETE .. RETURNING .. INTO - INSERT .. RETURNING .. INTO do not work - they return an error. They will be implemented separately, when needed. 2. UPDATE..RETURNING..INTO with --binlog_format=statement is not allowed and an error is raised. 3. Using OLD_VALUE(col) inside UPDATE..RETURNING..INTO is not allowed and an error is raised. 4. Multi-table updates, as well as single table updates with a subquery to the same table in WHERE (which get converted to multi-table) do not work and an error is raised. Notes: 1. ANALYZE and EXPLAIN Both ANALYZE UPDATE .. RETURNING .. INTO .. EXPLAIN UPDATE .. RETURNING .. INTO .. return this error: 'RETURNING..INTO' is not allowed in this context 2. Behavior on no data a. In case of degenerated plans (WHERE 1=0, LIMIT 0), no errors are raised. b. If the updated table contains no rows, the behavior depends on the engine, for example: - MyISAM returns no errors - InnoDB raises No data - zero rows fetched, selected, or processed This behavior is engine dependent because some engines (e.g. MyISAM) quickly know that the table has no records and execute the statement using a degenerated plan. c. If there are some rows, but non of them match the WHERE condition, then this error is raised: No data - zero rows fetched, selected, or processed d. If some rows where found but none of them actually got changed by the SET, still this error is raised: No data - zero rows fetched, selected, or processed The error message might be misleading. However, if we read it as "zero rows [that required updates] fetched", it looks OK. Let's not introduce a new error message for now. Helper changes: 1. The grammar in analyze_stmt_command was changed to have LEX::analyze_stmt set to true earlier, so LEX::set_returning_into_result() already knows if this is an ANALYZE statement. 2. The Sql_cmd_update constructor is now called earlier in the grammar, to be able to call Sql_cmd_update::set_with_old_value_items() in the SET and RETURNING clauses. 3. Sql_cmd_dml::lex is now set during the constructor time. It makes things easier: - Sql_cmd_update::returns_result_set() needs the lex. - Sql_cmd_delete::orig_multitable and Sql_cmd_update::orig_multitable are not needed any more. They were used only in Sql_cmd_delete::sql_command_code() and Sql_cmd_update::sql_command_code(). Sql_cmd_dml::sql_command_code() now returns lex->sql_command. The overrides Sql_cmd_delete::sql_command_code() and Sql_cmd_update::sql_command_code() were removed. --- mysql-test/main/delete_returning.result | 19 + mysql-test/main/delete_returning.test | 29 + mysql-test/main/insert_returning.result | 19 + mysql-test/main/insert_returning.test | 29 + mysql-test/main/replace_returning.result | 19 + mysql-test/main/replace_returning.test | 30 + mysql-test/main/update_returning.result | 1003 +++++++++++++++++ mysql-test/main/update_returning.test | 367 ++++++ mysql-test/main/update_returning_engines.inc | 195 ++++ .../main/update_returning_innodb.result | 322 ++++++ mysql-test/main/update_returning_innodb.test | 43 + .../main/update_returning_into_row_var.inc | 47 + .../binlog_update_returning_into_mixed.result | 30 + .../r/binlog_update_returning_into_row.result | 32 + .../binlog_update_returning_into_stmt.result | 22 + .../t/binlog_update_returning_into_mixed.test | 25 + .../t/binlog_update_returning_into_row.test | 25 + .../t/binlog_update_returning_into_stmt.test | 27 + .../rpl_update_returning_mixed_or_row.result | 50 + .../t/rpl_update_returning_mixed_or_row.test | 43 + .../sp-assoc-array-update-returning.result | 181 +++ .../sp-assoc-array-update-returning.test | 227 ++++ sql/select_result.h | 1 + sql/sp_head.cc | 23 +- sql/sql_class.h | 1 + sql/sql_cmd.h | 23 +- sql/sql_delete.h | 15 +- sql/sql_lex.cc | 18 + sql/sql_lex.h | 4 +- sql/sql_select.cc | 36 +- sql/sql_update.cc | 132 ++- sql/sql_update.h | 23 +- sql/sql_yacc.yy | 53 +- 33 files changed, 3055 insertions(+), 58 deletions(-) create mode 100644 mysql-test/main/update_returning.result create mode 100644 mysql-test/main/update_returning.test create mode 100644 mysql-test/main/update_returning_engines.inc create mode 100644 mysql-test/main/update_returning_innodb.result create mode 100644 mysql-test/main/update_returning_innodb.test create mode 100644 mysql-test/main/update_returning_into_row_var.inc create mode 100644 mysql-test/suite/binlog/r/binlog_update_returning_into_mixed.result create mode 100644 mysql-test/suite/binlog/r/binlog_update_returning_into_row.result create mode 100644 mysql-test/suite/binlog/r/binlog_update_returning_into_stmt.result create mode 100644 mysql-test/suite/binlog/t/binlog_update_returning_into_mixed.test create mode 100644 mysql-test/suite/binlog/t/binlog_update_returning_into_row.test create mode 100644 mysql-test/suite/binlog/t/binlog_update_returning_into_stmt.test create mode 100644 mysql-test/suite/rpl/r/rpl_update_returning_mixed_or_row.result create mode 100644 mysql-test/suite/rpl/t/rpl_update_returning_mixed_or_row.test create mode 100644 plugin/type_assoc_array/mysql-test/type_assoc_array/sp-assoc-array-update-returning.result create mode 100644 plugin/type_assoc_array/mysql-test/type_assoc_array/sp-assoc-array-update-returning.test diff --git a/mysql-test/main/delete_returning.result b/mysql-test/main/delete_returning.result index 847f810de630c..151e35960ddaa 100644 --- a/mysql-test/main/delete_returning.result +++ b/mysql-test/main/delete_returning.result @@ -227,3 +227,22 @@ show status like "Rows_sent"; Variable_name Value Rows_sent 3 drop table t1; +# +# MDEV-39563 Implement UPDATE ... RETURNING ... INTO +# +DELETE FROM t1 RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +ANALYZE DELETE FROM t1 RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +EXPLAIN DELETE FROM t1 RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10); +DELETE FROM t1 RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +ANALYZE DELETE FROM t1 RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +EXPLAIN DELETE FROM t1 RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +DROP TABLE t1; +# End of 13.1 tests diff --git a/mysql-test/main/delete_returning.test b/mysql-test/main/delete_returning.test index cdfb48e843fe4..bb22c98effc73 100644 --- a/mysql-test/main/delete_returning.test +++ b/mysql-test/main/delete_returning.test @@ -183,3 +183,32 @@ flush status; delete from t1 where a between 1 and 3 returning a,b; show status like "Rows_sent"; drop table t1; + +--echo # +--echo # MDEV-39563 Implement UPDATE ... RETURNING ... INTO +--echo # + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +DELETE FROM t1 RETURNING a INTO @a; + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +ANALYZE DELETE FROM t1 RETURNING a INTO @a; + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +EXPLAIN DELETE FROM t1 RETURNING a INTO @a; + +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10); + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +DELETE FROM t1 RETURNING a INTO @a; + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +ANALYZE DELETE FROM t1 RETURNING a INTO @a; + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +EXPLAIN DELETE FROM t1 RETURNING a INTO @a; + +DROP TABLE t1; + +--echo # End of 13.1 tests diff --git a/mysql-test/main/insert_returning.result b/mysql-test/main/insert_returning.result index 9c1233d7fa8ff..c97f00244826d 100644 --- a/mysql-test/main/insert_returning.result +++ b/mysql-test/main/insert_returning.result @@ -705,3 +705,22 @@ show status like "Rows_sent"; Variable_name Value Rows_sent 6 drop table t1; +# +# MDEV-39563 Implement UPDATE ... RETURNING ... INTO +# +INSERT INTO t1 VALUES (10) RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +ANALYZE INSERT INTO t1 VALUES (10) RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +EXPLAIN INSERT INTO t1 VALUES (10) RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10); +INSERT INTO t1 VALUES (10) RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +ANALYZE INSERT INTO t1 VALUES (10) RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +EXPLAIN INSERT INTO t1 VALUES (10) RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +DROP TABLE t1; +# End of 13.1 tests diff --git a/mysql-test/main/insert_returning.test b/mysql-test/main/insert_returning.test index 28bc0e495d743..e75ac0bed0b07 100644 --- a/mysql-test/main/insert_returning.test +++ b/mysql-test/main/insert_returning.test @@ -412,3 +412,32 @@ insert into t1 values (1,2),(2,4) returning a,b; insert into t1 select seq,seq from seq_10_to_13 returning a,b; show status like "Rows_sent"; drop table t1; + +--echo # +--echo # MDEV-39563 Implement UPDATE ... RETURNING ... INTO +--echo # + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +INSERT INTO t1 VALUES (10) RETURNING a INTO @a; + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +ANALYZE INSERT INTO t1 VALUES (10) RETURNING a INTO @a; + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +EXPLAIN INSERT INTO t1 VALUES (10) RETURNING a INTO @a; + +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10); + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +INSERT INTO t1 VALUES (10) RETURNING a INTO @a; + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +ANALYZE INSERT INTO t1 VALUES (10) RETURNING a INTO @a; + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +EXPLAIN INSERT INTO t1 VALUES (10) RETURNING a INTO @a; + +DROP TABLE t1; + +--echo # End of 13.1 tests diff --git a/mysql-test/main/replace_returning.result b/mysql-test/main/replace_returning.result index fef7aaddaa3fc..4676b43e22ba1 100644 --- a/mysql-test/main/replace_returning.result +++ b/mysql-test/main/replace_returning.result @@ -260,3 +260,22 @@ ERROR 21000: Subquery returns more than 1 row REPLACE INTO t2(id2,val2) SELECT t1.* FROM t1 WHERE id1=2 RETURNING t1.*; ERROR 42S02: Unknown table 'test.t1' DROP TABLE t1,t2; +# +# MDEV-39563 Implement UPDATE ... RETURNING ... INTO +# +REPLACE t1 SET a=a+1 RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +ANALYZE REPLACE INTO t1 VALUES (a=11, 'b11') RETURNING a,b INTO @a, @b; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +EXPLAIN REPLACE INTO t1 VALUES (a=11, 'b11') RETURNING a,b INTO @a, @b; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10); +REPLACE t1 SET a=a+1 RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +ANALYZE REPLACE INTO t1 VALUES (a=11, 'b11') RETURNING a,b INTO @a, @b; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +EXPLAIN REPLACE INTO t1 VALUES (a=11, 'b11') RETURNING a,b INTO @a, @b; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +DROP TABLE t1; +# End of 13.1 tests diff --git a/mysql-test/main/replace_returning.test b/mysql-test/main/replace_returning.test index 8b8a1c24e3e01..326258d86c3dd 100644 --- a/mysql-test/main/replace_returning.test +++ b/mysql-test/main/replace_returning.test @@ -190,3 +190,33 @@ REPLACE INTO t2(id2,val2) SELECT t1.* FROM t1 WHERE id1=2 RETURNING t1.*; DROP TABLE t1,t2; + + +--echo # +--echo # MDEV-39563 Implement UPDATE ... RETURNING ... INTO +--echo # + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +REPLACE t1 SET a=a+1 RETURNING a INTO @a; + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +ANALYZE REPLACE INTO t1 VALUES (a=11, 'b11') RETURNING a,b INTO @a, @b; + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +EXPLAIN REPLACE INTO t1 VALUES (a=11, 'b11') RETURNING a,b INTO @a, @b; + +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10); + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +REPLACE t1 SET a=a+1 RETURNING a INTO @a; + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +ANALYZE REPLACE INTO t1 VALUES (a=11, 'b11') RETURNING a,b INTO @a, @b; + +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +EXPLAIN REPLACE INTO t1 VALUES (a=11, 'b11') RETURNING a,b INTO @a, @b; + +DROP TABLE t1; + +--echo # End of 13.1 tests diff --git a/mysql-test/main/update_returning.result b/mysql-test/main/update_returning.result new file mode 100644 index 0000000000000..03a7a91c253d8 --- /dev/null +++ b/mysql-test/main/update_returning.result @@ -0,0 +1,1003 @@ +# +# MDEV-39563 Implement UPDATE ... RETURNING ... INTO +# +# +# UPDATE..RETURNING..INTO without INTO +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'); +CREATE PROCEDURE p1() +BEGIN +UPDATE t1 SET a=a+1 WHERE a>=20 RETURNING a,b; +END; +$$ +CALL p1; +a b +21 b20 +CALL p1; +a b +22 b20 +CALL p1; +a b +23 b20 +DROP PROCEDURE p1; +DROP TABLE t1; +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10); +UPDATE t1 SET a=OLD_VALUE(a); +UPDATE t1 SET a=OLD_VALUE(a) RETURNING a INTO @a; +ERROR HY000: Incorrect usage of OLD_VALUE(..) and RETURNING..INTO +UPDATE t1 SET a=OLD_VALUE(a)+1 RETURNING a INTO @a; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '+1 RETURNING a INTO @a' at line 1 +UPDATE t1 SET a=1+OLD_VALUE(a) RETURNING a INTO @a; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '(a) RETURNING a INTO @a' at line 1 +UPDATE t1 SET a=a+1 RETURNING OLD_VALUE(a) INTO @a; +ERROR HY000: Incorrect usage of OLD_VALUE(..) and RETURNING..INTO +UPDATE t1 SET a=a+1 RETURNING OLD_VALUE(a)+1 INTO @a; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '+1 INTO @a' at line 1 +UPDATE t1 SET a=a+1 RETURNING 1+OLD_VALUE(a) INTO @a; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '(a) INTO @a' at line 1 +UPDATE (SELECT OLD_VALUE(a) AS x FROM t1) AS dt SET dt.x=1; +ERROR HY000: The target table dt of the UPDATE is not updatable +DROP TABLE t1; +# +# ANALYZE/EXPLAIN UPDATE .. RETURNING .. INTO ..; +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +ANALYZE UPDATE t1 t1 SET a=11, b='b11' RETURNING a,b INTO @a, @b; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +SELECT @a,@b; +@a @b +NULL NULL +EXPLAIN UPDATE t1 t1 SET a=11, b='b11' RETURNING a,b INTO @a, @b; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +SELECT @a,@b; +@a @b +NULL NULL +DROP TABLE t1; +# +# Too few and too many INTO variables +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +UPDATE t1 t1 SET a=11 RETURNING a,b INTO @a; +ERROR 21000: The used SELECT statements have a different number of columns +SELECT @a; +@a +NULL +UPDATE t1 t1 SET a=11 RETURNING a,b INTO @a, @b, @c; +ERROR 21000: The used SELECT statements have a different number of columns +SELECT @a,@b,@c; +@a @b @c +NULL NULL NULL +PREPARE stmt FROM 'UPDATE t1 t1 SET a=11 RETURNING a,b INTO @a'; +EXECUTE stmt; +ERROR 21000: The used SELECT statements have a different number of columns +SELECT @a; +@a +NULL +PREPARE stmt FROM 'UPDATE t1 t1 SET a=11 RETURNING a,b INTO @a, @b, @c'; +EXECUTE stmt; +ERROR 21000: The used SELECT statements have a different number of columns +SELECT @a,@b,@c; +@a @b @c +NULL NULL NULL +DROP TABLE t1; +# +# Multi-table UPDATEs +# +CREATE TABLE t1 (a INT); +CREATE TABLE t2 (a INT); +UPDATE t1,t2 SET t1.a=t1.a+1 WHERE t1.a=t2.b AND t1.a=10 RETURNING t1.a INTO @a; +ERROR 42000: This version of MariaDB doesn't yet support 'RETURNING for multi-table UPDATE' +UPDATE t1 SET a=a+1 WHERE a=(SELECT MAX(a) FROM t1) RETURNING a INTO @a; +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +DROP TABLE t1,t2; +# +# Stored functions with UPDATE..RETURNING +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +CREATE FUNCTION f1() RETURNS INT +BEGIN +UPDATE t1 SET a=a+1 RETURNING a; +RETURN 1; +END; +$$ +ERROR 0A000: Not allowed to return a result set from a function +CREATE FUNCTION f1() RETURNS INT +BEGIN +UPDATE t1 SET a=a+1 RETURNING a INTO @a; +RETURN 1; +END; +$$ +SELECT f1(); +f1() +1 +SELECT @a; +@a +11 +SELECT * FROM t1; +a b +11 b10 +SET @a=NULL; +DROP FUNCTION f1; +DROP TABLE t1; +# +# Empty table do not raise errors +# +CREATE TABLE t1 (a INT); +SET @a=123; +UPDATE t1 SET a=10 RETURNING a INTO @a; +SELECT @a; +@a +123 +SELECT * FROM t1; +a +SET @a=NULL; +DROP TABLE t1; +# +# Degenerated plans do not raise errors +# +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10); +SET @a=123; +UPDATE t1 SET a=11 WHERE 1=0 RETURNING a INTO @a; +SELECT @a; +@a +123 +SELECT * FROM t1; +a +10 +UPDATE t1 SET a=11 LIMIT 0 RETURNING a INTO @a; +SELECT @a; +@a +123 +SELECT * FROM t1; +a +10 +SET @a=NULL; +DROP TABLE t1; +# +# A row that matches WHERE but does not get changed +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL, + `b` varchar(10) DEFAULT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10,'b10'); +SET @a=123; +UPDATE t1 SET a=10 WHERE a<20 RETURNING a INTO @a; +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT @a; +@a +123 +SELECT * FROM t1; +a b +10 b10 +SET @a=NULL; +DROP TABLE t1; +# +# No rows updated for various sql_mode values +# +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10); +SET @a=123; +SET sql_mode=''; +UPDATE t1 SET a=10 RETURNING a INTO @a; +Warnings: +Warning 1329 No data - zero rows fetched, selected, or processed +SELECT @a; +@a +123 +SET sql_mode=DEFAULT; +UPDATE t1 SET a=10 RETURNING a INTO @a; +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT @a; +@a +123 +UPDATE IGNORE t1 SET a=10 RETURNING a INTO @a; +Warnings: +Warning 1329 No data - zero rows fetched, selected, or processed +SELECT @a; +@a +123 +SET @a=NULL; +DROP TABLE t1; +# +# Too many rows updated for various sql_mode values +# +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10),(20),(30); +SET sql_mode=''; +SET @a=123; +UPDATE t1 SET a=a+1 RETURNING a INTO @a; +ERROR 42000: Result consisted of more than one row +SELECT @a; +@a +11 +SELECT * FROM t1; +a +11 +20 +30 +SET sql_mode=DEFAULT; +SET @a=123; +UPDATE t1 SET a=a+1 RETURNING a INTO @a; +ERROR 42000: Result consisted of more than one row +SELECT @a; +@a +12 +SELECT * FROM t1; +a +12 +20 +30 +SET @a=123; +UPDATE IGNORE t1 SET a=a+1 RETURNING a INTO @a; +ERROR 42000: Result consisted of more than one row +SELECT @a; +@a +13 +SELECT * FROM t1; +a +13 +20 +30 +SET @a=NULL; +DROP TABLE t1; +# +# Too many rows + CONTINUE HANDLER +# +SET @a=100, @b=200; +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10),(20),(30); +CREATE PROCEDURE p1() +BEGIN +DECLARE CONTINUE HANDLER FOR SQLEXCEPTION +BEGIN +SET @b=@a; +END; +UPDATE t1 SET a=a+1 ORDER BY a RETURNING a INTO @a; +SELECT 'Still here' AS comment; +END; +$$ +CALL p1(); +comment +Still here +SELECT @a,@b; +@a @b +11 11 +SELECT * FROM t1; +a +11 +20 +30 +CALL p1(); +comment +Still here +SELECT @a,@b; +@a @b +12 12 +SELECT * FROM t1; +a +12 +20 +30 +CALL p1(); +comment +Still here +SELECT @a,@b; +@a @b +13 13 +SELECT * FROM t1; +a +13 +20 +30 +SET @a=NULL, @b=NULL; +DROP PROCEDURE p1; +DROP TABLE t1; +# +# Too many rows + EXIT HANDLER +# +SET @a=100, @b=200; +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10),(20),(30); +CREATE PROCEDURE p1() +BEGIN +DECLARE EXIT HANDLER FOR SQLEXCEPTION +BEGIN +SET @b=@a; +RESIGNAL; +END; +UPDATE t1 SET a=a+1 ORDER BY a RETURNING a INTO @a; +SELECT 'Still here' AS comment; +END; +$$ +CALL p1(); +ERROR 42000: Result consisted of more than one row +SELECT @a,@b; +@a @b +11 11 +SELECT * FROM t1; +a +11 +20 +30 +CALL p1(); +ERROR 42000: Result consisted of more than one row +SELECT @a,@b; +@a @b +12 12 +SELECT * FROM t1; +a +12 +20 +30 +CALL p1(); +ERROR 42000: Result consisted of more than one row +SELECT @a,@b; +@a @b +13 13 +SELECT * FROM t1; +a +13 +20 +30 +SET @a=NULL, @b=NULL; +DROP PROCEDURE p1; +DROP TABLE t1; +# +# Normal case +# +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10),(20),(30); +SET @a=123; +UPDATE t1 SET a=a+1 WHERE a>=30 RETURNING a INTO @a; +SELECT @a; +@a +31 +SELECT * FROM t1; +a +10 +20 +31 +SET @a=NULL; +DROP TABLE t1; +# +# Normal case with LIMIT +# +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10),(20),(30); +SET @a=123; +UPDATE t1 SET a=a+1 ORDER BY a LIMIT 1 RETURNING a INTO @a; +SELECT @a; +@a +11 +SELECT * FROM t1; +a +11 +20 +30 +SET @a=NULL; +DROP TABLE t1; +# +# UPDATE..RETURNING..INTO for user variables +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CREATE PROCEDURE p1(pa INT) +BEGIN +SET @va=NULL; +SET @vb=NULL; +UPDATE t1 SET a=a+1 WHERE a>=pa RETURNING a,b INTO @va, @vb; +SELECT @va, @vb; +SET @va=NULL; +SET @vb=NULL; +END; +$$ +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT @va, @vb; +@va @vb +NULL NULL +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT @va, @vb; +@va @vb +NULL NULL +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT @va, @vb; +@va @vb +NULL NULL +SELECT * FROM t1; +a b +10 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT @va, @vb; +@va @vb +11 b10 +SELECT * FROM t1; +a b +11 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT @va, @vb; +@va @vb +12 b10 +SELECT * FROM t1; +a b +12 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT @va, @vb; +@va @vb +13 b10 +SELECT * FROM t1; +a b +13 b10 +20 b20 +30 b30 +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CALL p1(30); +@va @vb +31 b30 +CALL p1(30); +@va @vb +32 b30 +CALL p1(30); +@va @vb +33 b30 +SELECT * FROM t1; +a b +10 b10 +20 b20 +33 b30 +DROP PROCEDURE p1; +DROP TABLE t1; +# +# UPDATE..RETURNING..INTO for SP variables +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CREATE PROCEDURE p1(pa INT) +BEGIN +DECLARE va INT; +DECLARE vb VARCHAR(10); +UPDATE t1 SET a=a+1 WHERE a>=pa RETURNING a,b INTO va,vb; +SELECT va, vb; +END; +$$ +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT * FROM t1; +a b +10 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +11 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +12 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +13 b10 +20 b20 +30 b30 +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CALL p1(30); +va vb +31 b30 +CALL p1(30); +va vb +32 b30 +CALL p1(30); +va vb +33 b30 +SELECT * FROM t1; +a b +10 b10 +20 b20 +33 b30 +DROP PROCEDURE p1; +DROP TABLE t1; +# +# UPDATE..RETURNING..INTO row_var.not_existing_field +# +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +CREATE PROCEDURE p1_row_explicit() +BEGIN +DECLARE r0 ROW(a INT, b VARCHAR(10)); +UPDATE t1 SET a=a+1 RETURNING a,b INTO r0.a,r0.b_not_existing; +SELECT r0.a, r0.b; +END; +$$ +ERROR HY000: Row variable 'r0' does not have a field 'b_not_existing' +DROP TABLE t1; +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +CREATE PROCEDURE p1_row_type_of_table() +BEGIN +DECLARE r0 ROW TYPE OF t1; +UPDATE t1 SET a=a+1 RETURNING a,b INTO r0.a,r0.b_not_existing; +SELECT r0.a, r0.b; +END; +$$ +CALL p1_row_type_of_table; +ERROR HY000: Row variable 'r0' does not have a field 'b_not_existing' +DROP PROCEDURE p1_row_type_of_table; +DROP TABLE t1; +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +CREATE PROCEDURE p1_row_type_of_cursor() +BEGIN +DECLARE c1 CURSOR FOR SELECT * FROM t1; +BEGIN +DECLARE r0 ROW TYPE OF c1; +UPDATE t1 SET a=a+1 RETURNING a,b INTO r0.a,r0.b_not_existing; +SELECT r0.a, r0.b; +END; +END; +$$ +CALL p1_row_type_of_cursor; +ERROR HY000: Row variable 'r0' does not have a field 'b_not_existing' +DROP PROCEDURE p1_row_type_of_cursor; +DROP TABLE t1; +# +# UPDATE..RETURNING..INTO for a ROW variable +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CREATE PROCEDURE p1(pa INT) +BEGIN +DECLARE c1 CURSOR FOR SELECT * FROM t1; +BEGIN +DECLARE vrec ROW(a INT, b VARCHAR(10)); +UPDATE t1 SET a=a+1 WHERE a>=pa RETURNING a,b INTO vrec; +SELECT vrec.a, vrec.b; +END; +END; +$$ +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT * FROM t1; +a b +10 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +11 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +12 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +13 b10 +20 b20 +30 b30 +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CALL p1(30); +vrec.a vrec.b +31 b30 +CALL p1(30); +vrec.a vrec.b +32 b30 +CALL p1(30); +vrec.a vrec.b +33 b30 +SELECT * FROM t1; +a b +10 b10 +20 b20 +33 b30 +DROP PROCEDURE p1; +DROP TABLE t1; +# +# UPDATE..RETURNING..INTO for a ROW variable fields +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CREATE PROCEDURE p1(pa INT) +BEGIN +DECLARE c1 CURSOR FOR SELECT * FROM t1; +BEGIN +DECLARE vrec ROW(a INT, b VARCHAR(10)); +UPDATE t1 SET a=a+1 WHERE a>=pa RETURNING a,b INTO vrec.a, vrec.b; +SELECT vrec.a, vrec.b; +END; +END; +$$ +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT * FROM t1; +a b +10 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +11 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +12 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +13 b10 +20 b20 +30 b30 +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CALL p1(30); +vrec.a vrec.b +31 b30 +CALL p1(30); +vrec.a vrec.b +32 b30 +CALL p1(30); +vrec.a vrec.b +33 b30 +SELECT * FROM t1; +a b +10 b10 +20 b20 +33 b30 +DROP PROCEDURE p1; +DROP TABLE t1; +# +# UPDATE..RETURNING..INTO for a `ROW TYPE OF table1` variable +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CREATE PROCEDURE p1(pa INT) +BEGIN +DECLARE c1 CURSOR FOR SELECT * FROM t1; +BEGIN +DECLARE vrec ROW TYPE OF t1; +UPDATE t1 SET a=a+1 WHERE a>=pa RETURNING a,b INTO vrec; +SELECT vrec.a, vrec.b; +END; +END; +$$ +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT * FROM t1; +a b +10 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +11 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +12 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +13 b10 +20 b20 +30 b30 +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CALL p1(30); +vrec.a vrec.b +31 b30 +CALL p1(30); +vrec.a vrec.b +32 b30 +CALL p1(30); +vrec.a vrec.b +33 b30 +SELECT * FROM t1; +a b +10 b10 +20 b20 +33 b30 +DROP PROCEDURE p1; +DROP TABLE t1; +# +# UPDATE..RETURNING..INTO for a `ROW TYPE OF table1` variable fields +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CREATE PROCEDURE p1(pa INT) +BEGIN +DECLARE c1 CURSOR FOR SELECT * FROM t1; +BEGIN +DECLARE vrec ROW TYPE OF t1; +UPDATE t1 SET a=a+1 WHERE a>=pa RETURNING a,b INTO vrec.a, vrec.b; +SELECT vrec.a, vrec.b; +END; +END; +$$ +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT * FROM t1; +a b +10 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +11 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +12 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +13 b10 +20 b20 +30 b30 +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CALL p1(30); +vrec.a vrec.b +31 b30 +CALL p1(30); +vrec.a vrec.b +32 b30 +CALL p1(30); +vrec.a vrec.b +33 b30 +SELECT * FROM t1; +a b +10 b10 +20 b20 +33 b30 +DROP PROCEDURE p1; +DROP TABLE t1; +# +# UPDATE..RETURNING..INTO for a `ROW TYPE OF cursor1` variable +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CREATE PROCEDURE p1(pa INT) +BEGIN +DECLARE c1 CURSOR FOR SELECT * FROM t1; +BEGIN +DECLARE vrec ROW TYPE OF c1; +UPDATE t1 SET a=a+1 WHERE a>=pa RETURNING a,b INTO vrec; +SELECT vrec.a, vrec.b; +END; +END; +$$ +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT * FROM t1; +a b +10 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +11 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +12 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +13 b10 +20 b20 +30 b30 +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CALL p1(30); +vrec.a vrec.b +31 b30 +CALL p1(30); +vrec.a vrec.b +32 b30 +CALL p1(30); +vrec.a vrec.b +33 b30 +SELECT * FROM t1; +a b +10 b10 +20 b20 +33 b30 +DROP PROCEDURE p1; +DROP TABLE t1; +# +# UPDATE..RETURNING..INTO for a `ROW TYPE OF cursor1` variable fields +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CREATE PROCEDURE p1(pa INT) +BEGIN +DECLARE c1 CURSOR FOR SELECT * FROM t1; +BEGIN +DECLARE vrec ROW TYPE OF c1; +UPDATE t1 SET a=a+1 WHERE a>=pa RETURNING a,b INTO vrec.a, vrec.b; +SELECT vrec.a, vrec.b; +END; +END; +$$ +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +CALL p1(100); +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT * FROM t1; +a b +10 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +11 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +12 b10 +20 b20 +30 b30 +CALL p1(0); +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a b +13 b10 +20 b20 +30 b30 +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CALL p1(30); +vrec.a vrec.b +31 b30 +CALL p1(30); +vrec.a vrec.b +32 b30 +CALL p1(30); +vrec.a vrec.b +33 b30 +SELECT * FROM t1; +a b +10 b10 +20 b20 +33 b30 +DROP PROCEDURE p1; +DROP TABLE t1; +# End of 13.1 tests diff --git a/mysql-test/main/update_returning.test b/mysql-test/main/update_returning.test new file mode 100644 index 0000000000000..d6e438ccfb9e9 --- /dev/null +++ b/mysql-test/main/update_returning.test @@ -0,0 +1,367 @@ +--echo # +--echo # MDEV-39563 Implement UPDATE ... RETURNING ... INTO +--echo # + +--echo # +--echo # UPDATE..RETURNING..INTO without INTO +--echo # + +# First of all check that the patch for MDEV-39563 did not break +# anything in the usual UPDATE..RETURNING, without INTO. +# The second execution is important to test. + +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'); +DELIMITER $$; +CREATE PROCEDURE p1() +BEGIN + UPDATE t1 SET a=a+1 WHERE a>=20 RETURNING a,b; +END; +$$ +DELIMITER ;$$ +CALL p1; +CALL p1; +CALL p1; +DROP PROCEDURE p1; +DROP TABLE t1; + + +# +# Make sure that already `lex->m_sql_cmd != nullptr` when +# Sql_cmd_update::set_with_old_value_items() is called +# while parsing OLD_VALUE(). +# +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10); + +UPDATE t1 SET a=OLD_VALUE(a); +--error ER_WRONG_USAGE +UPDATE t1 SET a=OLD_VALUE(a) RETURNING a INTO @a; +--error ER_PARSE_ERROR +UPDATE t1 SET a=OLD_VALUE(a)+1 RETURNING a INTO @a; +--error ER_PARSE_ERROR +UPDATE t1 SET a=1+OLD_VALUE(a) RETURNING a INTO @a; + +--error ER_WRONG_USAGE +UPDATE t1 SET a=a+1 RETURNING OLD_VALUE(a) INTO @a; +--error ER_PARSE_ERROR +UPDATE t1 SET a=a+1 RETURNING OLD_VALUE(a)+1 INTO @a; +--error ER_PARSE_ERROR +UPDATE t1 SET a=a+1 RETURNING 1+OLD_VALUE(a) INTO @a; + +--error ER_NON_UPDATABLE_TABLE +UPDATE (SELECT OLD_VALUE(a) AS x FROM t1) AS dt SET dt.x=1; + +DROP TABLE t1; + + +--echo # +--echo # ANALYZE/EXPLAIN UPDATE .. RETURNING .. INTO ..; +--echo # + +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +ANALYZE UPDATE t1 t1 SET a=11, b='b11' RETURNING a,b INTO @a, @b; +SELECT @a,@b; +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +EXPLAIN UPDATE t1 t1 SET a=11, b='b11' RETURNING a,b INTO @a, @b; +SELECT @a,@b; +DROP TABLE t1; + + +--echo # +--echo # Too few and too many INTO variables +--echo # + +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); + +--error ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT +UPDATE t1 t1 SET a=11 RETURNING a,b INTO @a; +SELECT @a; +--error ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT +UPDATE t1 t1 SET a=11 RETURNING a,b INTO @a, @b, @c; +SELECT @a,@b,@c; + +# The for a wrong number of rows is done in select_dumpvar::prepare +# (when ROW variables are already resolved) +# So the error gets raised at the execution TIME + +PREPARE stmt FROM 'UPDATE t1 t1 SET a=11 RETURNING a,b INTO @a'; +--error ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT +EXECUTE stmt; +SELECT @a; +PREPARE stmt FROM 'UPDATE t1 t1 SET a=11 RETURNING a,b INTO @a, @b, @c'; +--error ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT +EXECUTE stmt; +SELECT @a,@b,@c; + +DROP TABLE t1; + + +--echo # +--echo # Multi-table UPDATEs +--echo # + +CREATE TABLE t1 (a INT); +CREATE TABLE t2 (a INT); +--error ER_NOT_SUPPORTED_YET +UPDATE t1,t2 SET t1.a=t1.a+1 WHERE t1.a=t2.b AND t1.a=10 RETURNING t1.a INTO @a; +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +UPDATE t1 SET a=a+1 WHERE a=(SELECT MAX(a) FROM t1) RETURNING a INTO @a; +DROP TABLE t1,t2; + + +--echo # +--echo # Stored functions with UPDATE..RETURNING +--echo # + +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +DELIMITER $$; +--error ER_SP_NO_RETSET +CREATE FUNCTION f1() RETURNS INT +BEGIN + UPDATE t1 SET a=a+1 RETURNING a; + RETURN 1; +END; +$$ +DELIMITER ;$$ +DELIMITER $$; +CREATE FUNCTION f1() RETURNS INT +BEGIN + UPDATE t1 SET a=a+1 RETURNING a INTO @a; + RETURN 1; +END; +$$ +DELIMITER ;$$ +SELECT f1(); +SELECT @a; +SELECT * FROM t1; +SET @a=NULL; +DROP FUNCTION f1; +DROP TABLE t1; + + +--echo # +--echo # Empty table do not raise errors +--echo # + +CREATE TABLE t1 (a INT); +SET @a=123; +UPDATE t1 SET a=10 RETURNING a INTO @a; +SELECT @a; +SELECT * FROM t1; +SET @a=NULL; +DROP TABLE t1; + + +--source update_returning_engines.inc + + +--echo # +--echo # UPDATE..RETURNING..INTO for user variables +--echo # + +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +DELIMITER $$; +CREATE PROCEDURE p1(pa INT) +BEGIN + SET @va=NULL; + SET @vb=NULL; + UPDATE t1 SET a=a+1 WHERE a>=pa RETURNING a,b INTO @va, @vb; + SELECT @va, @vb; + SET @va=NULL; + SET @vb=NULL; +END; +$$ +DELIMITER ;$$ + +--error ER_SP_FETCH_NO_DATA +CALL p1(100); +SELECT @va, @vb; +--error ER_SP_FETCH_NO_DATA +CALL p1(100); +SELECT @va, @vb; +--error ER_SP_FETCH_NO_DATA +CALL p1(100); +SELECT @va, @vb; +SELECT * FROM t1; + +--error ER_TOO_MANY_ROWS +CALL p1(0); +SELECT @va, @vb; +SELECT * FROM t1; +--error ER_TOO_MANY_ROWS +CALL p1(0); +SELECT @va, @vb; +SELECT * FROM t1; +--error ER_TOO_MANY_ROWS +CALL p1(0); +SELECT @va, @vb; +SELECT * FROM t1; + +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CALL p1(30); +CALL p1(30); +CALL p1(30); +SELECT * FROM t1; + +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # +--echo # UPDATE..RETURNING..INTO for SP variables +--echo # + +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +DELIMITER $$; +CREATE PROCEDURE p1(pa INT) +BEGIN + DECLARE va INT; + DECLARE vb VARCHAR(10); + UPDATE t1 SET a=a+1 WHERE a>=pa RETURNING a,b INTO va,vb; + SELECT va, vb; +END; +$$ +DELIMITER ;$$ + +--error ER_SP_FETCH_NO_DATA +CALL p1(100); +--error ER_SP_FETCH_NO_DATA +CALL p1(100); +--error ER_SP_FETCH_NO_DATA +CALL p1(100); +SELECT * FROM t1; + +--error ER_TOO_MANY_ROWS +CALL p1(0); +SELECT * FROM t1; +--error ER_TOO_MANY_ROWS +CALL p1(0); +SELECT * FROM t1; +--error ER_TOO_MANY_ROWS +CALL p1(0); +SELECT * FROM t1; + +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); +CALL p1(30); +CALL p1(30); +CALL p1(30); +SELECT * FROM t1; + +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # +--echo # UPDATE..RETURNING..INTO row_var.not_existing_field +--echo # + +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +DELIMITER $$; +--error ER_ROW_VARIABLE_DOES_NOT_HAVE_FIELD +CREATE PROCEDURE p1_row_explicit() +BEGIN + DECLARE r0 ROW(a INT, b VARCHAR(10)); + UPDATE t1 SET a=a+1 RETURNING a,b INTO r0.a,r0.b_not_existing; + SELECT r0.a, r0.b; +END; +$$ +DELIMITER ;$$ +DROP TABLE t1; + + +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +DELIMITER $$; +CREATE PROCEDURE p1_row_type_of_table() +BEGIN + DECLARE r0 ROW TYPE OF t1; + UPDATE t1 SET a=a+1 RETURNING a,b INTO r0.a,r0.b_not_existing; + SELECT r0.a, r0.b; +END; +$$ +DELIMITER ;$$ +--error ER_ROW_VARIABLE_DOES_NOT_HAVE_FIELD +CALL p1_row_type_of_table; +DROP PROCEDURE p1_row_type_of_table; +DROP TABLE t1; + + +CREATE TABLE t1 (a INT,b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +DELIMITER $$; +CREATE PROCEDURE p1_row_type_of_cursor() +BEGIN + DECLARE c1 CURSOR FOR SELECT * FROM t1; + BEGIN + DECLARE r0 ROW TYPE OF c1; + UPDATE t1 SET a=a+1 RETURNING a,b INTO r0.a,r0.b_not_existing; + SELECT r0.a, r0.b; + END; +END; +$$ +DELIMITER ;$$ +--error ER_ROW_VARIABLE_DOES_NOT_HAVE_FIELD +CALL p1_row_type_of_cursor; +DROP PROCEDURE p1_row_type_of_cursor; +DROP TABLE t1; + + +--echo # +--echo # UPDATE..RETURNING..INTO for a ROW variable +--echo # + +--let $TYPE= ROW(a INT, b VARCHAR(10)) +--let $INTO= vrec +--source update_returning_into_row_var.inc + +--echo # +--echo # UPDATE..RETURNING..INTO for a ROW variable fields +--echo # + +--let $TYPE= ROW(a INT, b VARCHAR(10)) +--let $INTO= vrec.a, vrec.b +--source update_returning_into_row_var.inc + +--echo # +--echo # UPDATE..RETURNING..INTO for a `ROW TYPE OF table1` variable +--echo # + +--let $TYPE= ROW TYPE OF t1 +--let $INTO= vrec +--source update_returning_into_row_var.inc + +--echo # +--echo # UPDATE..RETURNING..INTO for a `ROW TYPE OF table1` variable fields +--echo # + +--let $TYPE= ROW TYPE OF t1 +--let $INTO= vrec.a, vrec.b +--source update_returning_into_row_var.inc + +--echo # +--echo # UPDATE..RETURNING..INTO for a `ROW TYPE OF cursor1` variable +--echo # + +--let $TYPE= ROW TYPE OF c1 +--let $INTO= vrec +--source update_returning_into_row_var.inc + +--echo # +--echo # UPDATE..RETURNING..INTO for a `ROW TYPE OF cursor1` variable fields +--echo # + +--let $TYPE= ROW TYPE OF c1 +--let $INTO= vrec.a, vrec.b +--source update_returning_into_row_var.inc + +--echo # End of 13.1 tests diff --git a/mysql-test/main/update_returning_engines.inc b/mysql-test/main/update_returning_engines.inc new file mode 100644 index 0000000000000..474e588355aad --- /dev/null +++ b/mysql-test/main/update_returning_engines.inc @@ -0,0 +1,195 @@ +--echo # +--echo # Degenerated plans do not raise errors +--echo # + +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +INSERT INTO t1 VALUES (10); +SET @a=123; +UPDATE t1 SET a=11 WHERE 1=0 RETURNING a INTO @a; +SELECT @a; +SELECT * FROM t1; +UPDATE t1 SET a=11 LIMIT 0 RETURNING a INTO @a; +SELECT @a; +SELECT * FROM t1; +SET @a=NULL; +DROP TABLE t1; + + +--echo # +--echo # A row that matches WHERE but does not get changed +--echo # + +CREATE TABLE t1 (a INT, b VARCHAR(10)); +SHOW CREATE TABLE t1; +INSERT INTO t1 VALUES (10,'b10'); +SET @a=123; +--error ER_SP_FETCH_NO_DATA +UPDATE t1 SET a=10 WHERE a<20 RETURNING a INTO @a; +SELECT @a; +SELECT * FROM t1; +SET @a=NULL; +DROP TABLE t1; + + +--echo # +--echo # No rows updated for various sql_mode values +--echo # + +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +INSERT INTO t1 VALUES (10); +SET @a=123; +SET sql_mode=''; +UPDATE t1 SET a=10 RETURNING a INTO @a; +SELECT @a; +SET sql_mode=DEFAULT; +--error ER_SP_FETCH_NO_DATA +UPDATE t1 SET a=10 RETURNING a INTO @a; +SELECT @a; +UPDATE IGNORE t1 SET a=10 RETURNING a INTO @a; +SELECT @a; +SET @a=NULL; +DROP TABLE t1; + + +--echo # +--echo # Too many rows updated for various sql_mode values +--echo # + +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +INSERT INTO t1 VALUES (10),(20),(30); + +SET sql_mode=''; +SET @a=123; +--error ER_TOO_MANY_ROWS +UPDATE t1 SET a=a+1 RETURNING a INTO @a; +SELECT @a; +SELECT * FROM t1; + +SET sql_mode=DEFAULT; +SET @a=123; +--error ER_TOO_MANY_ROWS +UPDATE t1 SET a=a+1 RETURNING a INTO @a; +SELECT @a; +SELECT * FROM t1; + +SET @a=123; +--error ER_TOO_MANY_ROWS +UPDATE IGNORE t1 SET a=a+1 RETURNING a INTO @a; +SELECT @a; +SELECT * FROM t1; + +SET @a=NULL; +DROP TABLE t1; + + +--echo # +--echo # Too many rows + CONTINUE HANDLER +--echo # + +SET @a=100, @b=200; +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +INSERT INTO t1 VALUES (10),(20),(30); +DELIMITER $$; +CREATE PROCEDURE p1() +BEGIN + DECLARE CONTINUE HANDLER FOR SQLEXCEPTION + BEGIN + SET @b=@a; + END; + UPDATE t1 SET a=a+1 ORDER BY a RETURNING a INTO @a; + SELECT 'Still here' AS comment; +END; +$$ +DELIMITER ;$$ + +CALL p1(); +SELECT @a,@b; +SELECT * FROM t1; + +CALL p1(); +SELECT @a,@b; +SELECT * FROM t1; + +CALL p1(); +SELECT @a,@b; +SELECT * FROM t1; + +SET @a=NULL, @b=NULL; +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # +--echo # Too many rows + EXIT HANDLER +--echo # + +SET @a=100, @b=200; +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +INSERT INTO t1 VALUES (10),(20),(30); +DELIMITER $$; +CREATE PROCEDURE p1() +BEGIN + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + SET @b=@a; + RESIGNAL; + END; + UPDATE t1 SET a=a+1 ORDER BY a RETURNING a INTO @a; + SELECT 'Still here' AS comment; +END; +$$ +DELIMITER ;$$ + +--error ER_TOO_MANY_ROWS +CALL p1(); +SELECT @a,@b; +SELECT * FROM t1; + +--error ER_TOO_MANY_ROWS +CALL p1(); +SELECT @a,@b; +SELECT * FROM t1; + +--error ER_TOO_MANY_ROWS +CALL p1(); +SELECT @a,@b; +SELECT * FROM t1; + +SET @a=NULL, @b=NULL; +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # +--echo # Normal case +--echo # + +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +INSERT INTO t1 VALUES (10),(20),(30); +SET @a=123; +UPDATE t1 SET a=a+1 WHERE a>=30 RETURNING a INTO @a; +SELECT @a; +SELECT * FROM t1; +SET @a=NULL; +DROP TABLE t1; + + +--echo # +--echo # Normal case with LIMIT +--echo # + +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +INSERT INTO t1 VALUES (10),(20),(30); +SET @a=123; +UPDATE t1 SET a=a+1 ORDER BY a LIMIT 1 RETURNING a INTO @a; +SELECT @a; +SELECT * FROM t1; +SET @a=NULL; +DROP TABLE t1; diff --git a/mysql-test/main/update_returning_innodb.result b/mysql-test/main/update_returning_innodb.result new file mode 100644 index 0000000000000..e1613f1136254 --- /dev/null +++ b/mysql-test/main/update_returning_innodb.result @@ -0,0 +1,322 @@ +# +# MDEV-39563 Implement UPDATE ... RETURNING ... INTO +# +SET default_storage_engine=InnoDB; +# +# Degenerated plans do not raise errors +# +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10); +SET @a=123; +UPDATE t1 SET a=11 WHERE 1=0 RETURNING a INTO @a; +SELECT @a; +@a +123 +SELECT * FROM t1; +a +10 +UPDATE t1 SET a=11 LIMIT 0 RETURNING a INTO @a; +SELECT @a; +@a +123 +SELECT * FROM t1; +a +10 +SET @a=NULL; +DROP TABLE t1; +# +# A row that matches WHERE but does not get changed +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL, + `b` varchar(10) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10,'b10'); +SET @a=123; +UPDATE t1 SET a=10 WHERE a<20 RETURNING a INTO @a; +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT @a; +@a +123 +SELECT * FROM t1; +a b +10 b10 +SET @a=NULL; +DROP TABLE t1; +# +# No rows updated for various sql_mode values +# +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10); +SET @a=123; +SET sql_mode=''; +UPDATE t1 SET a=10 RETURNING a INTO @a; +Warnings: +Warning 1329 No data - zero rows fetched, selected, or processed +SELECT @a; +@a +123 +SET sql_mode=DEFAULT; +UPDATE t1 SET a=10 RETURNING a INTO @a; +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT @a; +@a +123 +UPDATE IGNORE t1 SET a=10 RETURNING a INTO @a; +Warnings: +Warning 1329 No data - zero rows fetched, selected, or processed +SELECT @a; +@a +123 +SET @a=NULL; +DROP TABLE t1; +# +# Too many rows updated for various sql_mode values +# +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10),(20),(30); +SET sql_mode=''; +SET @a=123; +UPDATE t1 SET a=a+1 RETURNING a INTO @a; +ERROR 42000: Result consisted of more than one row +SELECT @a; +@a +11 +SELECT * FROM t1; +a +10 +20 +30 +SET sql_mode=DEFAULT; +SET @a=123; +UPDATE t1 SET a=a+1 RETURNING a INTO @a; +ERROR 42000: Result consisted of more than one row +SELECT @a; +@a +11 +SELECT * FROM t1; +a +10 +20 +30 +SET @a=123; +UPDATE IGNORE t1 SET a=a+1 RETURNING a INTO @a; +ERROR 42000: Result consisted of more than one row +SELECT @a; +@a +11 +SELECT * FROM t1; +a +10 +20 +30 +SET @a=NULL; +DROP TABLE t1; +# +# Too many rows + CONTINUE HANDLER +# +SET @a=100, @b=200; +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10),(20),(30); +CREATE PROCEDURE p1() +BEGIN +DECLARE CONTINUE HANDLER FOR SQLEXCEPTION +BEGIN +SET @b=@a; +END; +UPDATE t1 SET a=a+1 ORDER BY a RETURNING a INTO @a; +SELECT 'Still here' AS comment; +END; +$$ +CALL p1(); +comment +Still here +SELECT @a,@b; +@a @b +11 11 +SELECT * FROM t1; +a +10 +20 +30 +CALL p1(); +comment +Still here +SELECT @a,@b; +@a @b +11 11 +SELECT * FROM t1; +a +10 +20 +30 +CALL p1(); +comment +Still here +SELECT @a,@b; +@a @b +11 11 +SELECT * FROM t1; +a +10 +20 +30 +SET @a=NULL, @b=NULL; +DROP PROCEDURE p1; +DROP TABLE t1; +# +# Too many rows + EXIT HANDLER +# +SET @a=100, @b=200; +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10),(20),(30); +CREATE PROCEDURE p1() +BEGIN +DECLARE EXIT HANDLER FOR SQLEXCEPTION +BEGIN +SET @b=@a; +RESIGNAL; +END; +UPDATE t1 SET a=a+1 ORDER BY a RETURNING a INTO @a; +SELECT 'Still here' AS comment; +END; +$$ +CALL p1(); +ERROR 42000: Result consisted of more than one row +SELECT @a,@b; +@a @b +11 11 +SELECT * FROM t1; +a +10 +20 +30 +CALL p1(); +ERROR 42000: Result consisted of more than one row +SELECT @a,@b; +@a @b +11 11 +SELECT * FROM t1; +a +10 +20 +30 +CALL p1(); +ERROR 42000: Result consisted of more than one row +SELECT @a,@b; +@a @b +11 11 +SELECT * FROM t1; +a +10 +20 +30 +SET @a=NULL, @b=NULL; +DROP PROCEDURE p1; +DROP TABLE t1; +# +# Normal case +# +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10),(20),(30); +SET @a=123; +UPDATE t1 SET a=a+1 WHERE a>=30 RETURNING a INTO @a; +SELECT @a; +@a +31 +SELECT * FROM t1; +a +10 +20 +31 +SET @a=NULL; +DROP TABLE t1; +# +# Normal case with LIMIT +# +CREATE TABLE t1 (a INT); +SHOW CREATE TABLE t1; +Table Create Table +t1 CREATE TABLE `t1` ( + `a` int(11) DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci +INSERT INTO t1 VALUES (10),(20),(30); +SET @a=123; +UPDATE t1 SET a=a+1 ORDER BY a LIMIT 1 RETURNING a INTO @a; +SELECT @a; +@a +11 +SELECT * FROM t1; +a +11 +20 +30 +SET @a=NULL; +DROP TABLE t1; +SET default_storage_engine=DEFAULT; +# +# Empty table +# +CREATE TABLE t1 (a INT) ENGINE=InnoDB; +SET @a=123; +UPDATE t1 SET a=a+1 WHERE a=0 RETURNING a INTO @a; +ERROR 02000: No data - zero rows fetched, selected, or processed +SELECT @a; +@a +123 +SET @a=NULL; +DROP TABLE t1; +# +# Too many rows inside a transaction +# +CREATE TABLE t1 (a INT) ENGINE=InnoDB; +INSERT INTO t1 VALUES (10),(20),(30); +START TRANSACTION; +SET @a=123; +UPDATE t1 SET a=a+1 RETURNING a INTO @a; +ERROR 42000: Result consisted of more than one row +SELECT @a; +@a +11 +SELECT * FROM t1; +a +10 +20 +30 +SET @a=NULL; +COMMIT; +DROP TABLE t1; +# End of 13.1 tests diff --git a/mysql-test/main/update_returning_innodb.test b/mysql-test/main/update_returning_innodb.test new file mode 100644 index 0000000000000..9f138f6a2ad1e --- /dev/null +++ b/mysql-test/main/update_returning_innodb.test @@ -0,0 +1,43 @@ +--source include/have_innodb.inc + +--echo # +--echo # MDEV-39563 Implement UPDATE ... RETURNING ... INTO +--echo # + + +SET default_storage_engine=InnoDB; +--source update_returning_engines.inc +SET default_storage_engine=DEFAULT; + + +--echo # +--echo # Empty table +--echo # + +CREATE TABLE t1 (a INT) ENGINE=InnoDB; +SET @a=123; +--error ER_SP_FETCH_NO_DATA +UPDATE t1 SET a=a+1 WHERE a=0 RETURNING a INTO @a; +SELECT @a; +SET @a=NULL; +DROP TABLE t1; + + +--echo # +--echo # Too many rows inside a transaction +--echo # + +CREATE TABLE t1 (a INT) ENGINE=InnoDB; +INSERT INTO t1 VALUES (10),(20),(30); +START TRANSACTION; +SET @a=123; +--error ER_TOO_MANY_ROWS +UPDATE t1 SET a=a+1 RETURNING a INTO @a; +SELECT @a; +SELECT * FROM t1; +SET @a=NULL; +COMMIT; +DROP TABLE t1; + + +--echo # End of 13.1 tests diff --git a/mysql-test/main/update_returning_into_row_var.inc b/mysql-test/main/update_returning_into_row_var.inc new file mode 100644 index 0000000000000..1fb7dfd784f5e --- /dev/null +++ b/mysql-test/main/update_returning_into_row_var.inc @@ -0,0 +1,47 @@ +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); + +DELIMITER $$; +eval CREATE PROCEDURE p1(pa INT) +BEGIN + DECLARE c1 CURSOR FOR SELECT * FROM t1; + BEGIN + DECLARE vrec $TYPE; + UPDATE t1 SET a=a+1 WHERE a>=pa RETURNING a,b INTO $INTO; + SELECT vrec.a, vrec.b; + END; +END; +$$ +DELIMITER ;$$ + +--error ER_SP_FETCH_NO_DATA +CALL p1(100); +--error ER_SP_FETCH_NO_DATA +CALL p1(100); +--error ER_SP_FETCH_NO_DATA +CALL p1(100); +SELECT * FROM t1; + +--error ER_TOO_MANY_ROWS +CALL p1(0); +SELECT * FROM t1; +--error ER_TOO_MANY_ROWS +CALL p1(0); +SELECT * FROM t1; +--error ER_TOO_MANY_ROWS +CALL p1(0); +SELECT * FROM t1; + +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'),(30,'b30'); + +CALL p1(30); +CALL p1(30); +CALL p1(30); +SELECT * FROM t1; + +DROP PROCEDURE p1; +DROP TABLE t1; + +--let $TYPE= +--let $INTO= diff --git a/mysql-test/suite/binlog/r/binlog_update_returning_into_mixed.result b/mysql-test/suite/binlog/r/binlog_update_returning_into_mixed.result new file mode 100644 index 0000000000000..108544efe9deb --- /dev/null +++ b/mysql-test/suite/binlog/r/binlog_update_returning_into_mixed.result @@ -0,0 +1,30 @@ +# +# MDEV-39563 Implement UPDATE ... RETURNING ... INTO +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +UPDATE t1 SET a=a+1 RETURNING a INTO @a; +PREPARE stmt FROM 'UPDATE t1 SET a=a+1 RETURNING a INTO @a1'; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; +DROP TABLE t1; +include/show_binlog_events.inc +Log_name Pos Event_type Server_id End_log_pos Info +master-bin.000001 # Gtid # # GTID #-#-# +master-bin.000001 # Query # # use `test`; CREATE TABLE t1 (a INT, b VARCHAR(10)) +master-bin.000001 # Gtid # # BEGIN GTID #-#-# +master-bin.000001 # Query # # use `test`; INSERT INTO t1 VALUES (10,'b10') +master-bin.000001 # Query # # COMMIT +master-bin.000001 # Gtid # # BEGIN GTID #-#-# +master-bin.000001 # Annotate_rows # # UPDATE t1 SET a=a+1 RETURNING a INTO @a +master-bin.000001 # Table_map # # table_id: # (test.t1) +master-bin.000001 # Update_rows_v1 # # table_id: # flags: STMT_END_F +master-bin.000001 # Query # # COMMIT +master-bin.000001 # Gtid # # BEGIN GTID #-#-# +master-bin.000001 # Annotate_rows # # UPDATE t1 SET a=a+1 RETURNING a INTO @a1 +master-bin.000001 # Table_map # # table_id: # (test.t1) +master-bin.000001 # Update_rows_v1 # # table_id: # flags: STMT_END_F +master-bin.000001 # Query # # COMMIT +master-bin.000001 # Gtid # # GTID #-#-# +master-bin.000001 # Query # # use `test`; DROP TABLE `t1` /* generated by server */ +# End of 13.1 tests diff --git a/mysql-test/suite/binlog/r/binlog_update_returning_into_row.result b/mysql-test/suite/binlog/r/binlog_update_returning_into_row.result new file mode 100644 index 0000000000000..d35bbd4b5381b --- /dev/null +++ b/mysql-test/suite/binlog/r/binlog_update_returning_into_row.result @@ -0,0 +1,32 @@ +# +# MDEV-39563 Implement UPDATE ... RETURNING ... INTO +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +UPDATE t1 SET a=a+1 RETURNING a INTO @a; +PREPARE stmt FROM 'UPDATE t1 SET a=a+1 RETURNING a INTO @a1'; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; +DROP TABLE t1; +include/show_binlog_events.inc +Log_name Pos Event_type Server_id End_log_pos Info +master-bin.000001 # Gtid # # GTID #-#-# +master-bin.000001 # Query # # use `test`; CREATE TABLE t1 (a INT, b VARCHAR(10)) +master-bin.000001 # Gtid # # BEGIN GTID #-#-# +master-bin.000001 # Annotate_rows # # INSERT INTO t1 VALUES (10,'b10') +master-bin.000001 # Table_map # # table_id: # (test.t1) +master-bin.000001 # Write_rows_v1 # # table_id: # flags: STMT_END_F +master-bin.000001 # Query # # COMMIT +master-bin.000001 # Gtid # # BEGIN GTID #-#-# +master-bin.000001 # Annotate_rows # # UPDATE t1 SET a=a+1 RETURNING a INTO @a +master-bin.000001 # Table_map # # table_id: # (test.t1) +master-bin.000001 # Update_rows_v1 # # table_id: # flags: STMT_END_F +master-bin.000001 # Query # # COMMIT +master-bin.000001 # Gtid # # BEGIN GTID #-#-# +master-bin.000001 # Annotate_rows # # UPDATE t1 SET a=a+1 RETURNING a INTO @a1 +master-bin.000001 # Table_map # # table_id: # (test.t1) +master-bin.000001 # Update_rows_v1 # # table_id: # flags: STMT_END_F +master-bin.000001 # Query # # COMMIT +master-bin.000001 # Gtid # # GTID #-#-# +master-bin.000001 # Query # # use `test`; DROP TABLE `t1` /* generated by server */ +# End of 13.1 tests diff --git a/mysql-test/suite/binlog/r/binlog_update_returning_into_stmt.result b/mysql-test/suite/binlog/r/binlog_update_returning_into_stmt.result new file mode 100644 index 0000000000000..d857bb6afa95f --- /dev/null +++ b/mysql-test/suite/binlog/r/binlog_update_returning_into_stmt.result @@ -0,0 +1,22 @@ +# +# MDEV-39563 Implement UPDATE ... RETURNING ... INTO +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +UPDATE t1 SET a=a+1 RETURNING a INTO @a; +ERROR HY000: Incorrect usage of --binlog-format=statement and RETURNING INTO +PREPARE stmt FROM 'UPDATE t1 SET a=a+1 RETURNING a INTO @a1'; +EXECUTE stmt; +ERROR HY000: Incorrect usage of --binlog-format=statement and RETURNING INTO +DEALLOCATE PREPARE stmt; +DROP TABLE t1; +include/show_binlog_events.inc +Log_name Pos Event_type Server_id End_log_pos Info +master-bin.000001 # Gtid # # GTID #-#-# +master-bin.000001 # Query # # use `test`; CREATE TABLE t1 (a INT, b VARCHAR(10)) +master-bin.000001 # Gtid # # BEGIN GTID #-#-# +master-bin.000001 # Query # # use `test`; INSERT INTO t1 VALUES (10,'b10') +master-bin.000001 # Query # # COMMIT +master-bin.000001 # Gtid # # GTID #-#-# +master-bin.000001 # Query # # use `test`; DROP TABLE `t1` /* generated by server */ +# End of 13.1 tests diff --git a/mysql-test/suite/binlog/t/binlog_update_returning_into_mixed.test b/mysql-test/suite/binlog/t/binlog_update_returning_into_mixed.test new file mode 100644 index 0000000000000..8867c6135e816 --- /dev/null +++ b/mysql-test/suite/binlog/t/binlog_update_returning_into_mixed.test @@ -0,0 +1,25 @@ +--source include/have_log_bin.inc +--source include/have_binlog_format_mixed.inc + +--echo # +--echo # MDEV-39563 Implement UPDATE ... RETURNING ... INTO +--echo # + +--disable_query_log +reset master; # get rid of previous tests binlog +--enable_query_log + +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +UPDATE t1 SET a=a+1 RETURNING a INTO @a; + +PREPARE stmt FROM 'UPDATE t1 SET a=a+1 RETURNING a INTO @a1'; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +DROP TABLE t1; + +--let $binlog_file = LAST +source include/show_binlog_events.inc; + +--echo # End of 13.1 tests diff --git a/mysql-test/suite/binlog/t/binlog_update_returning_into_row.test b/mysql-test/suite/binlog/t/binlog_update_returning_into_row.test new file mode 100644 index 0000000000000..cf4652c7f0205 --- /dev/null +++ b/mysql-test/suite/binlog/t/binlog_update_returning_into_row.test @@ -0,0 +1,25 @@ +--source include/have_log_bin.inc +--source include/have_binlog_format_row.inc + +--echo # +--echo # MDEV-39563 Implement UPDATE ... RETURNING ... INTO +--echo # + +--disable_query_log +reset master; # get rid of previous tests binlog +--enable_query_log + +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +UPDATE t1 SET a=a+1 RETURNING a INTO @a; + +PREPARE stmt FROM 'UPDATE t1 SET a=a+1 RETURNING a INTO @a1'; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +DROP TABLE t1; + +--let $binlog_file = LAST +source include/show_binlog_events.inc; + +--echo # End of 13.1 tests diff --git a/mysql-test/suite/binlog/t/binlog_update_returning_into_stmt.test b/mysql-test/suite/binlog/t/binlog_update_returning_into_stmt.test new file mode 100644 index 0000000000000..25d4debb6058b --- /dev/null +++ b/mysql-test/suite/binlog/t/binlog_update_returning_into_stmt.test @@ -0,0 +1,27 @@ +--source include/have_log_bin.inc +--source include/have_binlog_format_statement.inc + +--echo # +--echo # MDEV-39563 Implement UPDATE ... RETURNING ... INTO +--echo # + +--disable_query_log +reset master; # get rid of previous tests binlog +--enable_query_log + +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'); +--error ER_WRONG_USAGE +UPDATE t1 SET a=a+1 RETURNING a INTO @a; + +PREPARE stmt FROM 'UPDATE t1 SET a=a+1 RETURNING a INTO @a1'; +--error ER_WRONG_USAGE +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +DROP TABLE t1; + +--let $binlog_file = LAST +source include/show_binlog_events.inc; + +--echo # End of 13.1 tests diff --git a/mysql-test/suite/rpl/r/rpl_update_returning_mixed_or_row.result b/mysql-test/suite/rpl/r/rpl_update_returning_mixed_or_row.result new file mode 100644 index 0000000000000..0a1f7cc047ebd --- /dev/null +++ b/mysql-test/suite/rpl/r/rpl_update_returning_mixed_or_row.result @@ -0,0 +1,50 @@ +include/master-slave.inc +[connection master] +# +# MDEV-39563 Implement UPDATE ... RETURNING ... INTO +# +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10),(20),(30); +connection slave; +connection master; +BEGIN NOT ATOMIC +DECLARE va INT; +UPDATE t1 SET a=a+1 RETURNING a INTO va; +END; +$$ +ERROR 42000: Result consisted of more than one row +SELECT * FROM t1; +a +11 +20 +30 +connection slave; +SELECT * FROM t1; +a +11 +20 +30 +connection master; +BEGIN NOT ATOMIC +DECLARE va INT; +UPDATE t1 SET a=a+1 WHERE a=30 RETURNING a INTO va; -- Set 30 to 31 +INSERT INTO t1 VALUES (va); -- Make sure the value of 'v' is 31 +END; +$$ +SELECT * FROM t1; +a +11 +20 +31 +31 +connection slave; +SELECT * FROM t1; +a +11 +20 +31 +31 +connection master; +DROP TABLE t1; +# End of 13.1 tests +include/rpl_end.inc diff --git a/mysql-test/suite/rpl/t/rpl_update_returning_mixed_or_row.test b/mysql-test/suite/rpl/t/rpl_update_returning_mixed_or_row.test new file mode 100644 index 0000000000000..e09e5a9ee66de --- /dev/null +++ b/mysql-test/suite/rpl/t/rpl_update_returning_mixed_or_row.test @@ -0,0 +1,43 @@ +--source include/have_binlog_format_mixed_or_row.inc +--source include/master-slave.inc + +--echo # +--echo # MDEV-39563 Implement UPDATE ... RETURNING ... INTO +--echo # + +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (10),(20),(30); +--sync_slave_with_master + +--connection master +DELIMITER $$; +--error ER_TOO_MANY_ROWS +BEGIN NOT ATOMIC + DECLARE va INT; + UPDATE t1 SET a=a+1 RETURNING a INTO va; +END; +$$ +DELIMITER ;$$ +SELECT * FROM t1; +--sync_slave_with_master +SELECT * FROM t1; + +--connection master +DELIMITER $$; +BEGIN NOT ATOMIC + DECLARE va INT; + UPDATE t1 SET a=a+1 WHERE a=30 RETURNING a INTO va; -- Set 30 to 31 + INSERT INTO t1 VALUES (va); -- Make sure the value of 'v' is 31 +END; +$$ +DELIMITER ;$$ +SELECT * FROM t1; +--sync_slave_with_master +SELECT * FROM t1; + +--connection master +DROP TABLE t1; + +--echo # End of 13.1 tests + +--source include/rpl_end.inc diff --git a/plugin/type_assoc_array/mysql-test/type_assoc_array/sp-assoc-array-update-returning.result b/plugin/type_assoc_array/mysql-test/type_assoc_array/sp-assoc-array-update-returning.result new file mode 100644 index 0000000000000..cd332041f572f --- /dev/null +++ b/plugin/type_assoc_array/mysql-test/type_assoc_array/sp-assoc-array-update-returning.result @@ -0,0 +1,181 @@ +SET sql_mode=ORACLE; +# +# MDEV-39563 Implement UPDATE ... RETURNING ... INTO +# +# +# UPDATE..RETURNING..INTO into an array +# +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1); +CREATE PROCEDURE p1 AS +TYPE assoc0_t IS TABLE OF NUMBER INDEX BY INTEGER; +a0 assoc0_t; +BEGIN +UPDATE t1 SET a=2 RETURNING a INTO a0; +END; +$$ +CALL p1; +ERROR 21000: The used SELECT statements have a different number of columns +DROP PROCEDURE p1; +DROP TABLE t1; +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1); +CREATE PROCEDURE p1 AS +TYPE assoc0_t IS TABLE OF NUMBER INDEX BY INTEGER; +a0 assoc0_t; +BEGIN +UPDATE t1 SET a=2 RETURNING a,a+1 INTO a0; +END; +$$ +CALL p1; +ERROR 21000: The used SELECT statements have a different number of columns +DROP PROCEDURE p1; +DROP TABLE t1; +# +# UPDATE..RETURNING..INTO into a scalar element +# +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (2); +INSERT INTO t1 VALUES (3); +INSERT INTO t1 VALUES (4); +DECLARE +TYPE marks_t IS TABLE OF NUMBER INDEX BY INTEGER; +marks marks_t; +BEGIN +UPDATE t1 SET a=5 WHERE a=4 RETURNING a INTO marks(200); +SELECT marks(200); +END; +$$ +marks(200) +5 +DROP TABLE t1; +# +# UPDATE..INTO..RETURNING into a non-scalar element +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'); +CREATE PROCEDURE p1 AS +TYPE rec0_t IS RECORD (a INT, b VARCHAR(10)); +TYPE assoc0_t IS TABLE OF rec0_t INDEX BY INT; +a0 assoc0_t; +BEGIN +UPDATE t1 SET a=30,b='b30' WHERE a=20 RETURNING a,b INTO a0(30); +SELECT a0(30).a, a0(30).b; +END; +$$ +CALL p1(); +a0(30).a a0(30).b +30 b30 +DROP PROCEDURE p1; +DROP TABLE t1; +# +# UPDATE..INTO..RETURNING into a field of a non-scalar element +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'); +CREATE PROCEDURE p1(create_element BOOL, select_element BOOL) AS +TYPE rec0_t IS RECORD (a INT, b VARCHAR(10)); +TYPE assoc0_t IS TABLE OF rec0_t INDEX BY INT; +a0 assoc0_t; +BEGIN +IF (create_element) THEN +a0(20):= rec0_t(20,'b20'); +END IF; +UPDATE t1 SET a=a+1,b='b21' WHERE a>=20 RETURNING a,b INTO a0(20).a,a0(20).b; +IF (select_element) THEN +SELECT a0(20).a, a0(20).b; +END IF; +END; +$$ +CALL p1(TRUE,TRUE); +a0(20).a a0(20).b +21 b21 +CALL p1(FALSE,FALSE); +ERROR HY000: Element not found with key '20' +CALL p1(FALSE,TRUE); +ERROR HY000: Element not found with key '20' +DROP PROCEDURE p1; +DROP TABLE t1; +# +# UPDATE..RETURNING..INTO assoc_array_of_scalars('key').field +# Fails during the CREATE time. +# +CREATE TABLE t1 (a VARCHAR(20)); +CREATE PROCEDURE p1 IS +TYPE assoc_t IS TABLE OF VARCHAR2(20) INDEX BY INT; +assoc assoc_t; +BEGIN +UPDATE t1 SET a= 'a10' RETURNING a INTO assoc(10).last_name; +END; +$$ +ERROR HY000: Illegal parameter data type varchar for operation '`assoc`(..).`last_name`' +DROP TABLE t1; +# +# SELECT scalar INTO non-existent variable with key +# +CREATE TABLE t1 (a VARCHAR(10)); +DECLARE +BEGIN +UPDATE t1 SET a='Mike' RETURNING a INTO missing_var('Serg').first_name; +END; +$$ +ERROR 42000: Undeclared variable: missing_var +DROP TABLE t1; +# +# UPDATE..INTO..RETURNING: field on scalar element +# +CREATE TABLE t1 (a INT); +DECLARE +TYPE marks_t IS TABLE OF NUMBER INDEX BY INTEGER; +marks marks_t; +BEGIN +marks(200):= 88; +UPDATE t1 SET a=201 RETURNING a INTO marks(200).non_exist; +END; +$$ +ERROR HY000: Illegal parameter data type double for operation '`marks`(..).`non_exist`' +DROP TABLE t1; +# +# UPDATE..INTO..RETURNING into a non-existent field of a non-scalar element +# +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'); +CREATE PROCEDURE p1() AS +TYPE rec0_t IS RECORD (a INT, b VARCHAR(10)); +TYPE assoc0_t IS TABLE OF rec0_t INDEX BY INT; +a0 assoc0_t; +BEGIN +UPDATE t1 SET a=a+1,b='b21' WHERE a>=20 RETURNING a,b INTO a0(20).xa,a0(20).xb; +END; +$$ +ERROR 42S22: Unknown column 'xa' in 'a0' +DROP TABLE t1; +# +# Add tests with assoc array here when we add support for +# {INSERT|REPLACE|DELETE}..RETURNING..INTO +# +CREATE PROCEDURE p1() AS +TYPE assoc0_t IS TABLE OF INT INDEX BY INT; +a0 assoc0_t; +BEGIN +INSERT INTO t1 VALUES (10) RETURNING a INTO a0; +END; +$$ +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +CREATE PROCEDURE p1() AS +TYPE assoc0_t IS TABLE OF INT INDEX BY INT; +a0 assoc0_t; +BEGIN +REPLACE INTO t1 SET a=10 RETURNING a INTO a0; +END; +$$ +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +CREATE PROCEDURE p1() AS +TYPE assoc0_t IS TABLE OF INT INDEX BY INT; +a0 assoc0_t; +BEGIN +DELETE FROM t1 RETURNING a INTO a0; +END; +$$ +ERROR HY000: 'RETURNING..INTO' is not allowed in this context +# End of 13.1 tests diff --git a/plugin/type_assoc_array/mysql-test/type_assoc_array/sp-assoc-array-update-returning.test b/plugin/type_assoc_array/mysql-test/type_assoc_array/sp-assoc-array-update-returning.test new file mode 100644 index 0000000000000..a7e98912373fe --- /dev/null +++ b/plugin/type_assoc_array/mysql-test/type_assoc_array/sp-assoc-array-update-returning.test @@ -0,0 +1,227 @@ +SET sql_mode=ORACLE; + +--echo # +--echo # MDEV-39563 Implement UPDATE ... RETURNING ... INTO +--echo # + +--echo # +--echo # UPDATE..RETURNING..INTO into an array +--echo # + +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1); +DELIMITER $$; +CREATE PROCEDURE p1 AS + TYPE assoc0_t IS TABLE OF NUMBER INDEX BY INTEGER; + a0 assoc0_t; +BEGIN + UPDATE t1 SET a=2 RETURNING a INTO a0; +END; +$$ +DELIMITER ;$$ +--error ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT +CALL p1; +DROP PROCEDURE p1; +DROP TABLE t1; + +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1); +DELIMITER $$; +CREATE PROCEDURE p1 AS + TYPE assoc0_t IS TABLE OF NUMBER INDEX BY INTEGER; + a0 assoc0_t; +BEGIN + UPDATE t1 SET a=2 RETURNING a,a+1 INTO a0; +END; +$$ +DELIMITER ;$$ +--error ER_WRONG_NUMBER_OF_COLUMNS_IN_SELECT +CALL p1; +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # +--echo # UPDATE..RETURNING..INTO into a scalar element +--echo # +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (2); +INSERT INTO t1 VALUES (3); +INSERT INTO t1 VALUES (4); +DELIMITER $$; +DECLARE + TYPE marks_t IS TABLE OF NUMBER INDEX BY INTEGER; + marks marks_t; +BEGIN + UPDATE t1 SET a=5 WHERE a=4 RETURNING a INTO marks(200); + SELECT marks(200); +END; +$$ +DELIMITER ;$$ +DROP TABLE t1; + + +--echo # +--echo # UPDATE..INTO..RETURNING into a non-scalar element +--echo # + +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'); +DELIMITER $$; +CREATE PROCEDURE p1 AS + TYPE rec0_t IS RECORD (a INT, b VARCHAR(10)); + TYPE assoc0_t IS TABLE OF rec0_t INDEX BY INT; + a0 assoc0_t; +BEGIN + UPDATE t1 SET a=30,b='b30' WHERE a=20 RETURNING a,b INTO a0(30); + SELECT a0(30).a, a0(30).b; +END; +$$ +DELIMITER ;$$ +CALL p1(); +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # +--echo # UPDATE..INTO..RETURNING into a field of a non-scalar element +--echo # + +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'); +DELIMITER $$; +CREATE PROCEDURE p1(create_element BOOL, select_element BOOL) AS + TYPE rec0_t IS RECORD (a INT, b VARCHAR(10)); + TYPE assoc0_t IS TABLE OF rec0_t INDEX BY INT; + a0 assoc0_t; +BEGIN + IF (create_element) THEN + a0(20):= rec0_t(20,'b20'); + END IF; + UPDATE t1 SET a=a+1,b='b21' WHERE a>=20 RETURNING a,b INTO a0(20).a,a0(20).b; + IF (select_element) THEN + SELECT a0(20).a, a0(20).b; + END IF; +END; +$$ +DELIMITER ;$$ +CALL p1(TRUE,TRUE); +--error ER_ASSOC_ARRAY_ELEM_NOT_FOUND +CALL p1(FALSE,FALSE); +--error ER_ASSOC_ARRAY_ELEM_NOT_FOUND +CALL p1(FALSE,TRUE); +DROP PROCEDURE p1; +DROP TABLE t1; + + +--echo # +--echo # UPDATE..RETURNING..INTO assoc_array_of_scalars('key').field +--echo # Fails during the CREATE time. +--echo # +CREATE TABLE t1 (a VARCHAR(20)); +DELIMITER $$; +--error ER_ILLEGAL_PARAMETER_DATA_TYPE_FOR_OPERATION +CREATE PROCEDURE p1 IS + TYPE assoc_t IS TABLE OF VARCHAR2(20) INDEX BY INT; + assoc assoc_t; +BEGIN + UPDATE t1 SET a= 'a10' RETURNING a INTO assoc(10).last_name; +END; +$$ +DELIMITER ;$$ +DROP TABLE t1; + + +--echo # +--echo # SELECT scalar INTO non-existent variable with key +--echo # + +CREATE TABLE t1 (a VARCHAR(10)); +DELIMITER $$; +--error ER_SP_UNDECLARED_VAR +DECLARE +BEGIN + UPDATE t1 SET a='Mike' RETURNING a INTO missing_var('Serg').first_name; +END; +$$ +DELIMITER ;$$ +DROP TABLE t1; + + +--echo # +--echo # UPDATE..INTO..RETURNING: field on scalar element +--echo # +CREATE TABLE t1 (a INT); +DELIMITER $$; +--error ER_ILLEGAL_PARAMETER_DATA_TYPE_FOR_OPERATION +DECLARE + TYPE marks_t IS TABLE OF NUMBER INDEX BY INTEGER; + marks marks_t; +BEGIN + marks(200):= 88; + UPDATE t1 SET a=201 RETURNING a INTO marks(200).non_exist; +END; +$$ +DELIMITER ;$$ +DROP TABLE t1; + + +--echo # +--echo # UPDATE..INTO..RETURNING into a non-existent field of a non-scalar element +--echo # + +CREATE TABLE t1 (a INT, b VARCHAR(10)); +INSERT INTO t1 VALUES (10,'b10'),(20,'b20'); +DELIMITER $$; +--error ER_BAD_FIELD_ERROR +CREATE PROCEDURE p1() AS + TYPE rec0_t IS RECORD (a INT, b VARCHAR(10)); + TYPE assoc0_t IS TABLE OF rec0_t INDEX BY INT; + a0 assoc0_t; +BEGIN + UPDATE t1 SET a=a+1,b='b21' WHERE a>=20 RETURNING a,b INTO a0(20).xa,a0(20).xb; +END; +$$ +DELIMITER ;$$ +DROP TABLE t1; + + +--echo # +--echo # Add tests with assoc array here when we add support for +--echo # {INSERT|REPLACE|DELETE}..RETURNING..INTO +--echo # + +DELIMITER $$; +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +CREATE PROCEDURE p1() AS + TYPE assoc0_t IS TABLE OF INT INDEX BY INT; + a0 assoc0_t; +BEGIN + INSERT INTO t1 VALUES (10) RETURNING a INTO a0; +END; +$$ +DELIMITER ;$$ + +DELIMITER $$; +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +CREATE PROCEDURE p1() AS + TYPE assoc0_t IS TABLE OF INT INDEX BY INT; + a0 assoc0_t; +BEGIN + REPLACE INTO t1 SET a=10 RETURNING a INTO a0; +END; +$$ +DELIMITER ;$$ + +DELIMITER $$; +--error ER_NOT_ALLOWED_IN_THIS_CONTEXT +CREATE PROCEDURE p1() AS + TYPE assoc0_t IS TABLE OF INT INDEX BY INT; + a0 assoc0_t; +BEGIN + DELETE FROM t1 RETURNING a INTO a0; +END; +$$ +DELIMITER ;$$ + +--echo # End of 13.1 tests diff --git a/sql/select_result.h b/sql/select_result.h index 4bc1050388570..9d66cde51073e 100644 --- a/sql/select_result.h +++ b/sql/select_result.h @@ -65,6 +65,7 @@ class select_result :public select_result_sink select_result(THD *thd_arg): select_result_sink(thd_arg), est_records(0) {} void set_unit(SELECT_LEX_UNIT *unit_arg) { unit= unit_arg; } virtual ~select_result() = default; + virtual bool accepts_only_one_row() const { return false; } /** Change wrapped select_result. diff --git a/sql/sp_head.cc b/sql/sp_head.cc index 1c2b199777351..d11075f62e231 100644 --- a/sql/sp_head.cc +++ b/sql/sp_head.cc @@ -327,32 +327,23 @@ sp_get_flags_for_command(LEX *lex) case SQLCOM_UNINSTALL_PLUGIN: flags= sp_head::HAS_COMMIT_OR_ROLLBACK; break; - case SQLCOM_DELETE: - case SQLCOM_DELETE_MULTI: case SQLCOM_INSERT: case SQLCOM_REPLACE: case SQLCOM_REPLACE_SELECT: case SQLCOM_INSERT_SELECT: { - /* - DELETE normally doesn't return resultset, but there are 3 exceptions: - - DELETE ... RETURNING - - EXPLAIN DELETE ... - - ANALYZE DELETE ... - */ - if (!lex->has_returning() && !lex->describe && !lex->analyze_stmt) - flags= 0; - else - flags= sp_head::MULTI_RESULTS; + flags= Sql_cmd_dml::returns_result_set_generic(lex) ? + sp_head::MULTI_RESULTS : 0; break; } + case SQLCOM_DELETE: + case SQLCOM_DELETE_MULTI: case SQLCOM_UPDATE: case SQLCOM_UPDATE_MULTI: { - if (!lex->has_returning() && !lex->describe && !lex->analyze_stmt) - flags= 0; - else - flags= sp_head::MULTI_RESULTS; + Sql_cmd_dml *cmd= dynamic_cast(lex->m_sql_cmd); + DBUG_ASSERT(cmd); + flags= cmd->returns_result_set() ? sp_head::MULTI_RESULTS : 0; break; } default: diff --git a/sql/sql_class.h b/sql/sql_class.h index 07046ff0f3714..b64adea0ff75c 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -8010,6 +8010,7 @@ class select_dumpvar :public select_result_interceptor { :select_result_interceptor(thd_arg), row_count(0), m_var_sp_row(NULL) { var_list.empty(); } ~select_dumpvar() = default; + bool accepts_only_one_row() const override { return true; } int prepare(List &list, SELECT_LEX_UNIT *u) override; int send_data(List &items) override; bool send_eof() override; diff --git a/sql/sql_cmd.h b/sql/sql_cmd.h index bc5215cbbc66b..62ca8f77fd8e7 100644 --- a/sql/sql_cmd.h +++ b/sql/sql_cmd.h @@ -180,6 +180,7 @@ class Sql_cmd : public Sql_alloc struct LEX; class select_result; +class select_dumpvar; class Prelocking_strategy; class DML_prelocking_strategy; class Protocol; @@ -211,12 +212,30 @@ class Sql_cmd_dml : public Sql_cmd { public: + /** + @brief Check if the statement returns a result set in a general case + */ + static bool returns_result_set_generic(const LEX *lex); + /** @brief Check whether the statement changes the contents of used tables @return true if this is data change statement, false otherwise */ virtual bool is_data_change_stmt() const { return true; } + /** + @brief Set the RETURNING INTO result + @return true on error, false on success + */ + virtual bool set_returning_into_result(select_dumpvar *res); + + enum_sql_command sql_command_code() const override; + + /** + @brief Check if the statement returns a result set + */ + virtual bool returns_result_set() const= 0; + /** @brief Perform context analysis of the statement @param thd global context the processed statement @@ -238,8 +257,8 @@ class Sql_cmd_dml : public Sql_cmd ha_rows get_scanned_rows() { return scanned_rows; } protected: - Sql_cmd_dml() - : Sql_cmd(), lex(nullptr), result(nullptr), + Sql_cmd_dml(LEX *lex_arg) + : Sql_cmd(), lex(lex_arg), result(nullptr), m_empty_query(false), scanned_rows(0) {} diff --git a/sql/sql_delete.h b/sql/sql_delete.h index 7b6401a7965f5..012f9cc31b43a 100644 --- a/sql/sql_delete.h +++ b/sql/sql_delete.h @@ -44,14 +44,18 @@ class Sql_cmd_delete final : public Sql_cmd_dml { public: ha_rows deleted{0}; - Sql_cmd_delete(bool multitable_arg) - : orig_multitable(multitable_arg), multitable(multitable_arg), + Sql_cmd_delete(LEX *lex, bool multitable_arg) + : Sql_cmd_dml(lex), + multitable(multitable_arg), save_protocol(NULL) {} - enum_sql_command sql_command_code() const override + /** + @brief Check if the statement returns a result set + */ + bool returns_result_set() const override { - return orig_multitable ? SQLCOM_DELETE_MULTI : SQLCOM_DELETE; + return returns_result_set_generic(lex); } DML_prelocking_strategy *get_dml_prelocking_strategy() override @@ -95,9 +99,6 @@ class Sql_cmd_delete final : public Sql_cmd_dml */ bool delete_from_single_table(THD *thd); - /* Original value of the 'multitable' flag set by constructor */ - const bool orig_multitable; - /* True if the statement is a multitable delete or converted to such. For a single-table delete this flag is set to true if the statement diff --git a/sql/sql_lex.cc b/sql/sql_lex.cc index 6e1c6df4715b7..e47d33efc577b 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -14280,6 +14280,24 @@ LEX::parse_optimizer_hints(const Lex_comment_st &hints_str) } +bool LEX::set_returning_into_result(select_dumpvar *res) +{ + if (!analyze_stmt && !describe) + { + DBUG_ASSERT(res); + // The order of rows is not predicable, like in SELECT..LIMIT + set_stmt_unsafe(LEX::BINLOG_STMT_UNSAFE_LIMIT); + + if (Sql_cmd_dml *dml= dynamic_cast(m_sql_cmd)) + return dml->set_returning_into_result(res); // UPDATE, DELETE + // REPLACE, INSERT have no Sql_cmd_xxx yet + } + + my_error(ER_NOT_ALLOWED_IN_THIS_CONTEXT, MYF(0), "RETURNING..INTO"); + return true; +} + + /* @brief After we've finished parsing a SELECT, handle its hints. diff --git a/sql/sql_lex.h b/sql/sql_lex.h index 5e0258ee42bf3..fdc7ca1d5ebcf 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -845,6 +845,7 @@ typedef class st_select_lex_node SELECT_LEX_NODE; */ class THD; class select_result; +class select_dumpvar; class JOIN; class select_unit; class Procedure; @@ -3980,6 +3981,7 @@ struct LEX: public Query_tables_list DBUG_RETURN(select_lex); } + bool set_returning_into_result(select_dumpvar *res); void handle_parsed_optimizer_hints_in_last_select(); void resolve_optimizer_hints(); bool discard_optimizer_hints_in_last_select(); @@ -5213,7 +5215,7 @@ struct LEX: public Query_tables_list SELECT_LEX *returning() { return &builtin_select; } - bool has_returning() + bool has_returning() const { return !builtin_select.returning_list.is_empty(); } private: diff --git a/sql/sql_select.cc b/sql/sql_select.cc index dc9e7b6113333..70d9b4a11e38e 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -34933,6 +34933,38 @@ static void MYSQL_DML_DONE(THD *thd, int rc, ha_rows found, ha_rows changed) } +/** + @bfrief Check if the statement returns a result set +*/ +bool Sql_cmd_dml::returns_result_set_generic(const LEX *lex) +{ + // UPDATE is handles separately + DBUG_ASSERT(lex->sql_command != SQLCOM_UPDATE); + DBUG_ASSERT(lex->sql_command != SQLCOM_UPDATE_MULTI); + /* + DML normally doesn't return result sets, but there are 3 exceptions: + - {INSERT|UPDATE|DELETE} ... RETURNING + - EXPLAIN {INSERT|UPDATE|DELETE} ... + - ANALYZE {INSERT|UPDATE|DELETE} ... + */ + return lex->has_returning() || lex->describe || lex->analyze_stmt; +} + + +bool Sql_cmd_dml::set_returning_into_result(select_dumpvar *res) +{ + my_error(ER_NOT_ALLOWED_IN_THIS_CONTEXT, MYF(0), "RETURNING..INTO"); + return true; +} + + +enum_sql_command Sql_cmd_dml::sql_command_code() const +{ + DBUG_ASSERT(lex); // Set in the constructor + return lex->sql_command; +} + + /* @brief Perform actions needed before locking tables for a DML statement @@ -34953,7 +34985,7 @@ static void MYSQL_DML_DONE(THD *thd, int rc, ha_rows found, ha_rows changed) bool Sql_cmd_dml::prepare(THD *thd) { - lex= thd->lex; + DBUG_ASSERT(lex == thd->lex); SELECT_LEX_UNIT *unit= &lex->unit; DBUG_ASSERT(!is_prepared()); @@ -35014,7 +35046,7 @@ bool Sql_cmd_dml::prepare(THD *thd) bool Sql_cmd_dml::execute(THD *thd) { - lex = thd->lex; + DBUG_ASSERT(lex == thd->lex); ha_rows found= 0, changed= 0; bool res; diff --git a/sql/sql_update.cc b/sql/sql_update.cc index 847d073b48d78..367715ab22ffd 100644 --- a/sql/sql_update.cc +++ b/sql/sql_update.cc @@ -1026,6 +1026,17 @@ bool Sql_cmd_update::update_single_table(THD *thd) if (need_update) { + if (updated == 1 && lex->has_returning() && + returning_result->accepts_only_one_row()) + { + /* + A second record has been found. + Raise the error and break without updating the second record. + */ + error= 1; + my_error(ER_TOO_MANY_ROWS, MYF(0)); + break; + } if (table->versioned(VERS_TIMESTAMP) && thd->lex->sql_command == SQLCOM_DELETE) table->vers_update_end(); @@ -3042,6 +3053,53 @@ bool multi_update::send_eof() } +/** + @brief Check is the statement has RETURNING with INTO +*/ +bool Sql_cmd_update::with_returning_into() const +{ + return dynamic_cast(returning_result) != nullptr; +} + + +/** + @brief Set the RETURNING..INTO target list + @returns true on error, false on success +*/ +bool Sql_cmd_update::set_returning_into_result(select_dumpvar *res) +{ + /* + Due to the implementation, OLD_VALUE() would return the new value + into the INTO target (not the old value). Disallow it. + */ + if (m_with_old_value_items) + { + my_error(ER_WRONG_USAGE, MYF(0), "OLD_VALUE(..)", "RETURNING..INTO"); + return true; + } + returning_result= res; + return false; +} + + +/* + @brief Check if this UPDATE statement returns a result set +*/ +bool Sql_cmd_update::returns_result_set() const +{ + DBUG_ASSERT(lex->m_sql_cmd == this); + if (lex->describe || lex->analyze_stmt) + return true; // E.g. EXPLAIN UPDATE t1 SET a=a+1; + + if (!lex->has_returning()) + return false; // e.g. UPDATE t1 SET a=a+1; + + if (with_returning_into()) + return false; // e.g. UPDATE t1 SET a=a+1 RETURNING a INTO @a; + + return true; // e.g. UPDATE t1 SET a=a+1 RETURNING a; +} + /** @brief Check whether conversion to multi-table update is prohibited @@ -3286,6 +3344,7 @@ bool Sql_cmd_update::prepare_inner(THD *thd) bool Sql_cmd_update::execute_inner(THD *thd) { bool res= 0; + bool set_returning_result_to_null= false; Running_stmt_guard guard(thd, active_dml_stmt::UPDATING_STMT); if (!multitable) @@ -3295,23 +3354,74 @@ bool Sql_cmd_update::execute_inner(THD *thd) /* This is UPDATE ... RETURNING. It will return output to the client */ if (thd->lex->analyze_stmt) { + /* + Queries with INTO clause like: + ANALYZE UPDATE t1 SET a=a+1 RETURNING a INTO @a; + Should have been rejected earlier. + */ + DBUG_ASSERT(!returning_result); if (!(returning_result= new (thd->mem_root) select_send_analyze(thd))) { return true; } + set_returning_result_to_null= true; save_protocol= thd->protocol; thd->protocol= new Protocol_discard(thd); + // select_send_analyze::prepare() cannot fail + (void) returning_result->prepare(thd->lex->returning()->returning_list, + NULL); } else { - if (!(returning_result= new - (thd->mem_root) select_send(thd))) + if (!returning_result) { - return true; + // UPDATE t1 SET a=a+1 RETURNING a; + if (!(returning_result= new (thd->mem_root) select_send(thd))) + return true; + set_returning_result_to_null= true; + // select_send::prepare() cannot fail + (void) returning_result->prepare( + thd->lex->returning()->returning_list, + NULL); + } + else + { + // UPDATE t1 SET a=a+1 RETURNING a INTO @a; + // UPDATE t1 SET a=a+1 RETURNING a INTO spvar; + DBUG_ASSERT(with_returning_into()); + DBUG_ASSERT(!lex->describe); // EXPLAIN - rejected earlier + /* + To replicate a statement like: + UPDATE t1 SET a=a+1 RETURNING a INTO spvar; -- notice SP variable + we would need to cut the 'INTO spvar' part. + Let's disallow BINLOG_FORMAT_STMT for now for simplicity. + */ + if (!thd->is_current_stmt_binlog_disabled() && + thd->variables.binlog_format == BINLOG_FORMAT_STMT) + { + my_error(ER_WRONG_USAGE, MYF(0), "--binlog-format=statement", + "RETURNING INTO"); + return true; + } + /* + The below call can fail e.g. in case of a mismatch between the + number of columns (i.e. N!=M) in: + UPDATE..RETURNING INTO + */ + if (returning_result->prepare(thd->lex->returning()->returning_list, + NULL)) + return true; } } - if (thd->lex->has_returning()) - (void) returning_result->prepare(thd->lex->returning()->returning_list, NULL); + } + } + else + { + // RETURNING INTO is not supported for multi-table UPDATEs + if (with_returning_into()) + { + my_error(ER_NOT_ALLOWED_IN_THIS_CONTEXT, MYF(0), "RETURNING..INTO"); + return true; } } @@ -3346,6 +3456,18 @@ bool Sql_cmd_update::execute_inner(THD *thd) } } + if (set_returning_result_to_null) + { + /* + returning_result could be assigned to a select_send or + select_send_analyze instance above. + Let's set it to null again, to have a new + select_send/select_send_analyze instance created on the second + and further executions. + */ + returning_result= nullptr; + } + if (result) { /* In single table case, this->updated set by update_single_table */ diff --git a/sql/sql_update.h b/sql/sql_update.h index f663fbc99b537..aae8bc6ed9c69 100644 --- a/sql/sql_update.h +++ b/sql/sql_update.h @@ -46,15 +46,16 @@ class Sql_cmd_update final : public Sql_cmd_dml { public: ha_rows found{0}, updated{0}; - Sql_cmd_update(bool multitable_arg) - : orig_multitable(multitable_arg), multitable(multitable_arg), + Sql_cmd_update(LEX *lex, bool multitable_arg) + :Sql_cmd_dml(lex), + multitable(multitable_arg), + m_with_old_value_items(false), returning_result(nullptr), save_protocol(nullptr) {} - enum_sql_command sql_command_code() const override - { - return orig_multitable ? SQLCOM_UPDATE_MULTI : SQLCOM_UPDATE; - } + bool returns_result_set() const override; + + bool set_returning_into_result(select_dumpvar *res) override; DML_prelocking_strategy *get_dml_prelocking_strategy() override { @@ -67,6 +68,8 @@ class Sql_cmd_update final : public Sql_cmd_dml void set_as_multitable() { multitable= true; } + void set_with_old_value_items() { m_with_old_value_items= true; } + void get_dml_stat (ha_rows &found, ha_rows &changed) override { @@ -74,6 +77,8 @@ class Sql_cmd_update final : public Sql_cmd_dml changed= this->updated; } + bool with_returning_into() const; + protected: /** @brief Perform precheck of table privileges for update statements @@ -97,15 +102,15 @@ class Sql_cmd_update final : public Sql_cmd_dml */ bool update_single_table(THD *thd); - /* Original value of the 'multitable' flag set by constructor */ - const bool orig_multitable; - /* True if the statement is a multi-table update or converted to such. For a single-table update this flag is set to true if the statement is supposed to be converted to multi-table update. */ bool multitable; + + bool m_with_old_value_items; // Has OLD_VALUE(col) + select_result *returning_result; /* The prelocking strategy used when opening the used tables */ diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index c2fe9974c42df..9db8dc082b83a 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -1898,7 +1898,10 @@ rule: handler_rkey_function handler_read_or_scan single_multi opt_wild opt_and - select_var_list select_var_list_init help + select_var_list select_var_list_init + into_destination_select_var_list_init + opt_into_select_var_list_init + help opt_extended_describe shutdown opt_format_json prepare execute deallocate @@ -10052,6 +10055,10 @@ expr: my_error(ER_WRONG_USAGE, MYF(0), "OLD_VALUE", "non-UPDATE"); MYSQL_YYABORT; } + Sql_cmd_update *update= dynamic_cast( + lex->m_sql_cmd); + DBUG_ASSERT(update); + update->set_with_old_value_items(); $$= new (thd->mem_root) Item_old_field(thd, &lex->current_select->context, @@ -13992,13 +13999,28 @@ into_destination: MYSQL_YYABORT; } } - | select_var_list_init + | into_destination_select_var_list_init + ; + +into_destination_select_var_list_init: + select_var_list_init { Lex->uncacheable(UNCACHEABLE_SIDEEFFECT); status_var_increment(thd->status_var.feature_into_variable); } ; +opt_into_select_var_list_init: + /* empty */ { } + | INTO into_destination_select_var_list_init + { + if (Lex->set_returning_into_result( + dynamic_cast(Lex->result))) + MYSQL_YYABORT; + + } + ; + /* DO statement */ @@ -14522,17 +14544,19 @@ update: Lex->first_select_lex()->master_unit()->set_with_clause($1); if ($1) $1->attach_to(Lex->first_select_lex()); + if (!(Lex->m_sql_cmd= new (thd->mem_root) Sql_cmd_update(Lex, + false))) + MYSQL_YYABORT; } opt_low_priority opt_ignore update_table_list SET update_list { - bool is_multiupdate= false; - LEX *lex= Lex; SELECT_LEX *slex= Lex->first_select_lex(); if (slex->table_list.elements > 1) { Lex->sql_command= SQLCOM_UPDATE_MULTI; - is_multiupdate= true; + DBUG_ASSERT(dynamic_cast(Lex->m_sql_cmd)); + static_cast(Lex->m_sql_cmd)->set_as_multitable(); } else if (slex->get_table_list()->derived) { @@ -14541,9 +14565,6 @@ update: slex->get_table_list()->alias.str, "UPDATE"); MYSQL_YYABORT; } - if (!(lex->m_sql_cmd= - new (thd->mem_root) Sql_cmd_update(is_multiupdate))) - MYSQL_YYABORT; /* In case of multi-update setting write lock for all tables may be too pessimistic. We will decrease lock level if possible @@ -14647,7 +14668,7 @@ delete_part2: lex->last_table()->vers_conditions= lex->vers_conditions; lex->sql_command= SQLCOM_DELETE; if (!(lex->m_sql_cmd= - new (thd->mem_root) Sql_cmd_delete(false))) + new (thd->mem_root) Sql_cmd_delete(lex, false))) MYSQL_YYABORT; if (lex->check_main_unit_semantics()) MYSQL_YYABORT; @@ -14706,7 +14727,7 @@ single_multi: Select->order_list= *($3); lex->sql_command= SQLCOM_DELETE; if (!(lex->m_sql_cmd= - new (thd->mem_root) Sql_cmd_delete(false))) + new (thd->mem_root) Sql_cmd_delete(lex, false))) MYSQL_YYABORT; if (Lex->check_main_unit_semantics()) MYSQL_YYABORT; @@ -14717,7 +14738,7 @@ single_multi: LEX *lex= Lex; lex->sql_command= SQLCOM_DELETE_MULTI; if (!(lex->m_sql_cmd= - new (thd->mem_root) Sql_cmd_delete(true))) + new (thd->mem_root) Sql_cmd_delete(lex, true))) MYSQL_YYABORT; mysql_init_multi_delete(Lex); YYPS->m_lock_type= TL_READ_DEFAULT; @@ -14738,7 +14759,7 @@ single_multi: LEX *lex= Lex; lex->sql_command= SQLCOM_DELETE_MULTI; if (!(lex->m_sql_cmd= - new (thd->mem_root) Sql_cmd_delete(true))) + new (thd->mem_root) Sql_cmd_delete(lex, true))) MYSQL_YYABORT; mysql_init_multi_delete(Lex); YYPS->m_lock_type= TL_READ_DEFAULT; @@ -14808,6 +14829,8 @@ opt_returning: std::swap(thd->lex->returning()->returning_list, thd->lex->returning()->item_list); } + opt_into_select_var_list_init + { } ; opt_wild: @@ -15475,10 +15498,8 @@ describe_command: ; analyze_stmt_command: - ANALYZE_SYM opt_format_json explainable_command - { - Lex->analyze_stmt= true; - } + ANALYZE_SYM { Lex->analyze_stmt= true; } + opt_format_json explainable_command { } ; opt_extended_describe: