Transaction does not rollback all changes - sql-server

I've ran into a procedure in SQL Server 2017 that has a transaction within a try-catch block. It isn't nested, just got an identity table filled and cycled using cursor. So try-catch is within a loop, some other procedure is called. Sometimes that procedure fails with constraint violation error and it's perfectly fine to save whatever succeeded prior to it's inner exception. And then I bumped into commit in catch clause. It made me wondering and I written this code:
DECLARE #Table TABLE (ID INT NOT NULL PRIMARY KEY)
DECLARE #Input TABLE (ID INT)
INSERT INTO #Input
VALUES (1), (1), (2), (NULL), (3)
DECLARE #Output TABLE (ID INT)
--SET XACT_ABORT OFF
DECLARE #ID int
DECLARE [Sequence] CURSOR LOCAL FAST_FORWARD FOR
SELECT ID FROM #Input
OPEN [Sequence]
FETCH NEXT FROM [Sequence] INTO #ID
WHILE ##FETCH_STATUS = 0
BEGIN
BEGIN TRY
BEGIN TRAN
DECLARE #Msg nvarchar(max) = 'Inserting ''' + TRY_CAST(#ID as varchar(11)) + ''''
RAISERROR (#Msg, 0, 0) WITH NOWAIT
-- Order is important
--INSERT INTO #Table VALUES (#ID)
INSERT INTO #Output VALUES (#ID)
INSERT INTO #Table VALUES (#ID)
COMMIT TRAN
END TRY
BEGIN CATCH
SET #Msg = 'Caught ' + CAST(ERROR_NUMBER() as varchar(11)) + ' : ' + ERROR_MESSAGE()
RAISERROR (#Msg, 1, 1) WITH NOWAIT
IF XACT_STATE() = -1
BEGIN
SET #Msg = 'Uncommitable transaction [-1]'
RAISERROR (#Msg, 1, 1) WITH NOWAIT
ROLLBACK TRAN
END
IF XACT_STATE() = 1
BEGIN
SET #Msg = 'Commitable transaction [1]'
RAISERROR (#Msg, 1, 1) WITH NOWAIT
COMMIT TRAN
END
END CATCH
FETCH NEXT FROM [Sequence] INTO #ID
END
SELECT * FROM #Table
SELECT * FROM #Output
So as I tried interchanging the order of #Output and #Table inserts, I got different results, no matter what XACT_ABORT is set to or whether I commit or rollback transaction in the catch block. I was always sure, that everything gets rolled back and both #Output and #Table tables will be equal....
What I am doing wrong here? I this a default transaction behavior?

This is a fun one, but your code does what I'd expect it to. Table variables do not obey transactional semantics. Temporary tables do though! So if you need the ability to roll back mutations to your temporary "thing", use a table and not a variable.
Note though that your sequence will still have values pulled from it. Even it you also put that in the transaction.

As Ben Thul reminded, only temporary or normal tables should be used here. So when exception is caught and XACT_STATE() = 1 (Commitable transaction), COMMIT will keep whatever succeeded and ROLLBACK will undo the whole thing.
IF XACT_STATE() = 1
BEGIN
SET #Msg = 'Commitable transaction [1]'
RAISERROR (#Msg, 1, 1) WITH NOWAIT
COMMIT TRAN -- Keep changes or undo everything (ROLLBACK)
END
Output table results:
ROLLBACK: [1,2,3]
COMMIT : [1,1,2,NULL,3]

Related

SQLServer Transaction error when called from python

I'm facing an incredibly puzzling situation, we have a recently updated SQL Server from 2016 to 2019, on which a stored procedure usually called from a python script after some data integration, now fails with an error.
"[25000] [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Cannot roll back TR_NL. No transaction or savepoint of that name was found. (6401)
The stored procedure itself follows a quite standard TRY/CATCH structure
USE [MYBASE]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[p_myproc]
#error NVARCHAR(MAX)= 'Success' OUTPUT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #idMarche;
DECLARE #TranName VARCHAR(20);
IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'SOME_TABLE')
BEGIN
SELECT #TranName = 'TR_NL';
BEGIN TRANSACTION #TranName;
BEGIN TRY
/*
Bunch of updates, inserts etc. Some of which conditionals with nested IF BEGIN END etc.
*/
END TRY
BEGIN CATCH
SELECT #error ='Error Number: ' + ISNULL(CAST(ERROR_NUMBER() AS VARCHAR(10)), 'NA') + '; ' + Char(10) +
'Error Severity ' + ISNULL(CAST(ERROR_SEVERITY() AS VARCHAR(10)), 'NA') + '; ' + Char(10) +
'Error State ' + ISNULL(CAST(ERROR_STATE() AS VARCHAR(10)), 'NA') + '; ' + Char(10) +
'Error Line ' + ISNULL(CAST(ERROR_LINE() AS VARCHAR(10)), 'NA') + '; ' + Char(10) +
'Error Message ' + ISNULL(ERROR_MESSAGE(), 'NA')
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION #TranName;
END CATCH
IF ##TRANCOUNT > 0
COMMIT TRANSACTION #TranName;
END
/*
A few more similar blocks of conditional transactions
*/
IF #error = 'Success' OR #error IS NULL
BEGIN
/*
Drop some tables
*/
END
END
Following call works perfectly well in SSMS but fails with said error when sent from my python script
SET NOCOUNT ON;
DECLARE #return_value INTEGER;
DECLARE #error NVARCHAR(MAX);
EXEC #return_value = [dbo].[p_myproc] #error = #error OUTPUT;
SELECT #error AS erreur, #return_value AS retour;
The problem is that Python be default runs all commands in a transaction, unless you set autocommit=true. This means it is trying to roll back your transaction, but your error handler has done that already.
Your error handler is in any case flawed in a number of ways:
As mentioned, it doesn't handle nested transactions well.
It swallows exceptions, then selects the error message. This means that the client side code is not recognizing that an exception has occurred.
If there were multiple errors at the same time (common with DBCC and BACKUP commands) then only one is returned.
Instead, just use SET XACT_ABORT ON; at the top of your procedure, if you need to do any cleanup, make sure to re-throw the original exception using THROW;. Do not rollback the transaction, it will be rolled back automatically.
CREATE OR ALTER PROCEDURE [dbo].[p_myproc]
AS
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY;
IF EXISTS(SELECT 1
FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'SOME_TABLE')
BEGIN
BEGIN TRANSACTION;
/*
Bunch of updates, inserts etc. Some of which conditionals with nested IF BEGIN END etc.
*/
COMMIT TRANSACTION;
END;
/*
A few more similar blocks of conditional transactions
*/
/*
Drop some tables
*/
END TRY
BEGIN CATCH
-- do cleanup. Do NOT rollback
;
THROW; -- rethrows the original exception
END CATCH;
If no cleanup is needed then do not use a CATCH at all. XACT_ABORT will rollback the transaction anyway.
CREATE OR ALTER PROCEDURE [dbo].[p_myproc]
AS
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF EXISTS(SELECT 1
FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'SOME_TABLE')
BEGIN
BEGIN TRANSACTION;
/*
Bunch of updates, inserts etc. Some of which conditionals with nested IF BEGIN END etc.
*/
COMMIT TRANSACTION;
END
/*
A few more similar blocks of conditional transactions
*/
/*
Drop some tables
*/
See also the following links
How to use SET XACT_ABORT ON the right way
What is the point of TRY CATCH block when XACT_ABORT is turned ON?
Using multiple relating statements inside a stored procedure?

Looking to be sure that errors get caught properly in my stored procedure

I'm looking to see if I am able to capture my errors correctly in this stored procedure:
ALTER PROCEDURE [dbo].[sp_UpdateText]
(#aID AS INT,
#CompanyID AS INT,
#CompanyName AS VARCHAR(MAX))
AS
BEGIN
DECLARE #Result VARCHAR(MAX)
BEGIN TRY
SET #Result = (SELECT dbo.[udf_StripHTMLTags](#CompanyName)) -- UDF function that strips HTML tags off my text field
BEGIN TRANSACTION
UPDATE __TestTable1
SET CompanyName = #Result
WHERE aid = #aid AND CompanyID = #CompanyID
COMMIT TRANSACTION
END TRY
BEGIN CATCH
DECLARE #ErrorNumber INT = ERROR_NUMBER();
DECLARE #ErrorLine INT = ERROR_LINE();
PRINT 'ERROR NUMBER: ' + CAST(#ErrorNumber as Varchar(10));
PRINT 'ERROR LINE: ' + CAST (#ErrorLine as Varchar(10));
END CATCH
END
Go
I'm basically hoping that these BEGIN TRY BEGIN CATCH error capture methods will successfully capture errors, if arise? Any thought?
You should check out Erland's Guide to Error Handling
A suggestion from this inclusive guide would be to change your CATCH at a minimum to
BEGIN CATCH
IF ##trancount > 0 ROLLBACK TRANSACTION --roll back the tran
DECLARE #msg nvarchar(2048) = error_message() --error message is usually more helpful
DECLARE #ErrorNumber INT = ERROR_NUMBER();
DECLARE #ErrorLine INT = ERROR_LINE();
RAISERROR(#msg,16,1) --RAISE the error
RETURN 55555 --return a non-zero to application as non-success
END CATCH
There's a lot more in there which is why it's worth the read.
I almost forgot, SET XACT_ABORT, NOCOUNT ON at the top of your proc.
When you activate XACT_ABORT ON, almost all errors have the same
effect: any open transaction is rolled back and execution is aborted.
There are a few exceptions of which the most prominent is the
RAISERROR statement.
Note that “printing” the error would not store or log it anywhere, like the SQL Server Error log so you wouldn’t “catch” it at all.

undo stored procedure if there is an error on execution

alter proc insert_toplam 'deneme2'
#str nvarchar(100)
as
begin
insert into tblSekiller(sekilURL)
select #str
insert into tbl_fake
select #str
end
i want prevent procedure from inserting table_1 if somehow the procedure give an error on line insert into tbl_fake is it possible?
You can use TRY-CATCH and do the following:
BEGIN TRANSACTION;
BEGIN TRY
INSERT INTO tblSekiller(sekilURL)
SELECT #str
INSERT INTO tbl_fake
SELECT #str
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION; -- if your insert generated an error, rollback
END CATCH;
IF ##TRANCOUNT > 0
COMMIT TRANSACTION; -- otherwise, commit the transaction
GO

Parallel insert stored procedure fails in SQL SERVER

The below SP fails when running parallely in multiple threads.
Tried to use different isolation levels in the SP, but still i face the same error.
Violation of UNIQUE KEY constraint
'AK_MerchantTransactionEnd_MerchantReferenceCode_BankReferenceCode'.
Cannot insert duplicate key in object 'dbo.EpayTransaction'. The
duplicate key value is (20160503171525689435, 20160329221725169, 0).
Table has UNIQUE Constraint for MerchantReferenceCode_BankReferenceCode
CREATE PROCEDURE [dbo].[uspUpdateEpayConfirmation]
#merchantReferenceCode VARCHAR(30) ,
#bankGatewayCode VARCHAR(30) ,
#transactionNumber VARCHAR(100)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
DECLARE #timestamp DATETIME;
SET #timestamp = GETDATE();
IF EXISTS ( SELECT 1
FROM [dbo].EpayTransaction WITH (NOLOCK)
WHERE [dbo].EpayTransaction.[MerchantReferenceCode] = #merchantReferenceCode
)
BEGIN
RETURN 0;
END;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION;
-- update the epayment transaction information with the merchent reference code
-- updating the status of the transaction
UPDATE [dbo].[CustomerTransaction]
SET [dbo].[CustomerTransaction].[ModifiedBy] = 1 ,
[dbo].[CustomerTransaction].[ModifiedOn] = #timestamp
WHERE [dbo].[CustomerTransaction].[MerchantReferenceCode] = #merchantReferenceCode;
-- adding a record to EpayTransaction table to conclude the transaction is successful
INSERT INTO [dbo].EpayTransaction
( [dbo].EpayTransaction.[BankReferenceCode] ,
[dbo].EpayTransaction.[BankTransactionDate] ,
[dbo].EpayTransaction.[MerchantReferenceCode]
)
VALUES ( #bankGatewayCode ,
#timestamp ,
#merchantReferenceCode
);
COMMIT TRANSACTION;
RETURN 1;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
-- Raise an error with the details of the exception
DECLARE #errMsg NVARCHAR(4000) ,
#errSeverity INT;
SELECT #errMsg = ERROR_MESSAGE() ,
#errSeverity = ERROR_SEVERITY();
RAISERROR(#errMsg, #errSeverity, 1);
END CATCH;
END;

Transaction count after EXECUTE error

I have a stored procedure that looks something like:
CREATE PROCEDURE my_procedure
#val_1 INT,
#val_2 INT
AS
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO table_1(col_1, col_2)
VALUES (#val_1, #val_2);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
DECLARE
#ERROR_SEVERITY INT,
#ERROR_STATE INT,
#ERROR_NUMBER INT,
#ERROR_LINE INT,
#ERROR_MESSAGE NVARCHAR(4000);
SELECT
#ERROR_SEVERITY = ERROR_SEVERITY(),
#ERROR_STATE = ERROR_STATE(),
#ERROR_NUMBER = ERROR_NUMBER(),
#ERROR_LINE = ERROR_LINE(),
#ERROR_MESSAGE = ERROR_MESSAGE();
RAISERROR('Msg %d,
Line %d,
:%s',
#ERROR_SEVERITY,
#ERROR_STATE,
#ERROR_NUMBER,
#ERROR_LINE,
#ERROR_MESSAGE);
END CATCH
When this code is executed through the database, everything runs correctly. When execute through ADO.NET I get back the following error message:
"The INSERT statement conflicted with the FOREIGN KEY constraint "FK_table1_table2". The conflict occurred in database "my_database", table "dbo.table_1", column 'col_1'. Transaction count after EXECUTE indicates that a COMMIT or ROLLBACK TRANSACTION statement is missing. Previous count = 1, current count = 0. "
Is this happening because the XACT_ABORT setting is forcing a transaction from ADO.NET to be rolled back? What's the best way to go about avoiding this error?
you can check XACT_STATE() in your code and then commit or rollback, check it out here: Use XACT_STATE() To Check For Doomed Transactions
basically something like this will blow up
BEGIN TRANSACTION TranA
BEGIN TRY
DECLARE #cond INT;
SET #cond = 'A';
END TRY
BEGIN CATCH
PRINT 'a'
END CATCH;
COMMIT TRAN TranA
and when you check xact_state you can control it
BEGIN TRANSACTION TranA
BEGIN TRY
DECLARE #cond INT;
SET #cond = 'A';
END TRY
BEGIN CATCH
PRINT ERROR_MESSAGE();
END CATCH;
IF XACT_STATE() =0
BEGIN
COMMIT TRAN TranA
END
ELSE
BEGIN
ROLLBACK TRAN TranA
END
Also take a look at these two must read links
Implementing Error Handling with Stored Procedures and Error Handling in SQL Server – a Background.
IF XACT_STATE() =0 BEGIN COMMIT TRAN TranA END
will generate erro. XACT_STATE() = 0 means there is no transaction to commit or rollback
XACT_STATE() = 1 means there is commitable transaction
XACT_STATE() = -1 means there is uncommitable transaction which will be rollbacked by Database engine at the end of current context.

Resources