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.
Related
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.
So I was playing with transactions, and I have tried to subtract the funds from one transfer it to another. As you can see from the picture, the first update query wasn't successful...Unlike the second one, which executed successfully. Now, what I was expecting is, that when I hit the commit, I wouldn't see any changes. But that wasn't the case. Also, I have use START TRANSACTION (it implicitly sets autocommit to 0), rather than BEGIN command.
Here is the output of this:
What I am missing here?
I don't understand your confusion. Both your updates succeeded. The first one happened to not affect any rows, so only the second one actually changed the data.
You committed the transaction, so all changes take effect.
If you wanted to test transactions, roll back the transaction. Then, when you look at the data, you'll see that nothing changed.
Not any of your operations has failed.
In first update, where conditions were not satisfied and hence no any
row was updated.
In second one, where condition was satisfied for one record, hence that one record was updated.
As an addition to Gordon's answer and his mentioning of stored procedure, I will add an answer just for the future readers and for the completeness, because my real issue was how to rollback a transaction if some condition is not satisfied:
DELIMITER //
CREATE PROCEDURE transfer(IN sender INT, IN receiver INT)
BEGIN
START TRANSACTION;
SET #senderBalance = (SELECT balance FROM bank_acc WHERE acctnum = sender LIMIT 1);
select #senderBalance;
IF (#senderBalance < 50) THEN
ROLLBACK;
ELSE
update bank_acc set balance = balance - 50 where acctnum = sender;
update bank_acc set balance = balance + 50 where acctnum = receiver;
COMMIT;
END IF;
END//
DELIMITER ;
Later, you can use it like this:
call transfer(#sender := 20, #receiver := 10);
I am writing a trigger for controlling a column. The script works as I want but my problem is in the raiseerror. I want the trigger to work without showing the error message to the user.
Can anyone who knows what is the equivalent of raiseerror without showing the error message to the user?
I tried with rollback transaction which gve me another error message instead, and I tried with return which did not interrupt the execution of the trigger.
This is my trigger:
DECLARE #val varchar(9)
SELECT #val= [DC_Piece]
from INSERTED
where INSERTED [DC_Domaine]=0 and INSERTED.[DC_IdCol]=6
IF UPDATE([DC_Piece])
BEGIN
IF NOT EXISTS( select [DO_PIECE]
from DOCEN
where #val= [DO_Piece] and [DO_Domaine]=0 and [DO_Type]=6)
RAISERROR('STOP',11,1)
END
Please help me
You need to completely rewrite your trigger to take into account that it will be called once per statement (NOT per row!) and the Inserted and Deleted pseudo tables can contain multiple rows which you should consider.
So try something like this:
CREATE TRIGGER trg_abort_insert
ON dbo.YourTableNameHere
AFTER UPDATE
AS
-- check if any of the DC_Piece columns have been updated
IF EXISTS (SELECT *
FROM Inserted i
INNER JOIN Deleted d ON i.PrimaryKey = d.PrimaryKey -- link the two pseudo tables on primary key
WHERE i.DC_Piece <> d.DC_Piece -- DC_Piece has changed
AND i.DC_Domaine = 0
AND i.DC_IdCol = 6)
-- if your conditions are met --> just roll back the transaction
-- nothing will be stored, no message is shown to the user
ROLLBACK TRANSACTION
END
PROBLEM SUMMARY:
i made error handling that seems to be way too complicated and still does not solve all situations (as there can be situations where transaction gets in uncommitable state). I suspect i:
have missed something important and doing it wrong (can you explain what? and how should i do it then?).
haven't missed anything- just have to accept that error handling is still huge problem in SQL Server.
Can you offer better solution (for described situation below)?
ABOUT MY SITUATION:
I have (couple of) stored procedure in SQL Server, that is called from different places. Can generalize for 2 situations:
Procedure is called from .NET code, transaction is made and handled in SQL procedure
Procedure is called in other procedure (to be more specific- in Service Broker activation procedure), so the transaction is handled by outer procedure.
I made it so, that procedure returns result (1 for success, 0 for failure) + returns message for logging purposes in case of error.
Inside the procedure:
Set XACT_ABORT ON; -- transaction not to be made uncommitable because of triggers.
Declare #PartOfTran bit = 0; -- is used, to save status: 1- if this procedure is part of other transaction or 0- should start new transaction.
If this is part of other tran, then make save point. If not- then begin transaction.
Begin try block- do everything and if there is no mistakes AND if this is not nested transaction do commit. If it is nested transaction- commit will be made in caller procedure.
In case of error: if this is nested transaction and transaction is in commitable state- can do rollback to savepoint "MyTran". if its not part of transaction, rollback transaction called "MyTran". In all other cases- just return error code and message.
Code looks like this:
Create Procedure dbo.usp_MyProcedure
(
-- params here ...
#ReturnCode int out, -- 1 Success, != 1 Error
#ReturnMsg nvarchar(2048) out
)
AS
Begin
Set NoCount ON;
Set XACT_ABORT ON;
Declare #PartOfTran bit = 0;
IF(##TRANCOUNT > 0)
Begin
SET #PartOfTran = 1;
SAVE TRAN MyTran;
END
Else
BEGIN TRAN MyTran;
Begin Try
-- insert table1
-- update table2
-- ....
IF(#PartOfTran = 0)
COMMIT TRAN MyTran;
Select #ReturnCode = 1, #ReturnMsg = Null;
End Try
Begin Catch
IF (XACT_STATE() = 1 And #PartOfTran = 1) OR #PartOfTran = 0
Rollback Tran MyTran;
Select #ReturnCode = 0, #ReturnMsg = ERROR_MESSAGE();
End Catch
End
OTHER LITERATURE:
From my favorite bloggers have seen:
sommarskog - but i don't like that "outer_sp" has line "IF ##trancount > 0 ROLLBACK TRANSACTION", because in my case- outer procedure can be called in transaction, so in that case i have "Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0."
rusanu - actually almost the same as i wrote here (maybe idea comes from that blog post- i wrote my solution based on all i have read about this subject). This blog post still does not solve what should i do with uncommitable transactions. This is problem in case of Service Broker. How can i make correct logging of error message, if i have to rollback uncommitable transaction? i have ideas about this, but all of them seems like workarounds not elegant solutions.
You won't be able to achieve a solution that rolls back only the work done in usp_MyProcedure in any condition. Consider the most obvious example: deadlock. When your are notified of exception 1205 (you've been chosen as a deadlock victim) the transaction has already rolled back ( in order to allow progress). As error handling goes, the only safe option is to further raise and re-throw so that the caller has a chance to react. The 'uncommittable transaction' is just a variation on that theme: there is just no way error handling can recover from such a situation in a manner that is sensible for the caller, when the caller has started a transaction. The best thing is to raise (re-throw). This is why I used the pattern you've seen in my blog at Exception HAndling and Nested Transactions
Considering this in Service Broker context it means that there is no completely bullet proof, exception safe message handling routine. If you hit an uncommitable transaction (or a transaction that has already rolled back by the time you process the catch block, like 1205 deadlock) then your whole batch of received messages will have to rollback. Logging is usualy done in such situations after the outermost catch block (usually locate din the activated procedure). Here is pseudo code of how this would work:
usp_myActivatedProc
as
#commited = false;
#received = 0;
#errors = 0;
begin transaction
begin try
receive ... into #table;
#received = ##row_count;
foreach message in #table
save transaction
begin try
process one message: exec usp_myProcedure #msg
end try
begin catch
if xact_state()=1
rollback to savepoint
#errors += 1;
-- decide what to do with failed message, log
-- this failure may still be committed (receive won't roll back yet)
else
-- this is a lost cause, re-throw
raiserror
end catch
fetch next #table
endfor
commit
#commited = true;
end try
catch
#error_message = error_message();
if xact_state() != 0
rollback
end catch
if #commited = false
begin
insert into logging 'failed', #received, #error_message
end
-- insert table1
IF ##ERROR > 0
GOTO _FAIL
-- update table2
-- ....
IF ##ERROR > 0
GOTO _FAIL
GOTO _SUCCESS
_ERROR:
ROLLBACK TRAN
SET #ReturnCode = 1
RETURN
_FAIL:
ROLLBACK TRAN
SET #ReturnCode = 1
RETURN
_SUCCESS:
COMMIT TRAN
at the end of the tran insert the
GOTO _SUCCESS
so it will commit the tran if didnt hit any errors.
AND all of ur insert or update statement inside the tran insert the
IF ##ERROR > 0
GOTO _FAIL
so when it hit error.. it will go to the
_FAIL:
ROLLBACK TRAN
SET #ReturnCode = 1
RETURN
and u can set all ur return value at there
I need to perform several inserts in a single atomic transaction. For example:
start transaction;
insert ...
insert ...
commit;
However when MySQL encounters an error it aborts only the particular statement that caused the error. For example, if there is an error in the second insert statement the commit will still take place and the first insert statement will be recorded. Thus, when errors occur a MySQL transaction is not really a transaction. To overcome this problem I have used an error exit handler where I rollback the transaction. Now the transaction is silently aborted but I don't know what was the problem.
So here is the conundrum for you:
How can I both make MySQL abort a transaction when it encounters an error, and pass the error code on to the caller?
How can I both make MySQL abort a transaction when it encounters an error, and pass the error code on to the caller?
MySQL does pass error code to the caller and based on this error code the caller is free to decide whether it wants to commit work done up to the moment (ignoring the error with this particular INSERT statement) or to rollback the transaction.
This is unlike PostgreSQL which always aborts the transaction on error and this behavior is a source of many problems.
Update:
It's a bad practice to use an unconditional ROLLBACK inside the stored procedures.
Stored procedures are stackable and transactions are not, so a ROLLBACK within a nested stored procedure will roll back to the very beginning of the transaction, not to the state of the stored procedure execution.
If you want to use transactions to restore the database state on errors, use SAVEPOINT constructs and DECLARE HANDLER to rollback to the savepoints:
CREATE PROCEDURE prc_work()
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK TO sp_prc_work;
SAVEPOINT sp_prc_work;
INSERT …;
INSERT …;
…
END;
Failure in either insert will roll back all changes made by the procedure and exit it.
Using Mr. Quassnoi's example, here's my best approach to catching specific errors:
The idea is to use a tvariable to catch a simple error message, then you can catch sql states you think may happen to save custom messages to your variable:
DELIMITER $$
DROP PROCEDURE IF EXISTS prc_work $$
CREATE PROCEDURE prc_work ()
BEGIN
DECLARE EXIT HANDLER FOR SQLSTATE '23000'
BEGIN
SET #prc_work_error = 'Repeated key';
ROLLBACK TO sp_prc_work;
END;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SET #prc_work_error = 'Unknown error';
ROLLBACK TO sp_prc_work;
END;
START TRANSACTION;
SAVEPOINT sp_prc_work;
INSERT into test (id, name) VALUES (1, 'SomeText');
COMMIT;
END $$
DELIMITER ;
Then you just do your usual call, and do a select statement for the variable like:
call prc_work(); select #prc_work_error;
This will return either NULL if no error, or the message error in case of an error. If you need persistent error message you can optionally create a table to store it.
It's tedious and not very flexible because requires a DECLARE EXIT HANDLER segment for each status code you want to catch, it won't also show detailed error messages but hey, it works.