…With alliteration.
Hey there and thanks for reading. Today I wanted to talk about a few recent additions I made to one of the SQL schemas I run! I was tired of manually adding a LastWriteTime and LastModifiedBy column to each table as they were created in the database, so naturally, I wanted to automate it.
This is a two part process, and it might be a little hacky but it works well enough in development and staging databases. I’ll need to see what the overhead is like in production, but I don’t anticipate any issues.
First up, we need a stored procedure to add 2 columns to a table. Nice and easy:
USE Development;
GO
ALTER PROCEDURE AddAuditColumns
@TableName VARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @sql VARCHAR(MAX)
BEGIN
SET @sql = 'ALTER TABLE ' + @TableName + 'ADD LastWriteTime smalldatetime NOT NULL DEFAULT GETDATE()'
-- Change to NOW() for MySQL instead of SQLServer
EXEC(@sql)
SET @sql = 'ALTER TABLE ' + @TableName + 'ADD LastModifiedBy VARCHAR(60) NULL'
-- For purposes of the demo i'll allow null values
EXEC(@sql)
END
END
All of the code in this demo is available on my github page.
Cool, so that’s the easy part, you can execute that by passing any table in your schema to it and it’ll add the columns. LastWriteTime will have a default value of the time of execution or row addition. LastModifiedBy has no default due to how we use it, but you could set it to a value if you so choose.
The next part was a bit more challenging to figure out, I needed to parse event data from the CREATE_TABLE event and determine how to get the name parameter passed to it.
Event data is captured in a trigger by assigning EVENTDATA() to an XML variable. The data comes in a pretty easy to consume XML structure (i’ve formatted this one to make it look pretty, it’s normally displayed without whitespace or linebreaks).
The below event data is captured from the simple line:
CREATE TABLE MyTable (Integer int);
<EVENT_INSTANCE>
<EventType>CREATE_TABLE</EventType>
<PostTime>2015-09-22T15:40:15.827</PostTime>
<SPID>53</SPID>
<ServerName>SQLDEV01</ServerName>
<LoginName>Domain\Colin</LoginName>
<UserName>dbo</UserName>
<DatabaseName>Development</DatabaseName>
<SchemaName>dbo</SchemaName>
<ObjectName>MyTable</ObjectName>
<ObjectType>TABLE</ObjectType>
<TSQLCommand>
<SetOptions ANSI_NULLS="ON" ANSI_NULL_DEFAULT="ON" ANSI_PADDING=" ON" QUOTED_IDENTIFIER="ON" ENCRYPTED="FALSE" />
<CommandText>CREATE TABLE MyTable (Integer int);</CommandText>
</TSQLCommand>
</EVENT_INSTANCE>
As you can see, we have a lot to work with in this data set. In this example, we’re going to be particularly interested in the <ObjectName> object, since that contains the table name that we’ll want to pass to our stored procedure. Let’s create a trigger to do just that!
Use Development;
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TRIGGER [TRIG_ADD_AUDIT_ON_TABLE_CREATE] ON DATABASE
FOR CREATE_TABLE
AS
BEGIN
DECLARE @data XML
DECLARE @tablename varchar(max)
SET @data = EVENTDATA()
SET @tablename = CONVERT(varchar(max), @data.value('(/EVENT_INSTANCE/ObjectName)[1]','nvarchar(100)'))
EXEC AddAuditColumns(@tablename)
END
GO
SET ANSI_NULLS OFF
GO
SET QUOTED_IDENTIFIER OFF
GO
ENABLE TRIGGER [TRIG_ADD_AUDIT_ON_TABLE_CREATE] ON DATABASE
GO
That trigger above will snag the EVENTDATA() from the CREATE_TABLE event and add the columns defined in the stored procedure. This is a very extensible way to add columns on create and the best I could come up with on the fly.
Worth noting! If you want to enable this audit creation on the entire schema, you’ll need to modify the Trigger create a little bit, change:
ON DATABASE
to
ON SCHEMA
If there’s feedback or issues with the above scripts i’d love to see some pull requests on Github or just leave a comment.