diff --git a/mysql-test/r/delete_returning.result b/mysql-test/r/delete_returning.result new file mode 100644 index 000000000000..1de83dbad467 --- /dev/null +++ b/mysql-test/r/delete_returning.result @@ -0,0 +1,599 @@ +# +# Setup +# +CREATE TABLE t1 (a INT, b VARCHAR(32)); +INSERT INTO t1 VALUES +(7,'ggggggg'), (1,'a'), (3,'ccc'), +(4,'dddd'), (1,'A'), (2,'BB'), +(4,'DDDD'), (5,'EEEEE'), (7,'GGGGGGG'), (2,'bb'); +CREATE TABLE t1c SELECT * FROM t1; +CREATE TABLE t2 (c INT); +INSERT INTO t2 VALUES (4), (5), (7), (1); +CREATE TABLE t2c SELECT * FROM t2; +# +# Test 1: DELETE ... RETURNING * +# +DELETE FROM t1 WHERE a=2 RETURNING *; +a b +2 BB +2 bb +SELECT * FROM t1; +a b +7 ggggggg +1 a +3 ccc +4 dddd +1 A +4 DDDD +5 EEEEE +7 GGGGGGG +INSERT INTO t1 VALUES (2,'BB'), (2,'bb'); +# +# Test 2: DELETE ... RETURNING single column +# +DELETE FROM t1 WHERE a=2 RETURNING b; +b +BB +bb +SELECT * FROM t1; +a b +7 ggggggg +1 a +3 ccc +4 dddd +1 A +4 DDDD +5 EEEEE +7 GGGGGGG +# +# Test 3: DELETE ... RETURNING non-existing column (error) +# +INSERT INTO t1 VALUES (2,'BB'), (2,'bb'); +DELETE FROM t1 WHERE a=2 RETURNING c; +ERROR 42S22: Unknown column 'c' in 'field list' +# +# Test 4: DELETE ... RETURNING column and expression +# +DELETE FROM t1 WHERE a=2 RETURNING a, UPPER(b); +a UPPER(b) +2 BB +2 BB +SELECT * FROM t1; +a b +7 ggggggg +1 a +3 ccc +4 dddd +1 A +4 DDDD +5 EEEEE +7 GGGGGGG +INSERT INTO t1 VALUES (2,'BB'), (2,'bb'); +# +# Test 5: DELETE ... RETURNING with no rows matching (empty result set) +# +DELETE FROM t1 WHERE a=999 ORDER BY a RETURNING b; +b +SELECT * FROM t1; +a b +7 ggggggg +1 a +3 ccc +4 dddd +1 A +4 DDDD +5 EEEEE +7 GGGGGGG +2 BB +2 bb +# +# Test 6: DELETE ... RETURNING with aggregate or window function (error) +# +DELETE FROM t1 WHERE a=2 RETURNING MAX(b); +ERROR HY000: Invalid use of group function +DELETE FROM t1 WHERE a=2 RETURNING ROW_NUMBER() OVER (); +ERROR HY000: You cannot use the window function 'row_number' in this context.' +DELETE FROM t1 WHERE a=2 RETURNING SUM(a) OVER (ORDER BY a); +ERROR HY000: You cannot use the window function 'sum' in this context.' +# +# Test 7: DELETE ... RETURNING with correlated scalar subquery +# +DELETE FROM t1 WHERE a < 5 RETURNING a, (SELECT MIN(c) FROM t2 WHERE c = a+1); +a (SELECT MIN(c) FROM t2 WHERE c = a+1) +1 NULL +3 4 +4 5 +1 NULL +4 5 +2 NULL +2 NULL +SELECT * FROM t1; +a b +7 ggggggg +5 EEEEE +7 GGGGGGG +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# +# Test 8: DELETE ... RETURNING with subquery using GROUP_CONCAT +# +DELETE FROM t2 WHERE c < 5 +RETURNING (SELECT GROUP_CONCAT(b) FROM t1 GROUP BY a HAVING a = c); +(SELECT GROUP_CONCAT(b) FROM t1 GROUP BY a HAVING a = c) +dddd,DDDD +a,A +SELECT * FROM t2; +c +5 +7 +DELETE FROM t2; +INSERT INTO t2 SELECT * FROM t2c; +# +# Test 9: DELETE ... RETURNING with user-defined function +# +CREATE FUNCTION f1(arg INT) RETURNS TEXT +BEGIN +RETURN (SELECT GROUP_CONCAT(b) FROM t1 WHERE a = arg); +END| +DELETE FROM t2 WHERE c < 5 RETURNING f1(c); +f1(c) +dddd,DDDD +a,A +SELECT * FROM t2; +c +5 +7 +DELETE FROM t2; +INSERT INTO t2 SELECT * FROM t2c; +DROP FUNCTION f1; +# +# Test 10: DELETE from updatable view with RETURNING +# +CREATE VIEW v1 AS SELECT a, b FROM t1; +DELETE FROM v1 WHERE a < 3 RETURNING *; +a b +1 a +1 A +2 BB +2 bb +SELECT * FROM t1; +a b +7 ggggggg +3 ccc +4 dddd +4 DDDD +5 EEEEE +7 GGGGGGG +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +DROP VIEW v1; +# +# Test 11: DELETE from non-updatable view (error) +# +CREATE VIEW v1 AS SELECT a, COUNT(b) AS cnt FROM t1 GROUP BY a; +DELETE FROM v1 WHERE a < 5 RETURNING *; +ERROR HY000: The target table v1 of the DELETE is not updatable +DROP VIEW v1; +# +# Test 12: Multi-table DELETE with RETURNING (error) +# +DELETE t1 FROM t1 JOIN t2 ON t1.a = t2.c RETURNING *; +ERROR 42000: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'RETURNING *' at line 1 +# +# Test 13: Prepared statement with DELETE ... RETURNING +# +PREPARE stmt FROM "DELETE FROM t1 WHERE a=2 ORDER BY b LIMIT 1 RETURNING a, UPPER(b)"; +EXECUTE stmt; +a UPPER(b) +2 BB +SELECT * FROM t1; +a b +7 ggggggg +1 a +3 ccc +4 dddd +1 A +4 DDDD +5 EEEEE +7 GGGGGGG +2 bb +EXECUTE stmt; +a UPPER(b) +2 BB +SELECT * FROM t1; +a b +7 ggggggg +1 a +3 ccc +4 dddd +1 A +4 DDDD +5 EEEEE +7 GGGGGGG +DEALLOCATE PREPARE stmt; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# +# Test 14: DELETE ... RETURNING with ORDER BY and LIMIT +# +DELETE FROM t1 WHERE a=1 ORDER BY b LIMIT 1 RETURNING a, b; +a b +1 a +SELECT * FROM t1; +a b +7 ggggggg +3 ccc +4 dddd +1 A +2 BB +4 DDDD +5 EEEEE +7 GGGGGGG +2 bb +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# +# Test 15: DELETE ... RETURNING with aliases +# +DELETE FROM t1 WHERE a=3 RETURNING a AS id, UPPER(b) AS upper_b, LENGTH(b) AS len; +id upper_b len +3 CCC 3 +SELECT * FROM t1; +a b +7 ggggggg +1 a +4 dddd +1 A +2 BB +4 DDDD +5 EEEEE +7 GGGGGGG +2 bb +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# +# Test 16: DELETE ... RETURNING table-qualified column references +# +DELETE FROM t1 WHERE a=3 RETURNING t1.a, t1.b; +a b +3 ccc +SELECT * FROM t1; +a b +7 ggggggg +1 a +4 dddd +1 A +2 BB +4 DDDD +5 EEEEE +7 GGGGGGG +2 bb +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# +# Test 17: DELETE ... RETURNING with IN subquery +# +DELETE FROM t1 WHERE a=7 RETURNING a, b, a IN (SELECT c FROM t2) AS in_t2; +a b in_t2 +7 ggggggg 1 +7 GGGGGGG 1 +SELECT * FROM t1; +a b +1 a +3 ccc +4 dddd +1 A +2 BB +4 DDDD +5 EEEEE +2 bb +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# +# Test 18: DELETE ... RETURNING with EXISTS subquery +# +DELETE FROM t1 WHERE a=7 RETURNING a, b, EXISTS(SELECT * FROM t2 WHERE c = a) AS exists_t2; +a b exists_t2 +7 ggggggg 1 +7 GGGGGGG 1 +SELECT * FROM t1; +a b +1 a +3 ccc +4 dddd +1 A +2 BB +4 DDDD +5 EEEEE +2 bb +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# +# Test 19: DELETE ... RETURNING with arithmetic expressions and functions +# +DELETE FROM t1 WHERE a <= 2 +RETURNING a, b, a * 10 AS a_times_10, LEAST(a, LENGTH(b)) AS least_val; +a b a_times_10 least_val +1 a 10 1 +1 A 10 1 +2 BB 20 2 +2 bb 20 2 +SELECT * FROM t1; +a b +7 ggggggg +3 ccc +4 dddd +4 DDDD +5 EEEEE +7 GGGGGGG +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# +# Test 20: Subquery in RETURNING returning more than 1 row (error) +# +DELETE FROM t1 WHERE a=7 ORDER BY b LIMIT 1 RETURNING (SELECT c FROM t2); +ERROR 21000: Subquery returns more than 1 row +# +# Test 21: Stored procedure with DELETE ... RETURNING +# +CREATE PROCEDURE p1(IN val INT) +BEGIN +DELETE FROM t1 WHERE a = val RETURNING *; +INSERT INTO t1 VALUES (val, 'reinserted'); +END| +CALL p1(1); +a b +1 a +1 A +SELECT * FROM t1; +a b +7 ggggggg +3 ccc +4 dddd +2 BB +4 DDDD +5 EEEEE +7 GGGGGGG +2 bb +1 reinserted +DROP PROCEDURE p1; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# +# Test 22: DELETE ... RETURNING with ONLY_FULL_GROUP_BY sql_mode +# +SET @sql_mode_save = @@sql_mode; +SET sql_mode = 'ONLY_FULL_GROUP_BY'; +DELETE FROM t1 WHERE a > 5 RETURNING *; +a b +7 ggggggg +7 GGGGGGG +SET sql_mode = @sql_mode_save; +SELECT * FROM t1; +a b +1 a +3 ccc +4 dddd +1 A +2 BB +4 DDDD +5 EEEEE +2 bb +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# +# Test 23: DELETE ... RETURNING all rows (no WHERE) +# +DELETE FROM t1 RETURNING a, b; +a b +7 ggggggg +1 a +3 ccc +4 dddd +1 A +2 BB +4 DDDD +5 EEEEE +7 GGGGGGG +2 bb +SELECT * FROM t1; +a b +INSERT INTO t1 SELECT * FROM t1c; +# +# Test 24: DELETE ... RETURNING with PARTITION +# +CREATE TABLE t3 (a INT, b VARCHAR(32)) +PARTITION BY HASH(a) PARTITIONS 4; +INSERT INTO t3 SELECT * FROM t1c; +DELETE FROM t3 PARTITION (p0) WHERE a=4 RETURNING *; +a b +4 dddd +4 DDDD +SELECT * FROM t3 WHERE a=4; +a b +DROP TABLE t3; +# +# Test 25: EXPLAIN DELETE ... RETURNING (no data returned, no rows deleted) +# +EXPLAIN DELETE FROM t1 WHERE a=2 RETURNING *; +id select_type table partitions type possible_keys key key_len ref rows filtered Extra +1 DELETE t1 NULL ALL NULL NULL NULL NULL X 100.00 Using where +Warnings: +Note 1003 delete from `test`.`t1` where (`test`.`t1`.`a` = 2) +EXPLAIN FORMAT=TREE DELETE FROM t1 WHERE a=2 RETURNING *; +EXPLAIN +-> + +SELECT * FROM t1; +a b +7 ggggggg +1 a +3 ccc +4 dddd +1 A +2 BB +4 DDDD +5 EEEEE +7 GGGGGGG +2 bb +# +# Test 26: DELETE ... RETURNING with triggers +# +CREATE TABLE trigger_log (msg VARCHAR(100)); +CREATE TRIGGER t1_before_del BEFORE DELETE ON t1 +FOR EACH ROW +BEGIN +INSERT INTO trigger_log VALUES (CONCAT('before_del: a=', OLD.a, ' b=', OLD.b)); +END| +CREATE TRIGGER t1_after_del AFTER DELETE ON t1 +FOR EACH ROW +BEGIN +INSERT INTO trigger_log VALUES (CONCAT('after_del: a=', OLD.a, ' b=', OLD.b)); +END| +DELETE FROM t1 WHERE a=3 RETURNING a, b; +a b +3 ccc +SELECT * FROM trigger_log; +msg +before_del: a=3 b=ccc +after_del: a=3 b=ccc +SELECT * FROM t1; +a b +7 ggggggg +1 a +4 dddd +1 A +2 BB +4 DDDD +5 EEEEE +7 GGGGGGG +2 bb +DROP TRIGGER t1_before_del; +DROP TRIGGER t1_after_del; +DROP TABLE trigger_log; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# +# Test 27: RETURNING only shows rows that were actually deleted +# (BEFORE trigger rejects some rows) +# +CREATE TRIGGER t1_guard BEFORE DELETE ON t1 +FOR EACH ROW +BEGIN +IF OLD.a = 2 THEN +SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cannot delete a=2'; +END IF; +END| +DELETE FROM t1 WHERE a IN (1, 2) RETURNING a, b; +ERROR 45000: Cannot delete a=2 +SELECT * FROM t1 WHERE a IN (1, 2); +a b +1 a +1 A +2 BB +2 bb +DROP TRIGGER t1_guard; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# +# Test 28: DELETE ... RETURNING requires SELECT privilege +# +CREATE DATABASE delret_priv_db; +CREATE TABLE delret_priv_db.tp (a INT, b VARCHAR(32)); +INSERT INTO delret_priv_db.tp VALUES (1,'one'), (2,'two'), (3,'three'); +CREATE USER 'test_delret'@'localhost'; +GRANT DELETE ON delret_priv_db.tp TO 'test_delret'@'localhost'; +GRANT SELECT(a) ON delret_priv_db.tp TO 'test_delret'@'localhost'; +DELETE FROM tp WHERE a=999; +DELETE FROM tp WHERE a=2 RETURNING *; +ERROR 42000: SELECT command denied to user 'test_delret'@'localhost' for table 'tp' +GRANT SELECT ON delret_priv_db.tp TO 'test_delret'@'localhost'; +DELETE FROM tp WHERE a=2 RETURNING a, b; +a b +2 two +CREATE SQL SECURITY INVOKER VIEW delret_priv_db.v_inv AS +SELECT a, b FROM delret_priv_db.tp; +CREATE SQL SECURITY DEFINER VIEW delret_priv_db.v_def AS +SELECT a, b FROM delret_priv_db.tp; +CREATE USER 'test_delret_v'@'localhost'; +GRANT SELECT, DELETE ON delret_priv_db.v_inv TO 'test_delret_v'@'localhost'; +GRANT SELECT, DELETE ON delret_priv_db.v_def TO 'test_delret_v'@'localhost'; +GRANT DELETE ON delret_priv_db.tp TO 'test_delret_v'@'localhost'; +# DEFINER view: the definer has SELECT on the underlying table, so both +# a plain SELECT and DELETE ... RETURNING are allowed. +SELECT b FROM v_def; +b +one +three +DELETE FROM v_def WHERE a=3 RETURNING b; +b +three +# INVOKER view: the invoker lacks SELECT on the underlying table, so +# DELETE ... RETURNING is rejected the same way a plain SELECT is. +SELECT b FROM v_inv; +ERROR HY000: View 'delret_priv_db.v_inv' references invalid table(s) or column(s) or function(s) or definer/invoker of view lack rights to use them +DELETE FROM v_inv WHERE a=1 RETURNING b; +ERROR HY000: View 'delret_priv_db.v_inv' references invalid table(s) or column(s) or function(s) or definer/invoker of view lack rights to use them +DROP USER 'test_delret'@'localhost', 'test_delret_v'@'localhost'; +DROP DATABASE delret_priv_db; +# +# Test 29: ORDER BY may refer to an alias from the RETURNING list +# +DELETE FROM t1 WHERE a > 3 ORDER BY c LIMIT 2 RETURNING a + LENGTH(b) AS c; +c +8 +8 +SELECT * FROM t1; +a b +7 ggggggg +1 a +3 ccc +1 A +2 BB +5 EEEEE +7 GGGGGGG +2 bb +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# An alias hides a column of the same name, as in SELECT: ordering by +# the alias -a picks the row with the largest a, not the first b. +DELETE FROM t1 WHERE a IN (1,3) ORDER BY b LIMIT 1 RETURNING -a AS b; +b +-3 +SELECT * FROM t1; +a b +7 ggggggg +1 a +4 dddd +1 A +2 BB +4 DDDD +5 EEEEE +7 GGGGGGG +2 bb +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# A column not in the RETURNING list is still resolvable in ORDER BY +DELETE FROM t1 WHERE a = 2 ORDER BY b LIMIT 1 RETURNING a; +a +2 +SELECT * FROM t1; +a b +7 ggggggg +1 a +3 ccc +4 dddd +1 A +4 DDDD +5 EEEEE +7 GGGGGGG +2 bb +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +# Unknown name in ORDER BY is still an error +DELETE FROM t1 WHERE a = 2 ORDER BY nosuch RETURNING a; +ERROR 42S22: Unknown column 'nosuch' in 'order clause' +# +# Cleanup +# +DROP TABLE t1, t2, t1c, t2c; diff --git a/mysql-test/r/information_schema_keywords.result b/mysql-test/r/information_schema_keywords.result index cfda862f0afe..1bab8e335c63 100644 --- a/mysql-test/r/information_schema_keywords.result +++ b/mysql-test/r/information_schema_keywords.result @@ -537,7 +537,7 @@ RESUME 0 RETAIN 0 RETURN 1 RETURNED_SQLSTATE 0 -RETURNING 0 +RETURNING 1 RETURNS 0 REUSE 0 REVERSE 0 diff --git a/mysql-test/suite/json/r/json_value.result b/mysql-test/suite/json/r/json_value.result index fb6863bec764..b22accf1a42a 100644 --- a/mysql-test/suite/json/r/json_value.result +++ b/mysql-test/suite/json/r/json_value.result @@ -423,10 +423,10 @@ CREATE TABLE json_value(json_value JSON); SELECT JSON_VALUE(json_value, '$.a') AS json_value FROM json_value; json_value DROP TABLE json_value; -CREATE TABLE returning(returning JSON); -SELECT JSON_VALUE(returning, '$.a' RETURNING CHAR) AS returning FROM returning; +CREATE TABLE `returning`(`returning` JSON); +SELECT JSON_VALUE(`returning`, '$.a' RETURNING CHAR) AS `returning` FROM `returning`; returning -DROP TABLE returning; +DROP TABLE `returning`; # Test ON EMPTY clause SELECT JSON_VALUE('{"data": 123}', '$.num' RETURNING SIGNED) v; v diff --git a/mysql-test/suite/json/t/json_value.test b/mysql-test/suite/json/t/json_value.test index 5b27c9931d99..f999acc4df7c 100644 --- a/mysql-test/suite/json/t/json_value.test +++ b/mysql-test/suite/json/t/json_value.test @@ -276,10 +276,11 @@ CREATE TABLE json_value(json_value JSON); SELECT JSON_VALUE(json_value, '$.a') AS json_value FROM json_value; DROP TABLE json_value; -# RETURNING is a non-reserved word both in the standard and in MySQL. -CREATE TABLE returning(returning JSON); -SELECT JSON_VALUE(returning, '$.a' RETURNING CHAR) AS returning FROM returning; -DROP TABLE returning; +# RETURNING is a non-reserved word in the SQL standard, but reserved in +# MySQL, so it can only be used as an identifier when quoted. +CREATE TABLE `returning`(`returning` JSON); +SELECT JSON_VALUE(`returning`, '$.a' RETURNING CHAR) AS `returning` FROM `returning`; +DROP TABLE `returning`; --echo # Test ON EMPTY clause SELECT JSON_VALUE('{"data": 123}', '$.num' RETURNING SIGNED) v; diff --git a/mysql-test/t/delete_returning.test b/mysql-test/t/delete_returning.test new file mode 100644 index 000000000000..c689a83aa1a1 --- /dev/null +++ b/mysql-test/t/delete_returning.test @@ -0,0 +1,393 @@ +# +# Tests for DELETE ... RETURNING +# + +--echo # +--echo # Setup +--echo # + +CREATE TABLE t1 (a INT, b VARCHAR(32)); +INSERT INTO t1 VALUES + (7,'ggggggg'), (1,'a'), (3,'ccc'), + (4,'dddd'), (1,'A'), (2,'BB'), + (4,'DDDD'), (5,'EEEEE'), (7,'GGGGGGG'), (2,'bb'); + +CREATE TABLE t1c SELECT * FROM t1; + +CREATE TABLE t2 (c INT); +INSERT INTO t2 VALUES (4), (5), (7), (1); + +CREATE TABLE t2c SELECT * FROM t2; + +--echo # +--echo # Test 1: DELETE ... RETURNING * +--echo # +DELETE FROM t1 WHERE a=2 RETURNING *; +SELECT * FROM t1; +INSERT INTO t1 VALUES (2,'BB'), (2,'bb'); + +--echo # +--echo # Test 2: DELETE ... RETURNING single column +--echo # +DELETE FROM t1 WHERE a=2 RETURNING b; +SELECT * FROM t1; + +--echo # +--echo # Test 3: DELETE ... RETURNING non-existing column (error) +--echo # +INSERT INTO t1 VALUES (2,'BB'), (2,'bb'); +--error ER_BAD_FIELD_ERROR +DELETE FROM t1 WHERE a=2 RETURNING c; + +--echo # +--echo # Test 4: DELETE ... RETURNING column and expression +--echo # +DELETE FROM t1 WHERE a=2 RETURNING a, UPPER(b); +SELECT * FROM t1; +INSERT INTO t1 VALUES (2,'BB'), (2,'bb'); + +--echo # +--echo # Test 5: DELETE ... RETURNING with no rows matching (empty result set) +--echo # +DELETE FROM t1 WHERE a=999 ORDER BY a RETURNING b; +SELECT * FROM t1; + +--echo # +--echo # Test 6: DELETE ... RETURNING with aggregate or window function (error) +--echo # +--error ER_INVALID_GROUP_FUNC_USE +DELETE FROM t1 WHERE a=2 RETURNING MAX(b); +--error ER_WINDOW_INVALID_WINDOW_FUNC_USE +DELETE FROM t1 WHERE a=2 RETURNING ROW_NUMBER() OVER (); +--error ER_WINDOW_INVALID_WINDOW_FUNC_USE +DELETE FROM t1 WHERE a=2 RETURNING SUM(a) OVER (ORDER BY a); + +--echo # +--echo # Test 7: DELETE ... RETURNING with correlated scalar subquery +--echo # +DELETE FROM t1 WHERE a < 5 RETURNING a, (SELECT MIN(c) FROM t2 WHERE c = a+1); +SELECT * FROM t1; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # +--echo # Test 8: DELETE ... RETURNING with subquery using GROUP_CONCAT +--echo # +DELETE FROM t2 WHERE c < 5 + RETURNING (SELECT GROUP_CONCAT(b) FROM t1 GROUP BY a HAVING a = c); +SELECT * FROM t2; +DELETE FROM t2; +INSERT INTO t2 SELECT * FROM t2c; + +--echo # +--echo # Test 9: DELETE ... RETURNING with user-defined function +--echo # +DELIMITER |; +CREATE FUNCTION f1(arg INT) RETURNS TEXT +BEGIN + RETURN (SELECT GROUP_CONCAT(b) FROM t1 WHERE a = arg); +END| +DELIMITER ;| + +DELETE FROM t2 WHERE c < 5 RETURNING f1(c); +SELECT * FROM t2; +DELETE FROM t2; +INSERT INTO t2 SELECT * FROM t2c; +DROP FUNCTION f1; + +--echo # +--echo # Test 10: DELETE from updatable view with RETURNING +--echo # +CREATE VIEW v1 AS SELECT a, b FROM t1; +DELETE FROM v1 WHERE a < 3 RETURNING *; +SELECT * FROM t1; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; +DROP VIEW v1; + +--echo # +--echo # Test 11: DELETE from non-updatable view (error) +--echo # +CREATE VIEW v1 AS SELECT a, COUNT(b) AS cnt FROM t1 GROUP BY a; +--error ER_NON_UPDATABLE_TABLE +DELETE FROM v1 WHERE a < 5 RETURNING *; +DROP VIEW v1; + +--echo # +--echo # Test 12: Multi-table DELETE with RETURNING (error) +--echo # +--error ER_PARSE_ERROR +DELETE t1 FROM t1 JOIN t2 ON t1.a = t2.c RETURNING *; + +--echo # +--echo # Test 13: Prepared statement with DELETE ... RETURNING +--echo # +PREPARE stmt FROM "DELETE FROM t1 WHERE a=2 ORDER BY b LIMIT 1 RETURNING a, UPPER(b)"; +EXECUTE stmt; +SELECT * FROM t1; +EXECUTE stmt; +SELECT * FROM t1; +DEALLOCATE PREPARE stmt; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # +--echo # Test 14: DELETE ... RETURNING with ORDER BY and LIMIT +--echo # +DELETE FROM t1 WHERE a=1 ORDER BY b LIMIT 1 RETURNING a, b; +SELECT * FROM t1; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # +--echo # Test 15: DELETE ... RETURNING with aliases +--echo # +DELETE FROM t1 WHERE a=3 RETURNING a AS id, UPPER(b) AS upper_b, LENGTH(b) AS len; +SELECT * FROM t1; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # +--echo # Test 16: DELETE ... RETURNING table-qualified column references +--echo # +DELETE FROM t1 WHERE a=3 RETURNING t1.a, t1.b; +SELECT * FROM t1; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # +--echo # Test 17: DELETE ... RETURNING with IN subquery +--echo # +DELETE FROM t1 WHERE a=7 RETURNING a, b, a IN (SELECT c FROM t2) AS in_t2; +SELECT * FROM t1; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # +--echo # Test 18: DELETE ... RETURNING with EXISTS subquery +--echo # +DELETE FROM t1 WHERE a=7 RETURNING a, b, EXISTS(SELECT * FROM t2 WHERE c = a) AS exists_t2; +SELECT * FROM t1; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # +--echo # Test 19: DELETE ... RETURNING with arithmetic expressions and functions +--echo # +DELETE FROM t1 WHERE a <= 2 + RETURNING a, b, a * 10 AS a_times_10, LEAST(a, LENGTH(b)) AS least_val; +SELECT * FROM t1; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # +--echo # Test 20: Subquery in RETURNING returning more than 1 row (error) +--echo # +--error ER_SUBQUERY_NO_1_ROW +DELETE FROM t1 WHERE a=7 ORDER BY b LIMIT 1 RETURNING (SELECT c FROM t2); + +--echo # +--echo # Test 21: Stored procedure with DELETE ... RETURNING +--echo # +DELIMITER |; +CREATE PROCEDURE p1(IN val INT) +BEGIN + DELETE FROM t1 WHERE a = val RETURNING *; + INSERT INTO t1 VALUES (val, 'reinserted'); +END| +DELIMITER ;| + +CALL p1(1); +SELECT * FROM t1; +DROP PROCEDURE p1; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # +--echo # Test 22: DELETE ... RETURNING with ONLY_FULL_GROUP_BY sql_mode +--echo # +SET @sql_mode_save = @@sql_mode; +SET sql_mode = 'ONLY_FULL_GROUP_BY'; +DELETE FROM t1 WHERE a > 5 RETURNING *; +SET sql_mode = @sql_mode_save; +SELECT * FROM t1; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # +--echo # Test 23: DELETE ... RETURNING all rows (no WHERE) +--echo # +DELETE FROM t1 RETURNING a, b; +SELECT * FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # +--echo # Test 24: DELETE ... RETURNING with PARTITION +--echo # +CREATE TABLE t3 (a INT, b VARCHAR(32)) + PARTITION BY HASH(a) PARTITIONS 4; +INSERT INTO t3 SELECT * FROM t1c; + +DELETE FROM t3 PARTITION (p0) WHERE a=4 RETURNING *; +SELECT * FROM t3 WHERE a=4; +DROP TABLE t3; + +--echo # +--echo # Test 25: EXPLAIN DELETE ... RETURNING (no data returned, no rows deleted) +--echo # +--replace_column 10 X +EXPLAIN DELETE FROM t1 WHERE a=2 RETURNING *; +EXPLAIN FORMAT=TREE DELETE FROM t1 WHERE a=2 RETURNING *; +SELECT * FROM t1; + +--echo # +--echo # Test 26: DELETE ... RETURNING with triggers +--echo # +CREATE TABLE trigger_log (msg VARCHAR(100)); + +DELIMITER |; +CREATE TRIGGER t1_before_del BEFORE DELETE ON t1 +FOR EACH ROW +BEGIN + INSERT INTO trigger_log VALUES (CONCAT('before_del: a=', OLD.a, ' b=', OLD.b)); +END| + +CREATE TRIGGER t1_after_del AFTER DELETE ON t1 +FOR EACH ROW +BEGIN + INSERT INTO trigger_log VALUES (CONCAT('after_del: a=', OLD.a, ' b=', OLD.b)); +END| +DELIMITER ;| + +DELETE FROM t1 WHERE a=3 RETURNING a, b; +SELECT * FROM trigger_log; +SELECT * FROM t1; + +DROP TRIGGER t1_before_del; +DROP TRIGGER t1_after_del; +DROP TABLE trigger_log; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # +--echo # Test 27: RETURNING only shows rows that were actually deleted +--echo # (BEFORE trigger rejects some rows) +--echo # +DELIMITER |; +CREATE TRIGGER t1_guard BEFORE DELETE ON t1 +FOR EACH ROW +BEGIN + IF OLD.a = 2 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cannot delete a=2'; + END IF; +END| +DELIMITER ;| + +--error 1644 +DELETE FROM t1 WHERE a IN (1, 2) RETURNING a, b; +# Verify: no rows were deleted (statement aborted on trigger error) +SELECT * FROM t1 WHERE a IN (1, 2); +DROP TRIGGER t1_guard; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # +--echo # Test 28: DELETE ... RETURNING requires SELECT privilege +--echo # +CREATE DATABASE delret_priv_db; +CREATE TABLE delret_priv_db.tp (a INT, b VARCHAR(32)); +INSERT INTO delret_priv_db.tp VALUES (1,'one'), (2,'two'), (3,'three'); + +CREATE USER 'test_delret'@'localhost'; +GRANT DELETE ON delret_priv_db.tp TO 'test_delret'@'localhost'; +# Grant column-level SELECT on 'a' so WHERE a=... works for plain DELETE +GRANT SELECT(a) ON delret_priv_db.tp TO 'test_delret'@'localhost'; + +connect (con1, localhost, test_delret,,delret_priv_db); +connection con1; + +# Plain DELETE with WHERE should work (has DELETE + column SELECT on 'a') +DELETE FROM tp WHERE a=999; + +# DELETE ... RETURNING * requires table-level SELECT which we don't have +--error ER_TABLEACCESS_DENIED_ERROR +DELETE FROM tp WHERE a=2 RETURNING *; + +connection default; +disconnect con1; + +# Grant full table-level SELECT and verify RETURNING now works +GRANT SELECT ON delret_priv_db.tp TO 'test_delret'@'localhost'; + +connect (con2, localhost, test_delret,,delret_priv_db); +connection con2; + +DELETE FROM tp WHERE a=2 RETURNING a, b; + +connection default; +disconnect con2; + +# Through a view, privileges are checked on the view and, for SQL SECURITY +# INVOKER views, on the underlying table as well - exactly as for a SELECT +# through the same view. +CREATE SQL SECURITY INVOKER VIEW delret_priv_db.v_inv AS + SELECT a, b FROM delret_priv_db.tp; +CREATE SQL SECURITY DEFINER VIEW delret_priv_db.v_def AS + SELECT a, b FROM delret_priv_db.tp; + +CREATE USER 'test_delret_v'@'localhost'; +GRANT SELECT, DELETE ON delret_priv_db.v_inv TO 'test_delret_v'@'localhost'; +GRANT SELECT, DELETE ON delret_priv_db.v_def TO 'test_delret_v'@'localhost'; +# DELETE but no SELECT on the table the views read from +GRANT DELETE ON delret_priv_db.tp TO 'test_delret_v'@'localhost'; + +connect (con3, localhost, test_delret_v,,delret_priv_db); +connection con3; + +--echo # DEFINER view: the definer has SELECT on the underlying table, so both +--echo # a plain SELECT and DELETE ... RETURNING are allowed. +SELECT b FROM v_def; +DELETE FROM v_def WHERE a=3 RETURNING b; + +--echo # INVOKER view: the invoker lacks SELECT on the underlying table, so +--echo # DELETE ... RETURNING is rejected the same way a plain SELECT is. +--error ER_VIEW_INVALID +SELECT b FROM v_inv; +--error ER_VIEW_INVALID +DELETE FROM v_inv WHERE a=1 RETURNING b; + +connection default; +disconnect con3; + +DROP USER 'test_delret'@'localhost', 'test_delret_v'@'localhost'; +DROP DATABASE delret_priv_db; + +--echo # +--echo # Test 29: ORDER BY may refer to an alias from the RETURNING list +--echo # +DELETE FROM t1 WHERE a > 3 ORDER BY c LIMIT 2 RETURNING a + LENGTH(b) AS c; +SELECT * FROM t1; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # An alias hides a column of the same name, as in SELECT: ordering by +--echo # the alias -a picks the row with the largest a, not the first b. +DELETE FROM t1 WHERE a IN (1,3) ORDER BY b LIMIT 1 RETURNING -a AS b; +SELECT * FROM t1; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # A column not in the RETURNING list is still resolvable in ORDER BY +DELETE FROM t1 WHERE a = 2 ORDER BY b LIMIT 1 RETURNING a; +SELECT * FROM t1; +DELETE FROM t1; +INSERT INTO t1 SELECT * FROM t1c; + +--echo # Unknown name in ORDER BY is still an error +--error ER_BAD_FIELD_ERROR +DELETE FROM t1 WHERE a = 2 ORDER BY nosuch RETURNING a; + +--echo # +--echo # Cleanup +--echo # +DROP TABLE t1, t2, t1c, t2c; diff --git a/sql/parse_tree_nodes.cc b/sql/parse_tree_nodes.cc index 327208cc60f4..a167e0e39e2f 100644 --- a/sql/parse_tree_nodes.cc +++ b/sql/parse_tree_nodes.cc @@ -1308,7 +1308,16 @@ Sql_cmd *PT_delete::make_cmd(THD *thd) { if (opt_hints != nullptr && opt_hints->contextualize(&pc)) return nullptr; - return new (thd->mem_root) Sql_cmd_delete(is_multitable(), &delete_tables); + // Handle RETURNING clause: contextualize items and populate select->fields + if (opt_returning_list != nullptr) { + select->parsing_place = CTX_SELECT_LIST; + if (opt_returning_list->contextualize(&pc)) return nullptr; + select->parsing_place = CTX_NONE; + select->fields = opt_returning_list->value; + } + + return new (thd->mem_root) Sql_cmd_delete(is_multitable(), &delete_tables, + opt_returning_list != nullptr); } Sql_cmd *PT_update::make_cmd(THD *thd) { diff --git a/sql/parse_tree_nodes.h b/sql/parse_tree_nodes.h index f0451a7015ff..90bb5af284cd 100644 --- a/sql/parse_tree_nodes.h +++ b/sql/parse_tree_nodes.h @@ -2002,6 +2002,7 @@ class PT_delete final : public Parse_tree_root { Mem_root_array_YY join_table_list{}; Item *opt_where_clause; PT_order *opt_order_clause; + PT_item_list *opt_returning_list; Item *opt_delete_limit_clause; SQL_I_List delete_tables; @@ -2012,7 +2013,8 @@ class PT_delete final : public Parse_tree_root { Table_ident *table_ident_arg, const LEX_CSTRING &opt_table_alias_arg, List *opt_use_partition_arg, Item *opt_where_clause_arg, - PT_order *opt_order_clause_arg, Item *opt_delete_limit_clause_arg) + PT_order *opt_order_clause_arg, Item *opt_delete_limit_clause_arg, + PT_item_list *opt_returning_list_arg) : super(pos), m_with_clause(with_clause_arg), opt_hints(opt_hints_arg), @@ -2022,6 +2024,7 @@ class PT_delete final : public Parse_tree_root { opt_use_partition(opt_use_partition_arg), opt_where_clause(opt_where_clause_arg), opt_order_clause(opt_order_clause_arg), + opt_returning_list(opt_returning_list_arg), opt_delete_limit_clause(opt_delete_limit_clause_arg) { table_list.init_empty_const(); join_table_list.init_empty_const(); @@ -2044,6 +2047,7 @@ class PT_delete final : public Parse_tree_root { join_table_list(join_table_list_arg), opt_where_clause(opt_where_clause_arg), opt_order_clause(nullptr), + opt_returning_list(nullptr), opt_delete_limit_clause(nullptr) {} Sql_cmd *make_cmd(THD *thd) override; diff --git a/sql/sp.cc b/sql/sp.cc index f96c64f8154d..436ae1949d79 100644 --- a/sql/sp.cc +++ b/sql/sp.cc @@ -82,6 +82,7 @@ #include "sql/sp_head.h" // Stored_program_creation_ctx #include "sql/sp_pcontext.h" // sp_pcontext #include "sql/sql_class.h" +#include "sql/sql_cmd_dml.h" // Sql_cmd_dml #include "sql/sql_const.h" #include "sql/sql_db.h" // get_default_db_collation #include "sql/sql_digest_stream.h" @@ -2840,6 +2841,13 @@ uint sp_get_flags_for_command(LEX *lex) { break; default: flags = lex->is_explain() ? sp_head::MULTI_RESULTS : 0; + // A DML statement with a RETURNING clause produces a result set. + // This branch also covers commands with no Sql_cmd object at all, + // hence the null check. + if (lex->m_sql_cmd != nullptr && + lex->m_sql_cmd->sql_cmd_type() == SQL_CMD_DML && + down_cast(lex->m_sql_cmd)->has_returning()) + flags |= sp_head::MULTI_RESULTS; break; } return flags; diff --git a/sql/sql_cmd_dml.h b/sql/sql_cmd_dml.h index eaaa9156823e..b82468b0bd7d 100644 --- a/sql/sql_cmd_dml.h +++ b/sql/sql_cmd_dml.h @@ -103,13 +103,18 @@ class Sql_cmd_dml : public Sql_cmd { /// Signal that DML statement can have dynamic parameters bool are_dynamic_parameters_allowed() const final { return true; } + /// @return true if statement has a RETURNING clause, and thus produces a + /// result set instead of an OK packet + bool has_returning() const { return m_returning; } + protected: Sql_cmd_dml() : Sql_cmd(), lex(nullptr), result(nullptr), m_empty_query(false), - m_lazy_result(false) {} + m_lazy_result(false), + m_returning(false) {} /// @return true if query is guaranteed to return no data /** @@ -223,6 +228,7 @@ class Sql_cmd_dml : public Sql_cmd { Query_result *result; ///< Pointer to object for handling of the result bool m_empty_query; ///< True if query will produce no rows bool m_lazy_result; ///< True: prepare query result on next execution + bool m_returning; ///< True if statement has a RETURNING clause }; #endif /* SQL_CMD_DML_INCLUDED */ diff --git a/sql/sql_delete.cc b/sql/sql_delete.cc index 4b8a602cb5df..402d1bb9af48 100644 --- a/sql/sql_delete.cc +++ b/sql/sql_delete.cc @@ -62,6 +62,7 @@ #include "sql/opt_explain.h" // Modification_plan #include "sql/opt_explain_format.h" #include "sql/opt_trace.h" // Opt_trace_object +#include "sql/protocol.h" #include "sql/psi_memory_key.h" #include "sql/query_options.h" #include "sql/query_result.h" @@ -163,7 +164,11 @@ bool Sql_cmd_delete::precheck(THD *thd) { Table_ref *tables = lex->query_tables; if (!multitable) { - if (check_one_table_access(thd, DELETE_ACL, tables)) return true; + // DELETE ... RETURNING reads column data back to the client, so require + // SELECT privilege on the target table in addition to DELETE. + if (check_one_table_access( + thd, m_returning ? DELETE_ACL | SELECT_ACL : DELETE_ACL, tables)) + return true; } else { Table_ref *aux_tables = delete_tables->first; Table_ref **save_query_tables_own_last = lex->query_tables_own_last; @@ -197,6 +202,24 @@ bool Sql_cmd_delete::check_privileges(THD *thd) { return false; } +bool Sql_cmd_delete::send_returning_metadata(THD *thd) { + assert(m_returning); + return result->send_result_set_metadata( + thd, lex->query_block->fields, + Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF); +} + +bool Sql_cmd_delete::send_delete_completed(THD *thd, ha_rows deleted_rows, + bool send_metadata) { + if (!m_returning) { + my_ok(thd, deleted_rows); + return false; + } + if (send_metadata && send_returning_metadata(thd)) return true; + thd->set_row_count_func(deleted_rows); + return result->send_eof(thd); +} + /** Delete a set of rows from a single table. @@ -332,6 +355,7 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) { */ if (!using_limit && const_cond_result && !no_rows && !(specialflag & SPECIAL_NO_NEW_FUNC) && + !m_returning && // Must read rows individually for RETURNING ((!thd->is_current_stmt_binlog_format_row() || // not ROW binlog-format thd->is_current_stmt_binlog_disabled()) && // no binlog for this // command @@ -415,8 +439,8 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) { explain_single_table_modification(thd, thd, &plan, query_block); return err; } - my_ok(thd, 0); - return false; + // Empty result set for RETURNING, OK packet otherwise + return send_delete_completed(thd, 0, /*send_metadata=*/true); } } @@ -460,8 +484,8 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) { return err; } - my_ok(thd, 0); - return false; // Nothing to delete + // Nothing to delete: empty result set for RETURNING, OK packet otherwise + return send_delete_completed(thd, 0, /*send_metadata=*/true); } } // Ends scope for optimizer trace wrapper @@ -595,10 +619,13 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) { if (thd->is_error()) return true; if ((table->file->ha_table_flags() & HA_READ_BEFORE_WRITE_REMOVAL) && - !using_limit && !has_delete_triggers && range_scan && + !using_limit && !has_delete_triggers && range_scan && !m_returning && used_index(range_scan) != MAX_KEY) read_removal = table->check_read_removal(used_index(range_scan)); + // Send result set metadata for RETURNING before the delete loop + if (m_returning && send_returning_metadata(thd)) return true; + assert(limit > 0); // The loop that reads rows and delete those that qualify @@ -650,6 +677,18 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) { break; } } + + // Send RETURNING data after successful deletion and trigger execution. + // This runs after AFTER DELETE triggers, but record[0] is unchanged: + // DELETE triggers only have OLD.* (read-only), no NEW.*, so the + // trigger cannot modify record[0]. + if (m_returning) { + if (result->send_data(thd, query_block->fields)) { + error = 1; + break; + } + } + if (!--limit && using_limit) { error = -1; break; @@ -722,7 +761,8 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) { assert(transactional_table || deleted_rows == 0 || thd->get_transaction()->cannot_safely_rollback(Transaction_ctx::STMT)); if (error < 0) { - my_ok(thd, deleted_rows); + if (send_delete_completed(thd, deleted_rows, /*send_metadata=*/false)) + return true; DBUG_PRINT("info", ("%ld records deleted", (long)deleted_rows)); } return error > 0; @@ -738,6 +778,12 @@ bool Sql_cmd_delete::prepare_inner(THD *thd) { Query_block *const select = lex->query_block; Table_ref *const table_list = select->get_table_list(); + // RETURNING is not supported in multi-table DELETE + if (m_returning && multitable) { + my_error(ER_NOT_SUPPORTED_YET, MYF(0), "RETURNING in multi-table DELETE"); + return true; + } + bool apply_semijoin; Mem_root_array sj_candidates_local(thd->mem_root); @@ -825,7 +871,7 @@ bool Sql_cmd_delete::prepare_inner(THD *thd) { // enables it to perform optimizations like sort avoidance and semi-join // flattening even if features specific to single-table DELETE (that is, ORDER // BY and LIMIT) are used. - if (lex->using_hypergraph_optimizer()) { + if (lex->using_hypergraph_optimizer() && !m_returning) { if (!multitable) { Table_ref *const delete_table_ref = table_list->updatable_base_table(); TABLE *const table = delete_table_ref->table; @@ -836,6 +882,7 @@ bool Sql_cmd_delete::prepare_inner(THD *thd) { } if (!multitable && select->first_inner_query_expression() != nullptr && + !m_returning && should_switch_to_multi_table_if_subqueries(thd, select, table_list)) multitable = true; @@ -878,6 +925,34 @@ bool Sql_cmd_delete::prepare_inner(THD *thd) { if (select->resolve_limits(thd)) return true; /* purecov: inspected */ + // Resolve the RETURNING clause expressions. This is done before ORDER BY, so + // that select->fields is fully resolved when setup_order() runs, and so that + // ORDER BY may refer to an alias defined in the RETURNING list, as in + // DELETE FROM t1 ORDER BY c RETURNING a+b AS c. + if (m_returning) { + // Expand wildcards (*, t1.*) in the RETURNING list before + // setup_base_ref_items, since expansion adds items to the list. + if (select->with_wild && select->setup_wild(thd)) return true; + + // (Re-)allocate base_ref_items after wildcard expansion may have + // increased the number of fields. + if (select->setup_base_ref_items(thd)) return true; + + if (setup_fields(thd, /*want_privilege=*/SELECT_ACL, + /*allow_sum_func=*/false, + /*split_sum_funcs=*/false, + /*column_update=*/false, + /*typed_items=*/nullptr, &select->fields, + select->base_ref_items)) + return true; + + // Set up Query_result_send for sending result set to client + result = new (thd->mem_root) Query_result_send; + if (result == nullptr) return true; /* purecov: inspected */ + select->set_query_result(result); + select->master_query_expression()->set_query_result(result); + } + // check ORDER BY even if it can be ignored if (select->order_list.first) { Table_ref tables; @@ -886,6 +961,7 @@ bool Sql_cmd_delete::prepare_inner(THD *thd) { tables.alias = table_list->alias; assert(!select->group_list.elements); + // A no-op when the RETURNING list above already allocated the array. if (select->setup_base_ref_items(thd)) return true; /* purecov: inspected */ if (setup_order(thd, select->base_ref_items, &tables, &select->fields, select->order_list.first)) @@ -955,8 +1031,8 @@ bool Sql_cmd_delete::execute_inner(THD *thd) { return explain_single_table_modification(thd, thd, &plan, lex->query_block); } - my_ok(thd); - return false; + // Empty result set for RETURNING, OK packet otherwise + return send_delete_completed(thd, 0, /*send_metadata=*/true); } return (multitable && !lex->query_block->get_table_list()->is_json_duality_view()) diff --git a/sql/sql_delete.h b/sql/sql_delete.h index e58c49aa037a..77434be7213d 100644 --- a/sql/sql_delete.h +++ b/sql/sql_delete.h @@ -24,6 +24,7 @@ #ifndef SQL_DELETE_INCLUDED #define SQL_DELETE_INCLUDED +#include "my_base.h" // ha_rows #include "my_sqlcommand.h" #include "my_table_map.h" #include "sql/sql_cmd_dml.h" // Sql_cmd_dml @@ -37,8 +38,11 @@ class SQL_I_List; class Sql_cmd_delete final : public Sql_cmd_dml { public: - Sql_cmd_delete(bool multitable_arg, SQL_I_List *delete_tables_arg) - : multitable(multitable_arg), delete_tables(delete_tables_arg) {} + Sql_cmd_delete(bool multitable_arg, SQL_I_List *delete_tables_arg, + bool returning_arg = false) + : multitable(multitable_arg), delete_tables(delete_tables_arg) { + m_returning = returning_arg; + } enum_sql_command sql_command_code() const override { return multitable ? SQLCOM_DELETE_MULTI : SQLCOM_DELETE; @@ -59,6 +63,31 @@ class Sql_cmd_delete final : public Sql_cmd_dml { private: bool delete_from_single_table(THD *thd); + /** + Send the column definitions of the RETURNING result set to the client. + Must be called before the first row is sent. + + @param thd thread handler + + @returns false if success, true if error + */ + bool send_returning_metadata(THD *thd); + + /** + Report successful completion of the statement to the client: end-of-file + terminating the RETURNING result set, or an OK packet for a plain DELETE. + + @param thd thread handler + @param deleted_rows number of deleted rows + @param send_metadata send the RETURNING column definitions first. Needed + when the statement completes without having sent any + row, so that the client sees an empty result set. + + @returns false if success, true if error + */ + bool send_delete_completed(THD *thd, ha_rows deleted_rows, + bool send_metadata); + bool multitable; /** References to tables that are deleted from in a multitable delete statement. diff --git a/sql/sql_lex.h b/sql/sql_lex.h index ae07561d70ca..c31dddb475b9 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -2321,6 +2321,10 @@ class Query_block : public Query_term { [[nodiscard]] bool limit_offset_preserves_first_row() const; + /// Expand wildcard (*) items in the field list. Public for use by + /// Sql_cmd_delete (RETURNING clause). + bool setup_wild(THD *thd); + private: friend class Query_expression; friend class Condition_context; @@ -2383,7 +2387,6 @@ class Query_block : public Query_term { Item *resolve_rollup_item(THD *thd, Item *item); bool resolve_rollup(THD *thd); - bool setup_wild(THD *thd); bool setup_order_final(THD *thd); bool setup_group(THD *thd); void fix_after_pullout(Query_block *parent_query_block, diff --git a/sql/sql_select.cc b/sql/sql_select.cc index 6885beca799c..c1876389f8d8 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -1317,6 +1317,9 @@ bool Sql_cmd_dml::check_all_table_privileges(THD *thd) { } if (tr->is_deleted()) { want_privilege |= DELETE_ACL; + // A RETURNING clause reads the rows that are deleted, so SELECT + // privilege is required in addition to DELETE. + if (has_returning()) want_privilege |= SELECT_ACL; } if (want_privilege == 0) { want_privilege = SELECT_ACL; diff --git a/sql/sql_yacc.yy b/sql/sql_yacc.yy index 4e51a99908f7..59c0954679a8 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -1366,7 +1366,7 @@ CHARSET_INFO *warn_on_deprecated_user_defined_collation( %token REQUIRE_TABLE_PRIMARY_KEY_CHECK_SYM 996 /* MYSQL */ %token STREAM_SYM 997 /* MYSQL */ %token OFF_SYM 998 /* SQL-1999-R */ -%token RETURNING_SYM 999 /* SQL-2016-N */ +%token RETURNING_SYM 999 /* SQL-2016-N */ /* Here is an intentional gap in token numbers. @@ -1721,6 +1721,7 @@ CHARSET_INFO *warn_on_deprecated_user_defined_collation( fields_or_vars opt_field_or_var_spec row_value_explicit + opt_delete_returning %type option_type opt_var_type opt_rvalue_system_variable_type @@ -14051,8 +14052,9 @@ delete_stmt: opt_where_clause opt_order_clause opt_simple_limit + opt_delete_returning { - $$= NEW_PTN PT_delete(@$, $1, $2, $3, $5, $6, $7, $8, $9, $10); + $$= NEW_PTN PT_delete(@$, $1, $2, $3, $5, $6, $7, $8, $9, $10, $11); } | opt_with_clause DELETE_SYM @@ -14077,6 +14079,14 @@ delete_stmt: } ; +opt_delete_returning: + %empty { $$ = nullptr; } + | RETURNING_SYM select_item_list + { + $$ = $2; + } + ; + opt_wild: %empty | '.' '*' @@ -16376,7 +16386,6 @@ ident_keywords_unambiguous: | RESUME_SYM | RETAIN_SYM | RETURNED_SQLSTATE_SYM - | RETURNING_SYM | RETURNS_SYM | REUSE_SYM | REVERSE_SYM