SQL Server XACT_ABORT with exclusion - sql-server

I have a larger stored procedure which utilizes several TRY/CATCH blocks in order to catch and log individual errors. I have also wrapped a transaction around the entire contents of the procedure, so as to be able to roll back the entire thing in the event of an error raised somewhere along the way (in order to prevent a lot of messy cleanup); XACT_ABORT has been enabled since it would otherwise not roll back the entire transaction.
Key component:
There is a table in my database which gets a record inserted each time this procedure is run with the results of operations and details on what went wrong.
Funny thing is happening - actually, when I finally figured out what was wrong, it was pretty obvious... the the insert statement into my log table is getting rolled back as well, hence, if I am not running this out of SSMS, I will not be able to see that this was even run, as the rollback removes all trances of activity.
Question:
Would it be possible to have the entire transaction roll back with the exception of this single insert statement? I would still want to preserve the error message which I compile during the running of the stored procedure.
Thanks so much!
~Eli
Update 6/28
Here's a code sample of what I'm looking at. Key difference between this and the samples posed by #Alex and #gameiswar is that in my case, the try/catch blocks are all nested inside the single transaction. The purpose of this is to have multiple catches (for the multiple tables), though we would the entire mess to be rolled back even if the last update failed.
SET XACT_ABORT ON;
BEGIN TRANSACTION
DECLARE #message AS VARCHAR(MAX) = '';
-- TABLE 1
BEGIN TRY
UPDATE TABLE xx
SET yy = zz
END TRY
BEGIN CATCH
SET #message = 'TABLE 1 '+ ERROR_MESSAGE();
INSERT INTO LOGTABLE
SELECT
GETDATE(),
#message
RETURN;
END CATCH
-- TABLE 2
BEGIN TRY
UPDATE TABLE sss
SET tt = xyz
END TRY
BEGIN CATCH
SET #message = 'TABLE 2 '+ ERROR_MESSAGE();
INSERT INTO LOGTABLE
SELECT
GETDATE(),
#message
RETURN;
END CATCH
COMMIT TRANSACTION

You can try something like below ,which ensures you log the operation.This takes advantage of the fact that table variables dont get rollbacked..
Psuedo code only to give you idea:
create table test1
(
id int primary key
)
create table logg
(
errmsg varchar(max)
)
declare #errmsg varchar(max)
set xact_abort on
begin try
begin tran
insert into test1
select 1
insert into test1
select 1
commit
end try
begin catch
set #errmsg=ERROR_MESSAGE()
select #errmsg as "in block"
if ##trancount>0
rollback tran
end catch
set xact_abort off
select #errmsg as "after block";
insert into logg
select #errmsg
select * from logg

OK... I was able to solve this using a combination of the great suggestions put forth by Alex and GameisWar, with the addition of the T-SQL GOTO control flow statement.
The basic ideas was to store the error message in a variable, which survives a rollback, then have the Catch send you to a FAILURE label which will do the following:
Rollback the transaction
Insert a record into the log table, using the data from the aforementioned variable
Exit the stored procedure
I also use a second GOTO statement to make sure that a successful run will skip over the FAILURE section and commit the transaction.
Below is a code snippet of what the test SQL looked like. It worked like a charm, and I have already implemented this and tested it (successfully) in our production environment.
I really appreciate all the help and input!
SET XACT_ABORT ON
DECLARE #MESSAGE VARCHAR(MAX) = '';
BEGIN TRANSACTION
BEGIN TRY
INSERT INTO TEST_TABLE VALUES ('TEST'); -- WORKS FINE
END TRY
BEGIN CATCH
SET #MESSAGE = 'ERROR - SECTION 1: ' + ERROR_MESSAGE();
GOTO FAILURE;
END CATCH
BEGIN TRY
INSERT INTO TEST_TABLE VALUES ('TEST2'); --WORKS FINE
INSERT INTO TEST_TABLE VALUES ('ANOTHER TEST'); -- ERRORS OUT, DATA WOULD BE TRUNCATED
END TRY
BEGIN CATCH
SET #MESSAGE = 'ERROR - SECTION 2: ' + ERROR_MESSAGE();
GOTO FAILURE;
END CATCH
GOTO SUCCESS;
FAILURE:
ROLLBACK
INSERT INTO LOGG SELECT #MESSAGE
RETURN;
SUCCESS:
COMMIT TRANSACTION

I don't know details but IMHO general logic can be like this.
--set XACT_ABORT ON --not include it
declare #result varchar(max) --collect details in case you need it
begin transaction
begin try
--your logic here
--if something wrong RAISERROR(...#result)
--everything OK
commit
end try
begin catch
--collect error_message() and other into #result
rollback
end catch
insert log(result) values (#result)

Related

SQL Server / T-SQL : Raiserror cancels prior inserts

In a procedure, I want to make a test then Raiserror when it's actually the case. But before that, I want to log the error in a table. My code is like this
CREATE PROCEDURE proc
#val VARCHAR(50)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT OFF;
DECLARE #test VARCHAR(50)
SELECT #test = test
FROM test_table
WHERE ...
IF #test IS NULL
BEGIN
INSERT INTO log_table VALUES (#val);
RAISERROR ('Invalid value : %i', 16, 1, #val);
END
END
The code compiles. When executed with a bad value, the error is raised, but the insert is cancelled.
I tried turning xact_abort and nocount on and off but had no luck.
I tried encapsulating the insert request in BEGIN TRANSACTION/COMMIT but still get the same result.
What I noticed, my log_table which has an auto-increment id, gets incremented even when those inserts are being cancelled.
How can I raise and error but still persist the insert request?
Thanks
Consider using THROW instead:
CREATE TABLE dbo.log_table (val varchar(50));
GO
CREATE PROCEDURE dbo.[proc] #val varchar(50)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT OFF;
DECLARE #test varchar(50); --As i never set this, it'll go into the IF
IF (#test IS NULL)
BEGIN
INSERT INTO log_table
VALUES (#val);
THROW 51000, N'Invalid value.', 1;
END;
END;
GO
EXEC dbo.[proc] #val = 'Some Value';
GO
SELECT *
FROM dbo.log_table;
GO
DROP PROC dbo.[proc];
DROP TABLE dbo.log_table;
DB<>Fiddle
In order to write to a log table you have to rollback any pending transaction. Otherwise your log table INSERT may be rolled back by the calling code, or may fail because the transaction is doomed.
So something like:
CREATE Procedure myproc
#val varchar(50)
as
begin
set nocount on
set xact_abort on
begin transaction;
begin try
-- do stuff
commit transaction;
end try
begin catch
if ##trancount > 0 rollback;
declare #error_message varchar(max) = error_message()
INSERT INTO log_table values (#val);
throw;
end catch
end
So apparently, my procedure was working as expected in SQLServer side. The problem was that I was calling this procedure from Java/Spring native query method and had to be annotated with #Modifying and #Transactional since it's doing insertions. Thus when an exception is caught, it was automatically rolled back.
I didn't find a quick solution to bypass Spring's transaction. Now I think all I have to do is, catch the exception in App layer and log to the log_table in app layer too

Transaction count after EXECUTE issue

I have written a procedure like below lines of code
ALTER PROCEDURE [dbo].[CountrySave]
(
#CountryId uniqueidentifier,
#CountryName nvarchar(max)
)
AS
begin tran
if exists (select * from Country where CountryID =#CountryId)
begin
update Country set
CountryID = #CountryId,
CountryName =#CountryName
where CountryID = #CountryId
end
else
begin
insert INTO Country(CountryID, CountryName) values
(NewID(),#CountryName)
end
It throws "Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 0, current count = 1.
A transaction that was started in a MARS batch is still active at the end of the batch. The transaction is rolled back." error message when executed!!!
Please Help...
Add COMMIT TRAN
ALTER PROCEDURE [dbo].[CountrySave]
#CountryId uniqueidentifier,
#CountryName nvarchar(max)
AS
BEGIN
BEGIN TRY
BEGIN TRAN
if exists (select * from Country where CountryID =#CountryId)
begin
update Country
set CountryID = #CountryId,
CountryName =#CountryName
where CountryID = #CountryId;
end
else
begin
insert INTO Country(CountryID, CountryName)
values(NewID(),#CountryName)
end
COMMIT TRAN
END TRY
BEGIN CATCH
/* Error occured log it */
ROLLBACK
END CATCH
END
The error message is fairly clear. When you open (begin) a transaction, you will need to do something at the end of it as well.
So either you ROLLBACK the transaction (in case one of the statements within the transaction fails), or you COMMIT the transaction in order to actually implement all changes your statements made.
From MSDN:
BEGIN TRANSACTION represents a point at which the data referenced by a
connection is logically and physically consistent. If errors are
encountered, all data modifications made after the BEGIN TRANSACTION
can be rolled back to return the data to this known state of
consistency. Each transaction lasts until either it completes without
errors and COMMIT TRANSACTION is issued to make the modifications a
permanent part of the database, or errors are encountered and all
modifications are erased with a ROLLBACK TRANSACTION statement.
More information: https://msdn.microsoft.com/en-us/library/ms188929.aspx
Your Problem is that you begin a transaction but you never commit it / do a rollback.
Try this structure for your procedure, worked very well for me in the past:
CREATE PROCEDURE [dbo].SomeProc
(#Parameter INT)
AS
BEGIN
--if you want to be to only active transaction then uncomment this:
--IF ##TRANCOUNT > 0
--BEGIN
-- RAISERROR('Other Transactions are active at the moment - Please try again later',16,1)
--END
BEGIN TRANSACTION
BEGIN TRY
/*
DO SOMETHING
*/
COMMIT TRANSACTION
END TRY
BEGIN CATCH
--Custom Error could be raised here
--RAISERROR('Something bad happened when doing something',16,1)
ROLLBACK TRANSACTION
END CATCH
END

##trancount value is 1 in stored procedure without begin tran

I have following definition of stored procedure:
CREATE procedure dbo.ImportData
(
#SessionId VARCHAR(20)
,#ImportId int
)
as
begin
PRINT 'TRANCOUNT value = ' + CAST(##TRANCOUNT AS VARCHAR)
begin try
--business log
begin try
BEGIN CATCH
--business log
END CATCH
END
Whenever i am running my SP, i am getting ##Trancount value as 1 however i have not begin any transaction using BEGIN TRAN statement.
Please suggest this behavior of sql server of creating a transition after BEGIN statement of the SP definition.
Also, when i tried to replicate the same behavior by creating another SP without parameter and only one SELECT statement but i am getting Transition value as 0.
Please suggest this concept.
You have already opened transaction (maybe accidentally).
Try to disconnect from sql-server or commit (rollbcack) existing transactions.

SQL Transaction Error: The current transaction cannot be committed and cannot support operations that write to the log file

I'm having a similar issue to The current transaction cannot be committed and cannot support operations that write to the log file, but I have a follow-up question.
The answer there references Using TRY...CATCH in Transact-SQL, which I'll come back to in a second...
My code (inherited, of course) has the simplified form:
SET NOCOUNT ON
SET XACT_ABORT ON
CREATE TABLE #tmp
SET #transaction = 'insert_backtest_results'
BEGIN TRANSACTION #transaction
BEGIN TRY
--do some bulk insert stuff into #tmp
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION #transaction
SET #errorMessage = 'bulk insert error importing results for backtest '
+ CAST(#backtest_id as VARCHAR) +
'; check backtestfiles$ directory for error files ' +
' error_number: ' + CAST(ERROR_NUMBER() AS VARCHAR) +
' error_message: ' + CAST(ERROR_MESSAGE() AS VARCHAR(200)) +
' error_severity: ' + CAST(ERROR_SEVERITY() AS VARCHAR) +
' error_state ' + CAST(ERROR_STATE() AS VARCHAR) +
' error_line: ' + CAST(ERROR_LINE() AS VARCHAR)
RAISERROR(#errorMessage, 16, 1)
RETURN -666
END CATCH
BEGIN TRY
EXEC usp_other_stuff_1 #whatever
EXEC usp_other_stuff_2 #whatever
-- a LOT of "normal" logic here... inserts, updates, etc...
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION #transaction
SET #errorMessage = 'error importing results for backtest '
+ CAST(#backtest_id as VARCHAR) +
' error_number: ' + CAST(ERROR_NUMBER() AS VARCHAR) +
' error_message: ' + CAST(ERROR_MESSAGE() AS VARCHAR(200)) +
' error_severity: ' + CAST(ERROR_SEVERITY() AS VARCHAR) +
' error_state ' + CAST(ERROR_STATE() AS VARCHAR) +
' error_line: ' + CAST(ERROR_LINE() AS VARCHAR)
RAISERROR(#errorMessage, 16, 1)
RETURN -777
END CATCH
RETURN 0
I think I have enough information to just play with it and figure it out myself... unfortunately reproducing the error is proving damn near impossible. So I'm hoping that asking here will help clarify my understanding of the problem and solution.
This stored procedure is, intermittently, throwing errors like this one:
error importing results for backtest 9649 error_number: 3930 error_message: The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction. error_severity: 16 error_state 1 error_line: 217
So obviously the error is coming from the 2nd catch block
Based on what I've read in Using TRY...CATCH in Transact-SQL, I think what's happening is that when the exception is thrown, the use of XACT_ABORT is causing the transaction to be "terminated and rolled back"... and then the first line of the BEGIN CATCH is blindly attempting to roll back again.
I don't know why the original developer enabled XACT_ABORT, so I'm thinking the better solution (than removing it) would be to use XACT_STATE() to only roll back if there is a transaction (<>0). Does that sound reasonable? Am I missing something?
Also, the mention of logging in the error message makes me wonder: Is there another problem, potentially with configuration? Is our use of RAISEERROR() in this scenario contributing to the problem? Does that get logged, in some sort of case where logging isn't possible, as the error message alludes to?
You always need to check for XACT_STATE(), irrelevant of the XACT_ABORT setting. I have an example of a template for stored procedures that need to handle transactions in the TRY/CATCH context at Exception 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
There are a few misunderstandings in the discussion above.
First, you can always ROLLBACK a transaction... no matter what the state of the transaction. So you only have to check the XACT_STATE before a COMMIT, not before a rollback.
As far as the error in the code, you will want to put the transaction inside the TRY. Then in your CATCH, the first thing you should do is the following:
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION #transaction
Then, after the statement above, then you can send an email or whatever is needed. (FYI: If you send the email BEFORE the rollback, then you will definitely get the "cannot... write to log file" error.)
This issue was from last year, so I hope you have resolved this by now :-)
Remus pointed you in the right direction.
As a rule of thumb... the TRY will immediately jump to the CATCH when there is an error. Then, when you're in the CATCH, you can use the XACT_STATE to decide whether you can commit. But if you always want to ROLLBACK in the catch, then you don't need to check the state at all.
I have encountered this error while updating records from table which has trigger enabled.
For example - I have trigger 'Trigger1' on table 'Table1'.
When I tried to update the 'Table1' using the update query - it throws the same error. THis is because if you are updating more than 1 record in your query, then 'Trigger1' will throw this error as it doesn't support updating multiple entries if it is enabled on same table.
I tried disabling trigger before update and then performed update operation and it was completed without any error.
DISABLE TRIGGER Trigger1 ON Table1;
Update query --------
Enable TRIGGER Trigger1 ON Table1;
I encountered a similar issue to the above and was receiving the same error message. The above answers were helpful but not quite what I needed, which was actually a bit simpler.
I had a stored procedure that was structured as below:
SET XACT_ABORT ON
BEGIN TRY
--Stored procedure logic
BEGIN TRANSACTION
--Transaction logic
COMMIT TRANSACTION
--More stored procedure logic
END TRY
BEGIN CATCH
--Handle errors gracefully
END CATCH
TRY...CATCH was used to handle errors in the stored procedure logic. Just one part of the procedure contained a transaction, and if an error occurred during this it would not get picked up by the CATCH block, but would error out with the SQL Transaction Error message.
This was resolved by adding another TRY...CATCH wrapper that would ROLLBACK the transaction and THROW the error. This meant any errors in this step could be handled gracefully in the main CATCH block, as per the rest of the stored procedure.
SET XACT_ABORT ON
BEGIN TRY
--Stored procedure logic
BEGIN TRY
BEGIN TRANSACTION;
--Transaction logic
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK;
THROW;
END CATCH
--More stored procedure logic
END TRY
BEGIN CATCH
--Handle errors gracefully
END CATCH
None of this helped me so here is what fixed my issue.
A teammate configured a server trigger to monitor DDL changes.
Once I disabled it, I could install the package then I enabled it again and package is still working.
Had the exact same error in a procedure.
It turns out the user running it (a technical user in our case) did not have sufficient rigths to create a temporary table.
EXEC sp_addrolemember 'db_ddladmin', 'username_here';
did the trick

Best practices to delete a set of tables in sql 2008

Basically I want to keep the transaction very simple but I should be able to rollback if any error occurs in the later part.
Something like:
BEGIN TRANSACTION
DELETE SET 1(this will delete first set of table)
COMMIT
DELETE SET 2 (will delete second set of table)
If any error occurs while deleting set 2 I should be able to rollback set 1 transaction as well. Let me know if we have any options to do like this. Appreciate for your help.
If any error occurs while deleting set
2 i should be able to rollback set 1
transaction as well.Let me know if we
have any options to do like this.
Appreciate for your help.
Then, why don't you do this?
BEGIN TRY
BEGIN TRANSACTION -- Start the transaction
DELETE SET 1(this will delete first set of table)
DELETE SET 2 (will delete second set of table)
-- If we reach here, success!
COMMIT
END TRY
BEGIN CATCH
-- Whoops, there was an error
IF ##TRANCOUNT > 0
ROLLBACK
-- 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
Read here for a complete explanation.
If you mean deleting tables as in DELETE TABLE (I.e. the table, not the content), you are out of luck - DDL is not transacted.

Resources