Rollback transaction inside cursors and inside transactions - sql

I'm pretty new in T-SQL and I'm in trouble with some huge scripts with transactions, cursors and storage procedures. So, my code is something like this (this code is just an example of the structure of my scripts, in fact I have multiples procedures inside OuterProc cursor and multiple operations inside InnerProc cursor):
create proc InnerProc
as
begin
declare #Id int
begin tran
declare mycursor cursor local static read_only forward_only
for select Id
from MyOtherTable
open mycursor
fetch next from mycursor into #Id
while ##fetch_status = 0
begin
select 1/0
if ##ERROR <> 0
begin
rollback tran
return ##ERROR
end
fetch next from mycursor into #Id
end
close mycursor
deallocate mycursor
commit tran
end
create proc OuterProc
as
begin
declare #Id int
begin tran
declare mycursor cursor local static read_only forward_only
for select Id
from MyTable
open mycursor
fetch next from mycursor into #Id
while ##fetch_status = 0
begin
exec #error = InnerProc
if ##ERROR <> 0
begin
rollback tran
return
end
else
commit tran
fetch next from mycursor into #Id
end
close mycursor
deallocate mycursor
end
With this structure I have this error:
Msg 515, Level 16, State 2, Procedure InnerProc, Line 448
Cannot insert the value NULL into column 'InitialQuantity', table 'MySecondTable'; column does not allow nulls. INSERT fails.
The statement has been terminated.
Msg 266, Level 16, State 2, Procedure InnerProc, Line 0
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0.
Msg 3903, Level 16, State 1, Procedure CreateSASEExtraction, Line 79
The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.
What is wrong with my code? If something goes wrong inside innerProc, I want all operations for that outer cursor rollback and stop the inner cursor. If something goes wrong in the outerProc I want all operations for that cursor to rollback but I want that cursor continue to looping...
There is a better way to do this?
UPDATE:
After I correct some errors #Bernd Linde detected, I add a try-catch in InnerProc and I named the InnerProc transaction. Now I have this code:
create proc InnerProc
as
begin
declare #Id int
begin tran
begin try
declare mycursor cursor local static read_only forward_only
for select Id
from MyOtherTable
open mycursor
fetch next from mycursor into #Id
while ##fetch_status = 0
begin
select 1/0
if ##ERROR <> 0
return ##ERROR
fetch next from mycursor into #Id
end
close mycursor
deallocate mycursor
commit tran
return 0
end try
begin catch
return ##ERROR
end catch
end
create proc OuterProc
as
begin
declare #Id int
declare mycursor cursor local static read_only forward_only
for select Id
from MyTable
open mycursor
fetch next from mycursor into #Id
while ##fetch_status = 0
begin
begin tran
exec #error = InnerProc
if ##ERROR <> 0
begin
rollback tran
return
end
else
commit tran
fetch next from mycursor into #Id
end
close mycursor
deallocate mycursor
end
But now I have other error message:
Msg 266, Level 16, State 2, Procedure InnerProc, Line 0
Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 2.
How can I solve this?

From the first look, you are committing transactions inside your loops, but you are only starting them once outside the loop.
So each time the loop goes into it's second iteration, it will try to either commit or rollback a transaction that does not exist, hence why you are getting the error "The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION."
I would suggest reading up on transactions in SQLServer on MSDN here

After many attemps, finally I get it.
The InnerProc must only have COMMITs and the OuterProc will be responsible for rollback.
For that, when InnerProc causes some error that must be catch in the OuterProc and forced to act like a exception.
How I want to continue looping in the OuterProc, that procedure must have a try-catch where the looping is forced and the rollback is done.
For a better transaction number control I used the ##TRANCOUNT.
So I solve the problem with this code:
create proc InnerProc
as
begin
declare #Id int
begin try
begin tran
declare mycursor cursor local static read_only forward_only
for select Id
from MyOtherTable
open mycursor
fetch next from mycursor into #Id
while ##fetch_status = 0
begin
select 1/0
IF ##ERROR <> 0
begin
if ##TRANCOUNT > 0
rollback tran
close mycursor
deallocate mycursor
return ##ERROR
end
fetch next from mycursor into #Id
end
close mycursor
deallocate mycursor
commit tran
return 0
end try
begin catch
close mycursor
deallocate mycursor
return ##ERROR
end catch
end
create proc OuterProc
as
begin
declare #Id int
declare mycursor cursor local static read_only forward_only
for select Id
from MyTable
open mycursor
fetch next from mycursor into #Id
while ##fetch_status = 0
begin
begin tran
begin try
exec #error = InnerProc
if ##ERROR <> 0
RAISERROR('Exception',1,1)
if##TRANCOUNT > 0
commit tran
fetch next from mycursor into #Id, #Name, #CodeDGAE, #Code, #NUIT, #Project
end try
begin catch
if ##TRANCOUNT > 0
rollback tran
fetch next from mycursor into #Id, #Name, #CodeDGAE, #Code, #NUIT, #Project
end catch
end
close mycursor
deallocate mycursor
end

Related

Running through all rows in cursor even if there are errors

I am trying to delete two Ids from a table, in which id '1' can't be deleted as there is another table referring to Users table but id '2' can be deleted so I want that even if it will give me runtime error for id '1' , it should run successfully for '2', deleting id '2' from table
But with this code I am getting this error
Msg 3998, Level 16, State 1, Line 1
Uncommittable transaction is detected at the end of the batch. The transaction is rolled back.
DECLARE #Id int
DECLARE TempCursor CURSOR LOCAL FAST_FORWARD FOR
SELECT ID FROM Users
WHERE Id IN (1,2)
OPEN TempCursor
WHILE 1=1
BEGIN
FETCH NEXT FROM TempCursor
INTO #Id
IF ##FETCH_STATUS < 0 BREAK
BEGIN TRY
SET XACT_ABORT ON
BEGIN TRAN DeleteTrans
DELETE Users WHERE Id = #Id
COMMIT TRAN DeleteTrans
END TRY
BEGIN CATCH
print #AdviserBusinessId
END CATCH
END
CLOSE TempCursor ;
DEALLOCATE TempCursor ;

Does a script fetch the next item in the cursor if the transaction was rolled back based on this script setup?

I'm not too familiar with cursors, but I just need to know one relatively simple thing. Take a look at the structure of the script below and note where the cursor is instantiated and where it is closed/deallocated. If the script deadlocks where I've written /* most of the code here */ and the transaction is rolled back, then reattempted, what happens when the script tries to fetch next? Since the execution never reached the close/deallocate cursor lines, I feel as though on the second attempt the the cursor would fetch the second row. Note that I'm not claiming that this is correctly written - I feel as though the issue I have is due to the cursor being deallocated AFTER committing the transaction.
declare LPCursor cursor for
/*
...
*/
while (#deadlockretries <= #Maxlockretries)
begin
begin try
begin transaction
fetch next from LPCursor into #var1, #var2, #var3
while (##fetch_status = 0)
begin
/* most of the code here */
end
commit transaction
close LPCursor
deallocate LPCursor
end try
begin catch
if (error_number() = 1205)
begin
if xact_state() <> 0
begin
rollback transaction
end
end
end catch
end
It will fetch the next one, but I assume you know that doing it this way you would have a block of "processing" that would get missed because you rolled it back right in the middle. Either way here is your code modified to show you how it would move forward:
CREATE TABLE #tmp(VAL1 varchar(10), VAL2 varchar(10), VAL3 varchar(10))
INSERT INTO #tmp VALUES('val1_1','val1_2','val1_3')
INSERT INTO #tmp VALUES('val2_1','val2_2','val2_3')
INSERT INTO #tmp VALUES('val3_1','val3_2','val3_3')
INSERT INTO #tmp VALUES('val4_1','val4_2','val4_3')
INSERT INTO #tmp VALUES('val5_1','val5_2','val5_3')
INSERT INTO #tmp VALUES('val6_1','val6_2','val6_3')
INSERT INTO #tmp VALUES('val7_1','val7_2','val7_3')
DECLARE #deadlockretries int = 0
declare #Maxlockretries int = 3
declare #var1 varchar(10)
declare #var2 varchar(10)
declare #var3 varchar(10)
DECLARE LPCursor cursor for SELECT VAL1, val2, val3 from #tmp
open LPCursor
while (#deadlockretries <= #Maxlockretries)
begin
PRINT 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
begin try
begin transaction
fetch next from LPCursor into #var1, #var2, #var3
-- print #var1 + #var2 + #var3
while (##fetch_status = 0)
begin
print #var1 + #var2 + #var3
/* most of the code here */
select 1/0
fetch next from LPCursor into #var1, #var2, #var3
end
commit transaction
SET #deadlockretries = #Maxlockretries
close LPCursor
deallocate LPCursor
end try
begin catch
PRINT 'ERROR'
print error_number()
if (error_number() = 8134)
begin
if xact_state() <> 0
begin
PRINT 'ROLLBACK'
rollback transaction
end
end
end catch
PRINT 'END'
SET #deadlockretries += 1
end
IF CURSOR_STATUS('global','LPCursor')>=-1
BEGIN
DEALLOCATE LPCursor
END

Transaction within a cursor

Is the following the correct way to use transactions within a cursor:
SET CURSOR_CLOSE_ON_COMMIT ON;
DECLARE cur CURSOR LOCAL FOR
SELECT * FROM #ordersToProcess;
OPEN cur;
DECLARE #OrderId int;
FETCH NEXT FROM cur INTO #OrderId;
WHILE ##FETCH_STATUS = 0 BEGIN
BEGIN TRY
BEGIN TRAN;
EXEC process_order #OrderId;
COMMIT TRAN;
DEALLOCATE cur;
SET CURSOR_CLOSE_ON_COMMIT OFF;
END TRY
BEGIN CATCH
ROLLBACK TRAN;
DEALLOCATE cur;
SET CURSOR_CLOSE_ON_COMMIT OFF;
THROW;
END CATCH;
FETCH NEXT FROM cur INTO #OrderId;
END;
No. You have this code:
WHILE ##FETCH_STATUS = 0 BEGIN
BEGIN TRY
BEGIN TRAN;
EXEC process_order #OrderId;
COMMIT TRAN;
DEALLOCATE cur;
SET CURSOR_CLOSE_ON_COMMIT OFF;
END TRY
. . .
This runs one time through the loop, deallocates the cursor and then . . . well, you have a problem on the second time through the loop.
I think you intend to dealloc after the while loop.
Deallocate and Close after the cursor has finished:
DECLARE cur CURSOR LOCAL FOR
SELECT * FROM #ordersToProcess;
OPEN cur;
DECLARE #OrderId int;
FETCH NEXT FROM cur INTO #OrderId;
WHILE ##FETCH_STATUS = 0 BEGIN
BEGIN TRY
BEGIN TRAN;
EXEC process_order #OrderId;
COMMIT TRAN;
END TRY
BEGIN CATCH
ROLLBACK TRAN;
THROW;
END CATCH;
FETCH NEXT FROM cur INTO #OrderId;
END;
BEGIN TRY
CLOSE Cursor1
DEALLOCATE Cursor1
END TRY
BEGIN CATCH
--Do nothing
END CATCH

How to check whether all transactions in the loop(cursors) are success or not in sql?

I am using cursor to delete set of tables. I need to set flag to identify whether all transactions are success or not. Can you please give me some sample queries?
DEclare #intErrorCode int;
DECLARE #TblName NVARCHAR(MAX);
DECLARE TBL_Cursor CURSOR
FOR ( select name from sysobjects where name like 'tbl_flat%');
OPEN TBL_Cursor;
FETCH NEXT FROM TBL_Cursor INTO #TblName
WHILE (##FETCH_STATUS <> -1)
BEGIN
IF LEN(#TblName) >0
BEGIN
DECLARE #strsql nvarchar(max)
BEGIN
something here
BEGIN TRAN
EXEC sp_executesql #strsql
COMMIT TRAN
SELECT #intErrorCode = ##ERROR
IF (#intErrorCode <> 0) GOTO PROBLEM
END
END
FETCH NEXT FROM TBL_Cursor INTO #TblName
END
CLOSE TBL_Cursor
DEALLOCATE TBL_Cursor
PROBLEM:
IF (#intErrorCode <> 0) BEGIN
PRINT 'Unexpected error occurred!'
END
BEGIN TRY
BEGIN TRANSACTION
--your code
EXEC sp_executesql #strsql
COMMIT TRAN -- Transaction Success!
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRAN --RollBack in case of Error
-- you can Raise ERROR with RAISEERROR() Statement including the details of the exception
RAISERROR(ERROR_MESSAGE(), ERROR_SEVERITY(), 1)
END CATCH

Use "WHERE CURRENT OF" clause to update only the specific row on which the CURSOR is positioned in SQL

I have the following cursor in SQL:
DECLARE #Script varchar(max)
DECLARE #getScript CURSOR
SET #getScript = CURSOR FOR
SELECT [Script]
FROM ScriptTable
OPEN #getScript
FETCH NEXT
FROM #getScript INTO #Script
WHILE ##FETCH_STATUS = 0
BEGIN
BEGIN TRY
EXEC(#Script) --Working part. This executes the query stored in the Script column.
--For example INSERT INTO zTest VALUES(VAL1, VAL2, etc etc..)
UPDATE ScriptTable
SET DateDone = GETDATE(), IsDone = 1, Err = NULL
FETCH NEXT
FROM #getScript INTO #Script
END TRY
BEGIN CATCH
DECLARE #Err varchar(max)
SET #Err = ERROR_MESSAGE()
UPDATE ScriptTable
SET DateDone = GETDATE(), Err = #Err
END CATCH
END
CLOSE #getScript
DEALLOCATE #getScript
Q1:
Currently, I am getting the values inserted into the "zTest" table specified in my comments when I execute EXEC(#Script).
However, the second part where the "Update ScriptTable" is, updates all the rows in my Script Table. I know I need to specify the ID for the appropriate row that the cursor is moving through. Question is, how can I do that? I wan't to only update the appropriate row, move to the next then update that one.
Q2:
My next question is, in the CATCH block, I think I am creating an infinite loop as soon as there is an error in one of the queries in the Script Column of the ScriptTable as when I look at results, it just keeps going and going. I don't want to BREAK; the procedure as I want to write an error to the Err column and continue with the next rows till it reaches the end of #Script, then stop.
IDENT_CURRENT, Scope_Identity etc doesn't work because I haven't inserted anything into the Scripts Table.
Please help.
Regarding Q1, you have to have a primary key in order to use the cursor for updating (though there are workarounds).
In general you'll want syntax something like this:
update ScriptTable
SET DateDone = GETDATE(), IsDone = 1, Err = NULL
where ID of #getScript
Regarding Q2, it make sense that it's an infinite loop. When you use the TRY and CATCH clauses and it fails it doesn't execute any of the syntax in the TRY "area".
Therefor the FETCH NEXT gets skipped, and in the next loop the same error happens again.
Try to make sure there is always a FETCH NEXT in the loop.
Hope this helps you out a bit.
Here is my final code if anyone is interested:
DECLARE #Script varchar(max)
DECLARE #getScript CURSOR
SET #getScript = CURSOR FOR
SELECT [Script]
FROM ScriptControl
OPEN #getScript
FETCH NEXT
FROM #getScript INTO #Script
DECLARE #Counter int = 1
WHILE ##FETCH_STATUS = 0
BEGIN
BEGIN TRY
EXEC(#Script)
UPDATE ScriptControl
SET DateDone = GETDATE(), IsDone = 1, Error = NULL WHERE ID = #Counter
FETCH NEXT
FROM #getScript INTO #Script
SET #Counter = (#Counter + 1)
END TRY
BEGIN CATCH
DECLARE #Err varchar(max)
SET #Err = ERROR_MESSAGE()
UPDATE ScriptControl
SET CSC_EOD_DateDone = NULL, CSC_EOD_Err = #Err, CSC_EOD_IsDone = 0 WHERE CURRENT OF #getScript
FETCH NEXT
FROM #getScript INTO #Script
SET #Counter = (#Counter + 1)
END CATCH
END
CLOSE #getScript
DEALLOCATE #getScript
This:
DECLARE #ScriptControlId INT, #Script VARCHAR(MAX)
DECLARE #getScript CURSOR
SET #getScript = CURSOR FOR
SELECT [ID], [Script]
FROM ScriptControl
OPEN #getScript
FETCH NEXT
FROM #getScript INTO #ScriptControlId, #Script
WHILE ##FETCH_STATUS = 0
BEGIN
BEGIN TRY
EXEC(#Script)
UPDATE ScriptControl
SET DateDone = GETDATE(), IsDone = 1, Error = NULL
WHERE ID = #ScriptControlId
FETCH NEXT FROM #getScript INTO #ScriptControlId, #Script
END TRY
BEGIN CATCH
DECLARE #Err VARCHAR(MAX) = ERROR_MESSAGE()
UPDATE ScriptControl
SET DateDone = NULL, Error = #Err, IsDone = 0
WHERE CURRENT OF #getScript
FETCH NEXT FROM #getScript INTO #ScriptControlId, #Script
END CATCH
END
CLOSE #getScript
DEALLOCATE #getScript
Or this:
DECLARE #Script VARCHAR(MAX)
DECLARE #getScript CURSOR
SET #getScript = CURSOR FOR
SELECT [Script]
FROM ScriptControl
OPEN #getScript
FETCH NEXT
FROM #getScript INTO #Script
WHILE ##FETCH_STATUS = 0
BEGIN
BEGIN TRY
EXEC(#Script)
UPDATE ScriptControl
SET DateDone = GETDATE(), IsDone = 1, Error = NULL
WHERE CURRENT OF #getScript
FETCH NEXT FROM #getScript INTO #Script
END TRY
BEGIN CATCH
DECLARE #Err VARCHAR(MAX) = ERROR_MESSAGE()
UPDATE ScriptControl
SET DateDone = NULL, Error = #Err, IsDone = 0
WHERE CURRENT OF #getScript
FETCH NEXT FROM #getScript INTO #Script
END CATCH
END
CLOSE #getScript
DEALLOCATE #getScript