5

I was playing with dates using a C# program.

I want to filter on any table that has a DateTime, DateTime2, DateTimeOffset columns.

I store the LastRefreshDate as DateTimeOffSet in UTC and I use it to filter data on those tables. I adjust the offset (using NodaTime) of the LastRefreshDate based on the timezone used to store the dates in those tables. Usually, it is given by the user.

So I created a test sample to explain the problem. Usually, the SQL queries are dynamic and the parameters as well. Here is the sample code:

[TestMethod]
public void Test()
{
    using (SqlConnection connection = new SqlConnection("Server=myserver;Database=mydb;User ID=admin;Password=admin"))
    {
        connection.Open();
        using (SqlCommand command = new SqlCommand("SELECT [TimeStamp] FROM  [dbo].[DATA] WHERE [TimeStamp] >= @p0", connection))
        {
            string datestring = "2019-06-18 13:35:20.1133868 -04:00";

            // Does not work
            // DateTimeOffset p0 = DateTimeOffset.Parse(datestring, CultureInfo.InvariantCulture);
            // Does work
            DateTime p0 = DateTime.Parse(datestring, CultureInfo.InvariantCulture);
            command.Parameters.AddWithValue("@p0", p0);
            using (SqlDataReader reader = command.ExecuteReader())
            {
                var dataTable = new DataTable();
                dataTable.Load(reader);
                var result = dataTable.Rows.Count == 0;
            }
        }
    }
}

I created 2 SQL fiddles that demonstrate the issue. By the way, I ran the SQL Server Profiler and the generated queries are similar to the queries in the fiddles.

DateTime fiddle: http://sqlfiddle.com/#!18/a06be/1

declare @p0 datetime = '2019-06-18 13:35:20'
SELECT 
    [TimeStamp]
FROM 
    [dbo].[DATA]
WHERE 
    ([TimeStamp] >= @p0)

DateTimeOffSet fiddle: http://sqlfiddle.com/#!18/a06be/2

declare @p0 datetimeoffset(7) ='2019-06-18 13:35:20.1133868 -04:00'
SELECT [TimeStamp]
FROM 
    [dbo].[DATA] 
WHERE 
    ([TimeStamp] >= @p0 )

I did even more tests. By applying the cast directly, the SQL query works. It seems that SQL Server implicit conversion is not behaving in the same manner as an explicit cast. Here is the test case:

declare @p0 datetime
set @p0 = '2019-06-18 17:48:00.00'
declare @p1 datetimeoffset(7)
set @p1 = '2019-06-18 17:47:00.5385563 -04:00'

select 1 
where @p0 > cast(@p1 as datetime) -- working
--where @p0 > @p1                       -- not working
billybob
  • 2,859
  • 6
  • 35
  • 55

1 Answers1

3

A few things:

  • In SQL Server, if you use CAST, or CONVERT without specifying a style, the default style is 0, which when converting a datetimeoffset to either a datetime or datetime2 simply takes the date and time value from the datetimeoffset without considering the offset. If you want to take the offset into account, then use CONVERT and pass 1 for the style:

    DECLARE @p0 datetimeoffset = '2019-06-18 13:35:20.1133868 -04:00'
    SELECT convert(datetime, @p0, 0) as 'A', convert(datetime, @p0, 1) as 'B'
    -- A = 2019-06-18T13:35:20.113Z
    -- B = 2019-06-18T17:35:20.113Z
    
  • When querying a datetime or datetime2 field using a datetimeoffset parameter, the offset is indeed taken into account in the implicit conversion (it is like B above).

  • On the C# side, be careful about DateTime.Parse. By default, it emits a local time based value when an offset is provided. If you check, you'll see p0.Kind == DateTimeKind.Local. You could pass DateTimeStyles.AdjustToUniversal, but a better idea is to parse as a DateTimeOffset like you showed in your "doesn't work" code. But then instead of passing the full DateTimeOffset, pass the UtcDateTime property:

    DateTime p0 = DateTimeOffset.Parse(datestring, CultureInfo.InvariantCulture).UtcDateTime;
    
  • For both performance and stability reasons, you might consider using ParseExact or TryParseExact instead of Parse. Or, since you said you already are using Noda Time, you can use its text parsing features with an OffsetDateTimePattern. From there you'd either call .ToDateTimeOffset().UtcDateTime, or .ToInstant().ToDateTimeUtc().

  • Alternatively, you could just define your SQL database columns as datetimeoffset, then you can pass any DateTimeOffset parameter and it will be normalized to UTC when querying.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • Thanks a lot Matt for the clear explanation. It answers why the cast was passing. On the other hand, I used the C# example just to demonstrate the problem. What I really do in the backend, I store the LastRefreshDate in a DateTimeOffSet column. When the job kicks in, I fetch the LastRefreshDate in the code, I do the select of the table by using that LastRefreshDate date value. The user can specify what timezone the datetimes are stored (we can connect to any system). The problem that I have, is that we let the user track by any date column (DateTime, DateTime2, Date, DateTimeOffSet). – billybob Jun 20 '19 at 18:27
  • The problem is that since we can apply the incremental load on any system. Some systems do not respect the convention of storing the dates in UTC in their database. So I ask them the timezone so we can adjust the LastRefreshDate with their data. – billybob Jun 20 '19 at 18:31
  • Well, I can only respond to what you asked. The problem you just described in comments seems different, and I'm not sure there's enough detail there to understand what you're after. Different date/time column types will have different behaviors, so if you're trying to unify them in some way, you will likely run into problems. Maybe you need to test the schema of the table you're querying first? Hard to say without more detail. You might do well to ask a new question focusing on that part of it. Good luck. :) – Matt Johnson-Pint Jun 20 '19 at 19:10
  • 1
    It is exactly what I'm doing. Based, on the metadata of the table, I apply a different logic based on the type (DateTimeOffSet, DateTime2, etc). And btw, I loved your PluralSight course on how to handle datetimes properly in applications :) – billybob Jun 20 '19 at 20:28