Else statement bug or transaction effect? - sql-server

I'm using the following code in sql server 2005.
BEGIN TRANSACTION;
CREATE TABLE dbo.MyTable
(
idLang int NOT NULL IDENTITY (1, 1),
codeLang nvarchar(4) NOT NULL
) ON [PRIMARY];
IF ##ERROR = 0
BEGIN
PRINT 'before_commit';
COMMIT TRANSACTION;
PRINT 'after_commit';
END
ELSE
BEGIN
PRINT 'before_rollback';
ROLLBACK TRANSACTION;
PRINT 'after_rollback';
END
GO
1 - Display when MyTable doesn't exist (no error case) :
before_commit
after_commit
=> OK
2 - Display when MyTable exists (error case) :
'There is already an object named 'MyTable' in the database.'
=> Why the "else" statement is not executed ? (no print, no rollback)
I know the alternative with try-catch but i'd like to understand this strange case...
Thanks !

The CREATE TABLE will be checked during query compilation and fail, so none of the code in the batch is executed. Try adding:
SELECT ##TRANCOUNT
To the end of the script (i.e. after the GO), and you'll see the BEGIN TRANSACTION never occurred either.

I can't say specifically why your problem is occurring. Personally, I'm not sure I would use a transaction and error handling or a try/catch block to do this.
Have you tried querying the sys.tables table instead to check for its existence. Something of this ilk:
IF EXISTS(SELECT * FROM sys.tables WHERE object_id = object_id('MyTable'))
BEGIN
print 'table already exists'
END
ELSE
BEGIN
CREATE TABLE dbo.MyTable
(
idLang int NOT NULL IDENTITY (1, 1),
codeLang nvarchar(4) NOT NULL
) ON [PRIMARY];
END

Related

try catch non working if I not provide the alias in a join (Ambiguous column name 'name')

I would need to help to understand why the CATCH is not catching the error.
SCENARIO 1 - working OK.
BEGIN TRY
if object_id('tempdb..#temp') is not NULL drop table #temp
create table #temp (name varchar(1))
insert into #temp
values
('AA')
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS ErrorNumber;
END CATCH
ERROR_MESSAGE: String or binary data would be truncated.
SCENARIO 2
(where I forget to put the alias for the column)
BEGIN TRY
if object_id('tempdb..#temp') is not NULL drop table #temp
create table #temp (name varchar(1))
if object_id('tempdb..#temp01') is not NULL drop table #temp01
create table #temp01 (name varchar(1))
insert into #temp
values ('A')
insert into #temp01
values ('A')
select name
from #temp a
join #temp01 b on a.name = b.name
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS ErrorNumber;
END CATCH
In this case the error is not caught.
Can someone please explain why?
Thanks
TRY/CATCH is a runtime construct. In order to get to CATCH, your code has to run. Your code can’t run if it has a parse or compilation error. An even simpler example is the following:
BEGIN TRY;
SELECT; -- or SELECT foo; or any invalid syntax
END TRY
BEGIN CATCH;
PRINT 1;
END CATCH;
CATCH never happens here because SELECT; is invalid T-SQL and fails parsing, very much like your ambiguous column fails parsing, preventing CATCH from happening because the code never runs:
Msg 102, Level 15, State 1, Line 2
Incorrect syntax near ';'.
We can look at it the other way around too, so we don't get too hung up on CATCH "failing" to capture compilation errors. What if the
BEGIN TRY;
SELECT 1;
END TRY
BEGIN CATCH;
PRINT; -- invalid syntax!
END CATCH;
Msg 102, Level 15, State 1, Line 5
Incorrect syntax near ';'.
Note the line number: the syntax error here is inside CATCH, which shouldn't be reached, given that everything in TRY should work just fine. However, as with the previous example, the code inside both TRY and CATCH has to compile before it can run. If it can't compile, it's going to fail before TRY or CATCH ever become a thing.

SQL ELSE block still throws error when IF condition is true

I have the following T-SQL script that copies the value of an old column into a new one, then drops the old column. See here:
--step 1: create new column
IF NOT EXISTS(SELECT 1 from sys.columns
WHERE Name = N'UserColumn2'
AND Object_ID = Object_ID(N'Account'))
BEGIN
ALTER TABLE Account
ADD UserColumn2 int null
;
END
GO
;
--step 2: copy and drop
IF NOT EXISTS(SELECT 1 from sys.columns
WHERE Name = N'UserColumn1'
AND Object_ID = Object_ID(N'Account'))
BEGIN
PRINT 'Column ''UserColumn1'' does not exist.';
END
ELSE
BEGIN
UPDATE Account
SET UserColumn2 = UserColumn1
WHERE UserColumn1 is not null
;
BEGIN TRY
Declare #count int;
SELECT #count = Count(AccountID)
FROM Account
WHERE UserColumn2 <> UserColumn1
;
IF #count > 0
BEGIN
--PRINT 'Not all records were properly updated. UserColumn1 has not been dropped.';
THROW 50000,'Not all records were properly updated. UserColumn1 has not been dropped.',1
;
END
ELSE
BEGIN
ALTER TABLE Account
DROP Column UserColumn1
;
END
END TRY
BEGIN CATCH THROW; END CATCH
END
GO
;
The first step runs correctly but the second step still throws an error in the ELSE block even if the UserColumn1 column doesn't exist:
(note: this actually throws on line 24 for the code here. The code in my SSMS doesn't have the comments for 'step 1', etc.)
Why is this happening and how can I prevent it?
I've tried removing the NOT and moving the instructions out of the ELSE block but the behavior did not change. I've also tried writing the beginning of the second step like this:
IF (SELECT 1 from sys.columns
WHERE Name = N'UserColumn1'
AND Object_ID = Object_ID(N'Account')) <> null
and I get the same result.
The issue is that the entire sql text is parsed and compiled before it's executed, the error is being thrown at compile time.
You could workaround it by executing the update statement in its own process using dynamic sql - although there is nothing dynamic in this usage, it simply defers the compilation and execution of the update statement where it only happens in your else condition:
IF NOT EXISTS(SELECT 1 from sys.columns
WHERE Name = N'UserColumn1'
AND Object_ID = Object_ID(N'Account'))
BEGIN
PRINT 'Column ''UserColumn1'' does not exist.';
END
ELSE
BEGIN
EXEC sp_executesql N'
UPDATE Account
SET UserColumn2 = UserColumn1
WHERE UserColumn1 is not null;'
...
...

Identity key counter increment by one although it is in TRY Catch and Transaction is roll-backed ? SSMS 2008

Identity counter increment by one although it is in TRY Catch and Transaction is roll-backed ? SSMS 2008 is there any way i can stop it +1 or rollback it too.
In order to understand why this happened, Let's execute below sample code first-
USE tempdb
CREATE TABLE dbo.Sales
(ID INT IDENTITY(1,1), Address VARCHAR(200))
GO
BEGIN TRANSACTION
INSERT DBO.Sales
( Address )
VALUES ( 'Dwarka, Delhi' );
ROLLBACK TRANSACTION
Now, Execution plan for above query is-
The second last operator from right Compute Scalar is computing value for [Expr1003]=getidentity((629577281),(2),NULL) which is IDENTITY value for ID column. So this clearly indicates that IDENTITY values are fetched & Incremented prior to Insertion (INSERT Operator). So its by nature that even transaction rollback at later stage once created IDENTITY value is there.
Now, in order to reseed the IDENTITY value to Maximum Identity Value present in table + 1, you need sysadmin permission to execute below DBCC command -
DBCC CHECKIDENT
(
table_name
[, { NORESEED | { RESEED [, new_reseed_value ] } } ]
)
[ WITH NO_INFOMSGS ]
So the final query should include below piece of code prior to rollback statement:-
-- Code to check max ID value, and verify it again IDENTITY SEED
DECLARE #MaxValue INT = (SELECT ISNULL(MAX(ID),1) FROM dbo.Sales)
IF #MaxValue IS NOT NULL AND #MaxValue <> IDENT_CURRENT('dbo.Sales')
DBCC CHECKIDENT ( 'dbo.Sales', RESEED, #MaxValue )
--ROLLBACK TRANSACTION
So it is recommended to leave it on SQL Server.
You are right and the following code inserts record with [Col01] equal to 2:
CREATE TABLE [dbo].[DataSource]
(
[Col01] SMALLINT IDENTITY(1,1)
,[Col02] TINYINT
);
GO
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO [dbo].[DataSource] ([Col02])
VALUES (1);
SELECT 1/0
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END;
END CATCH;
GO
INSERT INTO [dbo].[DataSource] ([Col02])
VALUES (1);
SELECT *
FROM [dbo].[DataSource]
This is by design (as you can see in the documentation:
Consecutive values after server restart or other failures –SQL Server
might cache identity values for performance reasons and some of the
assigned values can be lost during a database failure or server
restart. This can result in gaps in the identity value upon insert. If
gaps are not acceptable then the application should use its own
mechanism to generate key values. Using a sequence generator with the
NOCACHE option can limit the gaps to transactions that are never
committed.
I try using NOCACHE sequence but it does not work on SQL Server 2012:
CREATE TABLE [dbo].[DataSource]
(
[Col01] SMALLINT
,[Col02] TINYINT
);
CREATE SEQUENCE [dbo].[MyIndentyty]
START WITH 1
INCREMENT BY 1
NO CACHE;
GO
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO [dbo].[DataSource] ([Col01], [Col02])
SELECT NEXT VALUE FOR [dbo].[MyIndentyty], 1
SELECT 1/0
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END;
END CATCH;
GO
INSERT INTO [dbo].[DataSource] ([Col01], [Col02])
SELECT NEXT VALUE FOR [dbo].[MyIndentyty], 1
SELECT *
FROM [dbo].[DataSource]
DROP TABLE [dbo].[DataSource];
DROP SEQUENCE [dbo].[MyIndentyty];
You can use MAX to solve this:
CREATE TABLE [dbo].[DataSource]
(
[Col01] SMALLINT
,[Col02] TINYINT
);
BEGIN TRY
BEGIN TRANSACTION;
DECLARE #Value SMALLINT = (SELECT MAX([Col01]) FROM [dbo].[DataSource]);
INSERT INTO [dbo].[DataSource] ([Col01], [Col02])
SELECT #Value, 1
SELECT 1/0
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
BEGIN
ROLLBACK TRANSACTION
END;
END CATCH;
GO
DECLARE #Value SMALLINT = ISNULL((SELECT MAX([Col01]) FROM [dbo].[DataSource]), 1);
INSERT INTO [dbo].[DataSource] ([Col01], [Col02])
SELECT #Value, 1
SELECT *
FROM [dbo].[DataSource]
DROP TABLE [dbo].[DataSource];
But you must pay attentions to your isolation level for potential issues:
If you want to insert many rows at the same time, do the following:
get the current max value
create table where to store the rows (that are going to be inserted) generating ranking (you can use identity column, you can use ranking function) and adding the max value to it
insert the rows

Why does division by zero still produce a resultset?

I am using sql server 2012
This is my query:
begin try
select 1/0
end try
begin catch
select 'div by 0'
end catch
It returns 2 different results:
but the result expected is div by 0 ? because the control after the failure of try is transferred to catch?
So why does it return 2 results?
The significant thing here is that divison by zero is a runtime error and terminates the statement at the point it has reached. The statement select 1/0 is perfectly valid in itself and the resultset can be created, it's during the evaluation of the expression 1/0 for the first and only row of data that the error occurs. If you extend your example a little you will see this better;
begin try
declare #t table (i float)
insert #t values (2),(1),(0),(-1)
select 1/i as r from #t
end try
begin catch
select 'div by 0'
end catch
In the first resultset produced you will now see 2 rows, then the division by zero occurs and no further rows are produced. What is interesting to note though is the 'rows affected' messages.
(4 row(s) affected)
(0 row(s) affected)
(1 row(s) affected)
The 4 row(s) are from insert #t and the 1 row(s) is from the select 'div by 0'. The terminated select 1/i as r from #t reports 0 row(s), despite the fact that we can see 2 rows were returned to SSMS. What is happening here is that although the resultset is created and rows are returned to the client, the SQL Server database engine supports the ACID properties, and because the select 1/i as r from #t did not complete fully it is rolled back (the select is being atomic). SSMS may have received some rows, but as far as SQL Server is concerned no rows were produced. Extending your example again to capture the rows demonstrates this;
declare #t table (i float)
declare #t2 table (i float)
insert #t values (2),(1),(0),(-1)
begin try
insert #t2 select 1/i as r from #t
end try
begin catch
select 'div by 0'
end catch
select * from #t2
When this is run you will see that #t2 contains no rows.
If you want to read up further on SQL Server's behaviour when division by zero occurs see ARITHABORT, ARITHIGNORE and ANSI_WARNINGS in Erland Sommarskog's error handling document.
Yes. It will show two results. It can be solved in two ways
1. Using with stored procedure
If you want to show result based on TRY and CATCH, try using an OUT parameter for returning result
CREATE PROCEDURE ProcedureName
#RESULT VARCHAR(100) OUTPUT
AS
BEGIN
begin try
select #RESULT = 1/0
end try
begin catch
select #RESULT = 'div by 0'
end catch
select #RESULT
END
2. Using without stored procedure
Declare a variable that holds the result based on TRY and CATCH
DECLARE #RESULT VARCHAR(100)
begin try
select #RESULT = 4/0
end try
begin catch
select #RESULT = 'div by 0'
end catch
select #RESULT

Is this good writen transaction in stored procedure

This is the first time that I use transactions and I just wonder am I make this right. Should I change something?
I insert post(wisp). When insert post I need to generate ID in commentableEntity table and insert that ID in wisp table.
ALTER PROCEDURE [dbo].[sp_CreateWisp]
#m_UserId uniqueidentifier,
#m_WispTypeId int,
#m_CreatedOnDate datetime,
#m_PrivacyTypeId int,
#m_WispText nvarchar(200)
AS
BEGIN TRANSACTION
DECLARE #wispId int
INSERT INTO dbo.tbl_Wisps
(UserId,WispTypeId,CreatedOnDate,PrivacyTypeId,WispText)
VALUES
(#m_UserId,#m_WispTypeId,#m_CreatedOnDate,#m_PrivacyTypeId,#m_WispText)
if ##ERROR <> 0
BEGIN
ROLLBACK
RAISERROR ('Error in adding new wisp.', 16, 1)
RETURN
END
SELECT #wispId = SCOPE_IDENTITY()
INSERT INTO dbo.tbl_CommentableEntity
(ItemId)
VALUES
(#wispId)
if ##ERROR <> 0
BEGIN
ROLLBACK
RAISERROR ('Error in adding commentable entity.', 16, 1)
RETURN
END
DECLARE #ceid int
select #ceid = SCOPE_IDENTITY()
UPDATE dbo.tbl_Wisps SET CommentableEntityId = #ceid WHERE WispId = #wispId
if ##ERROR <> 0
BEGIN
ROLLBACK
RAISERROR ('Error in adding wisp commentable entity id.', 16, 1)
RETURN
END
COMMIT
Using try/catch based on #gbn answer:
ALTER PROCEDURE [dbo].[sp_CreateWisp]
#m_UserId uniqueidentifier,
#m_WispTypeId int,
#m_CreatedOnDate datetime,
#m_PrivacyTypeId int,
#m_WispText nvarchar(200)
AS
SET XACT_ABORT, NOCOUNT ON
DECLARE #starttrancount int
BEGIN TRY
SELECT #starttrancount = ##TRANCOUNT
IF #starttrancount = 0
BEGIN TRANSACTION
DECLARE #wispId int
INSERT INTO dbo.tbl_Wisps
(UserId,WispTypeId,CreatedOnDate,PrivacyTypeId,WispText)
VALUES
(#m_UserId,#m_WispTypeId,#m_CreatedOnDate,#m_PrivacyTypeId,#m_WispText)
SELECT #wispId = SCOPE_IDENTITY()
INSERT INTO dbo.tbl_CommentableEntity
(ItemId)
VALUES
(#wispId)
DECLARE #ceid int
select #ceid = SCOPE_IDENTITY()
UPDATE dbo.tbl_Wisps SET CommentableEntityId = #ceid WHERE WispId = #wispId
IF #starttrancount = 0
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 AND #starttrancount = 0
ROLLBACK TRANSACTION
RAISERROR ('Error in adding new wisp', 16, 1)
END CATCH
GO
You'd use TRY/CATCH since SQL Server 2005+
Your rollback goes into the CATCH block but your code looks good otherwise (using SCOPE_IDENTITY() etc). I'd also use SET XACT_ABORT, NOCOUNT ON
This is my template: Nested stored procedures containing TRY CATCH ROLLBACK pattern?
Edit:
This allows for nested transactions as per DeveloperX's answer
This template also allows for higher level transactions as per Randy's comment
i think its not good all the time ,but if you want to use more than one stored procedure same time its not good be cause each stored procedure handles the transaction independently
but in this case,you should use try catch block , for exception handling , and preventing keeping transaction open on when an exception raising
I've never considered it a good idea to put transactions in a stored procedure. I think it's much better to start a transaction at a higher level so that can better coordinate multiple database (e.g. stored procedure) calls and treat them all as a single transaction.

Resources