Exit and rollback everything in script on error - sql-server

I have a TSQL script that does a lot of database structure adjustments but it's not really safe to just let it go through when something fails.
to make things clear:
using MS SQL 2005
it's NOT a stored procedure, just a script file (.sql)
what I have is something in the following order
BEGIN TRANSACTION
ALTER Stuff
GO
CREATE New Stuff
GO
DROP Old Stuff
GO
IF ##ERROR != 0
BEGIN
PRINT 'Errors Found ... Rolling back'
ROLLBACK TRANSACTION
RETURN
END
ELSE
PRINT 'No Errors ... Committing changes'
COMMIT TRANSACTION
just to illustrate what I'm working with ... can't go into specifics
now, the problem ...
When I introduce an error (to test if things get rolled back), I get a statement that the ROLLBACK TRANSACTION could not find a corresponding BEGIN TRANSACTION.
This leads me to believe that something when REALLY wrong and the transaction was already killed.
what I also noticed is that the script didn't fully quit on error and thus DID try to execute every statement after the error occured. (I noticed this when new tables showed up when I wasn't expecting them because it should have rollbacked)

When the error occurs, the transaction is rolled back automatically, and the current batch is aborted.
Execution continues into the next batch, however. So all the stuff in the batches after the error gets executed. And then when you check for errors later, you try to rollback an already rolled back transaction.
Also, to stop the entire script, not just the current batch, you should use:
raiserror('Error description here', 20, -1) with log
See my answer here for details on that one.
So you need to check for #error after each batch, I think something like this should work:
BEGIN TRANSACTION
GO
ALTER Stuff
GO
if ##error != 0 raiserror('Script failed', 20, -1) with log
GO
CREATE New Stuff
GO
if ##error != 0 raiserror('Script failed', 20, -1) with log
GO
DROP Old Stuff
GO
if ##error != 0 raiserror('Script failed', 20, -1) with log
GO
PRINT 'No Errors ... Committing changes'
COMMIT TRANSACTION

Try using RETURN. this will exit the script or procedure immediately and will not execute any of the following statements. You can use this in conjunction with BEGIN, ROLLBACK and COMMIT TRANSACTION statements to undo any data damage:
BEGIN
BEGIN TRANSACTION
<first batch>
IF ##error <> 0
begin
RAISERROR ('first batch failed',16,-1)
ROLLBACK TRANSACTION
RETURN
end
<second batch>
IF ##error <> 0
begin
RAISERROR ('second batch failed',16,-1)
ROLLBACK TRANSACTION
RETURN
end
PRINT 'WIN!'
COMMIT TRANSACTION
END

I didn't use the raiseerror solution, because it failed as I didn't have admin permissions. I extended the noexec on/off solution with the transaction handling as follows:
set noexec off
begin transaction
go
<First batch, do something here>
go
if ##error != 0 set noexec on;
<Second batch, do something here>
go
if ##error != 0 set noexec on;
<... etc>
declare #finished bit;
set #finished = 1;
SET noexec off;
IF #finished = 1
BEGIN
PRINT 'Committing changes'
COMMIT TRANSACTION
END
ELSE
BEGIN
PRINT 'Errors occured. Rolling back changes'
ROLLBACK TRANSACTION
END
Apparently the compiler "understands" the #finished variable in the IF, even if there was an error and the execution was disabled. However, the value is set to 1 only if the execution was not disabled. Hence I can nicely commit or rollback the transaction accordingly.

You could try something like this... If you are using Try block... The error level 16, (or most of application error), immediately transfers the control to the CATCH block without executing any further statements in the try block...
Begin Transaction
Begin Try
-- Do your Stuff
If (##RowCount <> 1) -- Error condition
Begin
Raiserror('Error Message',16,1)
End
Commit
End Try
Begin Catch
IF ##Trancount > 0
begin
Rollback Transaction
End
Declare #ErrMsg varchar(4000), #Errseverity int
SELECT #ErrMsg = ERROR_MESSAGE(),
#ErrSeverity = ERROR_SEVERITY()
RAISERROR(#ErrMsg, #ErrSeverity, 1)
End Catch
Hope this helps...

SET XACT_ABORT ON
BEGIN TRAN
-- Batch 1
GO
if ##TRANCOUNT = 0
SET NOEXEC ON;
GO
-- Batch 2
GO
if ##TRANCOUNT = 0
SET NOEXEC ON;
GO
-- Batch 3
GO
if ##TRANCOUNT > 0
COMMIT
GO

Related

How can ##trancount be zero within catch of a transaction?

I have this statement block with transaction and try/catch:
begin try
begin transaction;
insert ....
update ....
delete ....
commit transaction;
end try
begin catch
if (##trancount > 0)
rollback transaction;
throw;
end catch;
I am using ##trancount here without fully understanding what happens.
Why exactly could it ever happen that ##trancount is zero in this case?
The most common scenario to receive ##trancount = 0 requires a bit more elaborated logic than your sample uses.
If you have other stored procedures called in the try block, they might have their own understanding of how transactions should be managed, and either due to a poorly written code, or some other mishap, they can either commit the outer transaction by accident, or roll it back (remember, there is no really such thing in SQL Server as a nested transaction, so any rollback statement that doesn't reference a previously declared savepoint wipes out everything). The error in this case might vary, either the one that caused the inner procedure to misbehave in the first place, or if nothing else, you'll get error 266, "Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = %ld, current count = %ld."
Note that this behaviour can also be caused by rollback done by a trigger, as well.
It is possible that there might be some other situations when you end up with no current transaction in the catch block, but apparently they are so rare that I can't think of anything else off the top of my head.
Personally, I use the following template for all my stored procedures whenever possible:
create procedure dbo.ProcTemplate
(
#Error int = null output,
#Message nvarchar(2048) = null output
) as
/*
20191223, RW - to be completed
*/
set nocount, quoted_identifier, ansi_nulls, ansi_warnings, ansi_padding, concat_null_yields_null, arithabort on;
set xact_abort, implicit_transactions, numeric_roundabort off;
declare #XTran bit = cast(sign(##trancount) as bit);
begin try
if #XTran = 0
begin tran;
-- Put your code here
if #XTran = 0
commit;
end try
begin catch
if nullif(#Error, 0) is null
select #Error = error_number(), #Message = error_message();
if ##trancount > 0 and #XTran = 0
rollback;
end catch;
return;
go
One can argue that explicitly issuing set xact_abort off might result in some unpleasant side effects, such as a batch-terminating error (208, for instance) skips the catch and leaves the current transaction open. That's up to you; the trade-offs here are:
Better diagnostic. When all stored procs in a database follow this template, they bubble up the error to the outermost procedure by the means of output parameters and gracefully rollback everything.
Possibility to continue execution after the error. For example, log the error after the transaction has been rolled back and make sure the log record won't disappear with the rest of transaction.

set xact_abort on and try catch together

i have a try catch block in my sp with just a insert statement in the try. the catch check error code if it is pk violation, if it is then do update. but some times i get "The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
Uncommittable transaction is detected at the end of the batch. The transaction is rolled back." so i added xact_abort on, but then i keep getting "Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements." and i found this.
http://www.ashishsheth.com/post/2009/08/14/Set-XACT_ABORT-ON-and-TryCatch-block-in-Sql-Server-2005.aspx
if this true. will my catch code not run if there is a error in my try block with xact_abort on?
It is not true, at least with SQL SERVER 2008, that SET XACT_ABORT ON will cause an error to skip the CATCH block:
Here is the code I tried using the Northwind database
SET XACT_ABORT OFF
BEGIN TRY
SELECT 1, ##TRANCOUNT
BEGIN TRAN
UPDATE [dbo].[Categories]
SET Description='BLAH'
WHERE [CategoryID]=2
SELECT 2, ##TRANCOUNT
SELECT 1/0 as whoops
COMMIT
SELECT 3, ##TRANCOUNT
END TRY
BEGIN CATCH
SELECT 'In Catch. Error occured', 4, ##TRANCOUNT
IF (XACT_STATE()) = 0
BEGIN
SELECT
N'There is no transaction'
END;
IF (XACT_STATE()) = -1
BEGIN
SELECT
N'The transaction is in an uncommittable state.' +
'Rolling back transaction.'
ROLLBACK TRANSACTION;
END;
-- Test whether the transaction is committable.
IF (XACT_STATE()) = 1
BEGIN
SELECT
N'The transaction is committable.' +
'Committing transaction.'
COMMIT TRANSACTION;
END;
END CATCH
This will, obviously, force an error when it hits the SELECT 1/0 statement. With SET XACT_ABORT OFF, when the CATCH block is reached, the value returned by the XACT_STATE() function is 1, causing the code to run which COMMITs the transaction. When SET XACT_ABORT is on, the value returned, in the CATCH block is -1 so the code which ROLLs back the transaction is executed.
This is based on:
http://msdn.microsoft.com/en-us/library/ms175976.aspx
Let me add that, in that particular scenario (try insert, if PK violation then catch and update), it would be better to use IF EXISTS (select....) to see if the row is there and put your UPDATE statement there. Put your INSERT statement in ELSE block. Much cleaner.

How to rollback a transaction in a stored procedure?

Looking at the SQL Server Books Online, Microsoft seems to have an (incorrect) method of handling nested transactions in a stored procedure:
Nesting Transactions
Explicit transactions can be nested. This is primarily intended to support transactions in stored procedures that can be called either from a process already in a transaction or from processes that have no active transaction.
The example goes on to show a stored procedure that starts its own transaction ("The procedure enforces its transaction regardless of the transaction mode of any process that executes it."):
CREATE PROCEDURE TransProc #PriKey INT, #CharCol CHAR(3) AS
BEGIN TRANSACTION InProc
...
COMMIT TRANSACTION InProc;
This procedure can then either be called without a transaction running:
EXECUTE TransProc 3,'bbb';
Or with an explicit transaction:
BEGIN TRANSACTION OutOfProc;
EXEC TransProc 1, 'aaa';
COMMIT TRANSACTION OutOfProc
What they don't address is what happens when the stored produre:
fails with an error, but leaves the transaction running
fails with an error, but doesn't leave the transaction running
encounters an error, but continues executing with the transaction open
encounters an error, but continues executing with the transaction rolled back
There is no:
SET XACT_ABORT ON
##TRANCOUNT
anywhere in the canonical example.
If i didn't know any better, i would have thought that the line:
The following example shows the intended use of nested transactions.
should actually read
The following example shows the how not to use nested transactions.
Unless someone can make heads or tails of this BOL example?
You need to use the try catch block with the transaction. So in case you get the error in your catch block then you can rollback your transaction.
Please see the below sql server code for that.
BEGIN TRANSACTION;
BEGIN TRY
-- Some code
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
END CATCH;
CREATE PROCEDURE [usp_my_procedure_name]
AS
BEGIN
SET NOCOUNT ON;
DECLARE #trancount int;
SET #trancount = ##trancount;
BEGIN TRY
IF #trancount = 0
BEGIN TRANSACTION
ELSE
SAVE TRANSACTION usp_my_procedure_name;
-- Do the actual work here
lbexit:
IF #trancount = 0
COMMIT;
END TRY
BEGIN CATCH
DECLARE #error int,
#message varchar(4000),
#xstate int;
SELECT
#error = ERROR_NUMBER(),
#message = ERROR_MESSAGE(),
#xstate = XACT_STATE();
IF #xstate = -1
ROLLBACK;
IF #xstate = 1 AND #trancount = 0
ROLLBACK
IF #xstate = 1 AND #trancount > 0
ROLLBACK TRANSACTION usp_my_procedure_name;
RAISERROR ('usp_my_procedure_name: %d: %s', 16, 1, #error, #message);
END CATCH
END

SAVE TRANSACTION vs BEGIN TRANSACTION (SQL Server) how to nest transactions nicely

I have a stored procedure that needs to set a save point so that it can, under certain circumstances, undo everything it did and return an error code to the caller, or accept/commit it and return success to the caller. But I need it to work whether the caller has already started a transaction or not. The doc is extremely confusing on this subject. Here is what I think will work, but I'm not certain of all the ramifications.
The thing is - this Stored Procedure (SP) is called by others. So I don't know if they've started a transaction or not... Even if I require users to start a transaction to use my SP, I still have questions about the proper use of Save Points ...
My SP will test if a transaction is in progress, and if not, start one with BEGIN TRANSACTION. If a transaction is already in progress, it will instead create a save point with SAVE TRANSACTION MySavePointName, and save the fact this is what I did.
Then if I have to roll back my changes, if I did a BEGIN TRANSACTION earlier, then I will ROLLBACK TRANSACTION. If I did the save point, then I will ROLLBACK TRANSACTION MySavePointName. This scenario seems to work great.
Here is where I get a little confused - if I want to keep the work I've done, if I started a transaction I will execute COMMIT TRANSACTION. But if I created a save point? I tried COMMIT TRANSACTION MySavePointName, but then the caller tries to commit its transaction and gets an error:
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
So I'm wondering then - a save point can be rolled back (that works: ROLLBACK TRANSACTION MySavePointName will NOT roll back the caller's transaction). But perhaps one never needs to "commit" it? It just stays there, in case you need to roll back to it, but goes away once the original transaction is committed (or rolled back)?
If there is a "better" way to "nest" a transaction, please shed some light as well. I haven't figured out how to nest with BEGIN TRANSACTION but only rollback or commit my internal transaction. Seems ROLLBACK will always roll back to the top transaction, while COMMIT simply decrements ##trancount.
I believe I've figured this all out now, so I will answer my own question...
I've even blogged my findings if you want more details at http://geekswithblogs.net/bbiales/archive/2012/03/15/how-to-nest-transactions-nicely---quotbegin-transactionquot-vs-quotsave.aspx
So my SP starts with something like this, to start a new transaction if there is none, but use a Save Point if one is already in progress:
DECLARE #startingTranCount int
SET #startingTranCount = ##TRANCOUNT
IF #startingTranCount > 0
SAVE TRANSACTION mySavePointName
ELSE
BEGIN TRANSACTION
-- …
Then, when ready to commit the changes, you only need to commit if we started the transaction ourselves:
IF #startingTranCount = 0
COMMIT TRANSACTION
And finally, to roll back just your changes so far:
-- Roll back changes...
IF #startingTranCount > 0
ROLLBACK TRANSACTION MySavePointName
ELSE
ROLLBACK TRANSACTION
Extending Brian B's answer.
This ensures the save point name is unique and uses the new TRY/CATCH/THROW features of SQL Server 2012.
DECLARE #mark CHAR(32) = replace(newid(), '-', '');
DECLARE #trans INT = ##TRANCOUNT;
IF #trans = 0
BEGIN TRANSACTION #mark;
ELSE
SAVE TRANSACTION #mark;
BEGIN TRY
-- do work here
IF #trans = 0
COMMIT TRANSACTION #mark;
END TRY
BEGIN CATCH
IF xact_state() = 1 OR (#trans = 0 AND xact_state() <> 0) ROLLBACK TRANSACTION #mark;
THROW;
END CATCH
I have used this type of transaction manager in my Stored Procedures :
CREATE PROCEDURE Ardi_Sample_Test
#InputCandidateID INT
AS
DECLARE #TranCounter INT;
SET #TranCounter = ##TRANCOUNT;
IF #TranCounter > 0
SAVE TRANSACTION ProcedureSave;
ELSE
BEGIN TRANSACTION;
BEGIN TRY
/*
<Your Code>
*/
IF #TranCounter = 0
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF #TranCounter = 0
ROLLBACK TRANSACTION;
ELSE
IF XACT_STATE() <> -1
ROLLBACK TRANSACTION ProcedureSave;
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT #ErrorMessage = ERROR_MESSAGE();
SELECT #ErrorSeverity = ERROR_SEVERITY();
SELECT #ErrorState = ERROR_STATE();
RAISERROR (#ErrorMessage, #ErrorSeverity, #ErrorState);
END CATCH
GO

What happens here? SQL Server - XACT_ABORT ON + ##ERROR Checking . .

What happens with this type of scenario?
SET XACT_ABORT ON
BEGIN TRANSACTION
---DO SOMETHING HERE THAT CAUSES AN ERROR
COMMIT TRANSACTION
if ##error != 0
raiserror('SP failed. Step 7.', 20, -1) with log GO
My guess is that because XACT_ABORT is ON the COMMIT TRANSACTION never happens (because the whole thing is rolled back and terminated), and neither does the last statement (checking for ##error and then calling raiseerror).
Correct.
SET XACT_ABORT jumps out of the batch. Your IF is part of the same batch.
If you want some processing on error, then use BEGIN TRY etc
SET XACT_ABORT ON
BEGIN TRY
BEGIN TRANSACTION
---DO SOMETHING HERE THAT CAUSES AN ERROR
COMMIT TRANSACTION
END TRY
BEGIN CATCH
raiserror('SP failed. Step 7.', 20, -1) with log
END CATCH
GO
I'm also intrigued by severity 20 because it breaks the connection. Usually you'd use 16 which is user defined error.

Resources