Is there a way to pass information onto the Delete trigger such that it could know who deleted the record?
Yes: by using a very cool (and under utilized feature) called CONTEXT_INFO
. It is essentially session memory that exists in all scopes and is not bound by transactions. It can be used to pass info (any info--well, any that fits into the limited space) to triggers as well as back and forth between sub-proc / EXEC calls. And I have used it before for this exact same situation.
Test with the following to see how it works. Notice that I am converting to CHAR(128)
before the CONVERT(VARBINARY(128), ..
. This is to force blank-padding to make it easier to convert back to VARCHAR
when getting it out of CONTEXT_INFO()
since VARBINARY(128)
is right-padded with 0x00
s.
SELECT CONTEXT_INFO();
-- Initially = NULL
DECLARE @EncodedUser VARBINARY(128);
SET @EncodedUser = CONVERT(VARBINARY(128),
CONVERT(CHAR(128), 'I deleted ALL your records! HA HA!')
);
SET CONTEXT_INFO @EncodedUser;
SELECT CONTEXT_INFO() AS [RawContextInfo],
RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())) AS [DecodedUser];
Results:
0x492064656C6574656420414C4C20796F7572207265636F7264732120484120484121202020202020...
I deleted ALL your records! HA HA!
PUTTING IT ALL TOGETHER:
The app should call a "Delete" stored procedure that passes in the UserName (or whatever) that is deleting the record. I assume this is already the model being used since it sounds like you are already tracking Insert and Update operations.
The "Delete" stored procedure does:
DECLARE @EncodedUser VARBINARY(128);
SET @EncodedUser = CONVERT(VARBINARY(128),
CONVERT(CHAR(128), @UserName)
);
SET CONTEXT_INFO @EncodedUser;
-- DELETE STUFF HERE
The audit trigger does:
-- Set the INT value in LEFT (currently 50) to the max size of [UserWhoMadeChanges]
INSERT INTO AuditTable (IdOfRecordedAffected, UserWhoMadeChanges)
SELECT del.ID, COALESCE(
LEFT(RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())), 50),
'<unknown>')
FROM DELETED del;
Please note that, as @SeanGallardy pointed out in a comment, due to other procedures and/or ad hoc queries deleting records from this table, it is possible that either:
CONTEXT_INFO
has not been set and is still NULL
:
For this reason I have updated the above INSERT INTO AuditTable
to use a COALESCE
to default the value. Or, if you don't want a default and require a name, then you could do something similar to:
DECLARE @UserName VARCHAR(50); -- set to the size of AuditTable.[UserWhoMadeChanges]
SET @UserName = LEFT(RTRIM(CONVERT(VARCHAR(128), CONTEXT_INFO())), 50);
IF (@UserName IS NULL)
BEGIN
ROLLBACK TRAN; -- cancel the DELETE operation
RAISERROR('Please set UserName via "SET CONTEXT_INFO.." and try again.', 16 ,1);
END;
-- use @UserName in the INSERT...SELECT
CONTEXT_INFO
has been set to a value that is not a valid UserName, and hence might exceed the size of the AuditTable.[UserWhoMadeChanges]
field:
For this reason I added a LEFT
function to ensure that whatever is grabbed out of CONTEXT_INFO
will not break the INSERT
. As noted in the code, you just need to set the 50
to the actual size of the UserWhoMadeChanges
field.
UPDATE FOR SQL SERVER 2016 AND NEWER
SQL Server 2016 added an improved version of this per-session memory: Session Context. The new Session Context is essentially a hash table of Key-Value pairs with the "Key" being of type sysname
(i.e. NVARCHAR(128)
) and the "Value" being SQL_VARIANT
. Meaning:
- There is now a separation of values so less likely to conflict with other uses
- You can store various types, no longer needing to worry about the odd behavior when getting the value back out via
CONTEXT_INFO()
(for details, please see my post: Why Doesn’t CONTEXT_INFO() Return the Exact Value Set by SET CONTEXT_INFO?)
- You get a lot more space: 8000 bytes max per "Value", up to 256kb total across all keys (compared to the 128 bytes max of
CONTEXT_INFO
)
For details, please see the following documentation pages:
Triggers run, by default, under the security context of the principal who caused the trigger to fire.
In order to change this behavior, you'll need to create the trigger using the WITH EXECUTE AS OWNER
clause.
Below is an example which shows how that works. WITH EXECUTE AS OWNER
allows the trigger to run in the security context of the database owner, instead of the principal who is updating the table.
First, we create a test table:
USE tempdb;
IF OBJECT_ID('dbo.t') IS NOT NULL
BEGIN
DROP TRIGGER t_trig;
DROP TABLE dbo.t;
END
GO
CREATE TABLE dbo.t
(
ID INT NOT NULL
, ID2 INT NULL
);
GO
Here's the trigger code, with EXECUTE AS OWNER
:
CREATE TRIGGER t_trig ON dbo.t
WITH EXECUTE AS OWNER
AFTER INSERT
AS
BEGIN
UPDATE dbo.t
SET ID2 = ID
WHERE EXISTS (SELECT 1 FROM inserted i WHERE i.ID = dbo.t.ID);
END
GO
Now, we'll create a test login with low privileges that can be used to test the hypothesis that EXECUTE AS OWNER
allows an under-privileged principal access to functionality they otherwise would not have:
CREATE LOGIN tLogin WITH PASSWORD = 'QWERFsdf23454%';
CREATE USER tLogin FROM LOGIN tLogin WITH DEFAULT_SCHEMA = dbo;
We'll give them the ability to insert rows into dbo.T
, but prevent them from running UPDATE
statements:
GRANT INSERT ON dbo.t TO tLogin;
DENY UPDATE ON dbo.t TO tLogin;
Here we do the test:
EXECUTE AS USER = 'tLogin';
/*
Output here shows we're running under the tLogin
security context
*/
SELECT SUSER_SNAME();
/*
This will fail, with insufficient privileges
since we've DENY'd the UPDATE privilege to tLogin.
*/
UPDATE dbo.t SET ID2 = ID;
/*
this will run the UPDATE since the trigger
has EXECUTE AS OWNER
*/
INSERT INTO dbo.t(ID) VALUES (1);
/*
This takes us out of the tLogin security context
*/
REVERT
Here we can see the row in dbo.T
has the changes made by the trigger:
SELECT *
FROM dbo.t;
And here, we cleanup the low-privilege user:
DROP USER tLogin;
DROP LOGIN tLogin;
The output from running the above is:
Best Answer
As @AaronBertrand pointed out in his comment, objects are attached to other objects by object_id, not by object name. Having said that, there is a way to move data from one table to another without the need to use
sp_rename
.Since example code is the cleanest way to show how something works, I've created the following test-bed code to illustrate how to use
ALTER TABLE ... SWITCH
syntax to enable data migration.First, we'll create an "existing"
Product
andProductAudit
table, with a trigger on theProduct
table, that inserts rows into theProductAudit
table:Here, we'll insert 100,000 rows into the
Product
table:Results of the
SELECT
queries above:This will create a "temporary" table to hold the rows we want to retain:
This code "migrates" the data from the existing table into the new table, retaining the triggers present on the original table, without recreating them. The
ALTER TABLE ... SWITCH
syntax is a meta-data-only operation that is extremely quick, making this a great way to migrate data. The code below sets the transaction isolation level to lock the tables involved to prevent other processes inserting or updating data in any of the involved tables.Results:
Here, we'll insert another 10 rows into the
dbo.Product
table to prove the trigger still works:Results: