My bosses nor any of the DBAs know how to make triggers and I don't neither(new-ish programmer), they just copied/pasted some old triggers from the DB for examples for me. Anyway, my triggers copy data from one table to another after an update/insert. The insert/update that goes to the original table is vital, so if anything fails, I just want the trigger to fail and the original insert/update to still run just fine.
I am using MySQL to test, but we use DB2, but they won't give me access to test triggers in their DB2 environemt, so this the closest solution I can use to test the trigger logic.
I noticed that BEGIN ATOMIC is in the example triggers, does that do what I want? And what would be equivalent in MySQL, so I can test?
I have subselects in my triggers are these safe? Should I declare variables to help avoid issues?
--#SET TERMINATOR #
create table test_trigger (i int) in userspace1#
create table test_trigger_copy (i int) in userspace1#
create or replace trigger test_trigger_air
after insert on test_trigger
referencing new as n
for each row
begin
declare continue handler for sqlexception begin end;
insert into test_trigger_copy(i) values (case when mod (n.i, 2)=0 then cast(RAISE_ERROR('70001', 'No even numbers!') as int) else n.i end);
end#
insert into test_trigger(i) values 1, 2, 3#
select * from test_trigger_copy#
select * from test_trigger#
For DB2 (for LUW at least) databases.
The INSERT statement inside the trigger generates an exception when you try to insert an even number into the base table. The CONTINUE handler consumes exceptions, so you get all inserted numbers in the base table, but only odd numbers in the copy table.
A close MySQL equivalent to DB2's BEGIN ATOMIC, would be a combination of START TRANSACTION, COMMIT, EXIT HANDLER, and ROLLBACK. Instead of:
BEGIN ATOMIC
<statement(s)>
END
one could do the following in MySQL:
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
<statement(s)>
COMMIT;
END
As to subselects (you mean subqueries?) in the trigger, I do not see why they would not work, as long as you observe the restrictions.
Related
I can't seem to get this to work... The result is SQL Management Studio seems to hang, gives me the message
Msg 208, Level 16, State 1, Line 76 Invalid object name
If I try to close the code window, I get a warning that the transaction isn't committed, and asks if I would like to commit it. If I do, the truncate has happened, and the items are missing.
I'm trying to make sure the truncate does NOT happen or gets rolled back if the table in the "INSERT" statement is missing.
BEGIN TRY
BEGIN TRAN
TRUNCATE TABLE X
INSERT INTO TABLE X ([values...]) -- pseudo code; insert works fine if table is present
SELECT * FROM <potentially missing table>
COMMIT TRAN
END TRY
BEGIN CATCH
if (##TRANCOUNT > 0)
ROLLBACK
END CATCH
Based on the information provided it looks like it may be a problem with your syntax, but it is unclear without a CREATE TABLE statement and some working code. It could also be that you're not checking if the table exists before the SELECT. I just tested the below and it has the desired results.
BEGIN TRY
BEGIN TRAN
TRUNCATE TABLE [test_table]
INSERT INTO [test_table] VALUES ('...')
SELECT * FROM [test_table]
COMMIT
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
PRINT N'rolling back transaction' --for confirmation of the catch
ROLLBACK
END CATCH
Or to avoid the TRY/CATCH use IF EXISTS to check if the table exists before starting anything.
IF EXISTS (SELECT 1 FROM sys.tables WHERE name = 'test_table')
BEGIN
BEGIN TRAN
TRUNCATE TABLE [test_table]
INSERT INTO [test_table] VALUES ('...')
SELECT * FROM [test_table]
COMMIT
END
ELSE
BEGIN
-- do something else
PRINT N'The table does not exist'
END
Hope this helps!
2020-10-11:
I tried SET XACT_ABORT ON, prior to engaging a transaction.
Interestingly, when encountering a missing table (e.g. at SELECT stmt), the script STILL halts without dropping into the TRY/CATCH block.
Moreover, and actually worse, all prior SQL output is no longer available.
Notwithstanding, the transaction was rolled back, as there is no open Transaction, thereafter.
PROOF:
executing "select ##TRANCOUNT" returns 0
manually executing "ROLLBACK TRAN" states: "The ROLLBACK TRANSACTION
request has no corresponding BEGIN TRANSACTION."
I, therefore, decided to check for required tables, at script start/prior to engaging a Transaction, instead of using XACT_ABORT.
I'm trying to find a way to check ,before adding a new tuple in a table, if the tuple respect some condition and in case of one of the conditions is not respected do not allow the insert.
I've thought of something like
DELIMITER //
CREATE TRIGGER t BEFORE INSERT ON Table
FOR EACH ROW
CALL CHECK1(…);
CALL CHECK2(…);
CALL CHECK3(…);
//
DELIMITER;
Where check1,check2,check3 are procedures that raise an exception if the NEW.(attributes) that I pass do not respect condition in the inserting table and/or with other tables.
Is this a correct and/or good way to make what I'm trying to do?
What is the best way to do that?
The best way to do it, is to do the data validation using stored procedures, instead of triggers. The trigger strategy is useful if you only want to filter incoming data. If the objective is to cancel an operation entirely when data values are unsuitable, you cannot do this in MySQL using a trigger.
I'm answering to reply(with a comment my answer would be incomprehensible) and to give more details:
I've used 2 strategies to make my goal, here 2 examples
1)if the check is easy
DELIMITER $$
create trigger RV5_1 before insert on Customer
for each row begin
IF(DATEDIFF(CURDATE(),NEW.birthdate)/365<18)
THEN
SIGNAL sqlstate '45006' set message_text = "too young to be a customer";
END IF;
END;
$$
DELIMITER ;
2) if the check is not easy and need cursors, variables etc
DELIMITER $$
create trigger T2 before insert on Table
for each row begin
IF (check1(NEW.[_some_attribute/s_]) or
check2(NEW.[_some_attribute/s_]))
THEN
SIGNAL sqlstate '45002' set message_text = "invalid insert";
END IF;
END;
$$;
DELIMITER ;
where check1 and check2 are stored functions that returns 0 if it's ok or 1 if there are problem with the new tuple.
Maybe someone with the same problem will found this helpful.
In other words, if I have a trigger that calls a proc; should I be able to check ##rowcount in the first line of the proc? And should ##rowcount have a value based on the last SQL statment executed in the trigger?
I'm debugging a proc which I did not write. First line of the proc is
if ##rowcount=0
RETURN
The proc is called by a trigger. The last thing the trigger does before it calls the proc is to:
INSERT INTO #temp_table (some fields...)
SELECT some fields.. FROM inserted
EXEC SOMEPROC
What's happening is that ##rowcount in the proc is always 0 and the proc ends before it does anything. The fix is pretty easy in that I'd just move the ##rowcount check into the trigger and take it out of the proc.
But its curious to me why it was written this way in the first place. Almost like it used to work at some point and now it doesn't? I just wanted to check and see if maybe there was some change in behavior between SQL2005 and SQL2008 or some system setting that got flipped. Or did one of my predecessors just write bad code that never worked?
I don't like to use ##ROWCOUNT for checking the status of a trigger. Too many other statements in the procedure and in between can reset that to 0.
For example:
SELECT TOP (10) name FROM sys.objects;
SET ANSI_NULLS ON;
SELECT ##ROWCOUNT;
----
0
Rather, I would check the inserted/deleted pseudo-tables. So, for an insert or update trigger:
IF EXISTS (SELECT 1 FROM inserted)
BEGIN
...
For a delete trigger:
IF EXISTS (SELECT 1 FROM deleted)
BEGIN
...
Does MySQL permit callbacks in C such that when a change happens in the database, like an insert, that is performed by a different program or by the user at the command line, I can be notified?
I am guessing that it doesn't, because mysqlclient is a library, not a running thread. But I may as well ask.
Create a trigger like so.
DELIMITER $$
CREATE TRIGGER ad_mytable_each AFTER DELETE ON MyTable FOR EACH ROW
BEGIN
#write code that trigger After delete (hence the "ad_" prefix)
#For table MyTable (The _MyTable_ middle)
#On each row that gets inserted (_each suffix)
#
#You can see the old delete values by accesing the "old" virtual table.
INSERT INTO log VALUES (old.id, 'MyTable', old.field1, old.field2, now());
END$$
DELIMITER ;
There are triggers for INSERT, DELETE, UPDATE
And they can fire BEFORE or AFTER the action.
The trigger BEFORE the action can cancel the action by forcing an error, like so.
CREATE TRIGGER bd_mytable_each BEFORE DELETE ON MyTable FOR EACH ROW
BEGIN
#write code that trigger Before delete (hence the "db_" prefix)
declare DoError Boolean;
SET DoError = 0;
IF old.id = 1 THEN SET DoError = 1; END IF;
IF (DoError = 1) THEN SELECT * FROM Table_that_does_not_exist_to_force_error;
#seriously this example is in the manual.
END$$
DELIMITER ;
This will prevent deletion of record 1.
A before UPDATE Trigger can even change the values updated.
CREATE TRIGGER bu_mytable_each BEFORE UPDATE ON MyTable FOR EACH ROW
BEGIN
IF new.text = 'Doon sucks' THEN SET new.text = 'Doon rules';
END$$
DELIMITER ;
Hope you'll be Trigger happy.
MySQL's triggers allow you to hook into insert/update/delete queries and do something additional. You could log them in a separate table, for example.
Well you could attach a trigger to user defined function, and have it call an external program, that would then notify your code..
http://dev.mysql.com/doc/refman/5.0/en/faqs-triggers.html#qandaitem-B-5-1-10
You can use triggers combined with UDFs (user defined functions) so that the corresponding action on the database executes a trigger that calls a C/C++ function.
Just consider that this mechanism runs your code inside the mysql server process, not in the client side.
I'm having a rather strange problem with MySQL. Trying to create a procedure to update some fields in the database (the code is below).
The problem is with the line that is currently commented. It seems that if no SELECT statements get executed during the procedure MySQL query browser will return an error code of "-1, error executing SQL query".
I tried the same thing in HeidiSQL and the error was "cannot return result set". So I suppose the question is do I always have to select something in the procedure, or is there some other thing I missed.
The query works fine when the comment is removed.
DELIMITER /
DROP PROCEDURE IF EXISTS updateFavourites /
CREATE PROCEDURE updateFavourites(quota INT)
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE artist_id,releases INT;
DECLARE c_artist Cursor FOR
SELECT Artist.id_number,COUNT(Artist.id_number) FROM Artist
JOIN CD ON CD.is_fronted_by = Artist.id_number
GROUP BY Artist.id_number;
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000'
SET done=1;
IF quota > 0 THEN
OPEN c_artist;
REPEAT
FETCH c_artist INTO artist_id,releases;
IF NOT done THEN
IF releases >= quota THEN
UPDATE CD SET CD.rating='favourite' WHERE CD.is_fronted_by = artist_id;
END IF;
END IF;
UNTIL done END REPEAT;
CLOSE c_artist;
-- SELECT 'Great success';
ELSE
SELECT CONCAT('\'quota\' must be greater than 0.',' Got (',quota,')');
END IF;
END /
DELIMITER ;
Here's the sql to create the tables and some data:
DROP TABLE IF EXISTS CD;
DROP TABLE IF EXISTS Artist;
CREATE TABLE Artist (
id_number INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50),
);
CREATE TABLE CD (
catalog_no INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY,
is_fronted_by INT UNSIGNED,
rating ENUM ('favourite','top draw','good','so-so','poor','rubbish'),
CONSTRAINT fk_CD_Artist FOREIGN KEY (is_fronted_by) REFERENCES Artist(id_number) ON UPDATE CASCADE
);
INSERT INTO Artist VALUES(11,'Artist 1');
INSERT INTO Artist VALUES(10,'Artist 2');
INSERT INTO CD VALUES (7,11, 'top draw');
INSERT INTO CD VALUES (650,11,'good');
INSERT INTO CD VALUES (651,11,'good');
INSERT INTO CD VALUES (11,10,'favourite');
Query Browser is not for running scripts, just single query.
I tried your code by moving cursor into each query (except DELIMITER) and pressing Ctrl+Enter.
It created that stored procedure without problem. (just refresh schema on the left).
If you wish creating procedure, use menu "Script"->"Create stored procedure/function".
But better forget about QueryBrowser it is not supported at all (and actunally not useful).
If you have decent hardware and plenty resources, try Workbench 5.2 otherwise use SQLyog
Googling around, there are several reports of the same error, but little information to solve the problem. There's even a bug logged at mysql.com but it appears to have been abandoned without being resolved.
There's another StackOverflow question on the same error, but it's also unresolved.
All it means is that there is no result set from the query. Looking at the source code, it appears that sometimes an error status of MYX_SQL_ERROR is set when the query has no result set. Perhaps this is not an appropriate consequence?
I notice that when I use the mysql command-line client, it yields no error for calling a proc that returns no result set.
update: I tried to revive that MySQL bug report, and provide a good test case for them. They changed the bug from "no feedback" to "verified" -- so at least they acknowledge it's a bug in Query Browser:
[11 Dec 9:18] Sveta Smirnova
Bill,
thank you for the feedback. Verified
as described.
Although most likely this only be
fixed when MySQL Query Browser
functionality is part of MySQL
workbench.
I guess the workaround is to ignore the -1 error, or to test your stored procedures in the command-line mysql client, where the error does not occur.
The comment supposes the issue will disappear as the Query Browser functionality becomes part of MySQL Workbench. This is supposed to happen in MySQL Workbench 5.2. I'll download this beta and give it a try.
MySQL Workbench 5.2 is in Beta, but I would assume MySQL engineering can't predict when the Beta will become GA. Those kinds of predictions are hard enough under standard conditions, but there's a lot of extra uncertainty of MySQL's fate due to the unresolved Oracle acquisition.
update: Okay, I have tried MySQL Workbench 5.2.10 beta. I executed a stored procedure like this:
CREATE PROCEDURE FooProc(doquery SMALLINT)
BEGIN
IF doquery THEN
SELECT * FROM Foo;
END IF;
END
When I CALL FooProc(0) the response is no result set, and the status is simply "OK".
When I CALL FooProc(1) the response is the result of SELECT * FROM Foo as expected.
However, there's another bug related to calling procedures. Procedures may have multiple result sets, so it's hard to know when to close the statement when you execute a CALL query. The consequence is that MySQL Workbench 5.2 doesn't close the statement, and if you try to do another query (either CALL or SELECT) it gives you an error:
Commands out of sync; you can't run this command now.
MySQL doesn't support multiple concurrent open queries. So the last one must be closed before you can start a new one. But it isn't closing the CALL query. This bug is also logged at the MySQL site.
The bug about commands out of sync has been resolved. They say it's fixed in MySQL Workbench 5.2.11.
Try putting BEGIN and END blocks around the multiple statements in the IF block as such:
IF quota > 0 THEN
BEGIN
OPEN c_artist;
REPEAT
FETCH c_artist INTO artist_id,releases;
IF NOT done THEN
IF releases >= quota THEN
UPDATE CD SET CD.rating='favourite' WHERE CD.is_fronted_by = artist_id;
END IF;
END IF;
UNTIL done END REPEAT;
CLOSE c_artist;
END;
ELSE
SELECT CONCAT('\'quota\' must be greater than 0.',' Got (',quota,')');
END IF;