SQLServer Transaction error when called from python - sql-server

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?

Related

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.

Transaction does not rollback all changes

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]

How to select two record set only if second one was successful?

I am going to report result of a stored procedure (whether it was successful or has error) using a simple select statement before sending prepared record sets. so I just simply insert this select statement before sending real records sets. But even when I wrap these two select statement in a transaction to make them atomic yet if second select statement raises an error the first select executed and gives 'ok' and 'error' at the same time. here is the code:
CREATE PROCEDURE my_procedure
#id INT = NULL
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
BEGIN TRY
BEGIN TRANSACTION;
SELECT 1 AS [status], 'OK' AS [message];
SELECT 1/0;
COMMIT;
END TRY
BEGIN CATCH
ROLLBACK;
SELECT 0 AS [status], ERROR_MESSAGE() AS [message];
END CATCH;
END;
How could first select statement be done only if the the second statement is successful?
Maybe declare a couple of variables outsite of the TRY/CATCH. Then change their values in the CATCH if an error is thrown. After the TRY/CATCH, show the values of the variables.
ALTER PROCEDURE my_procedure
#id INT = NULL
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE #status BIT = 1; --set status variable here
DECLARE #message VARCHAR(MAX) = 'OK'; --set message variable here
BEGIN TRY
BEGIN TRANSACTION;
SELECT 1/0;
COMMIT;
END TRY
BEGIN CATCH
ROLLBACK;
SET #status = 0; --change value of #status in the CATCH block
SET #message = ERROR_MESSAGE();--change value of #message in the CATCH block
END CATCH;
--show the value of each variable
SELECT #status AS 'Status',#message AS 'Message'
END;
Have you tried using RAISERROR() or THROW()? Specifically, using RAISERROR() with a severity of 11-19 will force the execution to jump to the CATCH block. See details at https://learn.microsoft.com/en-us/sql/t-sql/language-elements/raiserror-transact-sql.
Specifically take a look at Example 1:
BEGIN TRY
-- RAISERROR with severity 11-19 will cause execution to
-- jump to the CATCH block.
RAISERROR ('Error raised in TRY block.', -- Message text.
16, -- Severity.
1 -- State.
);
END TRY
BEGIN CATCH
DECLARE #ErrorMessage NVARCHAR(4000);
DECLARE #ErrorSeverity INT;
DECLARE #ErrorState INT;
SELECT
#ErrorMessage = ERROR_MESSAGE(),
#ErrorSeverity = ERROR_SEVERITY(),
#ErrorState = ERROR_STATE();
-- Use RAISERROR inside the CATCH block to return error
-- information about the original error that caused
-- execution to jump to the CATCH block.
RAISERROR (#ErrorMessage, -- Message text.
#ErrorSeverity, -- Severity.
#ErrorState -- State.
);
END CATCH;

TSQL Transaction behaviour with disconnected queries - dynamic SQL

I have the below stored procedure, that dynamically calls a list of stored procedures. It's been in place for a few months and has been running fine (unfortunately my access to the server is pretty restricted so couldn't manage this any other way)
Alter Proc [Process].[UspLoad_LoadController]
(
#HoursBetweenEachRun Int
)
As
Begin
--find all procedures that need to be updated
Create Table [#ProcsToRun]
(
[PID] Int Identity(1 , 1)
, [SchemaName] Varchar(150)
, [ProcName] Varchar(150)
);
Insert [#ProcsToRun]
( [SchemaName]
, [ProcName]
)
Select [s].[name]
, [p].[name]
From [sys].[procedures] [p]
Left Join [sys].[schemas] [s]
On [s].[schema_id] = [p].[schema_id]
Where [s].[name] = 'Process'
And [p].[name] Like 'UspUpdate%';
Declare #MaxProcs Int
, #CurrentProc Int = 1;
Select #MaxProcs = Max([PID])
From [#ProcsToRun];
Declare #SQL Varchar(Max)
, #SchemaName sysname
, #ProcName sysname;
--run through each procedure, not caring if the count changes and only updating if there have been more than 23 hours since the last run
While #CurrentProc <= #MaxProcs
Begin
Select #SchemaName = [SchemaName]
, #ProcName = [ProcName]
From [#ProcsToRun]
Where [PID] = #CurrentProc;
Select #SQL = #SchemaName + '.' + #ProcName
+ ' #PrevCheck = 0,#HoursBetweenUpdates = '
+ Cast(#HoursBetweenEachRun As Varchar(5));
Exec (#SQL);
Set #CurrentProc = #CurrentProc + 1;
End;
End;
Go
However, the environment this is running in occasionally suffers from communications errors, with the query being cancelled whilst it is still executing.
My question is - can I wrap the entire procedure with a transaction statement and if I can what would happen in the event of the query being terminated early?
BEGIN Tran Test
Exec [Process].[UspLoad_LoadController] #HoursBetweenEachRun = 1;
COMMIT TRANSACTION Test
What I want to happen would be for the transaction to be rolled back - would this cater for this?
Yes it works,but you might have to see how many stored procs you have and impact of rollback.Normally you can use Set XACT_ABORT ON inside stored proc,but due to dynamic SQL,it wont have any effect..
Sample demo on how to wrap your proc
begin try
begin tran
exec usp_main
commit
end try
begin catch
rollback
end catch
some tests i did on trying to use XACT_ABORT with out any success.but wrapping your main proc in a tran and rolling back when any error occurs,rollback all stored procs too.
create table test2
(
id int)
create table test3
(
id int)
create proc usp_test2
as
begin
insert into test2
select 1
end
alter proc usp_test3
as
begin
insert into test3
select 1/0
end
alter proc usp_main
as
begin
set xact_abort on
declare #sql1 nvarchar(2000)
set #sql1='exec usp_test2'
declare #sql2 nvarchar(2000)
set #sql2='exec usp_test3'
exec (#sql1)
exec(#sql2)
end

How to expose more information about the failure of a stored proc in SQL agent

I have a SQL agent job setup and in that job there is a step to execute a stored proc. If that stored proc fails then the SQL agent job will display an error message but there is no other information. Something like a stacktrace or at least the stored proc that was running and the line number would be highly useful.
e.g.
If the following stored proc is executed then an error message like "Executed as user: NT AUTHORITY\NETWORK SERVICE. Start [SQLSTATE 01000] (Message 0) Invalid object name 'NonExistentTable'. [SQLSTATE 42S02] (Error 208). The step failed." with no indication where exactly the failure occured.
CREATE PROCEDURE TestSpLogging AS
BEGIN
PRINT 'Start'
SELECT * FROM NonExistentTable
PRINT 'End'
END
What's the best way to expose this information?
Using the approach detailed at http://www.sommarskog.se/error_handling_2005.html seems to be working sufficiently so far. It has only required an update to the top level stored procedure and will output the name of the stored procedure that failed and the line number to SQL agent.
The output error will look like this:
Executed as user: NT AUTHORITY\NETWORK SERVICE. *** [InnerInnerStoredProc2], 5. Errno 208: Invalid object name 'NonExistentTable'. [SQLSTATE 42000] (Error 50000) Start [SQLSTATE 01000] (Error 0). The step failed.
Summary of steps:
Create the following error handler stored procedure:
CREATE PROCEDURE error_handler_sp AS
DECLARE #errmsg nvarchar(2048),
#severity tinyint,
#state tinyint,
#errno int,
#proc sysname,
#lineno int
SELECT #errmsg = error_message(), #severity = error_severity(), -- 10
#state = error_state(), #errno = error_number(),
#proc = error_procedure(), #lineno = error_line()
IF #errmsg NOT LIKE '***%' -- 11
BEGIN
SELECT #errmsg = '*** ' + coalesce(quotename(#proc), '<dynamic SQL>') +
', ' + ltrim(str(#lineno)) + '. Errno ' +
ltrim(str(#errno)) + ': ' + #errmsg
RAISERROR(#errmsg, #severity, #state)
END
ELSE
RAISERROR(#errmsg, #severity, #state)
go
Wrap the top level stored proc in a try catch as follows
BEGIN TRY
SET NOCOUNT ON
SET XACT_ABORT ON
EXEC InnerStoredProc1
EXEC InnerStoredProc2
END TRY
BEGIN CATCH
IF ##trancount > 0 ROLLBACK TRANSACTION
EXEC error_handler_sp
RETURN 55555
END CATCH
One way to do this would be add some error handling to the stored procedure. Here is a simple method we use here is something like this
declare
#Error int
,#ErrorMsg varchar(1000)
,#StepName varchar(500)
,#ProcedureName sysname
,#dtDateTime datetime
select #ProcedureName = object_name(##procid)
begin try
select #StepName = 'Step 01: Select from table
PRINT 'Start'
SELECT * FROM NonExistentTable
PRINT 'End'
end try
begin catch
select #Error = ##ERROR
set #ErrorMsg = #ProcedureName + ' Error: ' + #StepName
+ ', dbErrorNbr:' + IsNull(convert(varchar(10),#Error),'Null')
raiserror (#ErrorMsg, 16, 1) with nowait
end catch

Resources