My initial goal is to have an automatic backup file created for every routine before it is deleted.
To do this, I created the following trigger at the server level.
ALTER TRIGGER [trg_DDL_BackupOfRoutines]
ON ALL SERVER
FOR DROP_TRIGGER
, DROP_VIEW
, DROP_FUNCTION
, DROP_PROCEDURE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- prevent kill transaction when raiserror is fired
DECLARE @XACT_ABORT_WasOn BIT = 0
IF (@@OPTIONS & 16384) = 16384
BEGIN
SET @XACT_ABORT_WasOn = 1
SET XACT_ABORT OFF;
END
DECLARE @Event XML = EVENTDATA()
DECLARE @DatabaseName NVARCHAR(100)
, @SchemaName NVARCHAR(50)
, @ObjectName NVARCHAR(100)
, @ObjectType NVARCHAR(50)
BEGIN TRY
----------------------------------------------------------------------------------------------------
-- Getting event data
SELECT @DatabaseName = @Event.value('(/EVENT_INSTANCE/DatabaseName)[1]', 'NVARCHAR(100)')
SELECT @SchemaName = @Event.value('(/EVENT_INSTANCE/SchemaName)[1]', 'NVARCHAR(50)')
SELECT @ObjectName = @Event.value('(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(100)')
SELECT @ObjectType = @Event.value('(/EVENT_INSTANCE/ObjectType)[1]', 'NVARCHAR(50)')
DECLARE @ObjectFullName VARCHAR(255)
SET @ObjectFullName = CONCAT(@SchemaName, '.', @ObjectName)
----------------------------------------------------------------------------------------------------
-- validate if exists file
DECLARE @Path VARCHAR(255)
, @Date VARCHAR(10)
, @FileName VARCHAR(500)
SET @Path = CONCAT('L:\ObsoleteObjects\', @DatabaseName, '\')
SET @FileName = CONCAT(@Path, @SchemaName, '.bak_', @ObjectName, '_', FORMAT(GETDATE(), 'yyyyMMdd'), '.sql')
DECLARE @FileInfo TABLE (
FileExists BIT
, FileIsADirectory BIT
, ParentDirectoryExists BIT
)
INSERT INTO @FileInfo
EXEC master.dbo.xp_fileexist @FileName
IF EXISTS (SELECT * FROM @FileInfo WHERE FileExists = 1)
BEGIN
GOTO Finish
END
ELSE
BEGIN
ROLLBACK
END
----------------------------------------------------------------------------------------------------
-- validate if folder exists
IF EXISTS (SELECT * FROM @FileInfo WHERE ParentDirectoryExists = 0)
BEGIN
DECLARE @QueryFolder VARCHAR(1000)
SET @QueryFolder="MD " + @Path
EXEC xp_cmdshell @QueryFolder
END
----------------------------------------------------------------------------------------------------
-- Get object definition
DECLARE @DefinitionQuery NVARCHAR(MAX)
, @Definition NVARCHAR(MAX)
SET @DefinitionQuery = CONCAT('USE [', @DatabaseName, ']
SELECT @Definition = OBJECT_DEFINITION(OBJECT_ID(', CHAR(39), @ObjectFullName, CHAR(39), '))
')
EXEC sp_executesql @DefinitionQuery
, N'@Definition NVARCHAR(MAX) OUTPUT'
, @Definition = @Definition OUTPUT
----------------------------------------------------------------------------------------------------
-- Generate backup file
DECLARE @OLE INT
DECLARE @FileID INT
EXECUTE sp_OACreate 'Scripting.FileSystemObject', @OLE OUT
EXECUTE sp_OAMethod @OLE, 'OpenTextFile', @FileID OUT, @FileName, 8, 1
EXECUTE sp_OAMethod @FileID, 'WriteLine', Null, @Definition
EXECUTE sp_OADestroy @FileID
EXECUTE sp_OADestroy @OLE
----------------------------------------------------------------------------------------------------
-- Continue with the drop
DECLARE @DropQuery NVARCHAR(MAX)
SET @DropQuery = CONCAT('USE [', @DatabaseName, ']
DROP ', @ObjectType, '[', @SchemaName, '].[', @ObjectName, ']')
EXEC (@DropQuery)
----------------------------------------------------------------------------------------------------
-- Define output message
DECLARE @OutputMessage NVARCHAR(MAX) = CONCAT('
The object has been removed.
A backup of file was automatically generated, can you find it in the following path: ', @FileName, '
Please ignore the message:
"The transaction ended in the trigger. The batch has been aborted."
The error message is generated because the initial "DROP" statement is canceled to get the object definition and then the "DROP" statement is executed again.
')
PRINT @OutputMessage
Finish:
----------------------------------------------------------------------------------------------------
-- ROLLBACK OPTION TO ORIGINAL STATE
IF (@XACT_ABORT_WasOn = 1)
BEGIN
SET XACT_ABORT ON;
END
END TRY
BEGIN CATCH
ROLLBACK
DECLARE @Error NVARCHAR(MAX) = ERROR_MESSAGE()
----------------------------------------------------------------------------------------------------
-- ROLLBACK OPTION TO ORIGINAL STATE
IF (@XACT_ABORT_WasOn = 1)
BEGIN
SET XACT_ABORT ON;
END
----------------------------------------------------------------------------------------------------
-- Show error
RAISERROR(@Error, 16, 1)
END CATCH
END
GO
The code works well and creates the backup file automatically, the only annoying thing is that it marks that the transaction was aborted in the trigger.
Msg 3609, Level 16, State 2, Line 10
The transaction ended in the trigger. The batch has been aborted.
Is there a way to not show that message?
I’ll preface this by saying: this is absolutely the wrong solution for what you are trying to do. You should use a proper source control and backup system (backups include all object definitions).
The other problem with your use of a trigger and a rollback is that anyone doing DDL inside their own outer transaction is going to be in for a nasty surprise when you roll it back.
Your primary issue is that you are ending the transaction but not starting a new one. The system expects that there is still an active transaction at the end of the trigger, but you need to roll it back in order to get the definition. So instead start a new transaction. And you don’t need to mess around with catching any exceptions then.
Also:
- You can get the XML data in a single query. You should use
/text()
for better performance. - Use the newer
sys.dm_os_file_exists
function if you need it. - Ideally don’t write to the file system at all from T-SQL, instead just write to a table.
- You don’t need
USE
in the dynamic SQL. You can instead putCONCAT(@DatabaseName, '.sys.sp_executesql')
into a variable and doEXEC @proc
. - To redo the
DROP
just pull out the original command. - Use
sys.sql_modules
instead ofOBJECT_DEFINTIION
for better reliability and locking. - Object names should be in
sysname
type (a synonym fornvarchar(128)
) and file names should be innvarchar(260)
.
CREATE OR ALTER TRIGGER [trg_DDL_BackupOfRoutines]
ON ALL SERVER
FOR DROP_TRIGGER
, DROP_VIEW
, DROP_FUNCTION
, DROP_PROCEDURE
AS
SET NOCOUNT ON;
-- prevent recursion
IF @@TRIGGER_NESTLEVEL(OBJECT_ID('trg_DDL_BackupOfRoutines', 'AFTER', 'DDL')) > 1
RETURN;
DECLARE @Event XML = EVENTDATA();
DECLARE @DatabaseName sysname
, @SchemaName sysname
, @ObjectName sysname
, @Command nvarchar(max);
SELECT
@DatabaseName = EventInstance.value('(DatabaseName/text())[1]', 'sysname'),
@SchemaName = EventInstance.value('(SchemaName/text())[1]', 'sysname'),
@ObjectName = EventInstance.value('(ObjectName/text())[1]', 'sysname'),
@ObjectType = EventInstance.value('(ObjectType/text())[1]', 'NVARCHAR(50)'),
@Command = EventInstance.valu('(TSQLCommand/CommandText/text())[1]', 'nvarchar(max)')
FROM @Event.nodes('/EVENT_INSTANCE') x1(EventInstance);
----------------------------------------------------------------------------------------------------
-- Get object definition
ROLLBACK;
DECLARE @proc NVARCHAR(1000) = CONCAT(@DatabaseName, '.sys.sp_executesql');
EXEC @proc
N'
INSERT master.sys.DefinitionBackup
(SchemaName, ObjectName, Definition)
SELECT @SchemaName, @ObjectName, m.definition
FROM sys.objects o
JOIN sys.schemas s ON s.schema_id = o.schema_id
JOIN sys.sql_modules m ON m.object_id = o.object_id
WHERE s.name = @SchemaName
AND o.name = @ObjectName;
'
, N'@ObjectName sysname,
@SchemaName sysname',
, @ObjectName = @ObjectName
, @SchemaName = @SchemaName;
----------------------------------------------------------------------------------------------------
-- Begin a new transaction and continue with the drop
BEGIN TRAN;
EXEC @proc @Command;
----------------------------------------------------------------------------------------------------
-- Define output message
DECLARE @OutputMessage NVARCHAR(MAX) = CONCAT('
The object has been removed.
A backup of file was automatically generated, can you find it in the following path: ', @FileName, '
Please ignore the message:
"The transaction ended in the trigger. The batch has been aborted."
The error message is generated because the initial "DROP" statement is canceled to get the object definition and then the "DROP" statement is executed again.
');
PRINT @OutputMessage;
By not doing things the way you are. Don’t
ROLLBACK
in theTRIGGER
, insteadTHROW
the appropriate error (with a relevant severity); let the outer scope handle the rollback.I rollback the transaction because I didn’t find another way to get the definition of the object, otherwise it won’t find the object.
How does
ROLLBACK
give you th definition of an object?I think the reason why it does not find the definition is because the server event is executed a little after the DROP, by the time I do the ROLLBACK the definition is already available again.
@ThomA Careful reading of the (rather poor) code indicates that OP is rolling back the
DROP
in order to grab the definition and save it to a file, then redo theDROP
. So they need to roll back the transaction but prevent the trigger/rollback error. The solution might be to start a new transaction dbfiddle.uk/gRjY-jX4 although the real answer is to use source control and backups.Show 2 more comments