Insert into #tmp select * from transTable error not going into catch block - try-catch

I have a "Insert into #tmp Select * from TransTable" statement within TRY block. This statement generates an error because #tmp has an identity column. But after error is raised, control does not go to CATCH block. Error raised has severity of 16 and hence as per my understanding it should go to CATCH block. Kindly explain as to why is this happening.
Let me elaborate the question , below is the scenario where we are going to dump records from one table to another but due to Identity (not turned on) it throw an error while execution. The expectation here is, it should go to catch block where as it doesn't any thought?
Create table TableA
(
ID int identity,
Value varchar(1)
)
Create table TableB
(
ID int identity,
Value varchar(1)
)
insert into TableA values ('A')
insert into TableA values ('B')
insert into TableA values ('C')
insert into TableA values ('D')
insert into TableA values ('E')
Begin Try
Truncate table TableB
Insert into TableB Select * from TableA
End Try
Begin Catch
SELECT
ERROR_NUMBER() AS ErrorNumber,
ERROR_SEVERITY() AS ErrorSeverity,
ERROR_STATE() as ErrorState,
ERROR_PROCEDURE() as ErrorProcedure,
ERROR_LINE() as ErrorLine,
ERROR_MESSAGE() as ErrorMessage;
End Catch
Error :
Msg 8101, Level 16, State 1, Line 26
An explicit value for the identity column in table 'TableB' can only be specified when a column list is used and IDENTITY_INSERT is ON.

If I'm not mistaking, when you're using SQL calls you need to "catch" SQL exceptions, which are different to regular exceptions.
What programming language are you using? What DB are you using? Could you post a sample code?
This is an example:
Try
' Execute insert statement
Catch sqlEx as SqlException
' Do something about the SQL exception
Catch ex as Exception
' Regular exception
End Try

Related

Transact-SQL transaction rollback not working properly when using GO commands

I have a migration script written in Transact-SQL which is using transactions in order to have a proper rollback if something goes wrong during the execution.
Unfortunately, this rollback behaviour is not working as expected when I'm using some GO utility statements in my script.
The issue can be reproduced with a simple script:
BEGIN TRANSACTION
-- Create a table with two nullable columns
CREATE TABLE [dbo].[t1](
[id] [nvarchar](36) NULL,
[name] [nvarchar](36) NULL
)
-- add one row having one NULL column
INSERT INTO [dbo].[t1] VALUES(NEWID(), NULL)
-- set one column as NOT NULLABLE
-- this fails because of the previous insert
ALTER TABLE [dbo].[t1] ALTER COLUMN [name] [nvarchar](36) NOT NULL
GO
-- create a table as next action, so that we can test whether the rollback happened properly
CREATE TABLE [dbo].[t2](
[id] [nvarchar](36) NOT NULL
)
GO
COMMIT TRANSACTION
When I execute this script, I get the following output:
(1 row affected)
Msg 515, Level 16, State 2, Line 23
Cannot insert the value NULL into column 'name', table 'test-transaction.dbo.t1'; column does not allow nulls. UPDATE fails.
The statement has been terminated.
Msg 3902, Level 16, State 1, Line 31
The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.
As expected, it is complaining that the column 'name' contains a NULL value but only the corresponding GO batch fails. The next batch is executed and the table t2 is successfully created.
My understanding of the GO documentation is that it should not impact the T-SQL transactions but this is not the case in my example.
How can I make the whole transaction be rolled back if any of the GO batch fails?
ps: if I remove the GO statements, the transaction rollback is working as expected. But I do need those GO statements, in order to ensure that some parts of the script are executed before others.
Some errors roll back the transaction. Don't bother figuring out which ones, because there's no simple rule.
A multi-batch script should have a single error handler scope that rolls back the transaction on error, and commits at the end. In TSQL you can do this with dynamic sql, eg
BEGIN TRANSACTION
BEGIN TRY
EXEC('
-- Create a table with two nullable columns
CREATE TABLE [dbo].[t1](
[id] [nvarchar](36) NULL,
[name] [nvarchar](36) NULL
)
')
EXEC('
-- add one row having one NULL column
INSERT INTO [dbo].[t1] VALUES(NEWID(), NULL)
')
-- set one column as NOT NULLABLE
-- this fails because of the previous insert
EXEC('
ALTER TABLE [dbo].[t1] ALTER COLUMN [name] [nvarchar](36) NOT NULL
')
EXEC('
-- create a table as next action, so that we can test whether the rollback happened properly
CREATE TABLE [dbo].[t2](
[id] [nvarchar](36) NOT NULL
)
')
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK;
THROW;
END CATCH
With SQLCMD you can use the -b option to abort the script on error.

ROLLBACK make data in INSERTED table is removed in AFTER INSERT TRIGGER

I give an example to show my problem. I created a table like this:
CREATE TABLE a
(
id INT
)
I then created an AFTER INSERT trigger to not allow insert id = 1 into table a:
CREATE TRIGGER [dbo].[insert_a]
ON [dbo].[a] AFTER INSERT
AS
BEGIN
DECLARE #id INT
SELECT #id = id FROM inserted
IF #id = 1
BEGIN
RAISERROR('1', 12, 1)
ROLLBACK;
END
SELECT * FROM inserted
END
Then I insert id = 1 into table a:
INSERT INTO a VALUES(1)
I get nothing from INSERTED table.
I realize that when I ROLLBACK then + the data in table a was rolled back (I know) and data in INSERTED table is also removed. Why is that?
If I change the AFTER INSERT trigger to an INSTEAD OF INSERT trigger:
ALTER TRIGGER [dbo].[insert_a]
ON [dbo].[a] INSTEAD OF INSERT
AS
BEGIN
DECLARE #id INT
SELECT #id = id FROM inserted
IF #id = 1
BEGIN
RAISERROR('1', 12, 1)
ROLLBACK
END
SELECT * FROM inserted
END
INSERT INTO a VALUES(1)
Then I get the result:
id
1
That means data in INSERTED table is not removed though have been ROLLBACK.
Help me explain deeply what happens inside trigger?
This is the intended behaviour as far as I know. It's just that AFTER may be a bit misleading depending on how you look at it.
"The trigger and the statement that fires it are treated as a single transaction, which can be rolled back from within the trigger. If a severe error is detected, the entire transaction automatically rolls back.".
https://msdn.microsoft.com/en-us/library/ms178110.aspx

How to cope with IDENTITY_INSERT for an INSTEAD OF trigger on a table with an IDENTITY column?

I have a table with an IDENTITY column, and I have an INSTEAD OF trigger on this table. If IDENTITY_INSERT got turned on, I would want to insert the value being manually specified for the IDENTITY column.
Is there any way to properly cope with this scenario, such as detecting the value of IDENTITY_INSERT? From reading, it looks like detecting the current value of IDENTITY_INSERT for a specific table has been impossible in the past. I don't know if it's possible with newer versions of SQL Server.
Table creation SQL:
CREATE TABLE [TestTable]
(
[Id] INTEGER IDENTITY NOT NULL PRIMARY KEY,
[ExampleField] BIT NOT NULL DEFAULT(1)
)
This is what I've currently tried, but it seems rather wasteful as IDENTITY_INSERT is likely to be off for most of the time, meaning that it's always going to be failing on the first insert attempt, which seems wasteful, performance-wise.
CREATE TRIGGER [dbo].[trTestTable_ioi] ON [dbo].[TestTable] INSTEAD OF INSERT
AS
BEGIN
BEGIN TRY
INSERT INTO [TestTable]([Id],[ExampleField])
SELECT [Id], [ExampleField]
FROM [inserted]
END TRY
BEGIN CATCH
INSERT INTO [TestTable]([ExampleField])
SELECT [ExampleField]
FROM [inserted]
END CATCH
END
If your IDENTITY seed and increment is such that the generated value will always be non-zero (positive seed and increment or negative seed and increment), you can check for non-zero values in the virtual inserted table and use that value when present. This relies on my observation that the identity value is zero with an INSTEAD OF trigger and IDENTITY_INSERT OFF. However, I could not find this behavior specifically documented so you should vet in your environment and use at your own risk.
Example:
CREATE TRIGGER [dbo].[trTestTable_ioi] ON [dbo].[TestTable] INSTEAD OF INSERT
AS
SET NOCOUNT ON;
IF EXISTS(SELECT * FROM inserted WHERE ID <> 0)
BEGIN
--IDENTITY_INSERT is ON
INSERT INTO [TestTable]([Id],[ExampleField])
SELECT [Id], [ExampleField]
FROM [inserted];
END
ELSE
BEGIN
--IDENTITY_INSERT is OFF
INSERT INTO [TestTable]([ExampleField])
SELECT [ExampleField]
FROM [inserted];
END;
GO
SET IDENTITY_INSERT dbo.TestTable OFF;
GO
--identity value auto-assigned
INSERT INTO TestTable VALUES(1);
GO
SET IDENTITY_INSERT dbo.TestTable ON;
GO
--explict identity value specified
INSERT INTO TestTable(ID, ExampleField) VALUES(11, 1);
GO
SET IDENTITY_INSERT dbo.TestTable OFF;
GO
--fails as expected because identity value cannot be specified with IDENTITY_INSERT OFF
INSERT INTO TestTable(ID, ExampleField) VALUES(11, 1);
GO
SET IDENTITY_INSERT dbo.TestTable ON;
GO
--fails as expected because identity value must be specified with IDENTITY_INSERT ON
INSERT INTO TestTable VALUES(1);
GO
The documentation for set identity_insert states:
At any time, only one table in a session can have the IDENTITY_INSERT
property set to ON. If a table already has this property set to ON,
and a SET IDENTITY_INSERT ON statement is issued for another table,
SQL Server returns an error message that states SET IDENTITY_INSERT is
already ON and reports the table it is set ON for.
A terrible idea is to create a temporary table and try setting identity_insert on for it. If there is an error, catch the message and extract the table name.
NB: Any time you are catching and parsing an error message you are fiddling with some fragile code. You should run, not walk, away now.
create table #Placeholder ( Id Int Identity );
begin try
set identity_insert #Placeholder on;
end try
begin catch
-- Sample error: IDENTITY_INSERT is already ON for table 'Scratch.dbo.Foo'. Cannot perform SET operation for table '#Placeholder'.
declare #Message as NVarChar(2048) = Error_Message();
print #Message;
declare #Prefix as NVarChar(2048) = 'IDENTITY_INSERT is already ON for table ''';
declare #Suffix as NVarChar(2048) = '''. Cannot perform SET operation for table ''#Placeholder''.';
declare #TableName as NVarChar(2048) = NULL;
if ( Left( #Message, Len( #Prefix ) ) = #Prefix and Right( #Message, Len( #Suffix ) ) = #Suffix )
set #TableName = Substring( #Message, Len( #Prefix ) + 1, Len( #Message ) - Len( #Prefix ) - Len( #Suffix ) );
else
print 'Unexpected error!';
print #TableName;
end catch
drop table #Placeholder;
Then there are the performance implications of creating/dropping a temporary table in a trigger.

Continue inserting data in tables skipping duplicate data issue

set xact_abort off;
begin tran
DECLARE #error int
declare #SQL nvarchar(max)
set #SQL=N'';
select #SQL=some select query to fetch insert scripts
begin try
exec sp_executesql #SQL
commit
end try
begin catch
select #error=##Error
if #error=2627
begin
continue inserting data
end
if #error<>2627
begin
rollback
end
end catch
I am unable to continue inserting data when any duplicate data comes. Is there any alternative way to continue running SQL queries irrespective of duplicate data? I don not want to alter the index or table.
I am unable to continue inserting data when any duplicate data comes. Is there any alternative way to continue running sql queries irrespective of duplicate data. I dont want to alter the index or table.
What you can do is change the insert scripts as you call them, in this pseudo statement:
select #SQL=some select query to fetch insert scripts
Change the generation script: instead of generating INSERT INTO ... VALUES(...) statements, generate IF NOT EXISTS(...) INSERT INTO ... VALUES(...) statements
These insert statements should first check if a key already exists in the table. If your insert statements are of the form
INSERT INTO some_table(keycol1,...,keycolN,datacol1,...,datacolM)VALUES(keyval1,...,keyvalN,dataval1,...,datavalM);
You can rewrite those as:
IF NOT EXISTS(SELECT 1 FROM some_table WHERE keycol1=keyval1 AND ... AND keycolN=keyvalN)
INSERT INTO some_table(keycol1,...,keycolN,datacol1,...,datacolM)VALUES(keyval1,...,keyvalN,dataval1,...,datavalM);
Change the generation script: instead of generating INSERT INTO ... SELECT ..., generate INSERT INTO ... SELECT ... WHERE NOT EXISTS(...) statements
You can change these statements to only insert if the key does not exist in the table yet. Suppose your insert statements are of the form:
INSERT INTO some_table(keycol1,...,keycolN,datacol1,...,datacolN)
SELECT _keycol1,...,_keycolN,datacol1,...,datacolN
FROM <from_clause>;
You can rewrite those as:
INSERT INTO some_table(keycol1,...,keycolN,datacol1,...,datacolN)
SELECT _keycol1,...,_keycolN,datacol1,...,datacolN
FROM <from_clause>
WHERE NOT EXISTS(SELECT 1 FROM some_table WHERE keycol1=_keycol1 AND ... AND keycolN=_keycolN);
Replace the target table name in #SQL with a temporary table (a so-called staging table), then insert from the temporary table to the target table using WHERE NOT EXISTS(...)
This way you would not have to change the insert generation script. First create a temporary table that has the exact same structure as the target table (not including the primary key). Then replace all instances of the target table name in #SQL with the name of the temporary table. Run the #SQL and afterwards insert from the temporary table to the target table using a WHERE NOT EXISTS(...).
Suppose the target table is named some_table, with key columns key_col1,...,key_colN and data columns datacol1, ..., datacolM.
SELECT * INTO #staging_table FROM some_table WHERE 1=0; -- create staging table with same columns as some_table
SET #SQL=REPLACE(#SQL,'some_table','#staging_table');
EXEC sp_executesql #SQL;
INSERT INTO some_table(keycol1,...,keycolN,datacol1,...,datacolN)
SELECT st.keycol1,...,st.keycolN,st.datacol1,...,st.datacolN
FROM #staging_table AS st
WHERE NOT EXISTS(SELECT 1 FROM some_table WHERE keycol1=st.keycol1 AND ... AND keycolN=st.keycolN);
DROP TABLE #staging_table;

Null values INSERTED in trigger

I want to copy content of one table to another table in the same database.
For this I wrote trigger on source table which triggered on AFTER INSERT UPDATE, there are 2 uniqueidentifier fields in the table which generates values based on newid() as default binding. Based on this uniqueidentifier I am checking whether the record is present on the destination table or not if present then it will update and if not present then insert dataset into the table.
Problem is when i insert a new record the INSERTED in trigger give me NULL values for the uniqueidentifier fields.
In may case only one row is either update or insert so cursor is not used.
Below is my code, I am getting null values in #OriginalTable_MoveDataUID and #OriginalTable_ProcedureUID. Both the MoveDataUID and ProcedureUID are uniqueidentifier fileds.
Please share your thoughts or any alternative for this.
ALTER TRIGGER [dbo].[spec_ref_movedata_procedures_ToUpdate]
ON [dbo].[spec_ref_movedata_procedures]
AFTER INSERT, UPDATE
AS
BEGIN
SET XACT_ABORT ON
BEGIN DISTRIBUTED TRANSACTION
DECLARE #OriginalTable_MoveDataUID NVarchar (100)
DECLARE #OriginalTable_ProcedureUID NVarchar (100)
DECLARE #PresentInHistoryYesNo int
SELECT #OriginalTable_MoveDataUID= MoveDataUID,#OriginalTable_ProcedureUID=ProcedureUID FROM INSERTED
-- inserted for checking purpose
INSERT INTO ERP_Test_NK_spec_ref_movedata_procedures_history_2 (MovedataUID,ProcedureUID) VALUES
(#OriginalTable_MoveDataUID,#OriginalTable_ProcedureUID)
SELECT #PresentInHistoryYesNo = count(*) from spec_ref_movedata_procedures_history WHERE MoveDataUID=#OriginalTable_MoveDataUID AND ProcedureUID=#OriginalTable_ProcedureUID
IF #PresentInHistoryYesNo = 0
BEGIN
-- insert opertions
print 'insert record'
END
ELSE IF #PresentInHistoryYesNo = 1
BEGIN
-- update opertions
print 'update record'
END
COMMIT TRANSACTION
SET XACT_ABORT OFF
END
Instead of using variables, you could do this:
INSERT INTO ERP_Test_NK_spec_ref_movedata_procedures_history_2 (MovedataUID,ProcedureUID)
SELECT MoveDataUID,ProcedureUID FROM INSERTED

Resources