Transaction won't rollback - sql-server

I have the following transaction. I want it to rollback if the SUM of points are not equal to 4000 and to COMMIT if it is. I execute the transaction and the number of points is not equal to 4000. But the transaction is executed nonetheless. Why is that?
BEGIN TRAN [Whist]
BEGIN TRY
END TRY
BEGIN CATCH
IF (SELECT SUM(Point) FROM dbo.Players) <> 4000
ROLLBACK TRANSACTION [Whist]
END CATCH
IF (SELECT SUM(Point) FROM dbo.Players) = 4000
COMMIT TRAN

Please run this SQL 2 ways. Once with #sum_of_points set to 4000 which will commit the transaction. Second time with #sum_of_points set to 4001 which will rollback the transaction.
BEGIN TRAN
BEGIN TRY
declare #sum_of_points int=4000;
IF (#sum_of_points <> 4000)
throw 50000, 'Not equals 4000 so do not commit the transaction', 1;
commit transaction;
print ('transaction was committed')
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
print ('transaction has been rolled back');
END CATCH

Related

"Nested" transactions, why does this happen, how do I avoid it?

As demonstrated here,
If I have an inner stored procedure, that always rolls back without an error.
CREATE PROCEDURE [inner] AS BEGIN
SET XACT_ABORT, NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
IF (1 + 1 = 2) BEGIN
ROLLBACK TRANSACTION;
END ELSE BEGIN
COMMIT TRANSACTION;
END
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 BEGIN
ROLLBACK TRANSACTION;
END
END CATCH
END
and an outer SP that calls the inner,
CREATE PROCEDURE [outer] AS BEGIN
SET XACT_ABORT, NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
EXEC [inner];
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 BEGIN
ROLLBACK TRANSACTION;
END
;THROW;
END CATCH
END
and, then I call the outer SP,
EXEC [outer];
I get this error
Msg 266 Level 16 State 2 Line 0
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements.
Previous count = 1, current count = 0.
Now, initially this is not what I was expecting.
What I think is happening,
axioms:
BEGIN TRANSACTION always increments ##TRANCOUNT by 1;
COMMIT TRANSACTION always decrements ##TRANCOUNT by 1;
ROLLBACK TRANSACTION always resets ##TRANCOUNT to 0;
There are no nested transactions!
When ##TRANCOUNT changes to any value, other than 0, nothing else happens.
so, step by step,
[outer] -> BEGIN TRANSACTION sets ##TRANCOUNT to 1.
[outer] -> calls [inner] with ##TRANCOUNT 1.
[inner] -> ROLLBACK TRANSACTION resets ##TRANCOUNT to 0.
[inner] -> returns to [outer] with ##TRANCOUNT 0.
SQL Server detects a mismatch in ##TRANCOUNT before and after the call to [inner] and,
therefore raises the error above.
So, my questions are,
Is my understanding correct and where is the canonical documentation of this?
What is the best way to write an Stored Procedure that may want to rollback its own changes, so that it can be called directly in its own batch or from within another transaction?
Yes, your understanding is correct.
The documentation is here:
In stored procedures, ROLLBACK TRANSACTION statements without a savepoint_name or transaction_name roll back all statements to the outermost BEGIN TRANSACTION. A ROLLBACK TRANSACTION statement in a stored procedure that causes ##TRANCOUNT to have a different value when the stored procedure completes than the ##TRANCOUNT value when the stored procedure was called produces an informational message. This message does not affect subsequent processing.
I'm not sure why it says "does not affect subsequent processing" as that is demonstrably false: instead it throws an error with severity 16, which can be caught with BEGIN CATCH.
If you do want to use nested transactions such as this, and be able to roll them back only partially, it becomes significantly more complicated.
You have to conditionally begin a transaction if there is none. Then you have to SAVE a savepoint. And then each rollback must conditionally roll back either the whole transaction or only the savepoint
CREATE PROCEDURE [inner] AS BEGIN
SET XACT_ABORT, NOCOUNT ON;
BEGIN TRY
DECLARE #tranCount int = ##TRANCOUNT;
IF #tranCount = 0
BEGIN TRANSACTION;
SAVE TRANSACTION innerSave;
IF (1 + 1 = 2) BEGIN
IF #tranCount = 0
ROLLBACK;
ELSE
ROLLBACK TRANSACTION innerSave;
END ELSE BEGIN
COMMIT TRANSACTION;
END
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 BEGIN
IF #tranCount = 0
ROLLBACK;
ELSE
ROLLBACK TRANSACTION innerSave;
END
END CATCH
END
db<>fiddle

SQL Server mismatching number of BEGIN and COMMIT statements nested transaction

I followed a recommended template for error handling in a transaction that should work when it's executed inside an existing transaction.
This is my template
CREATE PROCEDURE DoSomething
AS
BEGIN
SET NOCOUNT ON
DECLARE #trans INTEGER = ##TRANCOUNT
IF (#trans > 0)
SAVE TRANSACTION SavePoint
ELSE
BEGIN TRANSACTION
BEGIN TRY
-- code with a check that does a THROW if the requirements aren't met
IF (#trans = 0)
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF (#trans > 0)
ROLLBACK TRANSACTION SavePoint
ELSE
ROLLBACK TRANSACTION
;THROW
END CATCH
END
If I replace the THROW within the TRY block with a RAISERROR, the issue remains.
Test results:
EXEC fail scenario within transaction: Correct result (gives the right error message)
EXEC success scenario within transaction: Gives unexpected error.
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 2.
EXEC fail scenario outside transaction: Gives expected error.
EXEC success scenario outside transaction: Gives unexpected error. The error is the same as above, but every time you execute it, it increments by -1. Does this mean each time more stuff stays uncommitted?
This is how a test looks like:
BEGIN TRANSACTION
EXEC ...
ROLLBACK TRANSACTION
Does anyone know what's going wrong?
Uncomment var
CREATE PROCEDURE DoSomething
AS
BEGIN
SET NOCOUNT ON
DECLARE #trans INTEGER = ##TRANCOUNT
IF (#trans > 0)
SAVE TRANSACTION SavePoint
ELSE
BEGIN TRANSACTION
BEGIN TRY
DECLARE #f float;
SET #f = 0;
--var 1.
--print 1/0
--var 2.
print LOG(#f)
--var ok
--print 'ok'
END TRY
BEGIN CATCH
IF (#trans > 0 AND XACT_STATE() <> -1)
BEGIN
PRINT 'ROLLBACK SavePoint'
ROLLBACK TRANSACTION SavePoint
END
PRINT 'Error'
END CATCH
END
And execute
BEGIN TRANSACTION
EXEC DoSomething
IF XACT_STATE() = -1
BEGIN
PRINT 'ROLLBACK XACT'
ROLLBACK
END
IF ##TRANCOUNT > 0
BEGIN
PRINT 'COMMIT'
COMMIT
END

Rollback transaction from called stored procedure

I have a simple scenario: logger procedure and main procedure from which logger is called. I am trying to rollback transaction inside logger which is started in main, but getting errors. I am not sure why. Here are the two procs and the error message I receive:
CREATE PROCEDURE spLogger
AS
BEGIN
IF ##TRANCOUNT > 0
BEGIN
PRINT ##TRANCOUNT
ROLLBACK
END
END
GO
CREATE PROCEDURE spCaller
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
RAISERROR('', 16, 1)
COMMIT TRANSACTION
END TRY
BEGIN CATCH
EXEC spLogger
END CATCH
END
GO
EXEC spCaller
1 Msg 266, Level 16, State 2, Procedure spLogger, Line 15 Transaction
count after EXECUTE indicates a mismatching number of BEGIN and COMMIT
statements. Previous count = 1, current count = 0.
1) The error message is clear: number of active TXs at the end of SP should be the same as number of active TXs at the beginning.
So, when at execution of dbo.spLogger begins the number of active TXs (##TRANCOUNT) is 1 if we execute within this SP the ROLLBACK statement this'll cancel ALL active TXs and ##TRANCOUNT becomes 0 -> error/exception
2) If you want just to avoid writing IF ##TRANCOUNT ... ROLLBACK within every CATCH block of every user SP then don't it. I would call dbo.spLogger within CATCH block after ROLLBACK.
3) If I have to call SPs from other SP using TXs then I would use following template (source: Rusanu's blog)
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;
throw;
end catch
end
with few small changes:
a) SET XACT_ABORT ON
b) I would call dbo.spLogger within CATCH block only when there is ##TRANCOUNT = 0:
IF ##TRANCOUNT = 0
BEGIN
EXEC dbo.spLogger ... params ...
END
THROW -- or RAISERROR(#message, 16, #xstate)
Why ? Because if dbo.spLogger SP will insert rows into a dbo.DbException table when one TX is active then in case of ROLLBACK SQL Server will have to ROLLBACL also these rows.
Example:
SP1 -call-> SP2 -call-> SP3
|err/ex -> CATCH & RAISERROR (no full ROLLBACK)
<-----------
|err/ex -> CATCH & RAISERROR (no full ROLLBACK)
<-------------
|err/ex -> CATCH & FULL ROLLBACK & spLogger
4) Update
CREATE PROC TestTx
AS
BEGIN
BEGIN TRAN -- B
ROLLBACK -- C
END
-- D
GO
-- Test
BEGIN TRAN -- A - ##TRANCOUNT = 1
EXEC dbo.TestTx
/*
Number of active TXs (##TRANCOUNT) at the begining of SP is 1
B - ##TRANCOUNT = 2
C - ##TRANCOUNT = 0
D - Execution of SP ends. SQL Server checks & generate an err/ex
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0.
*/
COMMIT -- E - Because ##TRANCOUNT is 0 this statement generates
another err/ex The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
-- End of Test
5) See autonomous transactions: it requires SQL2008+.
An Autonomous transaction is essentially a nested transaction where
the inner transaction is not affected by the state of the outer
transaction. In other words, you can leave the context of current
transaction (outer transaction) and call another transaction
(autonomous transaction). Once you finish work in the autonomous
transaction, you can come back to continue on within current
transaction. What is done in the autonomous transaction is truly DONE
and won’t be changed no matter what happens to the outer transaction.
keeping aside all xact_abort stuff,i see no reason why you should get the error.So did some research and here are the observations
----This works
alter PROCEDURE spCaller
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
RAISERROR('', 16, 1)
COMMIT TRANSACTION
END TRY
BEGIN CATCH
rollback
END CATCH
END
GO
---Again this works,took the text of sp and kept it in catch block
alter PROCEDURE spCaller
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
RAISERROR('', 16, 1)
COMMIT TRANSACTION
END TRY
BEGIN CATCH
--rollback
IF ##TRANCOUNT > 0
BEGIN
PRINT ##TRANCOUNT
ROLLBACK
END
END CATCH
END
GO
After some research Found answer by Remus Rusanu here:
If your caller starts a transaction and the calee hits, say, a deadlock (which aborted the transaction), how is the callee going to communicate to the caller that the transaction was aborted and it should not continue with 'business as usual'? The only feasible way is to re-raise an exception, forcing the caller to handle the situation. If you silently swallow an aborted transaction and the caller continues assuming is still in the original transaction, only mayhem can ensure (and the error you get is the way the engine tries to protect itself).
In your case,you are getting the error only when using a stored proc and trying to raise the error ,since a stored proc starts a seperate data context.The error you are getting may be SQL way of telling that this wont work.

Procedure with multiple transactions how to set the error message to the output parameter

In Sql Server 2008 R2, I am creating a procedure with multiple transactions in it. Try..Catch Block is used for each transaction. I use output parameter to catch the error code and message since it will be caught by the main program. But When there is errors the output parameter are not correct set to the error message and code, what is problem here?
create procedure xxx (#P_Return_Status VARCHAR(1) OUTPUT, #P_Error_Code INT OUTPUT,)
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION TR1
.....
COMMIT TRANSACTIOn TR1
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRANSACTION TR1
END
Set #P_Error_Code = Error_Number();
Set #P_Error_Messages = LEFT(ERROR_MESSAGE (), 2000)
END CATCH
BEGIN TRY
BEGIN TRANSACTION TR2
.....
COMMIT TRANSACTIOn TR2
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRANSACTION TR2
END
Set #P_Error_Code = Error_Number();
Set #P_Error_Messages = LEFT(ERROR_MESSAGE (), 2000)
END CATCH
END
GO
Any help would be really appreciated!
I just don't see the point of putting two transaction inside this one procedure, just put all of the statements in one transaction and commit it or rollback it.
if it does need to be in separate transactions put these both try...catch in two separate procedures and call one sp from another sp's try block.
create procedure xxx
#P_Return_Status INT OUTPUT
,#P_Error_Code INT OUTPUT
,#P_Error_Messages VARCHAR(2000) OUTPUT
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION TR1
/* put statements from both transaction here
or
statements for TR1
AND
call the procedure containing code for TR2
*/
COMMIT TRANSACTIOn TR1
SET #P_Return_Status = 1;
END TRY
BEGIN CATCH
IF (##TRANCOUNT > 0)
BEGIN
ROLLBACK TRANSACTION TR1
END
Set #P_Error_Code = Error_Number();
Set #P_Error_Messages = LEFT(ERROR_MESSAGE (), 2000)
SET #P_Return_Status = 0;
END CATCH
END
GO

Rollback the inner transaction of nested transaction

suppose I have following sql statement in sql server 2008:
BEGIN TRANSACTION
SqlStatement1
EXEC sp1
SqlStatement3
COMMIT TRANSACTION
The code of sp1
BEGIN TRANSACTION
SqlStatement2
ROLLBACK TRANSACTION
My question is: Is SqlStatement3 actually executed?
SQL Server doesn't really support nested transactions. There is only one transaction at a time.
This one transaction has a basic nested transaction counter, ##TRANCOUNT. Each consecutive begin transaction increments the counter by one, each commit transaction reduces it by one. Only the commit that reduces the counter to 0 really commits the one transaction.
A rollback transaction undoes the one transaction and clears ##TRANCOUNT.
In your case, the funny result is that SqlStatement3 is run outside a transaction! Your final commit will throw an "The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION" exception, but the effects of SqlStatement3 are permanent.
For example:
create table #t (col1 int)
insert #t (col1) values (1)
BEGIN TRANSACTION
update #t set col1 = 2 -- This gets rolled back
BEGIN TRANSACTION
update #t set col1 = 3 -- This gets rolled back too
ROLLBACK TRANSACTION
update #t set col1 = 4 -- This is run OUTSIDE a transaction!
COMMIT TRANSACTION -- Throws error
select col1 from #t
Prints 4. Really. :)
You can use transaction savepoints. sp1 can use a pattern like the one described in Error Handling and Nested Transactions:
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
Such a pattern allow for the work done in sp1 to rollback, but keep the encompassing transaction active.
Nested transactions can be used. To only rollback the inner transaction, use a savepoint and rollback to the savepoint. In case the inner transaction does not whether it is nested or not, IF statements can be used to find out whether to set a savepoint and whether to rollback or to rollback to a savepoint:
BEGIN TRAN
DECLARE #WILL_BE_NESTED_TRANSACTION BIT = CASE WHEN (##TRANCOUNT > 0) THEN 1 ELSE 0 END
IF #WILL_BE_NESTED_TRANSACTION = 1
SAVE TRAN tran_save
BEGIN TRAN
-- do stuff
IF #WILL_BE_NESTED_TRANSACTION = 1
ROLLBACK TRAN tran_save
ELSE
ROLLBACK
ROLLBACK
Rollback transaction on its own rolls back all transactions.
http://msdn.microsoft.com/en-us/library/ms181299(v=sql.100).aspx
The statement will still be executed - try this
create table #t (i int)
insert #t values (1) -- t contains (1)
begin tran
update #t set i = i +1
select * from #t -- t contains (2)
begin tran
update #t set i = i +1
select * from #t -- t contains (3)
rollback tran -- transaction is rolled back
select * from #t -- t contains (1)
update #t set i = i +1
select * from #t -- t contains (2)
commit -- error occurs
select * from #t -- t contains (2)
drop table #t
Committing inner transactions is ignored by the SQL Server Database Engine. The transaction is either committed or rolled back based on the action taken at the end of the outermost transaction. If the outer transaction is committed, the inner nested transactions are also committed. If the outer transaction is rolled back, then all inner transactions are also rolled back, regardless of whether or not the inner transactions were individually committed.
Nesting Transactions in Microsoft TechNet

Resources