Switch a LastChanged datetime to datetime2

For years I’ve been using a datetime column as a “row version” or “time stamp” for most of my SQL tables. The column definition in SQL looks like this:

LastChanged datetime NOT NULL DEFAULT (GetDate())

A trigger updates the LastChanged:

IF EXISTS (SELECT * FROM sys.triggers WHERE object_id = OBJECT_ID(N'dbo.OnExampleChanged'))
    DROP TRIGGER dbo.OnExampleChanged
GO
CREATE TRIGGER dbo.OnExampleChanged
            ON dbo.Example
         AFTER UPDATE
AS
SET NOCOUNT ON

UPDATE dbo.Example
   SET LastChanged = DEFAULT
 WHERE Id IN (SELECT Id FROM inserted)
GO

In LINQ-to-SQL, the LastChanged column needs to be set up as:

Auto Generated Value: True
Auto-Sync:            Always
Time Stamp:           True
Update Check:         Never

However, if you try this with Entity Framework, you may experience some random errors when performing an SQL update, where the row fails to update. Entity Framework uses datetime2 by default and this seems to be the cause of the problem, as they have different precisions.

So, what’s the easiest way to update all our LastChanged columns? Start with the following SQL:

SELECT t.name AS TableName
      ,c.name AS ColumnName
      ,y.name AS TypeName

  FROM sys.tables  t
  JOIN sys.columns c ON t.object_id      = c.object_id
  JOIN sys.types   y ON c.system_type_id = y.system_type_id

WHERE c.name = 'LastChanged'
  AND y.name = 'datetime'

ORDER BY t.name
        ,c.name

Save the results to a CSV file.

Use the following C# string format:

-- Convert {0}.{1} to datetime2
IF EXISTS (SELECT *
             FROM sys.tables  t
             JOIN sys.columns c ON t.object_id      = c.object_id
             JOIN sys.types   y ON c.system_type_id = y.system_type_id
            WHERE t.name = '{0}'
              AND c.name = '{1}'
              AND y.name = '{2}')
BEGIN
    DECLARE @Name varchar(128),
            @Cmd  varchar(500)

    SET @Name = (SELECT name
                   FROM sysobjects so JOIN sysconstraints sc ON so.id = sc.constid
                  WHERE OBJECT_NAME(so.parent_obj) = '{0}'
                    AND so.xtype                   = 'D'
                    AND sc.colid                   = (SELECT colid FROM syscolumns
                                                       WHERE id   = OBJECT_ID(N'dbo.{0}')
                                                         AND name = '{1}'))

    SET @Cmd = 'ALTER TABLE {0} DROP CONSTRAINT ' + @Name

    EXEC(@Cmd)

    ALTER TABLE dbo.{0} ALTER COLUMN {1} datetime2 NOT NULL

    ALTER TABLE dbo.{0} ADD DEFAULT (SysDateTime()) FOR {1}
END
GO

Note that we now use SysDateTime() instead of GetDate(). The existing triggers will still work because they call DEFAULT.