handling transaction errors when objects don't exist - sql

I found and article in the MSDN Lbrary explaining that try/catch does not handle errors thrown when an object cannot be found. SO, even though I wrap a transaction in a try/catch, the rollback phrase will not execute:
BEGIN TRY
BEGIN TRANSACTION
SELECT 1 FROM dbo.TableDoesNotExists
PRINT ' Should not see this'
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
SELECT
ERROR_MESSAGE()
END CATCH
--PRINT 'Error Number before go: ' + CAST(##Error AS VARCHAR)
go
PRINT 'Error Count After go: ' + CAST(##Error AS VARCHAR)
PRINT 'Transaction Count ' + CAST(##TRANCOUNT AS VARCHAR)
What's the recommended way to handle errors thrown when an object does not exists, especially when there is a transaction involved. Should I tack this bit of code in place of the last two print statements:
IF ##ERROR <> 0 AND ##TRANCOUNT > 0
BEGIN
PRINT 'Rolling back txn'
ROLLBACK TRANSACTION
END
go
PRINT 'Transaction Count again: ' + CAST(##TRANCOUNT AS VARCHAR)

You can test for the existence of an object with OBJECT_ID():
IF OBJECT_ID('MyTable') IS NULL RAISERROR('Could not find MyTable.', 18, 0)

Why are you trying to retrieve data from a table which does not exist?
The fundamental building block of a database is a table. Not knowing what is in your schema is essentially trying to use SQL as a dynamic language, which it is not.
I would rethink your design; without knowing more about the tables in your database and its intended use it's hard for others to help in this regard. Add some more information to your question please.
EDIT
I've had a read of BOL and the recommended way to handle errors when an object does not exist is as follows:
You can use TRY…CATCH to handle errors
that occur during compilation or
statement-level recompilation by
executing the error-generating code in
a separate batch within the TRY block.
For example, you do this by placing
the code in a stored procedure or by
executing a dynamic Transact-SQL
statement using sp_executesql. This
allows TRY…CATCH to catch the error at
a higher level of execution than the
error occurrence.
I tested this out using the following procedure
CREATE PROCEDURE [dbo].[BrokenProcedure]
AS
BEGIN
SET NOCOUNT ON;
SELECT * FROM MissingTable
END
GO
Then called it within a TRY..CATCH block:
BEGIN TRY
PRINT 'Error Number before: ' + CAST(##Error AS VARCHAR)
EXECUTE [dbo].[BrokenProcedure]
PRINT ' Should not see this'
END TRY
BEGIN CATCH
PRINT 'Error Number in catch: ' + CAST(##Error AS VARCHAR)
END CATCH
Resulting in the following output:
Error Number before: 0
Error Number in catch: 208
It's not a perfect solution as you will have to create procedures (or use dynamic SQL) for all of your table access, but it's the method recommend by MS.

Now, you've hit on an interesting issue (well, to me anyway). You should not start a transaction because the batch won't compile. However, it can compile and then statement level recompilation fails later.
See this question What is wrong with my Try Catch in T-SQL?
However, in either case you can use SET XACT_ABORT ON. This adds predictability because one effect is to automatically roll back any transaction. It also suppress error 266. See this SO question too
SET XACT_ABORT ON
BEGIN TRY
BEGIN TRANSACTION
SELECT 1 FROM dbo.TableDoesNotExists
PRINT ' Should not see this'
COMMIT TRANSACTION
END TRY
BEGIN CATCH
-- not needed, but looks weird without a rollback.
-- you could forget SET XACT_ABORT ON
-- Use XACT_STATE avoid double rollback errors
IF XACT_STATE() <> 0
ROLLBACK TRANSACTION
SELECT
ERROR_MESSAGE()
END CATCH
go
--note, this can't be guaranteed to give anything
PRINT 'Error Count After go: ' + CAST(##Error AS VARCHAR)
PRINT 'Transaction Count ' + CAST(##TRANCOUNT AS VARCHAR)

Related

Rollback transaction if parameter name too long

I'm using the SimpleMembershipProvider that you get out of the box when you create an new .NET MVC application and I wanted to allow an admin user the ability to add roles. For learning purposes, (and I don't know if this is the correct way to do it) I wanted to limit the length of the description of the RoleName column to 15 characters, so I wrote the transaction:
create proc spInsertRole
(
#roleName varchar(50)
--really shouldn't be 50, but that's
--how I originally wrote my code
)
as
begin
set nocount on
begin try
begin tran
insert into dbo.webpages_Roles(RoleName)
values (#roleName)
commit transaction
end try
begin catch
select ERROR_MESSAGE() as ErrorMessage
if len(#roleName) > 15
rollback transaction
end catch
end
There is not a check constraint on the table for length of RoleName. This proc will compile but it will also let me add a RoleName of greater than 15 characters. What am I missing and what is the best way to do this?
You should check the length before you run the insert statement. By putting the length check in the catch block, you are telling the program to only check the length and roll back if there is some other error condition.
(My T-SQL is rusty and I don't have a database to test on so please verify before accepting. Also, given these changes, you probably don't need transactions anymore.)
create proc spInsertRole
(
#roleName varchar(50)
--really shouldn't be 50, but that's
--how I originally wrote my code
)
as
begin
set nocount on
begin try
begin tran
-- length check moved here. Raise error when > 15.
-- Severity (argument 2) needs to be higher than 10
-- to stop execution and trigger the catch block.
-- State (argument 3) is an arbitrary value between 0 and 255.
if len(#roleName) > 15
raiserror('Role name is too long.', 11, 5)
insert into dbo.webpages_Roles(RoleName)
values (#roleName)
commit transaction
end try
begin catch
select ERROR_MESSAGE() as ErrorMessage
-- length check was here. program will always roll back now.
rollback transaction
end catch
end
See RAISERROR for more information about how that works.

T-SQL install / upgrade script as a transaction

I'm trying to write a single T-SQL script which will upgrade a system which is currently in deployment. The script will contain a mixture of:
New tables
New columns on existing tables
New functions
New stored procedures
Changes to stored procedures
New views
etc.
As it's a reasonably large upgrade I want the script to rollback if a single part of it fails. I have an outline of my attempted code below:
DECLARE #upgrade NVARCHAR(32);
SELECT #upgrade = 'my upgrade';
BEGIN TRANSACTION #upgrade
BEGIN
PRINT 'Starting';
BEGIN TRY
CREATE TABLE x ( --blah...
);
ALTER TABLE y --blah...
);
CREATE PROCEDURE z AS BEGIN ( --blah...
END
GO --> this is causing trouble!
CREATE FUNCTION a ( --blah...
END TRY
BEGIN CATCH
PRINT 'Error with transaction. Code: ' + ##ERROR + '; Message: ' + ERROR_MESSAGE();
ROLLBACK TRANSACTION #upgrade;
PRINT 'Rollback complete';
RETURN;
END TRY
END
PRINT 'Upgrade successful';
COMMIT TRANSACTION #upgrade;
GO
Note - I know some of the syntax is not perfect - I'm having to re-key the code
It seems as though I can't put Stored Procedures into a transaction block. Is there a reason for this? Is it because of the use of the word GO? If so, how can I put SPs into a transaction block? What are the limitations as to what can go into a transaction block? Or, what would be a better alternative to what I'm trying to achieve?
Thanks
As Thomas Haratyk said in his answer, your issue was the "go". However, you can have as many batches in a transaction as you want. It's the try/catch that doesn't like this. Here's a simple proof-of-concept:
begin tran
go
select 1
go
select 2
go
rollback
begin try
select 1
go
select 2
go
end try
begin catch
select 1
end catch
Remove the GO and create your procedure by using dynamic sql or it will fail.
EXEC ('create procedure z
as
begin
print "hello world"
end')
GO is not a SQL keyword, it is a batch separator. So it cannot be included into a transaction.
Please refer to those topics for further information :
sql error:'CREATE/ALTER PROCEDURE' must be the first statement in a query batch?
Using "GO" within a transaction
http://msdn.microsoft.com/en-us/library/ms188037.aspx

TSQL error checking code not working

I'm trying to add an alert to my log for a running insert of a view into a table when there is an error executing the query in the view. When I run the view alone, I get an invalid input into SUBSTRING (the exact wording of the error I can't remember). When I run it as part of my view -> table stored procedure, the error is ignored, then I have to go digging for the offending line and make an exception in the view's code to omit that line from the results (I know, it sounds kludge-y, but I'm doing data reduction on huge web-log files from a specialized webapp), but I digress.
I've tried two different methods for trying to catch the error and neither are triggered in such a way to insert the row indicating an error in my execution result table (refresh_results). I think I may be missing some fundamental - perhaps the errors are being encapsulated in come way. If I can't detect the error, the only way to notice an error is if someone notices the number of entries into the table is low for a given period of time.
SELECT #TransactionName = 'tname';
BEGIN TRANSACTION #TransactionName;
BEGIN TRY
print 'tname ***In Try***';
if exists (select name from sysobjects where name='tablename')
begin
drop table tablename;
end
select * into tablename
from opendatasource('SQLNCLI', 'Data Source=DATABASE;UID=####;password=####').dbo.viewname;
COMMIT TRANSACTION #TransactionName;
END TRY
BEGIN CATCH
print 'tablename ***ERROR - check for SUBSTRING***';
begin transaction
set #result_table = 'tablename ***ERROR - check for SUBSTRING***'
select #result_time = getdate(),
#result_rows = count(logtime)
from tablename
insert INTO [dbo].[refresh_results] (result_time, result_table, result_rows)
values (#result_time, #result_table, #result_rows);
commit transaction
ROLLBACK TRANSACTION #TransactionName;
END CATCH
or
if exists (select name from sysobjects where name='tablename')
begin
drop table tablename;
end
select * into tablename
from opendatasource('SQLNCLI', 'Data Source=DATABASE;UID=####;password=####').dbo.viewname;
print '##error'
print ##error
if ##error <> 0
Begin
print 'tablename ***ERROR - check for SUBSTRING***';
set #result_table = 'tablename ***ERROR - check for SUBSTRING***'
select #result_time = getdate(),
#result_rows = count(logtime)
from tablename
insert INTO [dbo].[refresh_results] (result_time, result_table, result_rows)
values (#result_time, #result_table, #result_rows);
End
Your nested transactions aren't doing what you think. You are rolling back the error you thought you stored. Roll back the initial transaction and then, if you feel the need, start a new transaction for logging the error.
See here.
You have two seperate problems
In your first example you are running transactions that do the following:
BEGIN TRAN
...error...
BEGIN TRAN
...log error...
COMMIT TRAN
ROLLBACK TRAN
The inner transaction is rolled back with the outer transaction. Maybe try:
BEGIN TRAN
...error...
ROLLBACK TRAN
BEGIN TRAN
...log error...
ROLLBACK TRAN
The second example you are using ##ERROR. As I understand it as soon as you run something ##ERROR is replaced. That something I think includes the print statement.
If you change it to something like:
DECLARE #Error INT
select * into tablename
from opendatasource('SQLNCLI', 'Data Source=DATA3;UID=;password=').dbo.viewname;
SET #Error = ##ERROR
print '##error'
print #Error
if #Error <> 0
...log the error
The advantage of the TRY CATCH is that if you have an error it will catch it. The ##ERROR method works 100% but it only works on the last line run. so if you have an error with DROP TABLE tablename ##ERROR won't get it (unless you add another check)
Ok, so I had to use a helper procedure to add a log entry. I think what was going on is that the rollback was also rolling back the log entry.
This is what I had to do:
DECLARE #myError tinyint;
BEGIN TRY
BEGIN TRANSACTION;
if exists (select name from sys.sysobjects where name='table_name')
begin
drop table table_name
end
select * into table_name
from opendatasource('SQLNCLI', 'Data Source=###;UID=###;password=###').view_Table
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
set #myError = 1
ROLLBACK TRANSACTION;
END CATCH
if #myError <> 0
begin
exec dbo.table error
end
ELSE
EXEC exec dbo.table normal row

What is the scope of TRANSACTION in Sql server

I was creating a stored procedure and I see some differences between my methodology and my colleague's.
I am using SQL Server 2005
My Stored procedure looks like this
BEGIN TRAN
BEGIN TRY
INSERT INTO Tags.tblTopic
(Topic, TopicCode, Description)
VALUES(#Topic, #TopicCode, #Description)
INSERT INTO Tags.tblSubjectTopic
(SubjectId, TopicId)
VALUES(#SubjectId, ##IDENTITY)
COMMIT TRAN
END TRY
BEGIN CATCH
DECLARE #Error VARCHAR(1000)
SET #Error= 'ERROR NO : '+ERROR_NUMBER() + ', LINE NO : '+ ERROR_LINE() + ', ERROR MESSAGE : '+ERROR_MESSAGE()
PRINT #Error
ROLLBACK TRAN
END CATCH
And my colleague's way of writing looks like the below one
BEGIN TRY
BEGIN TRAN
INSERT INTO Tags.tblTopic
(Topic, TopicCode, Description)
VALUES(#Topic, #TopicCode, #Description)
INSERT INTO Tags.tblSubjectTopic
(SubjectId, TopicId)
VALUES(#SubjectId, ##IDENTITY)
COMMIT TRAN
END TRY
BEGIN CATCH
DECLARE #Error VARCHAR(1000)
SET #Error= 'ERROR NO : '+ERROR_NUMBER() + ', LINE NO : '+ ERROR_LINE() + ', ERROR MESSAGE : '+ERROR_MESSAGE()
PRINT #Error
ROLLBACK TRAN
END CATCH
Here the only difference between the two methods is the position of Begin TRAN.
According to me my colleague's method should not work when an exception occurs i.e. Rollback should not get executed because TRAN doesn't have scope in method 2. But when I tried to run both the methods, they were working in the same way.
In Method 1, scope of TRAN is outside of try block so it should be visible in both try block and catch block and should give result as per the scope methodology of programming works.
In Method 2, scope of TRAN is limited within Try block so Commit and Rollback should occur within the try block and should throw exception when a Rollback with no Begin Tran exists in catch block, but this is also working perfectly.
I am confused about how TRANSACTION works. Is it scope-free?
Transactions are not "scoped" in the way that programming languages are.
Transactions are nested for the current connection. Each BEGIN TRAN starts a new transaction and this transaction ends whenever a COMMIT or ROLLBACK is called, it does not matter where in your stored proc this is.
Transactions are nested for the
current connection. Each BEGIN TRAN
starts a new transaction and this
transaction ends whenever a COMMIT or
ROLLBACK is called, it does not matter
where in your stored proc this is.
only to add that ROLLBACK ends "all" open transactions for the connection...

Can I rollback Dynamic SQL in SQL Server / TSQL

Can I run a dynamic sql in a transaction and roll back using EXEC:
exec('SELECT * FROM TableA; SELECT * FROM TableB;');
Put this in a Transaction and use the ##error after the exec statement to do rollbacks.
eg. Code
BEGIN TRANSACTION
exec('SELECT * FROM TableA; SELECT * FROM TableB;');
IF ##ERROR != 0
BEGIN
ROLLBACK TRANSACTION
RETURN
END
ELSE
COMMIT TRANSACTION
If there are n dynamic sql statements and the error occurs in n/2 will the first 1 to ((n/2) - 1) statements be rolled back
Questions about the first answer
##Error won't pick up the error most likely
Which means that it might not pick up the error, which means a transaction might commit? Which defeats the purpose
TRY/CATCH in SQL Server 2005+
Yes I am using SQL Server 2005 but haven't used the Try Catch before
Would doing the below do the trick
BEGIN TRANSACTION
BEGIN TRY
exec('SELECT * FROM TableA; SELECT * FROM TableB;');
COMMIT TRANSACTION
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION
END CATCH
OR I looked at some more examples on the net
BEGIN TRY --Start the Try Block..
BEGIN TRANSACTION -- Start the transaction..
exec('SELECT * FROM TableA; SELECT * FROM TableB;');
COMMIT TRAN -- Transaction Success!
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRAN --RollBack in case of Error
RAISERROR(ERROR_MESSAGE(), ERROR_SEVERITY(), 1)
END CATCH
Yes. The TXNs belong to the current session/connection and dynamic SQL uses the same context.
However, ##ERROR won't pick up the error most likely: the status has to be checked immediately after the offending statement. I'd use TRY/CATCH, assuming SQL Server 2005+
Edit: The TRY/CATCH should work OK.
Don't take our word for it that try catch will work, test it yourself. Since this is dynamic sql the easiest thing to do is to make the first statement correct (and of course it mneeds to bean update,insert or delete or there is no need for atransaction) and then make a deliberate syntax error in the second statment. Then test that the update insert or delete in the first statment went through.
I also want to point out that dynamic sql as rule is a poor practice. Does this really need to be dynamic?