TSQL make trigger fail silently - sql-server

I have some code in an after insert trigger that may potentially fail. Such a failure isn't crucial and should not rollback the transaction. How can I trap the error inside the trigger and have the rest of the transaction execute normally?
The example below shows what I mean. The trigger intentionally creates an error condition with the result that the original insert ( "1" ) never inserts into the table. Try/Catch didn't seem to do the trick. A similar, older stack overflow question didn't yield an answer except for "prevent the error from occuring in the first place" - which isn't always possible/easy.
Any other ideas?
create table test
(
a int not null
);
go
create trigger testTrigger on test
after insert as
begin
insert into test select null;
end;
go
insert into test values ( 1 );

A trigger cannot fail and still have the transaction roll forward. You have a few options to ensure that the trigger does not fail.
1 - You can ensure that the after does not fail by duplicating the logic for checking the constraints and not attempting an operation which would violate the constraints:
i.e.
INSERT INTO test WHERE val IS NOT NULL
2 - You can defer the potentially failing action by using a queue design pattern where actions which may or may not fail are queued by enqueueing to a table where the enqueueing operation cannot possibly fail.
i.e.
INSERT INTO ACTION_QUEUE (action, parameters) VALUES ('INSERT INTO TEST', val)

Due to the way triggers are implemented in SQL Server, all constraint violations within the triggers doom the transactions.
This is the same as doing:
DROP TABLE test
CREATE TABLE test
(
a INT NOT NULL
)
GO
SET XACT_ABORT ON
GO
BEGIN TRANSACTION
BEGIN TRY
INSERT
INTO test
SELECT NULL
END TRY
BEGIN CATCH
INSERT
INTO test
SELECT 1
END CATCH
which results in a doomed transaction, except that there is no way to disable XACT_ABORT inside a trigger.
SQL Server also lacks autonomous transactions.
That's another reason why you should put all you logic into the stored procedures rather than triggers.

You can turn XACT_ABORT off inside the trigger (use caution)
You can have the trigger call a stored procedure. (I am now wrestling with the opposite problem: I want the transaction aborted, but because the logic is in an SP called from the trigger, and not the trigger itself, this isn't happening.)

Related

T-SQL Stored Proc Update not Happening with trigger

I'm using an after update trigger on a table purely for testing purposes in order to force an error with an SSIS package. It's basically just a trigger that calls RAISERROR() with a static message after an update happens.
This works ok in most instances. However, when I call a specific stored procedure that contains a try/catch (no explicit transactions involved), and it updates the table, the update doesn't happen or is getting rolled back somehow. My understanding of Try/Catch was that it would not rollback unless you explicitly implemented BEGIN/COMMIT/ROLLBACK TRANSACTION.
I seem to be misunderstanding Try/Catch, or I'm misunderstanding how triggers function. I normally try not to use triggers, but for this use-case it made sense.
If I comment out the Try/Catch, everything functions as I'd expect.
CREATE PROCEDURE dbo.MySproc
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
UPDATE dbo.MyTableToFireTrigger SET SomeColumn = 1 WHERE SomeColumn = 0
END TRY
BEGIN CATCH
--I'm able to log the after update trigger error message here, but it seems to be rolling back the update.
END CATCH;
END
Trigger create:
CREATE TRIGGER dbo.MyTrigger ON dbo.MyTableToCauseTrigger
AFTER UPDATE
AS
BEGIN
RAISERROR('Error', 16, 1);
END;
In a transaction if an error occurred that transaction will be rolled back. This refers to the transaction concept. When you call your stored procedure a transaction is started. Then it causes your trigger to be fired and the error raised. Automatically the transaction will be rolled back.

Is there any way to find out if there is an open wrapper transaction while inside a trigger?

When we are inside a trigger and have a transaction open before entering the trigger, the ##TranCount shows 1, same as when we don't have that transaction open.
So Is there any way to find out if there is an open wrapper transaction in this case?
PS: I have a table that fires this trigger. this table can be manipulated in different places with/without wrapper transaction. I need to know about the number of trans while inside the trigger to do proper action like rolling back the trans or leaving it.
I was curious about this behaviour, and I can confirm that it is as observed in SQL Server.
create table test (id int identity, t varchar(23))
create trigger trg_inser on test after insert as
select ##TRANCOUNT
-- test 1
insert into test(t) values ('test')
--=> returns 1
-- test 2
begin transaction
insert into test(t) values ('test')
rollback
--=> also returns 1
select ##TRANCOUNT
--=> returns 0
Books online documents this behaviour
A trigger operates as if there were an outstanding transaction in
effect when the trigger is executed. This is true whether the
statement firing the trigger is in an implicit or explicit
transaction.
When a statement begins executing in autocommit mode, there is an
implied BEGIN TRANSACTION to allow the recovery of all modifications
generated by the statement if it encounters an error. This implied
transaction has no effect on the other statements in the batch because
it is either committed or rolled back when the statement completes.
This implied transaction is still in effect, however, when a trigger
is called.
I don't think you will be able to differentiate between whether the transaction is explicit or implicit while you are in the trigger.
I suspect what you might need to do is some kind of a try...catch inside the trigger code that handles an error raised during a trigger

Is it necessary to use a transaction if other statements are dependent on the first?

For so long, I've omitted using SQL Transactions, mostly out of ignorance.
But let's say I have a procedure like this:
CREATE PROCEDURE CreatePerson
AS
BEGIN
declare #NewPerson INT
INSERT INTO PersonTable ( Columns... ) VALUES ( #Parameters... )
SET #NewPerson = SCOPE_IDENTITY()
INSERT INTO AnotherTable ( #PersonID, CreatedOn ) VALUES ( #NewPerson, getdate() )
END
GO
In the above example, the second insert depends on the first, as in it will fail if the first one fails.
Secondly, and for whatever reason, transactions are confusing me as far as proper implementation. I see one example here, another there, and I just opened up adventureworks to find another example with try, catch, rollback, etc.
I'm not logging errors. Should I use a transaction here? Is it worth it?
If so, how should it be properly implemented? Based on the examples I've seen:
CREATE PROCEURE CreatePerson
AS
BEGIN TRANSACTION
....
COMMIT TRANSACTION
GO
Or:
CREATE PROCEDURE CreatePerson
AS
BEGIN
BEGIN TRANSACTION
COMMIT TRANSACTION
END
GO
Or:
CREATE PROCEDURE CreatePerson
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
...
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END
END CATCH
END
Lastly, in my real code, I have more like 5 separate inserts all based on the newly generated ID for person. If you were me, what would you do? This question is perhaps redundant or a duplicate, but for whatever reason I can't seem to reconcile in my mind the best way to handle this.
Another area of confusion is the rollback. If a transaction must be committed as a single unit of operation, what happens if you don't use the rollback? Or is the rollback needed only in a Try/Catch similar to vb.net/c# error handling?
You are probably missing the point of this: transactions are suppose to make a set of separate actions into one, so if one fails, you can rollback and your database will stay as if nothing happened.
This is easier to see if, let's say, you are saving the details of a purchase in a store. You save the data of the customer (like Name or Address), but somehow in between, you missed the details (server crash). So now you know that John Doe bought something, but you don't know what. You Data Integrity is at stake.
Your third sample code is correct if you want to handle transactions in the SP. To return an error, you can try:
RETURN ##ERROR
After the ROLLBACK. Also, please review about:
set xact_abort on
as in: SQL Server - transactions roll back on error?
If the first insert succeeds and the second fails you will have a database in a bad state because SQL Server cannot read your mind. It will leave the first insert (change) in the database even though you probably wanted it all tosucceed or all fail.
To ensure this you should wrap all the statements in begin transaction as you illustrated in the last example. Its important to have a catch so any half completed transaction are explicitly rolled back and the resources (used by the transaction) released as soon as possible.

Does a rollback inside a INSERT AFTER or UPDATE AFTER trigger rollback the entire transaction

Does a rollback inside a INSERT AFTER or an UPDATE AFTER trigger rollback the entire transaction or just the current row that is the reason for trigger, and is it same with Commit ?
I tried to check it through my current projects code which uses MSTDC for transactions, and it appears as if though the complete transaction is aborted.
If a Rollback in the trigger does rollback the entire transaction, is there a workaround for to restrict it just the current rows.
I found a link for sybase on this, but nothing on sql server
Yes it will rollback the entire transaction.
It's all in the docs (see Remarks). Note the comment I've emphasised - that's pretty important I would say!!
If a ROLLBACK TRANSACTION is issued in a trigger:
All data modifications made to that point in the current transaction
are rolled back, including any made by the trigger.
The trigger continues executing any remaining statements after the
ROLLBACK statement. If any of these statements modify data, the
modifications are not rolled back. No nested triggers are fired by the
execution of these remaining statements.
The statements in the batch after the statement that fired the trigger
are not executed.
As you've already been let to know, the ROLLBACK command can't possibly be modified/tuned so that it only roll back the statements issued by the trigger.
If you do need a way to "rollback" actions performed by the trigger only, you could,
as a workaround, consider modifying your trigger in such a way that before performing the actions, the trigger makes sure those actions do not produce exceptional situations that would cause the entire transaction to rollback.
For instance, if your trigger inserts rows, add a check to make sure the new rows do not violate e.g. unique constraints (or foreign key constraints), something like this:
IF NOT EXISTS (
SELECT *
FROM TableA
WHERE … /* a condition to test if a row or rows you are about
to insert aren't going to violate any constraint */
)
BEGIN
INSERT INTO TableA …
END;
Or, if your trigger deletes rows, check if it doesn't attempt to delete rows referenced by other tables (in which case you typically need to know beforehand which tables might reference the rows):
IF NOT EXISTS (
SELECT * FROM TableB WHERE …
)
AND NOT EXISTS (
SELECT * FROM TableC WHERE …
)
AND …
BEGIN
DELETE FROM TableA WHERE …
END
Similarly, you'd need to make checks for update statements, if any.
Any rollback command will roll back everything till the ##trancount is 0 unless you specify some savepoints and it doesnt matter where you put rollback tran command.
The best way is to look into the code once again and confirm business requirement and see why do you need a rollback in trigger?

Ignoring errors in Trigger

I have a stored procedure which is called inside a trigger on Insert/Update/Delete.
The problem is that there is a certain code block inside this SP which is not critical.
Hence I want to ignore any erros arising from this code block.
I inserted this code block inside a TRY CATCH block. But to my surprise I got the following error:
The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
Then I tried using SAVE & ROLLBACK TRANSACTION along with TRY CATCH, that too failed with the following error:
The current transaction cannot be committed and cannot be rolled back to a savepoint. Roll back the entire transaction.
My server version is: Microsoft SQL Server 2008 (SP2) - 10.0.4279.0 (X64)
Sample DDL:
IF OBJECT_ID('TestTrigger') IS NOT NULL
DROP TRIGGER TestTrigger
GO
IF OBJECT_ID('TestProcedure') IS NOT NULL
DROP PROCEDURE TestProcedure
GO
IF OBJECT_ID('TestTable') IS NOT NULL
DROP TABLE TestTable
GO
CREATE TABLE TestTable (Data VARCHAR(20))
GO
CREATE PROC TestProcedure
AS
BEGIN
SAVE TRANSACTION Fallback
BEGIN TRY
DECLARE #a INT = 1/0
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION Fallback
END CATCH
END
GO
CREATE TRIGGER TestTrigger
ON TestTable
FOR INSERT, UPDATE, DELETE
AS
BEGIN
EXEC TestProcedure
END
GO
Code to replicate the error:
BEGIN TRANSACTION
INSERT INTO TestTable VALUES('data')
IF ##ERROR > 0
ROLLBACK TRANSACTION
ELSE
COMMIT TRANSACTION
GO
I was going through the same torment, and I just solved it!!!
Just add this single line at the very first step of your TRIGGER and you're going to be fine:
SET XACT_ABORT OFF;
In my case, I'm handling the error feeding a specific table with the batch that caused the error and the error variables from SQL.
Default value for XACT_ABORT is ON, so the entire transaction won't be commited even if you're handling the error inside a TRY CATCH block (just as I'm doing). Setting its value for OFF will cause the transaction to be commited even when an error occurs.
However, I didn't test it when the error is not handled...
For more info:
SET XACT_ABORT (Transact-SQL) | Microsoft Docs
I'd suggest re-architecting this so that you don't poison the original transaction - maybe have the transaction send a service broker message (or just insert relevant data into some form of queue table), so that the "non-critical" part can take place in a completely independent transaction.
E.g. your trigger becomes:
CREATE TRIGGER TestTrigger
ON TestTable
FOR INSERT, UPDATE, DELETE
AS
BEGIN
INSERT INTO QueueTable (Col1,Col2)
SELECT COALESCE(i.Col1,d.Col1),COALESCE(i.Col2,d.Col2) from inserted i,deleted d
END
GO
You shouldn't do anything inside a trigger that might fail, unless you do want to force the transaction that initiated the trigger action to also fail.
This is a very similar question to Why try catch does not suppress exception in trigger
Also see the answer here T-SQL try catch transaction in trigger
I don’t think you can use savepoints inside a trigger. I mean, you can but I googled about it and I saw a few people saying that they don’t work. If you replace your “save transaction” for a begin transaction, it compiles. Of course it is not necessary because you have the outer transaction control and the inner rollback would rollback everything.

Resources