Create sql trigger dynamically and rollback if error - sql

I'm creating a stored procedure that will create 3 triggers (insert, update, delete) given a table name.
Here is an example to illustrate the issue I'm experiencing :
CREATE PROCEDURE [dbo].[sp_test]
AS
BEGIN
BEGIN TRAN
-- Create trigger 1
DECLARE #sql NVARCHAR(MAX) = 'CREATE TRIGGER test1 ON TableXML AFTER INSERT AS BEGIN END'
EXEC sp_executesql #sql
-- Create trigger 2, but this one will fail because Table_1 contain an ntext field.
SET #sql = 'CREATE TRIGGER test1 ON Table_1 AFTER INSERT AS
BEGIN
select * from inserted
END'
EXEC sp_executesql #sql
COMMIT TRAN
END
So I thought that wrapping the call in a transaction, the first trigger won't be created. Since the second will fail. BUT the first trigger is created anyway .... How can I prevent this from happening. I want the whole thing to be atomics.

Try this, with BEGIN TRY
CREATE PROCEDURE [dbo].[sp_test]
AS
BEGIN
BEGIN TRY
BEGIN TRAN
DECLARE #sql NVARCHAR(MAX) = 'CREATE TRIGGER test1 ON TableXML AFTER INSERT AS BEGIN END'
EXEC sp_executesql #sql
SET #sql = 'CREATE TRIGGER test1 ON Table_1 AFTER INSERT AS
BEGIN
select * from inserted
END'
EXEC sp_executesql #sql
COMMIT TRAN
END TRY
BEGIN CATCH
RAISERROR('Errormessage', 18, 1)
ROLLBACK TRAN
END CATCH
END

You have no error handling or rollback statement in your procedure.

In some databases, DDL statements such as CREATE TRIGGER will automatically commit themselves; if sql-server is one of them, you can't. (This is true of Oracle and MySQL; not true of RDB; not sure about sql-server.)

You don't have a Rollback call on an error.
Using SQL Server's Try/Catch, you could do something like what Vidar mentioned, or you if Sql Server automatically commits triggers (as Brian H mentioned as a posibility) you could instead have in your Catch block:
BEGIN CATCH
RAISERROR('Errormessage', 18, 1)
DROP Trigger test1
END CATCH

Related

Create SQL Server procedure in a transaction

I need to create two procedures in a SQL Server transaction. If failure, I need to rollback the create(s) and any other executed queries in this transaction. I know the create statement must be the first statement in query batch, but I need to know how handle the transaction with multiple batches.
BEGIN TRANSACTION
CREATE PROCEDURE [dbo].[SP_SP-1]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SQL statements
END
GO
CREATE PROCEDURE [dbo].[SP_SP-2]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SP-2 statements
END
GO
UPDATE Table
SET Value = '1.0.0.5'
COMMIT TRANSACTION / ROLLBACK TRANSACTION
Below is one method to execute multiple batches in a transaction. This uses a temp table to indicate if any batch erred and perform a final COMMIT or ROLLLBACK accordingly.
Another method is to encapsulate statements that must be in single-statement batch (CREATE PROCEDURE, CREATE VIEW, etc.) but that can get rather ugly when quotes within the literal text must be escaped.
CREATE TABLE #errors (error varchar(5));
GO
BEGIN TRANSACTION
GO
CREATE PROCEDURE [dbo].[USP_SP-1]
#id bigint
AS
BEGIN
SET NOCOUNT ON;
-- SP Statments
END;
GO
IF ##ERROR <> 0 INSERT INTO #errors VALUES('error');
GO
CREATE PROCEDURE [dbo].[USP_SP-2]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SP-2 Statments
END;
GO
IF ##ERROR <> 0 INSERT INTO #errors VALUES('error');
GO
UPDATE Table SET Value='1.0.0.5'
GO
IF ##ERROR <> 0 INSERT INTO #errors VALUES('error');
GO
IF EXISTS(SELECT 1 FROM #errors)
BEGIN
IF ##TRANCOUNT > 0 ROLLBACK;
END
ELSE
BEGIN
IF ##TRANCOUNT > 0 COMMIT;
END;
GO
IF OBJECT_ID(N'tempdb..#errors', 'U') IS NOT NULL
DROP TABLE #errors;
GO
I suggest you to study more about this subject in Handling Transactions in Nested SQL Server Stored Procedures.
From the beginning, your syntax is wrong. You cannot begin a transaction and then create a procedure, you need to do just the opposite:
CREATE PROCEDURE [dbo].[SP_SP-1]
#id bigint
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
SET NOCOUNT ON;
-- SP-2 Statments
Update Table set Value='1.0.0.5'
END TRY
BEGIN CATCH
--handle error and perform rollback
ROLLBACK
SELECT ERROR_NUMBER() AS ErrorNumber
SELECT ERROR_MESSAGE() AS ErrorMessage
END CATCH
END
It is best practice to use TRY and CATCH when attempting to perform update inside transaction scope.
Please read more and investigate using the link I provided to get a bigger picture.
To use Transaction, you need to know what is the meaning of transaction. It's meaning of 'Unit of work either in commit state or rollback state'.
So when you use transaction, you must know that where you declare and where you close. So you must start and end transaction in the parent procedure only than it will work as a unit of work i.e. whatever no of query execute of DML statement, it uses the same transaction.
I do not understand why your update statement outside of procedure and transaction portion too.
It should be (See my comments, you can use TRY Catch same as c sharp) :
Create PROCEDURE [dbo].[SP_SP-1]
#id bigint
AS
BEGIN
Begin Transaction
SET NOCOUNT ON;
-- SP Statments
Exec SP_SP-2 #id --here you can pass the parameter to another procedure, but do not use transaction in another procedure, other wise it will create another transaction
If ##Error > 0 than
Rollback
Else
Commit
End
END
GO
--Do not use transaction in another procedure, otherwise, it will create another transaction which has own rollback and commit and do not participate in the parent transaction
Create PROCEDURE [dbo].[SP_SP-2]
#id BIGINT
AS
BEGIN
SET NOCOUNT ON;
-- SP-2 Statments
END
GO
i find this solution to execute the procedure as string execution , it`s a workaround to execute what i want
Begin Try
Begin Transaction
EXEC ('
Create PROCEDURE [dbo].[SP_1]
#id bigint
AS
BEGIN
SET NOCOUNT ON;
SP-1
END
GO
Create PROCEDURE [dbo].[SP_Inc_Discovery_RunDoc]
#id bigint
AS
BEGIN
SET NOCOUNT ON;
Sp-2
END')
Update Table set Value='1.0.0.5'
Commit
End Try
Begin Catch
Rollback
Declare #Msg nvarchar(max)
Select #Msg=Error_Message();
RaisError('Error Occured: %s', 20, 101,#Msg) With Log;
End Catch

Managing transaction in stored procedure with Dynamic SQL

I have a stored procedure with Dynamic SQL. Is it possible to include a batch of dynamic SQL inside an explicit transaction with COMMIT or ROLLBACK depending on the value of ##ERROR?
Following similar stored procedure. It is simplified in order to demonstration purpose.
CREATE PROCEDURE [dbo].[sp_Example]
AS
BEGIN
BEGIN TRANSACTION
DECLARE #ID VARCHAR(10)
INSERT INTO [dbo].[Deparment] (Name,Location,PhoneNumber) VALUES ('DeparmentName','DeparmentLocation','0112232332')
SELECT #ID =SCOPE_IDENTITY()
IF ##ERROR <> 0
BEGIN
ROLLBACK
RAISERROR ('Error in Inserting Deparment.', 16, 1)
RETURN
END
SET #InsertQuery = '
DECLARE #Name varchar(100)
SELECT #Name = Name
FROM dbo.[Deparment]
WHERE DepartmentId= ''' + #ID +'''
INSERT INTO [dbo].[Employee](Name,Age,Salary,DepartmentName)VALUES(''EMPLOYEE NAME'',''25'',''200000'','''+#NAME'')''
EXEC(#InsertQuery)
IF ##ERROR <> 0
BEGIN
ROLLBACK
RAISERROR ('Error in Inserting Employee.', 16, 1)
RETURN
END
COMMIT
END
Does outer Transaction scope applies to Dynamic query ?
The "outer" transaction will apply to everything that is executed. There is no way in SQL Server to not execute under the running transaction (which can be annoying of you want to log errors).

"WHEN OTHERS THEN NULL" in SQL Server 2005

tracking_table is a log table declared as follows:
create table tracking_table (my_command nvarchar(500), my_date datetime);
Please suppose you have the following block of SQL SERVER 2005 code, declared within a SQL Server 2005 job:
DECLARE #my_statement NVARCHAR(500)
delete from tracking_table
SET #my_statement = 'ALTER INDEX ALL ON my_user.dbo.my_fact_table REBUILD WITH (FILLFACTOR = 90)'
insert into tracking_table values (#my_statement,getdate())
EXEC (#my_statement)
SET #my_statement = 'ALTER INDEX ALL ON my_user.dbo.my_second_table REBUILD WITH (FILLFACTOR = 90)'
insert into tracking_table (#my_statement,getdate())
EXEC (#my_statement)
At runtime, if the first statement (ALTER INDEX ALL ON my_user.dbo.my_fact_table REBUILD WITH (FILLFACTOR=90)) fails, the second statement which acts on my_second table WON'T be executed.
I would like to know how could I modify the SQL Server 2005 code, in order to skip any error, going forward (in Oracle I would say, WHEN OTHERS THEN NULL).
How could I achieve this?
Thank you in advance for your kind help.
I cannot advice to suppress errors, but if you really want to do it, I think you can try:
declare #my_statement nvarchar(500)
begin try
delete from tracking_table
end try
begin catch
print null // or errormessage
end catch
begin try
set #my_statement = 'ALTER INDEX ALL ON my_user.dbo.my_fact_table REBUILD WITH (FILLFACTOR = 90)'
insert into tracking_table values (#my_statement,getdate())
exec (#my_statement)
end try
begin catch
print null // or errormessage
end catch
begin try
set #my_statement = 'ALTER INDEX ALL ON my_user.dbo.my_second_table REBUILD WITH (FILLFACTOR = 90)'
insert into tracking_table (#my_statement,getdate())
exec (#my_statement)
end try
begin catch
print null // or errormessage
end catch
you can also create procedure
create procedure sp_executesql_Suppress_Errors
(
#stmt nvarchar(max)
)
as
begin
begin try
exec sp_executesql
#stmt = #stmt
end try
begin catch
print null // or errormessage
end catch
end
and then call it with your statements. I also advice you to use exec sp_executesql instead of exec (see Dynamic SQL - EXEC(#SQL) versus EXEC SP_EXECUTESQL(#SQL))

Can you detect INSERT-EXEC scenario's?

Is it possible to DETECT whether the current stored procedure is being called by an INSERT-EXEC statement?
Yes, I understand we may want to no longer use INSERT-EXEC statements...that is NOT the question I am asking.
The REASON I am using INSERT-EXEC is because i am hoping to promote re-use of stored procedures rather than re-writing the same SQL all the time.
Here's why I care:
Under the INSERT-EXEC scenario the original error message will get lost once a ROLLBACK is requested. As such, any records created will now be orphaned.
Example:
ALTER PROCEDURE [dbo].[spa_DoSomething]
(
#SomeKey INT,
#CreatedBy NVARCHAR(50)
)
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION
-- SQL runs and throws an error of some kind.
COMMIT TRAN
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0
ROLLBACK TRAN
-- If this procedure is called using an INSERT-EXEC
-- then the original error will be lost at this point because
-- "Cannot use the ROLLBACK statement within an INSERT-EXEC statement."
-- will come-up instead of the original error.
SET #ErrorMessage = ERROR_MESSAGE();
SET #ErrorSeverity = ERROR_SEVERITY();
SET #ErrorState = ERROR_STATE();
RAISERROR(#ErrorMessage, #ErrorSeverity, #ErrorState)
END CATCH
RETURN ##Error
END
I've come up with a bit of a kludge (ok, it's a big kludge), based on the fact that you can't have nested INSERT--EXECUTE... statements. Basically, if your "problem" procedure is the target of an INSERT--EXECUTE, and itself contains an INSERT--EXECUTE, then an error will be raised. To make this work, you'd have to have a (quite probably pointless) INSERT--EXECUTE call in the procedure, and wrap it in a TRY--CATCH block with appropriate handling. Awkward and obtuse, but if nothing else comes up it might be worth a try.
Use the following to test it out. This will create three procedures:
IF objectproperty(object_id('dbo.Foo1'), 'isProcedure') = 1
DROP PROCEDURE dbo.Foo1
IF objectproperty(object_id('dbo.Foo2'), 'isProcedure') = 1
DROP PROCEDURE dbo.Foo2
IF objectproperty(object_id('dbo.Foo3'), 'isProcedure') = 1
DROP PROCEDURE dbo.Foo3
GO
-- Returns a simple data set
CREATE PROCEDURE Foo1
AS
SET NOCOUNT on
SELECT name
from sys.databases
GO
-- Calls Foo1, loads data into a local temp table, then returns those contents
CREATE PROCEDURE Foo2
AS
SET NOCOUNT on
CREATE TABLE #Temp (DBName sysname not null)
BEGIN TRY
INSERT #Temp (DBName)
EXECUTE Foo1
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 8164
PRINT 'Nested INSERT EXECUTE'
ELSE
PRINT 'Unanticipated err: ' + cast(ERROR_NUMBER() as varchar(10))
END CATCH
SELECT *
from #Temp
GO
-- Calls Foo2, loads data into a local temp table, then returns those contents
CREATE PROCEDURE Foo3
AS
SET NOCOUNT on
CREATE TABLE #Temp2 (DBName sysname not null)
INSERT #Temp2 (DBName)
EXECUTE Foo2
SELECT *
from #Temp2
GO
EXECUTE Foo1 will return the "base" data set.
EXECUTE Foo2 will call Foo1, load the data into a temp table, and then return the contents of that table.
EXECUTE Foo3 attempts to do the same thing as Foo2, but it calls Foo2. This results in a nested INSERT--EXECUTE error, which is detected and handled by Foo2's TRY--CATCH.
Maybe ##NESTLEVEL can help:
http://msdn.microsoft.com/en-us/library/ms187371.aspx

MSSQL Prevent rollback when trigger fails

I have an after insert/update/delete trigger, which inserts a new record in an AuditTable every time an insert/update/delete is made to a specific table. If the insertion in the AuditTable fails I'd like the first record to be inserted anyway and the error logged in a further table "AuditErrors".
This is what I have so far and I tried many different things but I can't get this to work if the trigger insert into the AuditTable fails (I test this by misspelling the name of a column in the AuditTable insert). NB: #sql is the insert into the AuditTable.
DECLARE #TranCounter INT
SET #TranCounter = ##TRANCOUNT
IF #TranCounter > 0
SAVE TRANSACTION AuditInsert;
ELSE
BEGIN TRANSACTION;
BEGIN TRY
EXEC (#sql)
IF #TranCounter = 0
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
-- roll back
IF #TranCounter = 0
ROLLBACK TRANSACTION;
ELSE
IF XACT_STATE() <> -1
ROLLBACK TRANSACTION AuditInsert;
-- insert error into database
IF #TranCounter > 0
SAVE TRANSACTION AuditInsert;
ELSE
BEGIN TRANSACTION;
BEGIN TRY
INSERT INTO [dbo].[AuditErrors] ([AuditErrorCode], [AuditErrorMsg]) VALUES (ERROR_NUMBER(), ERROR_MESSAGE())
IF #TranCounter = 0
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
-- roll back
IF #TranCounter = 0
ROLLBACK TRANSACTION;
ELSE
IF XACT_STATE() <> -1
ROLLBACK TRANSACTION AuditInsert;
END CATCH
END CATCH
This is the only way I know of separating the original transaction from the trigger action. In this example the original insert completes even though the audit insert fails. Tested on 2008R2.
It's not pretty but it won't rollback the transaction!
It worked just fine with trusted authentication:
create table TestTable(
ID int identity(1,1) not null
,Info varchar(50) not null
)
GO
create table AuditTable(
AuditID int identity(1,1) not null
,TestTableID int not null
,Info varchar(10) -- The failure is the mismatch in length
)
GO
create procedure insertAudit #id int, #Info varchar(50)
as
set nocount on;
begin try
insert into AuditTable(TestTableID,Info)
values(#id,#Info);
end try
begin catch
select 0
end catch;
GO
create trigger trg_TestTable on TestTable
AFTER INSERT
as
begin
set nocount on;
declare #id int,
#info varchar(50),
#cmd varchar(500),
#rc int;
select #id=ID,#info=Info from inserted;
select #cmd = 'osql -S '+##SERVERNAME+' -E -d '+DB_NAME()+' -Q "exec insertAudit #id='+cast(#id as varchar(20))+',#Info='''+#info+'''"';
begin try
exec #rc=sys.xp_cmdshell #cmd
select #rc;
end try
begin catch
select 0;
end catch;
end
GO
Drop the Audit table and it still completes the original transaction.
Cheers!
Instead of using sqlcmd, you may consider playing with BEGIN TRAN/ROLLBACK a little bit.
Note that, even tho a rollback command will undo every change made since the start of the statement which caused the trigger to fire, any changes made by subsequent commands will not.
All you have to do is to repeat the execution of the code in #sql if the transaction in which data is inserted in the audit table gets rolled back:
TRIGGER BEGINS
<INSERT INSERTED AND DELETED TABLES INTO TABLE VARIABLES, U'LL NEED THEM>
BEGIN TRY
BEGIN TRAN
INSERT INTO AUDITTABLE SELECT * FROM #INSERTED
COMMIT
END TRY
BEGIN CATCH
ROLLBACK
REDO ORIGINAL INSERT/UPDATE/DELETE USING TRIGGER TABLE VARIABLES (#INSERTED AND #DELETED)
INSERT INTO AUDITERROS...
END CATCH
BEGIN TRAN -- THIS IS TO FOOL SQL INTO THINKING THERE'S STILL A TRANSACTION OPEN
TRIGGER ENDS