UPDATE modified date trigger only if modified date is not supplied

sql-server, triggers

Solution

You can't use any of these methods that pretend that `inserted` could only ever contain one row (what if I update more than one test in a single statement?). Instead, you need to perform a join-based update, and you can just use `COALESCE()` to determine whether a different value was supplied for that column (when it hasn't, replace it with `GETUTCDATE()`). Here is the approach I prefer:

CREATE TRIGGER  [dbo].[ModifiedDateUpdateTrigger]
ON [dbo].[Tests]
FOR UPDATE
AS 
BEGIN
  SET NOCOUNT ON;

  UPDATE t
    SET ModifiedDate = COALESCE(NULLIF(i.ModifiedDate,d.ModifiedDate), GETUTCDATE())
    FROM dbo.Tests AS t
    INNER JOIN inserted AS i
     ON t.TestID = i.TestID
    INNER JOIN deleted AS d
     ON t.TestID = d.TestID;
END
GO

The reason for the extra complication is this. Consider a simple table:

CREATE TABLE dbo.Tests(TestID INT, ModifiedDate DATETIME, x VARCHAR(1));

Now, two rows:

INSERT dbo.Tests(TestID) SELECT 1,GETDATE() UNION ALL SELECT 2,GETDATE();

We want to update the `ModifiedDate` column in this scenario:

UPDATE dbo.Tests SET x = 'y';

But also in this scenario:

UPDATE dbo.Tests SET x = 'y', ModifiedDate = CASE
  WHEN TestID = 1 THEN '19000101' ELSE ModifiedDate END
  WHERE TestID IN (1,2);

Both of these are handled adequately as multi-row updates from the trigger, but the latter might be handled incorrectly if you just assume that a value supplied in `inserted` is a new one. You need to compare it to `deleted` to ensure that it has actually changed. The only thing this makes it hard to do? Set the value explicitly to `NULL` (well, without disabling the trigger). :-) In this case it will actually replace the `NULL` you passed with `GETUTCDATE()`. Not that you'd ever want to do that, but I didn't want to leave it unsaid.

update

In the comments you mentioned one case where you want to prevent the ModifiedDate from getting "bumped" if you hard-coded the same ModifiedDate value in subsequent UPDATE statements. I couldn't find a way of differentiating in an after trigger (since the table has already been changed, and at that point it's impossible to tell if that was from the current update or a previous one), but I did find a way to differentiate in an instead of trigger. So if you can change to an instead of trigger, you can do this - the only additional complication is that you have to also account for any other columns that might not be mentioned explicitly in the update statement or that you want to handle differently depending on before/after values. Here is the trigger:

CREATE TRIGGER  [dbo].[ModifiedDateUpdateTrigger]
ON [dbo].[Tests]
INSTEAD OF UPDATE
AS 
BEGIN
  SET NOCOUNT ON;

  UPDATE src 
    SET src.Name = i.Name, /* other columns that might get updated */
      src.ModifiedDate = CASE 
        WHEN i.ModifiedDate <> src.ModifiedDate THEN CASE 
          WHEN UPDATE(ModifiedDate) THEN i.ModifiedDate ELSE GETUTCDATE() END
        WHEN i.ModifiedDate = src.ModifiedDate THEN CASE 
          WHEN NOT UPDATE(ModifiedDate) THEN GETUTCDATE() ELSE src.ModifiedDate END
        ELSE GETUTCDATE() 
      END
    FROM dbo.Tests AS src
    INNER JOIN inserted AS i
    ON i.TestID = src.TestID
    INNER JOIN deleted AS d
    ON i.TestID = d.TestID;
END
GO

And here is a demo: http://sqlfiddle.com/#!3/4a00b5/1

I'm not 100% confident that the `ELSE` condition is required, but exhausted all the time I could invest in testing for today.

Problem

I have some tables that have "Created" and "Modified" dates on them. I'd like to be able to update the latter ONLY if the modified date was not supplied by the application layer. We need to have the application layer be able to set the modified date to a specific value in the event that an offline transaction (such as a device with no internet connection syncs up at a later date) occurs, but we can't always ensure that ever application developer will always remember to set the modified date in the application code. Given the following table: ``` CREATE TABLE [dbo].[Tests]( [TestID] [int] IDENTITY(1,1) NOT NULL, [Name] [nvarchar](50) NOT NULL, [CreatedDate] [datetime2](7) NOT NULL, [ModifiedDate] [datetime2](7) NOT NULL, CONSTRAINT [PK_Tests] PRIMARY KEY CLUSTERED ( [TestID] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO ALTER TABLE [dbo].[Tests] ADD CONSTRAINT [DF_Tests_CreatedDate] DEFAULT (getutcdate()) FOR [CreatedDate] GO ALTER TABLE [dbo].[Tests] ADD CONSTRAINT [DF_Tests_ModifiedDate] DEFAULT (getutcdate()) FOR [ModifiedDate] GO ``` It would seem that when no "modified date" is supplied in the update, instead of either updating or throwing an exception, simply stays the same. Example Test Cases: Generated LINQ-to-Entities code: ``` exec sp_executesql N'update [dbo].[Tests] set [Name] = @0 where ([TestID] = @1) ',N'@0 nvarchar(50),@1 int',@0=N'Test_635267931494843908',@1=3 ``` should update modified date to GETUTCDATE() SQL code: ``` UPDATE Tests SET Name = 'Test' + CONVERT(nvarchar(255), NEWID()) WHERE TestID =3 ``` should update modified date to GETUTCDATE() SQL Code with modified date: ``` UPDATE Tests SET Name = 'Test' + CONVERT(nvarchar(255), NEWID()), ModifiedDate = '2014-01-31 19:10:48' WHERE TestID =3 ``` should update modified date to '2014-01-31 19:10:48' I have tried the following: ``` CREATE TRIGGER [dbo].[ModifiedDateUpdateTrigger] ON [dbo].[Tests] AFTER UPDATE AS BEGIN UPDATE dbo.Tests SET ModifiedDate = GETUTCDATE() FROM dbo.Tests as t INNER JOIN inserted as i on i.TestID = t.TestID INNER JOIN deleted as d on d.TestID = t.TestID WHERE t.ModifiedDate <> i.ModifiedDate AND d.ModifiedDate <> i.ModifiedDate END ``` and ``` CREATE TRIGGER [dbo].[ModifiedDateUpdateTrigger] ON [dbo].[Tests] FOR UPDATE AS BEGIN DECLARE @m datetime2(7) = (select MODIFIEDDATE FROM inserted) print @m IF (select MODIFIEDDATE FROM inserted) IS NULL BEGIN RETURN END UPDATE dbo.Tests SET ModifiedDate = GETUTCDATE() FROM INSERTED i WHERE i.TestID = Tests.TestID END ``` and finally: ``` CREATE TRIGGER [dbo].[ModifiedDateUpdateTrigger] ON [dbo].[Tests] FOR UPDATE AS BEGIN DECLARE @m datetime2(7) = (select MODIFIEDDATE FROM inserted) print @m IF (select MODIFIEDDATE FROM inserted) <> (SELECT i.ModifiedDate FROM Tests as t INNER JOIN inserted as i on t.TestID = i.TestID) BEGIN RETURN END UPDATE dbo.Tests SET ModifiedDate = GETUTCDATE() FROM INSERTED i WHERE i.TestID = Tests.TestID END ``` None of these work for all the desire test case.

Original source