From 4c956fa15be059e35f8ed0de85dc72d5827cee4d Mon Sep 17 00:00:00 2001 From: Dmitry Shulga Date: Mon, 27 Jan 2025 16:29:25 +0700 Subject: [PATCH] MDEV-34724: Skipping a row operation from a trigger Implementation of this task adds ability to raise the signal with SQLSTATE '02TRG' from a BEFORE INSERT/UPDATE/DELETE trigger and handles this signal as an indicator meaning 'to throw away the current row' on processing the INSERT/UPDATE/DELETE statement. The signal with SQLSTATE '02TRG' has special meaning only in case it is raised inside BEFORE triggers, for AFTER trigger's this value of SQLSTATE isn't treated in any special way. In according with SQL standard, the SQLSTATE class '02' means NO DATA and sql_errno for this class is set to value ER_SIGNAL_NOT_FOUND by current implementation of MariaDB server. Implementation of this task assigns the value ER_SIGNAL_SKIP_ROW_FROM_TRIGGER to sql_errno in Diagnostics_area in case the signal is raised from a trigger and SQLSTATE has value '02TRG'. To catch signal with SQLTSATE '02TRG' and handle it in special way, the methods Table_triggers_list::process_triggers select_insert::store_values select_create::store_values Rows_log_event::process_triggers and the overloaded function fill_record_n_invoke_before_triggers were extended with extra out parameter for returning the flag whether to skip the current values being processed by INSERT/UPDATE/DELETE statement. This extra parameter is passed as nullptr in case of AFTER trigger and BEFORE trigger this parameter points to a variable to store a marker whether to skip the current record or store it by calling write_record(). --- mysql-test/main/mdev-34724.result | 414 ++++++++++++++++++++++++++++++ mysql-test/main/mdev-34724.test | 373 +++++++++++++++++++++++++++ sql/log_event.h | 2 +- sql/log_event_server.cc | 51 ++-- sql/share/errmsg-utf8.txt | 2 + sql/sql_base.cc | 49 +++- sql/sql_base.h | 6 +- sql/sql_class.h | 6 +- sql/sql_delete.cc | 36 ++- sql/sql_error.cc | 17 +- sql/sql_error.h | 2 +- sql/sql_insert.cc | 57 ++-- sql/sql_load.cc | 27 +- sql/sql_trigger.cc | 79 +++++- sql/sql_trigger.h | 1 + sql/sql_update.cc | 28 +- sql/table.cc | 11 +- 17 files changed, 1097 insertions(+), 64 deletions(-) create mode 100644 mysql-test/main/mdev-34724.result create mode 100644 mysql-test/main/mdev-34724.test diff --git a/mysql-test/main/mdev-34724.result b/mysql-test/main/mdev-34724.result new file mode 100644 index 00000000000..03980b045f7 --- /dev/null +++ b/mysql-test/main/mdev-34724.result @@ -0,0 +1,414 @@ +# Test case 1: check that a row being inserted +# can be filtered out by means running of a statement +# SIGNAL SQLSTATE '02TRG' from the trigger body +CREATE TABLE t1 (a INT); +CREATE TRIGGER t1_bi BEFORE INSERT ON t1 FOR EACH ROW +BEGIN +IF NEW.a = 1 THEN +SIGNAL SQLSTATE '02TRG'; +END IF; +END +$ +# Execution of the following INSERT statement produces warnings +# for the row (1) filtered out by the statement SIGNAL SQLSTATE '02TRG' +# invoked from the trigger +INSERT INTO t1 VALUES (1), (2), (3); +Warnings: +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bi +# Expected output is the rows (2), (3) +SELECT * FROM t1; +a +2 +3 +# Clean up +TRUNCATE TABLE t1; +# Test case 2: run the statement INSERT SELECT and check +# that the rows (1) is filtered out by the trigger `t1_bi` +CREATE TABLE t2 (a INT); +INSERT INTO t2 VALUES (1), (2), (1), (3), (5); +# Execution of the following INSERT ... SELECT statement produces warnings +# for the row (1) filtered out by the statement SIGNAL SQLSTATE '02TRG' +# invoked from the trigger +INSERT INTO t1 SELECT * FROM t2; +Warnings: +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bi +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bi +# Expected output is the rows (2), (3), (5) +SELECT * FROM t1; +a +2 +3 +5 +# Clean up +DROP TABLE t1, t2; +# Test case 3: check for cooperation of the feature +# 'skipping rows from a trigger' and execution of +# the statement LOAD +CREATE TABLE t1 (a INT); +CREATE TRIGGER t1_bi BEFORE INSERT ON t1 FOR EACH ROW +BEGIN +IF NEW.a = 1 THEN +SIGNAL SQLSTATE '02TRG'; +END IF; +END +$ +# Test case 3.1: check for LOAD DATA INFILE +# Prepare a file with dump of the table `t2` and +# then use it for loading data into the table `t1` +CREATE TABLE t2 (a INT); +INSERT INTO t2 VALUES (1), (2), (1), (3), (5); +SELECT * INTO OUTFILE 'MYSQLTEST_VARDIR/tmp/t2' FROM t2; +# Execution of the following LOAD DATA INFILE statement produces warnings +# for the rows (1) filtered out by the statement SIGNAL SQLSTATE '02TRG' +# invoked from the trigger +LOAD DATA INFILE 'MYSQLTEST_VARDIR/tmp/t2' INTO TABLE t1; +Warnings: +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bi +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bi +# Querying of the table `t1` should return three rows: (2), (3), (5) +SELECT * FROM t1; +a +2 +3 +5 +# Clean up +TRUNCATE TABLE t1; +# Test case 3.2: check for LOAD XML INFILE +# The same test with loading data from a file in presence of +# a BEFORE INSERT trigger, but in this case the data to be loaded +# is in xml format. +# Prepare a file with dump of the table `t2` in xml format and +# then use it for loading data into the table `t1` +# Execution of the following LOAD XML INFILE statement produces warnings +# for the rows (1) filtered out by the statement SIGNAL SQLSTATE '02TRG' +# invoked from the trigger +LOAD XML INFILE 'MYSQLTEST_VARDIR/tmp/loadxml-dump.xml' INTO TABLE t1 ROWS IDENTIFIED BY ''; +Warnings: +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bi +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bi +SELECT * FROM t1; +a +2 +3 +5 +# Clean up +DROP TABLE t1, t2; +# Test case 4: check that a row being deleted +# can be filtered out by means running of a statement +# SIGNAL SQLSTATE '02TRG' from the trigger body +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1), (2), (3), (1); +CREATE TRIGGER t1_bi BEFORE DELETE ON t1 FOR EACH ROW +BEGIN +IF OLD.a = 1 THEN +SIGNAL SQLSTATE '02TRG'; +END IF; +END +$ +# No rows deleted in result of running the following statement +# Execution of the following DELETE statement produces warnings +# for the row (1) filtered out by the statement SIGNAL SQLSTATE '02TRG' +# invoked from the trigger +DELETE FROM t1 WHERE a = 1; +Warnings: +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bi +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bi +# Show that the rows satisfying the condition a = 1 are retained +# in the table. Expected output is the rows (1), (2), (3), (1) +SELECT * FROM t1; +a +1 +2 +3 +1 +# Shows that rows with a column value different +# from '1' are deleted successfully +DELETE FROM t1 WHERE a = 2; +# Expected output is the rows (1), (3), (1) +SELECT * FROM t1; +a +1 +3 +1 +# Check that the DELETE statement without condition takes into +# account the fact that some of rows should be skipped. +DELETE FROM t1; +Warnings: +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bi +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bi +# Expected output is the rows (1), (1) since they are explicilty +# skipped by the trigger logic +SELECT * FROM t1; +a +1 +1 +# Clean up +DROP TABLE t1; +# Test case 5: check that AFTER INSERT/UPDATE/DELETE trigger is not fired +# in case a row skipped by corresponding BEFORE trigger +# +CREATE TABLE t1 (a INT); +CREATE TABLE t2 (a INT); +# Test case 5.1: check for the pair BEFORE INSERT/AFTER INSERT +CREATE TRIGGER t1_bi BEFORE INSERT ON t1 FOR EACH ROW +BEGIN +IF NEW.a = 1 THEN +SIGNAL SQLSTATE '02TRG'; +END IF; +END +$ +CREATE TRIGGER t1_ai AFTER INSERT ON t1 FOR EACH ROW +BEGIN +INSERT INTO t2 VALUES (NEW.a); +END +$ +INSERT INTO t1 VALUES (1), (2), (3); +Warnings: +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bi +SELECT * FROM t1; +a +2 +3 +SELECT * FROM t2; +a +2 +3 +# Clean up +DROP TABLE t1, t2; +# Test case 5.2: check for the pair BEFORE DELETE/AFTER DELETE +CREATE TABLE t1 (a INT); +CREATE TABLE t2 (a INT); +INSERT INTO t1 VALUES (1), (2), (3); +CREATE TRIGGER t1_bd BEFORE DELETE ON t1 FOR EACH ROW +BEGIN +IF OLD.a = 1 THEN +SIGNAL SQLSTATE '02TRG'; +END IF; +END +$ +CREATE TRIGGER t1_ad AFTER DELETE ON t1 FOR EACH ROW +BEGIN +INSERT INTO t2 VALUES (OLD.a); +END +$ +DELETE FROM t1; +Warnings: +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bd +# The row (1) is skipped by implementation of the trigger t1_bd, +# therefore the row (1) isn't inserted into the table t2 since +# the trigger t1_ad isn't fired for the row (1) +SELECT * FROM t1; +a +1 +SELECT * FROM t2; +a +2 +3 +# Clean up +DROP TABLE t1, t2; +# Test case 5.3: check for the pair BEFORE UPDATE/AFTER UPDATE +CREATE TABLE t1 (a INT); +CREATE TABLE t2 (old_a INT, new_a INT); +INSERT INTO t1 VALUES (1), (2), (3); +CREATE TRIGGER t1_bu BEFORE UPDATE ON t1 FOR EACH ROW +BEGIN +IF OLD.a = 1 THEN +SIGNAL SQLSTATE '02TRG'; +END IF; +END +$ +CREATE TRIGGER t1_au AFTER UPDATE ON t1 FOR EACH ROW +BEGIN +INSERT INTO t2 VALUES (OLD.a, NEW.a); +END +$ +# The following statement UPDATE doens't modify the row (1) +# since this row is explicitly ignored by implementation of +# trigger t1_bu, therefore the trigger t1_au is not fired +# for this row and the row (1, 11) not inserted into the table t2 +UPDATE t1 SET a = a + 10; +Warnings: +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bu +# Expected output of the following statement SELECT is (1), (12), (13) +SELECT * FROM t1; +a +1 +12 +13 +# Expected output of query from the table t2 is (2, 12), (3, 13) +SELECT * FROM t2; +old_a new_a +2 12 +3 13 +# Clean up +DROP TABLE t1, t2; +# Test case 6: check cooperation of UPDATE with the +# 'skipping a row from a trigger' feature +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1), (2), (1), (3), (5); +CREATE TRIGGER t1_bd BEFORE UPDATE ON t1 FOR EACH ROW +BEGIN +IF OLD.a = 1 THEN +SIGNAL SQLSTATE '02TRG'; +END IF; +END +$ +# Check for update with condition +UPDATE t1 SET a = 1000 WHERE a = 1; +Warnings: +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bd +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bd +# Expected result is the rows (1), (2), (1), (3), (5) +SELECT * FROM t1; +a +1 +2 +1 +3 +5 +# Check for unconditional update +UPDATE t1 SET a = a + 100; +Warnings: +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bd +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bd +# Expected result is the rows (1), (102), (1), (103), (105) +SELECT * FROM t1; +a +1 +102 +1 +103 +105 +# Multi-update +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1), (2), (1), (3), (5); +CREATE TABLE t2 (a INT); +INSERT INTO t2 VALUES (1), (3); +# In multi-update the trigger skips an update of +# the first table only (the one that has an associated trigger), +# the second table (without a trigger) is still updated +UPDATE t1, t2 SET t1.a = t1.a + 300, t2.a = t2.a + 300 WHERE t1.a = t2.a; +Warnings: +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bd +Error 4207 The row is skipped by a trigger implementation +Note 4094 At line 4 in test.t1_bd +# Expected results is the rows (1), (2), (1), (303), (5) +SELECT * FROM t1; +a +1 +2 +1 +303 +5 +# Expected results is the rows (301), (303) +SELECT * FROM t2; +a +301 +303 +# Clean up +DROP TABLE t1, t2; +# Test case 7: check that MESSAGE_TEXT and MYSQL_ERRNOR still +# can be assigned by a user for SQLSTATE '02TRG' +CREATE TABLE t1 (a INT); +CREATE TRIGGER t1_bi BEFORE INSERT ON t1 FOR EACH ROW +BEGIN +IF NEW.a = 1 THEN +SIGNAL SQLSTATE '02TRG' SET MYSQL_ERRNO=1, MESSAGE_TEXT='This value is intentionally ignored'; +END IF; +END +$ +# Execution of the following INSERT statement produces warnings +# for the row (1) filtered out by the statement SIGNAL SQLSTATE '02TRG' +# invoked from the trigger. The errno has value 1 and the message is +# is the text message 'This value is intentionally ignored' +INSERT INTO t1 VALUES (1), (2), (3); +Warnings: +Error 1 This value is intentionally ignored +Note 4094 At line 4 in test.t1_bi +# Expected output is the rows (2), (3) +SELECT * FROM t1; +a +2 +3 +# Clean up +DROP TABLE t1; +# Test case 8: check that SQLSTATE '02TRG' doesn't have any special +# meaning in AFTER triggers +# Test case 8.1: check it for AFTER INSERT trigger +CREATE TABLE t1 (a INT); +CREATE TRIGGER t1_ai AFTER INSERT ON t1 FOR EACH ROW +BEGIN +IF NEW.a = 1 THEN +SIGNAL SQLSTATE '02TRG'; +END IF; +END +$ +# There is no a handler for the signal raised from inside +# the trigger t1_ai, so the statement INSERT INTO fails +# with the error ER_SIGNAL_NOT_FOUND +INSERT INTO t1 VALUES (1); +ERROR 02TRG: Unhandled user-defined not found condition +SELECT * FROM t1; +a +1 +# Clean up +DROP TABLE t1; +# Test case 8.2: check it for AFTER UPDATE trigger +CREATE TABLE t1 (a INT); +CREATE TRIGGER t1_au AFTER UPDATE ON t1 FOR EACH ROW +BEGIN +IF OLD.a = 1 THEN +SIGNAL SQLSTATE '02TRG'; +END IF; +END +$ +INSERT INTO t1 VALUES (1); +# There is no a handler for the signal raised from inside +# the trigger t1_au, so the statement UPDATE fails +# with the error ER_SIGNAL_NOT_FOUND +UPDATE t1 SET a = 10; +ERROR 02TRG: Unhandled user-defined not found condition +SELECT * FROM t1; +a +10 +# Clean up +DROP TABLE t1; +# Test case 8.3: check it for AFTER DELETE trigger +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1); +CREATE TRIGGER t1_ad AFTER DELETE ON t1 FOR EACH ROW +BEGIN +IF OLD.a = 1 THEN +SIGNAL SQLSTATE '02TRG'; +END IF; +END +$ +# There is no a handler for the signal raised from inside +# the trigger t1_ad, so the statement DELETE fails +# with the error ER_SIGNAL_NOT_FOUND +DELETE FROM t1; +ERROR 02TRG: Unhandled user-defined not found condition +SELECT * FROM t1; +a +# Clean up +DROP TABLE t1; +# End of 11.8 tests diff --git a/mysql-test/main/mdev-34724.test b/mysql-test/main/mdev-34724.test new file mode 100644 index 00000000000..fb8ffa8be13 --- /dev/null +++ b/mysql-test/main/mdev-34724.test @@ -0,0 +1,373 @@ +# Tests for the task MDEV-34724: Skipping a row operation from a trigger + +--echo # Test case 1: check that a row being inserted +--echo # can be filtered out by means running of a statement +--echo # SIGNAL SQLSTATE '02TRG' from the trigger body + +CREATE TABLE t1 (a INT); +--delimiter $ +CREATE TRIGGER t1_bi BEFORE INSERT ON t1 FOR EACH ROW +BEGIN + IF NEW.a = 1 THEN + SIGNAL SQLSTATE '02TRG'; + END IF; +END +$ +--delimiter ; + +--echo # Execution of the following INSERT statement produces warnings +--echo # for the row (1) filtered out by the statement SIGNAL SQLSTATE '02TRG' +--echo # invoked from the trigger +INSERT INTO t1 VALUES (1), (2), (3); +--echo # Expected output is the rows (2), (3) +SELECT * FROM t1; +--echo # Clean up +TRUNCATE TABLE t1; + +--echo # Test case 2: run the statement INSERT SELECT and check +--echo # that the rows (1) is filtered out by the trigger `t1_bi` +CREATE TABLE t2 (a INT); +INSERT INTO t2 VALUES (1), (2), (1), (3), (5); +--echo # Execution of the following INSERT ... SELECT statement produces warnings +--echo # for the row (1) filtered out by the statement SIGNAL SQLSTATE '02TRG' +--echo # invoked from the trigger +INSERT INTO t1 SELECT * FROM t2; +--echo # Expected output is the rows (2), (3), (5) +SELECT * FROM t1; +--echo # Clean up +DROP TABLE t1, t2; + +--echo # Test case 3: check for cooperation of the feature +--echo # 'skipping rows from a trigger' and execution of +--echo # the statement LOAD +CREATE TABLE t1 (a INT); +--delimiter $ +CREATE TRIGGER t1_bi BEFORE INSERT ON t1 FOR EACH ROW +BEGIN + IF NEW.a = 1 THEN + SIGNAL SQLSTATE '02TRG'; + END IF; +END +$ +--delimiter ; + +--echo # Test case 3.1: check for LOAD DATA INFILE +--echo # Prepare a file with dump of the table `t2` and +--echo # then use it for loading data into the table `t1` +CREATE TABLE t2 (a INT); +INSERT INTO t2 VALUES (1), (2), (1), (3), (5); +--disable_ps2_protocol +--disable_cursor_protocol +--replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR +--eval SELECT * INTO OUTFILE '$MYSQLTEST_VARDIR/tmp/t2' FROM t2 +--enable_cursor_protocol +--enable_ps2_protocol +--echo # Execution of the following LOAD DATA INFILE statement produces warnings +--echo # for the rows (1) filtered out by the statement SIGNAL SQLSTATE '02TRG' +--echo # invoked from the trigger +--replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR +--eval LOAD DATA INFILE '$MYSQLTEST_VARDIR/tmp/t2' INTO TABLE t1 +--echo # Querying of the table `t1` should return three rows: (2), (3), (5) +SELECT * FROM t1; + +--echo # Clean up +--remove_file $MYSQLTEST_VARDIR/tmp/t2 +TRUNCATE TABLE t1; + +--echo # Test case 3.2: check for LOAD XML INFILE +--echo # The same test with loading data from a file in presence of +--echo # a BEFORE INSERT trigger, but in this case the data to be loaded +--echo # is in xml format. + +--echo # Prepare a file with dump of the table `t2` in xml format and +--echo # then use it for loading data into the table `t1` +# Running the $MYSQL_DUMP tool against an embedded server does not work. +--source include/not_embedded.inc + +--replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR +--exec $MYSQL_DUMP --xml test t2 > "$MYSQLTEST_VARDIR/tmp/loadxml-dump.xml" 2>&1 + +--echo # Execution of the following LOAD XML INFILE statement produces warnings +--echo # for the rows (1) filtered out by the statement SIGNAL SQLSTATE '02TRG' +--echo # invoked from the trigger +--replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR +--eval LOAD XML INFILE '$MYSQLTEST_VARDIR/tmp/loadxml-dump.xml' INTO TABLE t1 ROWS IDENTIFIED BY '' + +SELECT * FROM t1; + +--echo # Clean up +--remove_file $MYSQLTEST_VARDIR/tmp/loadxml-dump.xml +DROP TABLE t1, t2; + +--echo # Test case 4: check that a row being deleted +--echo # can be filtered out by means running of a statement +--echo # SIGNAL SQLSTATE '02TRG' from the trigger body +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1), (2), (3), (1); +--delimiter $ +CREATE TRIGGER t1_bi BEFORE DELETE ON t1 FOR EACH ROW +BEGIN + IF OLD.a = 1 THEN + SIGNAL SQLSTATE '02TRG'; + END IF; +END +$ +--delimiter ; + +--echo # No rows deleted in result of running the following statement +--echo # Execution of the following DELETE statement produces warnings +--echo # for the row (1) filtered out by the statement SIGNAL SQLSTATE '02TRG' +--echo # invoked from the trigger +DELETE FROM t1 WHERE a = 1; +--echo # Show that the rows satisfying the condition a = 1 are retained +--echo # in the table. Expected output is the rows (1), (2), (3), (1) +SELECT * FROM t1; + +--echo # Shows that rows with a column value different +--echo # from '1' are deleted successfully +DELETE FROM t1 WHERE a = 2; +--echo # Expected output is the rows (1), (3), (1) +SELECT * FROM t1; + +--echo # Check that the DELETE statement without condition takes into +--echo # account the fact that some of rows should be skipped. +DELETE FROM t1; +--echo # Expected output is the rows (1), (1) since they are explicilty +--echo # skipped by the trigger logic +SELECT * FROM t1; + +--echo # Clean up +DROP TABLE t1; + +--echo # Test case 5: check that AFTER INSERT/UPDATE/DELETE trigger is not fired +--echo # in case a row skipped by corresponding BEFORE trigger +--echo # +CREATE TABLE t1 (a INT); +CREATE TABLE t2 (a INT); +--echo # Test case 5.1: check for the pair BEFORE INSERT/AFTER INSERT +--delimiter $ +CREATE TRIGGER t1_bi BEFORE INSERT ON t1 FOR EACH ROW +BEGIN + IF NEW.a = 1 THEN + SIGNAL SQLSTATE '02TRG'; + END IF; +END +$ +--delimiter ; + +--delimiter $ +CREATE TRIGGER t1_ai AFTER INSERT ON t1 FOR EACH ROW +BEGIN + INSERT INTO t2 VALUES (NEW.a); +END +$ +--delimiter ; + +INSERT INTO t1 VALUES (1), (2), (3); +SELECT * FROM t1; +SELECT * FROM t2; +--echo # Clean up +DROP TABLE t1, t2; + +--echo # Test case 5.2: check for the pair BEFORE DELETE/AFTER DELETE +CREATE TABLE t1 (a INT); +CREATE TABLE t2 (a INT); +INSERT INTO t1 VALUES (1), (2), (3); +--delimiter $ +CREATE TRIGGER t1_bd BEFORE DELETE ON t1 FOR EACH ROW +BEGIN + IF OLD.a = 1 THEN + SIGNAL SQLSTATE '02TRG'; + END IF; +END +$ +--delimiter ; + +--delimiter $ +CREATE TRIGGER t1_ad AFTER DELETE ON t1 FOR EACH ROW +BEGIN + INSERT INTO t2 VALUES (OLD.a); +END +$ +--delimiter ; + +DELETE FROM t1; +--echo # The row (1) is skipped by implementation of the trigger t1_bd, +--echo # therefore the row (1) isn't inserted into the table t2 since +--echo # the trigger t1_ad isn't fired for the row (1) +SELECT * FROM t1; +SELECT * FROM t2; +--echo # Clean up +DROP TABLE t1, t2; + +--echo # Test case 5.3: check for the pair BEFORE UPDATE/AFTER UPDATE +CREATE TABLE t1 (a INT); +CREATE TABLE t2 (old_a INT, new_a INT); +INSERT INTO t1 VALUES (1), (2), (3); +--delimiter $ +CREATE TRIGGER t1_bu BEFORE UPDATE ON t1 FOR EACH ROW +BEGIN + IF OLD.a = 1 THEN + SIGNAL SQLSTATE '02TRG'; + END IF; +END +$ + +CREATE TRIGGER t1_au AFTER UPDATE ON t1 FOR EACH ROW +BEGIN + INSERT INTO t2 VALUES (OLD.a, NEW.a); +END +$ +--delimiter ; + +--echo # The following statement UPDATE doens't modify the row (1) +--echo # since this row is explicitly ignored by implementation of +--echo # trigger t1_bu, therefore the trigger t1_au is not fired +--echo # for this row and the row (1, 11) not inserted into the table t2 +UPDATE t1 SET a = a + 10; + +--echo # Expected output of the following statement SELECT is (1), (12), (13) +SELECT * FROM t1; +--echo # Expected output of query from the table t2 is (2, 12), (3, 13) +SELECT * FROM t2; + +--echo # Clean up +DROP TABLE t1, t2; + +--echo # Test case 6: check cooperation of UPDATE with the +--echo # 'skipping a row from a trigger' feature +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1), (2), (1), (3), (5); + +--delimiter $ +CREATE TRIGGER t1_bd BEFORE UPDATE ON t1 FOR EACH ROW +BEGIN + IF OLD.a = 1 THEN + SIGNAL SQLSTATE '02TRG'; + END IF; +END +$ +--delimiter ; + +--echo # Check for update with condition +UPDATE t1 SET a = 1000 WHERE a = 1; +--echo # Expected result is the rows (1), (2), (1), (3), (5) +SELECT * FROM t1; + +--echo # Check for unconditional update +UPDATE t1 SET a = a + 100; +--echo # Expected result is the rows (1), (102), (1), (103), (105) +SELECT * FROM t1; + +--echo # Multi-update +TRUNCATE TABLE t1; +INSERT INTO t1 VALUES (1), (2), (1), (3), (5); + +CREATE TABLE t2 (a INT); +INSERT INTO t2 VALUES (1), (3); +--echo # In multi-update the trigger skips an update of +--echo # the first table only (the one that has an associated trigger), +--echo # the second table (without a trigger) is still updated +UPDATE t1, t2 SET t1.a = t1.a + 300, t2.a = t2.a + 300 WHERE t1.a = t2.a; +--echo # Expected results is the rows (1), (2), (1), (303), (5) +SELECT * FROM t1; +--echo # Expected results is the rows (301), (303) +SELECT * FROM t2; +--echo # Clean up +DROP TABLE t1, t2; + +--echo # Test case 7: check that MESSAGE_TEXT and MYSQL_ERRNOR still +--echo # can be assigned by a user for SQLSTATE '02TRG' + +CREATE TABLE t1 (a INT); +--delimiter $ +CREATE TRIGGER t1_bi BEFORE INSERT ON t1 FOR EACH ROW +BEGIN + IF NEW.a = 1 THEN + SIGNAL SQLSTATE '02TRG' SET MYSQL_ERRNO=1, MESSAGE_TEXT='This value is intentionally ignored'; + END IF; +END +$ +--delimiter ; + +--echo # Execution of the following INSERT statement produces warnings +--echo # for the row (1) filtered out by the statement SIGNAL SQLSTATE '02TRG' +--echo # invoked from the trigger. The errno has value 1 and the message is +--echo # is the text message 'This value is intentionally ignored' +INSERT INTO t1 VALUES (1), (2), (3); +--echo # Expected output is the rows (2), (3) +SELECT * FROM t1; +--echo # Clean up +DROP TABLE t1; + +--echo # Test case 8: check that SQLSTATE '02TRG' doesn't have any special +--echo # meaning in AFTER triggers + +--echo # Test case 8.1: check it for AFTER INSERT trigger +CREATE TABLE t1 (a INT); +--delimiter $ +CREATE TRIGGER t1_ai AFTER INSERT ON t1 FOR EACH ROW +BEGIN + IF NEW.a = 1 THEN + SIGNAL SQLSTATE '02TRG'; + END IF; +END +$ +--delimiter ; + +--echo # There is no a handler for the signal raised from inside +--echo # the trigger t1_ai, so the statement INSERT INTO fails +--echo # with the error ER_SIGNAL_NOT_FOUND +--error ER_SIGNAL_NOT_FOUND +INSERT INTO t1 VALUES (1); + +SELECT * FROM t1; +--echo # Clean up +DROP TABLE t1; + +--echo # Test case 8.2: check it for AFTER UPDATE trigger +CREATE TABLE t1 (a INT); +--delimiter $ +CREATE TRIGGER t1_au AFTER UPDATE ON t1 FOR EACH ROW +BEGIN + IF OLD.a = 1 THEN + SIGNAL SQLSTATE '02TRG'; + END IF; +END +$ +--delimiter ; + +INSERT INTO t1 VALUES (1); +--echo # There is no a handler for the signal raised from inside +--echo # the trigger t1_au, so the statement UPDATE fails +--echo # with the error ER_SIGNAL_NOT_FOUND +--error ER_SIGNAL_NOT_FOUND +UPDATE t1 SET a = 10; +SELECT * FROM t1; +--echo # Clean up +DROP TABLE t1; + +--echo # Test case 8.3: check it for AFTER DELETE trigger +CREATE TABLE t1 (a INT); +INSERT INTO t1 VALUES (1); +--delimiter $ +CREATE TRIGGER t1_ad AFTER DELETE ON t1 FOR EACH ROW +BEGIN + IF OLD.a = 1 THEN + SIGNAL SQLSTATE '02TRG'; + END IF; +END +$ +--delimiter ; + +--echo # There is no a handler for the signal raised from inside +--echo # the trigger t1_ad, so the statement DELETE fails +--echo # with the error ER_SIGNAL_NOT_FOUND +--error ER_SIGNAL_NOT_FOUND +DELETE FROM t1; +SELECT * FROM t1; + +--echo # Clean up +DROP TABLE t1; + +--echo # End of 11.8 tests diff --git a/sql/log_event.h b/sql/log_event.h index 486e89b9324..afa391fe07b 100644 --- a/sql/log_event.h +++ b/sql/log_event.h @@ -4949,7 +4949,7 @@ protected: &m_curr_row_end, &m_master_reclength, m_rows_end); } bool process_triggers(trg_event_type event, trg_action_time_type time_type, - bool old_row_is_record1); + bool old_row_is_record1, bool *skip_row_indicator); /** Helper function to check whether there is an auto increment diff --git a/sql/log_event_server.cc b/sql/log_event_server.cc index 57bd3313acc..a53bf654902 100644 --- a/sql/log_event_server.cc +++ b/sql/log_event_server.cc @@ -6688,7 +6688,8 @@ Write_rows_log_event::do_after_row_operations(int error) bool Rows_log_event::process_triggers(trg_event_type event, trg_action_time_type time_type, - bool old_row_is_record1) + bool old_row_is_record1, + bool *skip_row_indicator) { bool result; DBUG_ENTER("Rows_log_event::process_triggers"); @@ -6697,12 +6698,14 @@ bool Rows_log_event::process_triggers(trg_event_type event, { result= m_table->triggers->process_triggers(thd, event, time_type, - old_row_is_record1); + old_row_is_record1, + skip_row_indicator); } else result= m_table->triggers->process_triggers(thd, event, time_type, - old_row_is_record1); + old_row_is_record1, + skip_row_indicator); DBUG_RETURN(result); } @@ -6875,12 +6878,18 @@ int Rows_log_event::write_row(rpl_group_info *rgi, const bool overwrite) if (table->s->long_unique_table) table->update_virtual_fields(table->file, VCOL_UPDATE_FOR_WRITE); + bool trg_skip_row= false; if (invoke_triggers && - unlikely(process_triggers(TRG_EVENT_INSERT, TRG_ACTION_BEFORE, TRUE))) + unlikely(process_triggers(TRG_EVENT_INSERT, TRG_ACTION_BEFORE, true, + &trg_skip_row))) { DBUG_RETURN(HA_ERR_GENERIC); // in case if error is not set yet } + /* In case any of triggers signals to skip the current row, do it. */ + if (trg_skip_row) + return false; + // Handle INSERT. if (table->versioned(VERS_TIMESTAMP)) { @@ -7056,7 +7065,7 @@ int Rows_log_event::write_row(rpl_group_info *rgi, const bool overwrite) DBUG_PRINT("info",("Deleting offending row and trying to write new one again")); if (invoke_triggers && unlikely(process_triggers(TRG_EVENT_DELETE, TRG_ACTION_BEFORE, - TRUE))) + true, &trg_skip_row))) error= HA_ERR_GENERIC; // in case if error is not set yet else { @@ -7066,17 +7075,18 @@ int Rows_log_event::write_row(rpl_group_info *rgi, const bool overwrite) table->file->print_error(error, MYF(0)); DBUG_RETURN(error); } - if (invoke_triggers && + if (invoke_triggers && !trg_skip_row && unlikely(process_triggers(TRG_EVENT_DELETE, TRG_ACTION_AFTER, - TRUE))) + true, nullptr))) DBUG_RETURN(HA_ERR_GENERIC); // in case if error is not set yet } /* Will retry ha_write_row() with the offending row removed. */ } } - if (invoke_triggers && - unlikely(process_triggers(TRG_EVENT_INSERT, TRG_ACTION_AFTER, TRUE))) + if (invoke_triggers && !trg_skip_row && + unlikely(process_triggers(TRG_EVENT_INSERT, TRG_ACTION_AFTER, true, + nullptr))) error= HA_ERR_GENERIC; // in case if error is not set yet DBUG_RETURN(error); @@ -7963,10 +7973,12 @@ int Delete_rows_log_event::do_exec_row(rpl_group_info *rgi) #endif thd_proc_info(thd, message); + bool trg_skip_row= false; if (invoke_triggers && - unlikely(process_triggers(TRG_EVENT_DELETE, TRG_ACTION_BEFORE, FALSE))) + unlikely(process_triggers(TRG_EVENT_DELETE, TRG_ACTION_BEFORE, false, + &trg_skip_row))) error= HA_ERR_GENERIC; // in case if error is not set yet - if (likely(!error)) + if (likely(!error) && !trg_skip_row) { if (m_vers_from_plain && m_table->versioned(VERS_TIMESTAMP)) { @@ -7981,8 +7993,9 @@ int Delete_rows_log_event::do_exec_row(rpl_group_info *rgi) error= m_table->file->ha_delete_row(m_table->record[0]); } } - if (invoke_triggers && likely(!error) && - unlikely(process_triggers(TRG_EVENT_DELETE, TRG_ACTION_AFTER, FALSE))) + if (invoke_triggers && likely(!error) && !trg_skip_row && + unlikely(process_triggers(TRG_EVENT_DELETE, TRG_ACTION_AFTER, false, + nullptr))) error= HA_ERR_GENERIC; // in case if error is not set yet m_table->file->ha_index_or_rnd_end(); } @@ -8085,6 +8098,7 @@ Update_rows_log_event::do_exec_row(rpl_group_info *rgi) const LEX_CSTRING &table_name= m_table->s->table_name; const char quote_char= get_quote_char_for_identifier(thd, table_name.str, table_name.length); + bool trg_skip_row= false; my_snprintf(msg, sizeof msg, "Update_rows_log_event::find_row() on table %c%.*s%c", quote_char, int(table_name.length), table_name.str, quote_char); @@ -8180,12 +8194,18 @@ Update_rows_log_event::do_exec_row(rpl_group_info *rgi) thd_proc_info(thd, message); if (invoke_triggers && - unlikely(process_triggers(TRG_EVENT_UPDATE, TRG_ACTION_BEFORE, TRUE))) + unlikely(process_triggers(TRG_EVENT_UPDATE, TRG_ACTION_BEFORE, true, + &trg_skip_row))) { error= HA_ERR_GENERIC; // in case if error is not set yet goto err; } + if (trg_skip_row) + { + error= 0; + goto err; + } if (m_table->versioned()) { if (m_table->versioned(VERS_TIMESTAMP)) @@ -8211,7 +8231,8 @@ Update_rows_log_event::do_exec_row(rpl_group_info *rgi) } if (invoke_triggers && likely(!error) && - unlikely(process_triggers(TRG_EVENT_UPDATE, TRG_ACTION_AFTER, TRUE))) + unlikely(process_triggers(TRG_EVENT_UPDATE, TRG_ACTION_AFTER, true, + nullptr))) error= HA_ERR_GENERIC; // in case if error is not set yet diff --git a/sql/share/errmsg-utf8.txt b/sql/share/errmsg-utf8.txt index 73c146472fe..32b2fac5a4c 100644 --- a/sql/share/errmsg-utf8.txt +++ b/sql/share/errmsg-utf8.txt @@ -12293,3 +12293,5 @@ ER_VECTOR_FORMAT_INVALID eng "Invalid vector format at offset: %d for '%-.100s'. Must be a valid JSON array of numbers." ER_VEC_DISTANCE_TYPE eng "Cannot determine distance type for VEC_DISTANCE, index is not found" +ER_SIGNAL_SKIP_ROW_FROM_TRIGGER + eng "The row is skipped by a trigger implementation" diff --git a/sql/sql_base.cc b/sql/sql_base.cc index d7c118c0fdb..b08c543127d 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -9154,6 +9154,32 @@ static bool not_null_fields_have_null_values(TABLE *table) return false; } + +/** + The auxiliary function to determine whether a row should be skipped + or handled after invocation of triggers. + + @return true in case either there is no BEFORE INSERT triggers or + the statement SIGNAL SQLSTATE '02TRG' wasn't executed by + BEFORE INSERT triggers, else return false meaning that the row + mustn't be processed by the INSERT statement. +*/ + +static inline bool no_need_to_skip_a_row(bool *skip_row_indicator) +{ + /* + A row currently being processed mustn't be skipped in case the parameter + skip_row_indicator passed into fill_record_n_invoke_before_triggers() + is not null (that is true for BEFORE INSERT triggers) and a value stored by + this pointer has the value false, meaning that no SIGNAL statement with + the SQLSTATE = '02TRG' was raised on processing triggers. + */ + return + !skip_row_indicator || + !*skip_row_indicator; +} + + /** Fill fields in list with values from the list of items and invoke before triggers. @@ -9164,6 +9190,9 @@ static bool not_null_fields_have_null_values(TABLE *table) @param values values to fill with @param ignore_errors TRUE if we should ignore errors @param event event type for triggers to be invoked + @param [out] skip_row_indicator the flag whose value tells whether to skip + the current record by INSERT/LOAD statements + or process it @detail This function assumes that fields which values will be set and triggers @@ -9179,7 +9208,8 @@ bool fill_record_n_invoke_before_triggers(THD *thd, TABLE *table, List &fields, List &values, bool ignore_errors, - enum trg_event_type event) + enum trg_event_type event, + bool *skip_row_indicator) { int result; Table_triggers_list *triggers= table->triggers; @@ -9191,7 +9221,7 @@ fill_record_n_invoke_before_triggers(THD *thd, TABLE *table, { if (triggers->process_triggers(thd, event, TRG_ACTION_BEFORE, - true, &fields) || + true, skip_row_indicator, &fields) || not_null_fields_have_null_values(table)) { return TRUE; @@ -9201,7 +9231,8 @@ fill_record_n_invoke_before_triggers(THD *thd, TABLE *table, Re-calculate virtual fields to cater for cases when base columns are updated by the triggers. */ - if (table->vfield && fields.elements) + if (table->vfield && fields.elements && + no_need_to_skip_a_row(skip_row_indicator)) { Item *fld= (Item_field*) fields.head(); Item_field *item_field= fld->field_for_view_update(); @@ -9344,6 +9375,9 @@ err: @param values values to fill with @param ignore_errors TRUE if we should ignore errors @param event event type for triggers to be invoked + @param [out] skip_row_indicator the flag whose value tells whether to skip + the current record by INSERT statement or + process it @detail This function assumes that fields which values will be set and triggers @@ -9358,7 +9392,8 @@ err: bool fill_record_n_invoke_before_triggers(THD *thd, TABLE *table, Field **ptr, List &values, bool ignore_errors, - enum trg_event_type event) + enum trg_event_type event, + bool *skip_row_indicator) { bool result; Table_triggers_list *triggers= table->triggers; @@ -9366,13 +9401,15 @@ fill_record_n_invoke_before_triggers(THD *thd, TABLE *table, Field **ptr, result= fill_record(thd, table, ptr, values, ignore_errors, false, false); if (!result && triggers && *ptr) - result= triggers->process_triggers(thd, event, TRG_ACTION_BEFORE, TRUE) || + result= triggers->process_triggers(thd, event, TRG_ACTION_BEFORE, true, + skip_row_indicator) || not_null_fields_have_null_values(table); /* Re-calculate virtual fields to cater for cases when base columns are updated by the triggers. */ - if (!result && triggers && *ptr) + if (!result && triggers && *ptr && + no_need_to_skip_a_row(skip_row_indicator)) { DBUG_ASSERT(table == (*ptr)->table); if (table->vfield) diff --git a/sql/sql_base.h b/sql/sql_base.h index 00870fae88d..6b1dee77f54 100644 --- a/sql/sql_base.h +++ b/sql/sql_base.h @@ -173,12 +173,14 @@ bool fill_record_n_invoke_before_triggers(THD *thd, TABLE *table, List &fields, List &values, bool ignore_errors, - enum trg_event_type event); + enum trg_event_type event, + bool *skip_row_indicator); bool fill_record_n_invoke_before_triggers(THD *thd, TABLE *table, Field **field, List &values, bool ignore_errors, - enum trg_event_type event); + enum trg_event_type event, + bool *skip_row_indicator); bool insert_fields(THD *thd, Name_resolution_context *context, const Lex_ident_db &db_name, const Lex_ident_table &table_name, diff --git a/sql/sql_class.h b/sql/sql_class.h index 92cca6e4448..208e4ba10e6 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -2130,7 +2130,7 @@ public: #define SUB_STMT_TRIGGER 1 #define SUB_STMT_FUNCTION 2 #define SUB_STMT_STAT_TABLES 4 - +#define SUB_STMT_BEFORE_TRIGGER 8 class Sub_statement_state { @@ -6666,7 +6666,7 @@ class select_insert :public select_result_interceptor { int prepare(List &list, SELECT_LEX_UNIT *u) override; int prepare2(JOIN *join) override; int send_data(List &items) override; - virtual bool store_values(List &values); + virtual bool store_values(List &values, bool *trg_skip_row); virtual bool can_rollback_data() { return 0; } bool prepare_eof(); bool send_ok_packet(); @@ -6711,7 +6711,7 @@ public: int prepare(List &list, SELECT_LEX_UNIT *u) override; int binlog_show_create_table(TABLE **tables, uint count); - bool store_values(List &values) override; + bool store_values(List &values, bool *trg_skip_row) override; bool send_eof() override; void abort_result_set() override; bool can_rollback_data() override { return 1; } diff --git a/sql/sql_delete.cc b/sql/sql_delete.cc index 17cd70788a2..7749bf86608 100644 --- a/sql/sql_delete.cc +++ b/sql/sql_delete.cc @@ -276,7 +276,7 @@ int update_portion_of_time(THD *thd, TABLE *table, if (likely(!res) && table->triggers) res= table->triggers->process_triggers(thd, TRG_EVENT_INSERT, - TRG_ACTION_AFTER, true); + TRG_ACTION_AFTER, true, nullptr); restore_record(table, record[1]); if (res) table->file->restore_auto_increment(prev_insert_id); @@ -872,14 +872,20 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) delete_history); if (delete_record) { + bool trg_skip_row= false; + if (!delete_history && table->triggers && table->triggers->process_triggers(thd, TRG_EVENT_DELETE, - TRG_ACTION_BEFORE, FALSE)) + TRG_ACTION_BEFORE, FALSE, + &trg_skip_row)) { error= 1; break; } + if (trg_skip_row) + continue; + // no LIMIT / OFFSET if (returning && result->send_data(returning->item_list) < 0) { @@ -911,7 +917,8 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd) deleted++; if (!delete_history && table->triggers && table->triggers->process_triggers(thd, TRG_EVENT_DELETE, - TRG_ACTION_AFTER, FALSE)) + TRG_ACTION_AFTER, false, + nullptr)) { error= 1; break; @@ -1256,12 +1263,19 @@ int multi_delete::send_data(List &values) if (secure_counter < 0) { + bool trg_skip_row= false; + /* We are scanning the current table */ DBUG_ASSERT(del_table == table_being_deleted); if (table->triggers && table->triggers->process_triggers(thd, TRG_EVENT_DELETE, - TRG_ACTION_BEFORE, FALSE)) + TRG_ACTION_BEFORE, false, + &trg_skip_row)) DBUG_RETURN(1); + + if (trg_skip_row) + continue; + table->status|= STATUS_DELETED; error= table->delete_row(); @@ -1272,7 +1286,8 @@ int multi_delete::send_data(List &values) thd->transaction->stmt.modified_non_trans_table= TRUE; if (table->triggers && table->triggers->process_triggers(thd, TRG_EVENT_DELETE, - TRG_ACTION_AFTER, FALSE)) + TRG_ACTION_AFTER, false, + nullptr)) DBUG_RETURN(1); } else if (!ignore) @@ -1435,14 +1450,20 @@ int multi_delete::do_table_deletes(TABLE *table, SORT_INFO *sort_info, bool will_batch= !table->file->start_bulk_delete(); while (likely(!(local_error= info.read_record())) && likely(!thd->killed)) { + bool trg_skip_row= false; + if (table->triggers && unlikely(table->triggers->process_triggers(thd, TRG_EVENT_DELETE, - TRG_ACTION_BEFORE, FALSE))) + TRG_ACTION_BEFORE, false, + &trg_skip_row))) { local_error= 1; break; } + if (trg_skip_row) + continue; + local_error= table->delete_row(); if (unlikely(local_error) && !ignore) { @@ -1460,7 +1481,8 @@ int multi_delete::do_table_deletes(TABLE *table, SORT_INFO *sort_info, deleted++; if (table->triggers && table->triggers->process_triggers(thd, TRG_EVENT_DELETE, - TRG_ACTION_AFTER, FALSE)) + TRG_ACTION_AFTER, false, + nullptr)) { local_error= 1; break; diff --git a/sql/sql_error.cc b/sql/sql_error.cc index 89dde3cf190..122f76814ab 100644 --- a/sql/sql_error.cc +++ b/sql/sql_error.cc @@ -235,7 +235,8 @@ Sql_condition::get_message_octet_length() const } -void Sql_state_errno_level::assign_defaults(const Sql_state_errno *from) +void Sql_state_errno_level::assign_defaults(THD *thd, + const Sql_state_errno *from) { DBUG_ASSERT(from); int sqlerrno= from->get_sql_errno(); @@ -255,7 +256,17 @@ void Sql_state_errno_level::assign_defaults(const Sql_state_errno *from) else if (Sql_state::is_not_found()) /* SQLSTATE class "02": not found. */ { m_level= Sql_condition::WARN_LEVEL_ERROR; - m_sql_errno= sqlerrno ? sqlerrno : ER_SIGNAL_NOT_FOUND; + if (sqlerrno) + m_sql_errno= sqlerrno; + else + { + if ((thd->in_sub_stmt & (SUB_STMT_TRIGGER | SUB_STMT_BEFORE_TRIGGER)) == + (SUB_STMT_TRIGGER | SUB_STMT_BEFORE_TRIGGER) && + strcmp(get_sqlstate(), "02TRG") == 0) + m_sql_errno= ER_SIGNAL_SKIP_ROW_FROM_TRIGGER; + else + m_sql_errno= ER_SIGNAL_NOT_FOUND; + } } else /* other SQLSTATE classes : error. */ { @@ -268,7 +279,7 @@ void Sql_state_errno_level::assign_defaults(const Sql_state_errno *from) void Sql_condition::assign_defaults(THD *thd, const Sql_state_errno *from) { if (from) - Sql_state_errno_level::assign_defaults(from); + Sql_state_errno_level::assign_defaults(thd, from); if (!get_message_text()) set_builtin_message_text(ER(get_sql_errno())); } diff --git a/sql/sql_error.h b/sql/sql_error.h index 4751444e2b6..ead860010ad 100644 --- a/sql/sql_error.h +++ b/sql/sql_error.h @@ -167,7 +167,7 @@ protected: /** Severity (error, warning, note) of this condition. */ enum_warning_level m_level; - void assign_defaults(const Sql_state_errno *value); + void assign_defaults(THD *thd, const Sql_state_errno *value); public: /** diff --git a/sql/sql_insert.cc b/sql/sql_insert.cc index 74b84ddb60d..0fcc92a7d6f 100644 --- a/sql/sql_insert.cc +++ b/sql/sql_insert.cc @@ -1057,6 +1057,8 @@ bool mysql_insert(THD *thd, TABLE_LIST *table_list, while ((values= its++)) { + bool trg_skip_row= false; + thd->get_stmt_da()->inc_current_row_for_warning(); if (fields.elements || !value_count) { @@ -1070,7 +1072,8 @@ bool mysql_insert(THD *thd, TABLE_LIST *table_list, if (unlikely(fill_record_n_invoke_before_triggers(thd, table, fields, *values, 0, - TRG_EVENT_INSERT))) + TRG_EVENT_INSERT, + &trg_skip_row))) { if (values_list.elements != 1 && ! thd->is_error()) { @@ -1119,7 +1122,8 @@ bool mysql_insert(THD *thd, TABLE_LIST *table_list, table-> field_to_fill(), *values, 0, - TRG_EVENT_INSERT))) + TRG_EVENT_INSERT, + &trg_skip_row))) { if (values_list.elements != 1 && ! thd->is_error()) { @@ -1131,6 +1135,8 @@ bool mysql_insert(THD *thd, TABLE_LIST *table_list, } } + if (trg_skip_row) + continue; /* with triggers a field can get a value *conditionally*, so we have to repeat has_no_default_value() check for every row @@ -2077,13 +2083,20 @@ int write_record(THD *thd, TABLE *table, COPY_INFO *info, select_result *sink) */ DBUG_ASSERT(info->update_fields->elements == info->update_values->elements); + + bool trg_skip_row= false; + if (fill_record_n_invoke_before_triggers(thd, table, *info->update_fields, *info->update_values, info->ignore, - TRG_EVENT_UPDATE)) + TRG_EVENT_UPDATE, + &trg_skip_row)) goto before_trg_err; + if (trg_skip_row) + goto ok; + bool different_records= (!records_are_comparable(table) || compare_record(table)); /* @@ -2158,7 +2171,8 @@ int write_record(THD *thd, TABLE *table, COPY_INFO *info, select_result *sink) insert_id_for_cur_row= table->file->insert_id_for_cur_row= 0; trg_error= (table->triggers && table->triggers->process_triggers(thd, TRG_EVENT_UPDATE, - TRG_ACTION_AFTER, TRUE)); + TRG_ACTION_AFTER, true, + nullptr)); info->copied++; } @@ -2254,11 +2268,17 @@ int write_record(THD *thd, TABLE *table, COPY_INFO *info, select_result *sink) } else { + bool trg_skip_row= false; + if (table->triggers && table->triggers->process_triggers(thd, TRG_EVENT_DELETE, - TRG_ACTION_BEFORE, TRUE)) + TRG_ACTION_BEFORE, true, + &trg_skip_row)) goto before_trg_err; + if (trg_skip_row) + continue; + if (!table->versioned(VERS_TIMESTAMP)) error= table->file->ha_delete_row(table->record[1]); else @@ -2280,7 +2300,8 @@ int write_record(THD *thd, TABLE *table, COPY_INFO *info, select_result *sink) thd->transaction->stmt.modified_non_trans_table= TRUE; if (table->triggers && table->triggers->process_triggers(thd, TRG_EVENT_DELETE, - TRG_ACTION_AFTER, TRUE)) + TRG_ACTION_AFTER, true, + nullptr)) { trg_error= 1; goto after_trg_or_ignored_err; @@ -2327,7 +2348,8 @@ after_trg_n_copied_inc: thd->record_first_successful_insert_id_in_cur_stmt(table->file->insert_id_for_cur_row); trg_error= (table->triggers && table->triggers->process_triggers(thd, TRG_EVENT_INSERT, - TRG_ACTION_AFTER, TRUE)); + TRG_ACTION_AFTER, true, + nullptr)); ok: /* @@ -4279,9 +4301,10 @@ int select_insert::send_data(List &values) { DBUG_ENTER("select_insert::send_data"); bool error=0; + bool trg_skip_row= false; thd->count_cuted_fields= CHECK_FIELD_WARN; // Calculate cuted fields - if (store_values(values)) + if (store_values(values, &trg_skip_row)) DBUG_RETURN(1); thd->count_cuted_fields= CHECK_FIELD_ERROR_FOR_NULL; if (unlikely(thd->is_error())) @@ -4300,10 +4323,11 @@ int select_insert::send_data(List &values) } } - error= write_record(thd, table, &info, sel_result); + if (!trg_skip_row) + error= write_record(thd, table, &info, sel_result); table->auto_increment_field_not_null= FALSE; - if (likely(!error)) + if (likely(!error) && !trg_skip_row) { if (table->triggers || info.handle_duplicates == DUP_UPDATE) { @@ -4337,7 +4361,7 @@ int select_insert::send_data(List &values) } -bool select_insert::store_values(List &values) +bool select_insert::store_values(List &values, bool *trg_skip_row) { DBUG_ENTER("select_insert::store_values"); bool error; @@ -4345,10 +4369,12 @@ bool select_insert::store_values(List &values) table->reset_default_fields(); if (fields->elements) error= fill_record_n_invoke_before_triggers(thd, table, *fields, values, - true, TRG_EVENT_INSERT); + true, TRG_EVENT_INSERT, + trg_skip_row); else error= fill_record_n_invoke_before_triggers(thd, table, table->field_to_fill(), - values, true, TRG_EVENT_INSERT); + values, true, TRG_EVENT_INSERT, + trg_skip_row); DBUG_RETURN(error); } @@ -5138,10 +5164,11 @@ bool binlog_drop_table(THD *thd, TABLE *table) } -bool select_create::store_values(List &values) +bool select_create::store_values(List &values, bool *trg_skip_row) { return fill_record_n_invoke_before_triggers(thd, table, field, values, - true, TRG_EVENT_INSERT); + true, TRG_EVENT_INSERT, + trg_skip_row); } diff --git a/sql/sql_load.cc b/sql/sql_load.cc index 04913182f29..06b761ba4b9 100644 --- a/sql/sql_load.cc +++ b/sql/sql_load.cc @@ -1013,7 +1013,7 @@ read_fixed_length(THD *thd, COPY_INFO &info, TABLE_LIST *table_list, List_iterator_fast it(fields_vars); Item *item; TABLE *table= table_list->table; - bool err, progress_reports; + bool err= false, progress_reports; ulonglong counter, time_to_report_progress; DBUG_ENTER("read_fixed_length"); @@ -1090,10 +1090,11 @@ read_fixed_length(THD *thd, COPY_INFO &info, TABLE_LIST *table_list, thd->get_stmt_da()->current_row_for_warning()); } + bool trg_skip_row= false; if (thd->killed || fill_record_n_invoke_before_triggers(thd, table, set_fields, set_values, ignore_check_option_errors, - TRG_EVENT_INSERT)) + TRG_EVENT_INSERT, &trg_skip_row)) DBUG_RETURN(1); switch (table_list->view_check_option(thd, ignore_check_option_errors)) { @@ -1104,7 +1105,8 @@ read_fixed_length(THD *thd, COPY_INFO &info, TABLE_LIST *table_list, DBUG_RETURN(-1); } - err= write_record(thd, table, &info); + if (!trg_skip_row) + err= write_record(thd, table, &info); table->auto_increment_field_not_null= FALSE; if (err) DBUG_RETURN(1); @@ -1238,13 +1240,21 @@ read_sep_field(THD *thd, COPY_INFO &info, TABLE_LIST *table_list, } } + bool trg_skip_row= false; if (unlikely(thd->killed) || unlikely(fill_record_n_invoke_before_triggers(thd, table, set_fields, set_values, ignore_check_option_errors, - TRG_EVENT_INSERT))) + TRG_EVENT_INSERT, + &trg_skip_row))) DBUG_RETURN(1); + if (trg_skip_row) + { + read_info.next_line(); + continue; + } + switch (table_list->view_check_option(thd, ignore_check_option_errors)) { case VIEW_CHECK_SKIP: @@ -1361,12 +1371,19 @@ read_xml_field(THD *thd, COPY_INFO &info, TABLE_LIST *table_list, DBUG_ASSERT(!item); + bool trg_skip_row= false; if (thd->killed || fill_record_n_invoke_before_triggers(thd, table, set_fields, set_values, ignore_check_option_errors, - TRG_EVENT_INSERT)) + TRG_EVENT_INSERT, &trg_skip_row)) DBUG_RETURN(1); + if (trg_skip_row) + { + read_info.next_line(); + continue; + } + switch (table_list->view_check_option(thd, ignore_check_option_errors)) { case VIEW_CHECK_SKIP: diff --git a/sql/sql_trigger.cc b/sql/sql_trigger.cc index 09d95bf8f06..3e14f742fbd 100644 --- a/sql/sql_trigger.cc +++ b/sql/sql_trigger.cc @@ -2568,6 +2568,41 @@ end: } +/** + Check that a BEFORE trigger has raised the signal to inform that + a current row being processed must be skipped. + + @param da Diagnostics area + @param[out] skip_row_indicator where to store the fact about skipping + the row + @param time_type time when trigger is invoked (i.e. before or + after) + + @return true in case the current row must be skipped, else false +*/ + +static inline bool do_skip_row_indicator(Diagnostics_area *da, + bool *skip_row_indicator, + trg_action_time_type time_type) +{ + if (!skip_row_indicator) + return false; + + if (time_type == TRG_ACTION_BEFORE && + /* + The '02' class signals a 'no data' condition, the subclass '02TRG' + means 'no data in trigger' and this condition shouldn't be treated + as an error. + */ + strcmp(da->get_sqlstate(), "02TRG") == 0) + { + *skip_row_indicator= true; + return true; + } + return false; +} + + /** Execute trigger for given (event, time) pair. @@ -2578,6 +2613,8 @@ end: @param event @param time_type @param old_row_is_record1 + @param[out] skip_row_indicator the flag to tell whether a row must be + skipped by the INSERT statement @return Error status. @retval FALSE on success. @@ -2588,6 +2625,7 @@ bool Table_triggers_list::process_triggers(THD *thd, trg_event_type event, trg_action_time_type time_type, bool old_row_is_record1, + bool *skip_row_indicator, List *fields_in_update_stmt) { bool err_status; @@ -2595,6 +2633,18 @@ bool Table_triggers_list::process_triggers(THD *thd, Trigger *trigger; SELECT_LEX *save_current_select; + /* + skip_row_indicator != nullptr for BEFORE INSERT/UPDATE/DELETE triggers + */ + DBUG_ASSERT((time_type == TRG_ACTION_BEFORE && skip_row_indicator) || + (time_type == TRG_ACTION_AFTER && !skip_row_indicator)); + /* + In case skip_indicator points to an out variable, its initial value + must be false + */ + DBUG_ASSERT(!skip_row_indicator || + (skip_row_indicator && *skip_row_indicator == false)); + if (check_for_broken_triggers()) return TRUE; @@ -2618,8 +2668,21 @@ bool Table_triggers_list::process_triggers(THD *thd, */ DBUG_ASSERT(trigger_table->pos_in_table_list->trg_event_map & trg2bit(event)); - thd->reset_sub_statement_state(&statement_state, SUB_STMT_TRIGGER); - + if (time_type == TRG_ACTION_AFTER) + thd->reset_sub_statement_state(&statement_state, SUB_STMT_TRIGGER); + else + /* + For time type TRG_ACTION_BEFORE, set extra flag SUB_STMT_BEFORE_TRIGGER + at sub statement state in addition to SUB_STMT_TRIGGER in order to + be able to reset the m_sql_errno to the value + ER_SIGNAL_SKIP_ROW_FROM_TRIGGER + in case signal is raised with SQLSTATE "02TRG" from within a + BEFORE trigger and don't modify m_sql_errno in case the signal is raised + from AFTER trigger. + @see Sql_state_errno_level::assign_defaults + */ + thd->reset_sub_statement_state(&statement_state, + SUB_STMT_TRIGGER | SUB_STMT_BEFORE_TRIGGER); /* Reset current_select before call execute_trigger() and restore it after return from one. This way error is set @@ -2656,6 +2719,18 @@ bool Table_triggers_list::process_triggers(THD *thd, &trigger_table->s->db, &trigger_table->s->table_name, &trigger->subject_table_grants); + + if (err_status && + do_skip_row_indicator(thd->get_stmt_da(), skip_row_indicator, + time_type)) + { + /* Reset DA that is set on handling the statement + SIGNAL SSQLSTATE "02TRG" + raised from within a trigger */ + err_status= false; + thd->get_stmt_da()->reset_diagnostics_area(); + } + status_var_increment(thd->status_var.executed_triggers); } while (!err_status && (trigger= trigger->next)); thd->bulk_param= save_bulk_param; diff --git a/sql/sql_trigger.h b/sql/sql_trigger.h index e2ac168dd83..45abb75bbca 100644 --- a/sql/sql_trigger.h +++ b/sql/sql_trigger.h @@ -260,6 +260,7 @@ public: bool process_triggers(THD *thd, trg_event_type event, trg_action_time_type time_type, bool old_row_is_record1, + bool *skip_row_indicator, List *fields_in_update_stmt= nullptr); void empty_lists(); bool create_lists_needed_for_files(MEM_ROOT *root); diff --git a/sql/sql_update.cc b/sql/sql_update.cc index 015869cb142..0c1decb3221 100644 --- a/sql/sql_update.cc +++ b/sql/sql_update.cc @@ -984,9 +984,18 @@ update_begin: cut_fields_for_portion_of_time(thd, table, table_list->period_conditions); + bool trg_skip_row= false; if (fill_record_n_invoke_before_triggers(thd, table, *fields, *values, 0, - TRG_EVENT_UPDATE)) + TRG_EVENT_UPDATE, + &trg_skip_row)) break; /* purecov: inspected */ + if (trg_skip_row) + { + updated_or_same++; + thd->get_stmt_da()->inc_current_row_for_warning(); + + continue; + } found++; @@ -1115,6 +1124,7 @@ error: if (table->triggers && unlikely(table->triggers->process_triggers(thd, TRG_EVENT_UPDATE, TRG_ACTION_AFTER, true, + nullptr, fields))) { error= 1; @@ -2365,11 +2375,17 @@ int multi_update::send_data(List ¬_used_values) table->status|= STATUS_UPDATED; store_record(table,record[1]); + bool trg_skip_row= false; if (fill_record_n_invoke_before_triggers(thd, table, *fields_for_table[offset], *values_for_table[offset], 0, - TRG_EVENT_UPDATE)) + TRG_EVENT_UPDATE, + &trg_skip_row)) DBUG_RETURN(1); + + if (trg_skip_row) + continue; + /* Reset the table->auto_increment_field_not_null as it is valid for only one row. @@ -2442,6 +2458,7 @@ int multi_update::send_data(List ¬_used_values) if (table->triggers && unlikely(table->triggers->process_triggers(thd, TRG_EVENT_UPDATE, TRG_ACTION_AFTER, true, + nullptr, fields_for_table[offset]))) DBUG_RETURN(1); } @@ -2700,12 +2717,18 @@ int multi_update::do_updates() if (table->vfield && table->update_virtual_fields(table->file, VCOL_UPDATE_FOR_WRITE)) goto err2; + + bool trg_skip_row= false; if (table->triggers && table->triggers->process_triggers(thd, TRG_EVENT_UPDATE, TRG_ACTION_BEFORE, true, + &trg_skip_row, fields_for_table[offset])) goto err2; + if (trg_skip_row) + continue; + if (!can_compare_record || compare_record(table)) { int error; @@ -2764,6 +2787,7 @@ int multi_update::do_updates() if (table->triggers && unlikely(table->triggers->process_triggers(thd, TRG_EVENT_UPDATE, TRG_ACTION_AFTER, true, + nullptr, fields_for_table[offset]))) goto err2; } diff --git a/sql/table.cc b/sql/table.cc index 034e11427fe..84abe704701 100644 --- a/sql/table.cc +++ b/sql/table.cc @@ -9467,16 +9467,23 @@ int TABLE::period_make_insert(Item *src, Field *dst) res= update_generated_fields(); } + bool trg_skip_row= false; if (likely(!res) && triggers) res= triggers->process_triggers(thd, TRG_EVENT_INSERT, - TRG_ACTION_BEFORE, true); + TRG_ACTION_BEFORE, true, &trg_skip_row); + + if (trg_skip_row) + { + restore_record(this, record[1]); + return false; + } if (likely(!res)) res = file->ha_write_row(record[0]); if (likely(!res) && triggers) res= triggers->process_triggers(thd, TRG_EVENT_INSERT, - TRG_ACTION_AFTER, true); + TRG_ACTION_AFTER, true, nullptr); restore_record(this, record[1]); if (res)