use of GOTO & Labels in SQL inside SQL Transactions - sql

I have a long running script that I want to wrap in transactions which could have up to 100+ transactions in it.
I would like to implement some error handling and re-use of code. I was thinking of using a label and GOTO statements.
BEGIN TRY
BEGIN TRANSACTION abc
--DO SOMETHING
COMMIT TRANSACTION abc;
END TRY
BEGIN CATCH
GOTO ERROR_OUT
END CATCH
ERROR_OUT:
SELECT ERROR_MESSAGE() [ErrorMessage];
ROLLBACK;
Is this something that should be done while using many transactions?
In my simple tests where I forced an error I noticed that the ERROR_MESSGE() SELECT statement did not return results.
Is there a way to get that to select statement to return results when using a label?

Declare #Table table (Num float,Den float,Pct float)
Insert into #Table values (25,100,0),(50,0,0)
BEGIN TRY
BEGIN TRANSACTION abc
-- Force a Divide by Zero on 2nd record
Update #Table Set Pct=Num/Den
COMMIT TRANSACTION abc;
Select * from #Table
GOTO CLEAN_OUT
END TRY
BEGIN CATCH
Declare #Error varchar(max)=Error_Message()
GOTO ERROR_OUT
END CATCH
ERROR_OUT:
ROLLBACK;
Select ErrorMessage=#Error
Select * From #Table
CLEAN_OUT:
Go
Returns
ErrorMessage
Divide by zero error encountered.
Num Den Pct
25 100 0
50 0 0
And then try it with (50,125,0)

In general, programmers are discouraged from using goto. The reason is that it is a control-flow primitive, and alternatives exist for many commonly used cases: if/then/else, while, and so on.
However, there are circumstances where goto is quite useful. And error handling is one of them. If you google "goto error handling", it is not hard to find explanations such as this, or this (although for C, I think the reasoning is very similar).
I have often used goto in SQL Server stored procedures for exactly this purpose. Although in my case, the goal was to achieve proper auditing and error processing when leaving the stored procedure.
Your use-case would seem to be a fine example where goto is a very reasonable way to code the block. I do find the "hundreds of transactions" part more questionable. Personally, I like to be explicit about when and where transactions are processed, so I would be more likely to write:
. . .
begin catch:
rollback ;
goto error_out;
end;
That is, to explicitly rollback the transaction "next to" where the transaction begins, rather than doing that in some far-away code block.

Related

Try and catch not working for stored procedure

I'm creating a procedure to delete a row from my plants Table. The try and catch statement was to delete it but if the plantID doesn't match any plant, catch the error and print the error message.
However, the catch isn't working and I'm unsure what how to fix it.
CREATE PROC spDeletePlant
#PlantID INT,
#PlantCode INT,
#PlantName VARCHAR(25),
#Description VARCHAR(50),
#ListPrice MONEY,
#BatchID INT,
#CategoryID INT
AS
BEGIN TRY
DELETE FROM Plants
WHERE PlantID = #PlantID
END TRY
BEGIN CATCH
THROW 50001, 'PlantID is incorrect.', 1
END CATCH;
Forgive the formatting, I am new to this site.
A query not matching any rows is not an error - when you run the delete statement on its own, you don't see an error raised.
If you want to automatically throw an error if no rows are deleted you can simply do:
DELETE FROM Plants
WHERE PlantID = #PlantID
IF ##Rowcount = 0
BEGIN
THROW 50001, 'PlantID is incorrect.', 1
END
Your code is working but is not doing what you expect. This is because delete statement doesn't not throw any error in case row is not found.
What you can do is :
Do a previous select to look for the register being deleted, otherwise throw an error. Look #Stu answer..
Check the affected rows variable. Which DB are you using? In MSSQL for example you can check the ##ROWCOUNT
PD:
Are you sure you want to throw an exception when a row is not found?
Usually Exceptions are reserved for an exceptional things such as I/O errors, bad params etc..
Additionally, you should take in a count that Exceptions costly so is not a good way of returning data.
I think you should check the number of affected rows.

Stored procedure best practice? Should they check if foreign keys exist before inserting?

When building a stored procedure that inserts into a simple link table, should it check if the FK's exist and gracefully return an error or just let SQL throw an exception?
What is best practice?
What is most efficient?
Just in case someone doesn't understand my question:
Table A
Table B
Table AB
Should I do:
IF EXISTS(SELECT 1 FROM A WHERE Id = #A) AND EXISTS(SELECT 1 FROM B WHERE Id = #B)
BEGIN
INSERT AB (AId,BId) VALUES (#A, #B)
END
ELSE
--handle gracefully, return error code or something
or
INSERT AB (AId,BId) VALUES (#A, #B)
and let SQL throw an exception
Thanks
If the tables are under your control, there is no reason to perform an extra check. Just assume they are set up correctly, and let SQL handle any error. Constantly checking that you have indeed done what you intended to do is overly defensive programming that adds unnecessary complexity to your code.
For example, you wouldn't write code like this:
i = 1;
if (i != 1)
{
print "Error: i is not 1!";
}
And I view this situation as similar.
If the tables are not under your control it may be useful to handle the error gracefully. For example, if this procedure can run on an arbitrary set of tables created by the user, or if it will be distributed to external users who are required to set up the tables in their own database, you may want to add some custom error handling. The purpose of this would be to give the user a clearer description of what went wrong.
As a basic concept, validating values before a potentially error raising code is a good thing. However, in this case, there could be (at least theoretically) a change in table a or table b between the exists checks and the insert statement, that would raise the fk violation error.
I would do something like this:
BEGIN TRY
INSERT AB (AId,BId) VALUES (#A, #B)
SELECT NULL As ErrorMessage
END TRY
BEGIN CATCH
SELECT ERROR_MESSAGE() AS ErrorMessage
END CATCH
The ERROR_MESSAGE() function returns the error that was rasied in the try block.
Then in the executing code you can simply check if the returned error message is null. If it is, you know the insert was successful. If not, you can handle this exception how ever you see fit.
Usually i will check Fk. If the Fk isn't exist, then throw a error friendly, and the insert statement will not be execute, database will not lock the table too.

SQL Server TRY CATCH FINALLY

I have a scenario where I need something similar to .NET's try-catch-finally block.
On my try, I will CREATE a #temp table, INSERT data to it & process other data sets based on #temp.
On CATCH then RAISERROR.
Is it possible to have a FINALLY block to DROP #temp?
Below is the pseudo code:
BEGIN TRY
CREATE TABLE #temp
(
--columns
)
--Process data with other data sets
END TRY
BEGIN CATCH
EXECUTE usp_getErrorMessage
END CATCH
BEGIN FINALLY
DROP TABLE #temp
END FINALLY
While not exactly the same as FINALLY, the T-SQL version of Try-Catch does allow that code that needs execute after both the Try and Catch blocks can occur after the end of the END CATCH statement.
Using the question code as an example:
BEGIN TRY
CREATE TABLE #temp
(
--columns
)
--Process data with other data sets
END TRY
BEGIN CATCH
EXECUTE usp_getErrorMessage
END CATCH;
IF OBJECT_ID('tempdb..#temp') IS NOT NULL -- Check for table existence
DROP TABLE #temp;
The DROP TABLE command will execute whether the Try or Catch execute.
See: BOL Try...Catch
Instead of creating a table you could just declare a table variable (which will automatically go away when the query ends).
BEGIN TRY
DECLARE #temp TABLE
(
--columns
)
--do stuff
END TRY
BEGIN CATCH
--do other stuff
END CATCH
there is no FINALLY equivalent.
an alternative may be table variables but is not exactly the same and must be evaluated on a case by case basis. there is a SO question with details very useful to make an informed choice.
with table variables you don't need to clean up like you do with temp tables
"FINALLY" is often, but not always, functionally identical to having the "final" code follow the TRY/CATCH (without a formal "FINALLY" block). Where it is different is the case where something in the TRY/CATCH blocks could cause execution to end, such as a return statement.
For example, a pattern I've used is to open a cursor, then have the cursor-using code in the TRY block, with the cursor close/deallocate following the TRY/CATCH block. This works fine if the blocks won't exit the code being executed. However, if the TRY CATCH block does, for example, a RETURN (which sounds like a bad idea), if there were a FINALLY block, it would get executed, but with the "final" code placed after the TRY / CATCH, as T-SQL requires, should those code blocks cause the execution to end, that final code won't be called, potentially leaving an inconsistent state.
So, while very often you can just put the code after the TRY/CATCH, it will be a problem if anything in those blocks could terminate without falling through to the cleanup code.
Local temp tables (e.g., "#Temp") are automatically dropped when the SQL connection ends. It's good practice to include an explicit DROP command anyway, but if it doesn't execute, the table will still be dropped.
If you must ensure that a DROP executes as soon as possible, you'll have to repeat the DROP command in a CATCH clause, since there's no FINALLY:
-- create temp table;
BEGIN TRY
-- use temp table;
-- drop temp table;
END TRY
BEGIN CATCH
-- drop temp table;
THROW; -- rethrow the error
END CATCH
Table variables are an alternative: they're dropped when the variable goes out of scope. However, table variables do not support statistics, so if the table variable is large and used in multiple queries, it may not perform as well as a temp table.
using custom error number to indicate there no real error, just final code?
-- create temp table;
BEGIN TRY
-- use temp table;
THROW 50555;
END TRY
BEGIN CATCH
-- drop temp table;
IF ERROR_NUMBER() <> 50555
THROW; -- rethrow the error
END CATCH
The correct answer in this case is the one proposed by #Dave Bennett; after the TRY/CATCH block check for the existence of the table and drop it.
But what if you are raising an exception out of your CATCH and you need to do some "FINALLY" type processing?
Could it be as simple as setting a variable in the CATCH and checking it after you fall out of the CATCH?
DECLARE #is_error BIT = 0;
BEGIN TRY
--Process data with other data sets
END TRY
BEGIN CATCH
-- Your exception handling code here
SET #is_error = 1;
END CATCH
-- Your "FINALLY" code here.
-- Then Check if you need to RAISERROR
IF #is_error = 0
BEGIN
-- Your success code
END
ELSE
BEGIN
-- Your fail code
-- RAISERROR
END;
With T-SQL, code placed after the TRY-CATCH block will get executed. So the answer to the question is simply to add a DROP TABLE IF EXISTS #temp right after the END CATCH like so:
BEGIN TRY
CREATE TABLE #temp(
--columns
)
--Process data with other data sets
END TRY
BEGIN CATCH
EXECUTE usp_getErrorMessage
END CATCH
-- Anything under this line will execute regardless of if the code reached the CATCH block or not.
DROP TABLE IF EXISTS #temp; -- Works in SQL Server 2016+, for older versions see here: https://stackoverflow.com/a/31171650/15596537

Ole db command & transactions. General Error

I have an OLE DB Command that originally contained a simple three line update:
update raw_CommissionPaid set PolicyNumber = ? where PolicyNumber = ?
and this worked fine, did exactly as expected and all was good.
I recently decided though that this has the potential to go wrong and do a lot of damage so i decided i would put it all in a transaction, monitor the rows affected and roll back if it changed more than what i was expecting, so i changed my update to this:
BEGIN TRY BEGIN TRAN UpdateCommissionsPaidPolNo
update raw_CommissionPaid set PolicyNumber = ? where PolicyNumber = ?
IF ##ROWCOUNT <> 1 RAISERROR('Row count <> 1', 11, 1)
COMMIT TRAN END TRY BEGIN CATCH
ROLLBACK TRAN UpdateCommissionsPaidPolNo
PRINT 'UpdateCommissionsPaidPolicyNumber script failed'
SELECT
ERROR_MESSAGE() as ErrorMessage END CATCH
However this gave me the "Syntax error or general error" message that it usually gives. I remembered from a previous issue that sometimes it cant map parameters to the ?'s if they are embedded within other SQL, i thought that might be the issue so changed it to this just incase:
Declare #FNumber varchar(20) declare #LNumber varchar(20)
set #FNumber = ? set #LNumber = ?
BEGIN TRY BEGIN TRAN UpdateCommissionsPaidPolNo
update raw_CommissionPaid set PolicyNumber = #FNumber where
PolicyNumber = #LNumber
IF ##ROWCOUNT <> 1 RAISERROR('Row count <> 1', 11, 1)
COMMIT TRAN END TRY BEGIN CATCH
ROLLBACK TRAN UpdateCommissionsPaidPolNo
PRINT 'UpdateCommissionsPaidPolicyNumber script failed'
SELECT
ERROR_MESSAGE() as ErrorMessage END CATCH
but from this i still get :
Syntax Error, permission violation or other non specific error
i realised it might be because of the print, or the returning of the error message, but removing them doesnt change anything, it still fails
im sure the sql is valid as i tested it in SQL server management studio.
Anyone faced this? Is there a setting im supposed to change to allow this kind of change?
Thanks in advance
After reading This site i learned that if i set the data flow tasks TransactionOption property to supported then if it fails it supposedly rolls the failing section back itself.
Armed with this knowledge i figured then the only thing i needed to do was make it realise that multiple rows being updated counted as an error. So changing it to just this:
update raw_CommissionPaid set PolicyNumber = ? where PolicyNumber = ?
IF ##ROWCOUNT <> 1 RAISERROR('Row count <> 1', 11, 1)
should make it roll itself back.

Why does the Try/Catch not complete in SSMS query window?

This sample script is supposed to create two tables and insert a row into each of them.
If all goes well, we should see OK and have two tables with data. If not, we should see FAILED and have no tables at all.
Running this in a query window displays an error for the second insert (as it should), but does not display either a success or failed message. The window just sits waiting for a manual rollback. ??? What am I missing in either the transactioning or the try/catch?
begin try
begin transaction
create table wpt1 (id1 int, junk1 varchar(20))
create table wpt2 (id2 int, junk2 varchar(20))
insert into wpt1 select 1,'blah'
insert into wpt2 select 2,'fred',0 -- <<< deliberate error on this line
commit transaction
print 'OK'
end try
begin catch
rollback transaction
print 'FAILED'
end catch
The problem is that your error is of a high severity, and is a type that breaks the connection immediately. TRY-CATCH can handle softer errors, but it does not catch all errors.
Look for - What Errors Are Not Trapped by a TRY/CATCH Block:
It looks like after the table is created, the following inserts are parsed (recompiled), which trigger statement level recompilations and breaks the batch.