diff --git a/mysql-test/r/delete_returning.result b/mysql-test/r/delete_returning.result new file mode 100644 index 000000000000..0259f9e396e6 --- /dev/null +++ b/mysql-test/r/delete_returning.result @@ -0,0 +1,515 @@ +DROP TABLE IF EXISTS t1, t2; +DROP VIEW IF EXISTS v1; +DROP PROCEDURE IF EXISTS p1; +DROP FUNCTION IF EXISTS f1; +# +# 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 function (error) +# +DELETE FROM t1 WHERE a=2 RETURNING MAX(b); +ERROR HY000: Invalid use of group function +# +# 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) +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 +DROP USER 'test_delret'@'localhost'; +DROP DATABASE delret_priv_db; +# +# Cleanup +# +DROP TABLE t1, t2, t1c, t2c; diff --git a/mysql-test/suite/json/r/json_value.result b/mysql-test/suite/json/r/json_value.result index e25525457534..f7b3419b548a 100644 --- a/mysql-test/suite/json/r/json_value.result +++ b/mysql-test/suite/json/r/json_value.result @@ -433,10 +433,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 d14b65006482..284ef9aa824d 100644 --- a/mysql-test/suite/json/t/json_value.test +++ b/mysql-test/suite/json/t/json_value.test @@ -277,9 +277,12 @@ 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; +# However it cannot be used unquoted as an identifier in MySQL because +# it is not in ident_keywords_unambiguous (to avoid a grammar conflict with +# DELETE ... RETURNING). Backtick-quoting works. +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..cb8fe7c69ad8 --- /dev/null +++ b/mysql-test/t/delete_returning.test @@ -0,0 +1,338 @@ +# +# Tests for DELETE ... RETURNING +# + +--disable_warnings +DROP TABLE IF EXISTS t1, t2; +DROP VIEW IF EXISTS v1; +DROP PROCEDURE IF EXISTS p1; +DROP FUNCTION IF EXISTS f1; +--enable_warnings + +--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 function (error) +--echo # +--error ER_INVALID_GROUP_FUNC_USE +DELETE FROM t1 WHERE a=2 RETURNING MAX(b); + +--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 *; +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; + +DROP USER 'test_delret'@'localhost'; +DROP DATABASE delret_priv_db; + +--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 a44c9661befd..bbbf91e8493d 100644 --- a/sql/parse_tree_nodes.cc +++ b/sql/parse_tree_nodes.cc @@ -974,7 +974,17 @@ 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 db98bcdc5262..3b4f4104f486 100644 --- a/sql/parse_tree_nodes.h +++ b/sql/parse_tree_nodes.h @@ -1943,6 +1943,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; @@ -1953,7 +1954,9 @@ 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), @@ -1963,6 +1966,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(); @@ -1985,6 +1989,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 391fc0e11055..d5807b31ad9b 100644 --- a/sql/sp.cc +++ b/sql/sp.cc @@ -90,6 +90,7 @@ #include "sql/sql_parse.h" // parse_sql #include "sql/sql_show.h" // append_identifier #include "sql/sql_table.h" // write_bin_log +#include "sql/sql_delete.h" // Sql_cmd_delete #include "sql/strfunc.h" // lex_string_strmake #include "sql/system_variables.h" #include "sql/table.h" @@ -2433,6 +2434,11 @@ uint sp_get_flags_for_command(LEX *lex) { break; default: flags = lex->is_explain() ? sp_head::MULTI_RESULTS : 0; + // DELETE ... RETURNING produces a result set + if (lex->sql_command == SQLCOM_DELETE && + lex->m_sql_cmd != nullptr && + down_cast(lex->m_sql_cmd)->has_returning()) + flags |= sp_head::MULTI_RESULTS; break; } return flags; diff --git a/sql/sql_delete.cc b/sql/sql_delete.cc index a2c50b02d1ca..5739b4f93e41 100644 --- a/sql/sql_delete.cc +++ b/sql/sql_delete.cc @@ -64,6 +64,7 @@ #include "sql/psi_memory_key.h" #include "sql/query_options.h" #include "sql/query_result.h" +#include "sql/protocol.h" #include "sql/range_optimizer/partition_pruning.h" #include "sql/range_optimizer/path_helpers.h" #include "sql/range_optimizer/range_optimizer.h" @@ -155,7 +156,14 @@ 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 (m_returning) { + if (check_one_table_access(thd, DELETE_ACL | SELECT_ACL, tables)) + return true; + } else { + if (check_one_table_access(thd, 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; @@ -182,6 +190,15 @@ bool Sql_cmd_delete::precheck(THD *thd) { bool Sql_cmd_delete::check_privileges(THD *thd) { DBUG_TRACE; + // DELETE ... RETURNING requires SELECT privilege on the target table. + if (m_returning) { + Table_ref *const table_list = lex->query_block->get_table_list(); + assert(table_list != nullptr); + if (table_list == nullptr) return true; // Fail-closed if unexpectedly null + if (check_single_table_access(thd, SELECT_ACL, table_list, false)) + return true; + } + if (check_all_table_privileges(thd)) return true; if (lex->query_block->check_column_privileges(thd)) return true; @@ -322,6 +339,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 @@ -405,7 +423,16 @@ 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); + if (m_returning) { + if (result->send_result_set_metadata( + thd, query_block->fields, + Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF)) + return true; + thd->set_row_count_func(0); + if (result->send_eof(thd)) return true; + } else { + my_ok(thd, 0); + } return false; } } @@ -450,7 +477,16 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) { return err; } - my_ok(thd, 0); + if (m_returning) { + if (result->send_result_set_metadata( + thd, query_block->fields, + Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF)) + return true; + thd->set_row_count_func(0); + if (result->send_eof(thd)) return true; + } else { + my_ok(thd, 0); + } return false; // Nothing to delete } } // Ends scope for optimizer trace wrapper @@ -586,9 +622,18 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) { if ((table->file->ha_table_flags() & HA_READ_BEFORE_WRITE_REMOVAL) && !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) { + if (result->send_result_set_metadata( + thd, query_block->fields, + Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF)) + return true; + } + assert(limit > 0); // The loop that reads rows and delete those that qualify @@ -619,6 +664,17 @@ 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; @@ -683,7 +739,12 @@ 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 (m_returning) { + thd->set_row_count_func(deleted_rows); + if (result->send_eof(thd)) return true; + } else { + my_ok(thd, deleted_rows); + } DBUG_PRINT("info", ("%ld records deleted", (long)deleted_rows)); } return error > 0; @@ -699,6 +760,13 @@ 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); @@ -781,11 +849,12 @@ 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) { multitable = true; } if (!multitable && select->first_inner_query_expression() != nullptr && + !m_returning && should_switch_to_multi_table_if_subqueries(thd, select, table_list)) multitable = true; @@ -837,11 +906,71 @@ bool Sql_cmd_delete::prepare_inner(THD *thd) { assert(!select->group_list.elements); if (select->setup_base_ref_items(thd)) return true; /* purecov: inspected */ - if (setup_order(thd, select->base_ref_items, &tables, &select->fields, + // When RETURNING is present, select->fields contains unresolved RETURNING + // items. ORDER BY in DELETE resolves against table columns, not RETURNING, + // so pass an empty field list. + mem_root_deque empty_fields(thd->mem_root); + if (setup_order(thd, select->base_ref_items, &tables, + m_returning ? &empty_fields : &select->fields, select->order_list.first)) return true; } + // Resolve RETURNING clause expressions + if (m_returning) { + // Defense-in-depth: verify SELECT privilege on the target table. + // RETURNING reads row data, so the user must have SELECT in addition + // to DELETE. (precheck() also checks this, but re-verify here after + // tables are opened.) + Table_ref *target = select->get_table_list(); + assert(target != nullptr); + // For views, target->table is nullptr (the TABLE* lives on the + // underlying base table); for base tables it must be set by now. + assert(target->is_view() || target->table != nullptr); + if (target == nullptr) return true; + if (check_single_table_access(thd, SELECT_ACL, target, false)) + return true; + + // Ensure name resolution context includes the delete target table, + // so that subqueries in RETURNING can resolve outer column references. + if (select->context.first_name_resolution_table == nullptr) { + select->context.first_name_resolution_table = table_list; + } + + // 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; + + // setup_fields with SELECT_ACL marks columns in the table's read bitmap + 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; + + // Verify no aggregate functions in RETURNING + for (Item *item : select->fields) { + if (item->has_aggregation()) { + my_error(ER_INVALID_GROUP_FUNC_USE, MYF(0)); + return true; + } + } + + // Set up Query_result_send for sending result set to client + Prepared_stmt_arena_holder ps_holder(thd); + result = new (thd->mem_root) Query_result_send(); + if (result == nullptr) return true; + select->set_query_result(result); + select->master_query_expression()->set_query_result(result); + } + thd->want_privilege = want_privilege_saved; thd->mark_used_columns = mark_used_columns_saved; @@ -905,7 +1034,17 @@ bool Sql_cmd_delete::execute_inner(THD *thd) { return explain_single_table_modification(thd, thd, &plan, lex->query_block); } - my_ok(thd); + if (m_returning) { + // Send empty result set (metadata + EOF) for RETURNING + if (result->send_result_set_metadata( + thd, lex->query_block->fields, + Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF)) + return true; + thd->set_row_count_func(0); + if (result->send_eof(thd)) return true; + } else { + my_ok(thd); + } return false; } return multitable ? Sql_cmd_dml::execute_inner(thd) diff --git a/sql/sql_delete.h b/sql/sql_delete.h index e58c49aa037a..e230ed66a07b 100644 --- a/sql/sql_delete.h +++ b/sql/sql_delete.h @@ -37,8 +37,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; @@ -48,6 +51,8 @@ class Sql_cmd_delete final : public Sql_cmd_dml { bool accept(THD *thd, Select_lex_visitor *visitor) override; + bool has_returning() const { return m_returning; } + protected: bool precheck(THD *thd) override; bool check_privileges(THD *thd) override; @@ -66,6 +71,9 @@ class Sql_cmd_delete final : public Sql_cmd_dml { optimization, use the Table_ref::updating property instead. */ SQL_I_List *delete_tables; + + /// True if DELETE has a RETURNING clause + bool m_returning; }; /// Find out which of the delete target tables can be deleted from immediately diff --git a/sql/sql_lex.h b/sql/sql_lex.h index 659c71c7f0a4..c65ec033348f 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -2250,6 +2250,10 @@ class Query_block : public Query_term { bool is_row_count_valid_for_semi_join(); + /// 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; @@ -2309,7 +2313,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_yacc.yy b/sql/sql_yacc.yy index cc4becfaa136..f5e9894e9d16 100644 --- a/sql/sql_yacc.yy +++ b/sql/sql_yacc.yy @@ -1365,6 +1365,17 @@ void 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 */ +/* + RETURNING is declared non-reserved () so that + information_schema.KEYWORDS reports it correctly (reserved=0). However it is + intentionally NOT listed in ident_keywords_unambiguous - doing so creates a + reduce/reduce conflict with opt_delete_returning (the parser cannot tell + whether RETURNING after DELETE ... FROM t starts a table alias or the + RETURNING clause). The practical effect is that unquoted RETURNING cannot be + used as an identifier; users must backtick-quote it (`returning`). Resolving + the conflict properly would require significant grammar restructuring. + This is the smallest-impact trade-off. +*/ %token RETURNING_SYM 999 /* SQL-2016-N */ /* Here is an intentional gap in token numbers. @@ -1673,6 +1684,7 @@ void 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 @@ -13453,8 +13465,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 @@ -13479,6 +13492,14 @@ delete_stmt: } ; +opt_delete_returning: + %empty { $$ = nullptr; } + | RETURNING_SYM select_item_list + { + $$ = $2; + } + ; + opt_wild: %empty | '.' '*' @@ -15591,7 +15612,7 @@ ident_keywords_unambiguous: | RESUME_SYM | RETAIN_SYM | RETURNED_SQLSTATE_SYM - | RETURNING_SYM + /* RETURNING_SYM removed, now reserved for DELETE ... RETURNING */ | RETURNS_SYM | REUSE_SYM | REVERSE_SYM