How to rollback a transaction in TSQL when string data is truncated? - sql-server

Currently I have a large import process that I'm trying to wrap inside a transaction so if anything breaks - i could rollback. The issue I have is that when the TSQL inside the trans blows up, it won't rollback when the following SQL error occurs
Msg 8152, Level 16, State 14, Line 249
String or binary data would be truncated.
The statement has been terminated.
The below wraps this import TSQL
DECLARE #error INT
SELECT #error = 0
BEGIN TRANSACTION
--** begin import TSQL
--** end import TSQL
SELECT #error = ##error
IF #error != 0 GOTO handle_error
COMMIT
handle_error:
IF #error != 0
BEGIN
ROLLBACK
END

If your on SQL 2005 you can try:
BEGIN TRANSACTION
BEGIN TRY
--Run your Statements
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
DECLARE #Msg NVARCHAR(MAX)
SELECT #Msg=ERROR_MESSAGE()
RAISERROR('Error Occured: %s', 20, 101,#msg) WITH LOG
END CATCH

How about turning on xact_abort
set xact_abort on

I would also point out that if you are receiving this error often, you need to revise the size of the column you are entering data into or adjust your cleaning process to prep the data before putting it into the prod table. In SSIS, You could also have the data that deosn't meet the standard size go to a bad data table and process the rest.

Related

Insert in catch block causes error: The current transaction cannot be committed and cannot support operations that write to the log file

I have two procedures, one outer procedure and one inner procedure, where I would like to understand the behaviour of the error handling. The inner procedure provokes an error and is trying to insert something in the catch block into a table. After that the error is raised, passed to the outer procedure and then should roll back the transaction.
I'm trying to understand why my code is throwing the error message:
Msg 50000, Level 11, State 1, Procedure dbo.OuterProcedure, Line 21 [Batch Start Line 9]
The current transaction cannot be committed and cannot support operations that write to the log file. Roll back the transaction.
I would expect the following message:
Msg 50000, Level 11, State 1, Procedure dbo.OuterProcedure, Line 21 [Batch Start Line 9]
Error converting data type varchar to numeric.
I know that the issue comes from the catch block in the inner procedure and it happens because I'm trying to insert something into my log table before raising the error. When I switch those statements or delete the insert, I get the actual error message. I also know that it is not smart to do the logging in the inner procedure and inside a transaction that is rolled back anyways.
I would like to understand what is making this transaction a "doomed" transaction even though the XACT_ABORT is set to off.
Full code:
My main procedure:
CREATE PROCEDURE [dbo].[OuterProcedure]
AS
BEGIN
SET XACT_ABORT OFF;
BEGIN TRY
BEGIN TRANSACTION ;
-- do other stuff
EXEC [dbo].[innerprocedure];
-- do other stuff
COMMIT TRANSACTION ;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
DECLARE #ErrText NVARCHAR(2000);
SET #ErrText = ISNULL(ERROR_MESSAGE(), 'nothing')
RAISERROR(#ErrText, 11, 1) WITH NOWAIT
END CATCH;
END;
My inner procedure:
CREATE PROCEDURE [dbo].[InnerProcedure]
AS
BEGIN
SET XACT_ABORT OFF;
SET NOCOUNT ON;
BEGIN TRY
-- do other stuff
-- provoke error
SELECT
CASE
WHEN 1 = 0
THEN 0.0
ELSE ''
END;
-- do other stuff
END TRY
BEGIN CATCH
DECLARE #ErrText NVARCHAR(2000);
SELECT
#ErrText = ISNULL(ERROR_MESSAGE(), 'nothing');
INSERT INTO [dbo].[logtable]
(
[Message]
, [ErrNr]
)
VALUES
( #ErrText
, -1
);
RAISERROR(#LogText, 11, 0) WITH NOWAIT;
END CATCH;
END;
I would like to understand what is making this transaction a "doomed"
transaction even though the XACT_ABORT is set to off.
XACT_STATE() is -1 in the catch block so the transaction is doomed.
SELECT
CASE
WHEN 1 = 0
THEN 0.0
ELSE ''
END;
Throws error
Error converting data type varchar to numeric.
"Most conversion errors" is one of the error types that Erland Sommarskog puts in the category of errors.
Batch Abortion with Rollback This is the strongest reaction SQL Server
can take to a user error. These are errors that abort execution on the
spot if there is no CATCH handler on the stack and they also roll back
any open transaction. If there is a CATCH handler, the error is
caught, but any open transaction is doomed and must be rolled back.
The behaviour is the same, no matter whether XACT_ABORT is ON or OFF.
The categorisation of error behaviours is somewhat cryptic, undocumented and not intuitive. Read his article for more details.

Commit transaction without begin transaction

I accidentally ran into a situation that I didn't put Begin Transaction at the beginning of my stored procedure and just wrote Commit Transaction as you can see below
ALTER PROCEDURE dbo.spTest
AS
BEGIN
DECLARE #MyId INT=1
BEGIN TRY
UPDATE Test
SET
-- Id -- this column value is auto-generated
CharName = 'david'
WHERE id=4
--Just to test locking behavior
WHILE(1=1)
BEGIN
SET #MyId=2;
END
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
END CATCH
END
I expected SQL Server to give me a run time error but it didn't happen. Of course I should mention that based on my test it didn't acquire any lock on the table due to the lack of Begin Transaction but what is the point of COMMIT TRANSACTION and ROLLBACK TRANSACTION in such a condition and why didn't SQL Server raise any error?
Edit:
if i remove while block and put WaitFor Sql raise error when reaches to COMMIT TRANSACTION
ALTER PROCEDURE dbo.spTest
AS
BEGIN
UPDATE Test
SET CharName = 'david'
WHERE id=4
PRINT 'waiting for a minute '
WAITFOR DELAY '00:00:10';
COMMIT TRANSACTION
END
Now i am receiving this error
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION
what is the point of COMMIT TRANSACTION and ROLLBACK TRANSACTION in such a condition?
There is no point in this case
and why didn't SQL Server raise any error?
I don't see any code that would raise an error. It would help if you could explain where and why you think an error should be raised
With regards to whatever you're actually doing here;
If the purpose of this proc is to hold a transaction open, you'd need something more like this:
ALTER PROCEDURE dbo.spTest
AS
BEGIN
BEGIN TRANSACTION
UPDATE Test
SET CharName = 'david'
WHERE id=4
--Endless loop
WHILE(1=1)
BEGIN
PRINT 'waiting for a minute inside a transaction. Try something from another session'
WAITFOR DELAY '00:01';
END
-- Transaction will actually never be committed
-- Because this line will never be reached
-- because it's preceded by an endless loop
COMMIT TRANSACTION
END
The TRY / CATCH is a bit of a distraction. I've removed it.

Multiple Roll back for single Transcation

I have to update multiple number of rows in table.
My requirement is, If for any reason , the update result returns 0, then the entire transaction should be rolled back.
At the same time if there is any exception occurred, then also the complete transaction must be rolled back.
In short I need to roll back the entire update transaction either if update statement returns 0 or if any exception has been occurred while updating the table.
This is the code I used.
CREATE TYPE [DBO].[EMPLOYEETYPETABLETYPE] AS TABLE
( EmployeeStatusKey INT, EmployeeStatusName VARCHAR(50) )
CREATE PROCEDURE [dbo].[usp_UpdateEmployeeStatusType]
#EmploymentStatusDetails [DBO].[EMPLOYEETYPETABLETYPE] READONLY
AS
BEGIN
SET NOCOUNT ON;
DECLARE #TransactionName varchar(20) = 'UpdateEmployeeStatus';
DECLARE #rowcount1 INT
BEGIN
BEGIN TRY
BEGIN TRANSACTION #TransactionName
UPDATE ES1
SET
ES1.EmployeeStatusName=ES2.EmployeeStatusName
FROM
[dbo].[EmployeeStatusTypes] ES1
INNER JOIN
#EmploymentStatusDetails ES2
ON
ES1.EmployeeStatusKey= ES2.EmployeeStatusKey
SET
#ROWCOUNT1=##ROWCOUNT
IF #rowcount1 =0
GOTO PROBLEM
PROBLEM:
ROLLBACK TRAN #TransactionName
COMMIT
END TRY
BEGIN CATCH
SET #ROWCOUNT1=0
ROLLBACK TRAN #TransactionName
END CATCH
IF #rowcount1 =0
SELECT -178,#rowcount1;
ELSE
SELECT 178,#rowcount1;
END
END
I am passing a datatable to the stored procedure from the C# code.
When I execute the Sp, No error is thrown But When I call it from the C# code I am getting the Exception
Exception: The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.
Please help and Thanks in advance....
Remove that awful GOTO
IF #rowcount1 =0
ROLLBACK TRAN #TransactionName
ELSE
COMMIT TRAN #TransactionName
And review TRY-CATCH documentation. You have to check whether there is any transaction to commit or rollback by checking XACT_STATE() value.
I tested your script with simplified one:
BEGIN TRY
print 'A:' + cast(##trancount as varchar)
BEGIN TRANSACTION tranName
print 'B:' + cast(##trancount as varchar)
GOTO PROBLEM -- you can comment this to simulate there is no error
PROBLEM:
begin
ROLLBACK TRAN tranName
print 'there is a problem'
end
print 'C:' + cast(##trancount as varchar)
COMMIT
END TRY
BEGIN CATCH
print 'D:' + cast(##trancount as varchar)
ROLLBACK TRAN tranName
END CATCH
when there is a problem, output is like
A:0
B:1
there is a problem
C:0
D:0
Msg 3903, Level 16, State 1, Line 18
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.
if I comment the GOTO PROBLEM so there is not a problem, output is like this:
A:0
B:1
there is a problem
C:0
D:0
Msg 3903, Level 16, State 1, Line 18
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.
you see that the section under GOTO, still is executing.
finally, the COMMIT still happens where there is not transaction available, so COMMIT throws and error. This means your script throws an exception.
If you get rid of the GOTO, you'll be good. using GOTO is a bad practice anyway.

SET XACT_ABORT ON Being ignored, transaction continues (SQL Server 2008 R2)

Perhaps I am missing something, but even though the RAISERRORs below have severity of 16 (as per documentation) the transaction is still committed as if XACT_ABORT ON has no effect.
CREATE PROCEDURE [ExploringGroups].[RemoveMember]
#groupId uniqueidentifier,
#adminUsername nvarchar(50),
#targetUsername nvarchar(50)
AS
SET XACT_ABORT ON
BEGIN TRANSACTION
DECLARE
#adminUserId uniqueidentifier = dbo.fn_userId(#adminUsername),
#targetUserId uniqueidentifier = dbo.fn_userId(#targetUsername)
IF #targetUserId IS NULL OR ExploringGroups.IsMember(#groupId, #targetUserId) = 0
RAISERROR('Target user was not located', 16, 1)
IF ExploringGroups.IsInRole(#groupId, #adminUserId, 'adm') = 0
RAISERROR('Specified user is not an administrator of this group', 16, 2)
IF #adminUserId = #targetUserId
RAISERROR('You cannot remove yourself', 16, 3)
-- statements below still execute and commit even though there was an error raised above
DELETE FROM ExploringGroups.MemberRole WHERE GroupId = #groupId AND UserId = #targetUserId
DELETE FROM ExploringGroups.Membership WHERE GroupId = #groupId AND UserId = #targetUserId
COMMIT
RETURN 0
Calling
exec exploringgroups.removemember '356048C5-BAB3-45C9-BE3C-A7227225DFDD', 'Crypton', 'Crypton'
Produces
Msg 50000, Level 16, State 2, Procedure RemoveMember, Line 20
Specified user is not an administrator of this group
Msg 50000, Level 16, State 3, Procedure RemoveMember, Line 24
You cannot remove yourself
I thought XACT_ABORT was supposed to roll back the whole transaction if it's set to ON?
Actually, it behaves exactly as it was supposed to. XACT_ABORT really caused the transaction to roll back, so were there any data modifications up to the point of the error they will be rolled back. However, it didn't affect the flow of execution, and it didn't stop running the stored procedure, so the following two DELETEs were executed as implicite transactions. Explicit RAISERRORs don't abort the batch.
See this simplified version:
create table #t(i int);
insert #t values(1);
go
alter procedure sp
as
set xact_abort on
begin tran
raiserror ('x', 16, 1);
print 'deleting';
delete #t;
commit;
go
exec sp
go
select * from #t
go
The only funny thing was that the error about COMMIT not having a corresponding BEGIN TRAN was swallowed.
With SEH, it would jump into CATCH block.
You should be using a "THROW" statement versus the RAISERROR approach.
Use a BEGIN TRY and BEGIN CATCH and commit the transaction normally or rollback in the CATCH block.
BEGIN TRY
-- Do your inserts or throw error
-- Commit transaction
END TRY
BEGIN CATCH
-- Rollback
END CATCH;

TSQL mutual exclusive access in a stored procedure

Several web servers access a SQL Server to get a numeric code, when this code doesn't exist, it has to be autogenerated by the SQL Server.
I need to ensure that even if two concurrent calls come in and the code doesn't exist, only one code is created and both calls return the same code. So I have to do something like this:
begin lock
if code exists
return code
else
generate code
return code
end lock
I've been reading a little about isolation levels and table locking, but I have a terrible mess with all that. First I thought that a SERIALIZABLE isolation level is what I need, but apparently it's not.
So, what would you do to accomplish a "lock" in TSQL?
Thanks a lot.
UPDATE:
I got this error when I try to set the serializable level using this as example:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE get_code
AS
BEGIN
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
GO
BEGIN TRANSACTION
select code from codes where granted is null;
END
GO
Msg 1018, Level 15, State 1, Procedure
get_code, Line 4 Incorrect syntax near
'SERIALIZABLE'. If this is intended as
a part of a table hint, A WITH keyword
and parenthesis are now required. See
SQL Server Books Online for proper
syntax. Msg 102, Level 15, State 1,
Line 5 Incorrect syntax near 'END'.
What does it means?
SERIALIZABLE is an isolation level for locking, not a semaphore.
It won't work in this case all you'll do is persist a read lock to the end of the TXN that doesn't prevent another process into the code reading.
You need to use sp_getapplock in Transaction mode. You can configure it to wait, bomb immediately etc: up to you
This is based on my template from Nested stored procedures containing TRY CATCH ROLLBACK pattern?
ALTER PROCEDURE get_code
AS
SET XACT_ABORT, NOCOUNT ON
DECLARE #starttrancount int, #result int;
BEGIN TRY
SELECT #starttrancount = ##TRANCOUNT
IF #starttrancount = 0 BEGIN TRANSACTION
EXEC #result = sp_getapplock 'get_code', 'Exclusive', 'Transaction', 0
IF #result < 0
RAISERROR('INFO: One at a time please`!', 16, 1);
[...Perform work...]
IF #starttrancount = 0
COMMIT TRANSACTION
ELSE
EXEC sp_releaseapplock 'get_code';
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 AND #starttrancount = 0
ROLLBACK TRANSACTION
RAISERROR [rethrow caught error using #ErrorNumber, #ErrorMessage, etc]
END CATCH
GO
This is how I did it. Given a table MetaInfo with columns MetaKey varchar(max) and MeatValueLong bigInt.
Note, in my case there goal was to exclusively get a increasing value without duplicates. I used a rowlock to create the isolation on this single operation. (Yes I know I could have used inserting and an auto-increment key, but there was an addition requirement that the caller can pass in a minimum value.)
CREATE PROCEDURE [dbo].[uspGetNextID]
(
#inID bigInt
)
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRANSACTION
-- This section can be removed if you want to pass in an id.
SET #inID = 0
UPDATE MetaInfo WITH (ROWLOCK)
SET MetaValueLong = CASE
WHEN ISNULL(MetaValueLong,0) > #inID THEN MetaValueLong+1
ELSE #inID+1
END
WHERE MetaKey = 'Internal-ID-Last'
SELECT MetaValueLong
FROM MetaInfo
WHERE MetaKey = 'Internal-ID-Last'
COMMIT TRANSACTION
END
yes, SET ISOLATION LEVEL SERIALIZABLE is exactly what you need. It does not permit dirty writes and dirty reads. All db-objets that are inside serializable transactions are locked so other connections will be able read/write only when first one does commit or rollback.

Resources