Skip to content

Commit

Permalink
SqlServer: Translate DateTimeOffset.ToUnixTime[Seconds|Milliseconds]
Browse files Browse the repository at this point in the history
Fixes #28925
  • Loading branch information
Marusyk authored and bricelam committed Mar 30, 2023
1 parent bbf4d95 commit c747489
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ public class SqlServerDateTimeMethodTranslator : IMethodCallTranslator
{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMilliseconds), new[] { typeof(double) })!, "millisecond" }
};

private static readonly Dictionary<MethodInfo, string> _methodInfoDateDiffMapping = new()
{
{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeSeconds), Type.EmptyTypes)!, "second" },
{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeMilliseconds), Type.EmptyTypes)!, "millisecond" }
};

private static readonly MethodInfo AtTimeZoneDateTimeOffsetMethodInfo = typeof(SqlServerDbFunctionsExtensions)
.GetRuntimeMethod(
nameof(SqlServerDbFunctionsExtensions.AtTimeZone), new[] { typeof(DbFunctions), typeof(DateTimeOffset), typeof(string) })!;
Expand All @@ -39,6 +45,8 @@ public class SqlServerDateTimeMethodTranslator : IMethodCallTranslator
.GetRuntimeMethod(
nameof(SqlServerDbFunctionsExtensions.AtTimeZone), new[] { typeof(DbFunctions), typeof(DateTime), typeof(string) })!;

private static readonly SqlConstantExpression UnixEpoch = new (Expression.Constant(DateTimeOffset.UnixEpoch), null);

private readonly ISqlExpressionFactory _sqlExpressionFactory;
private readonly IRelationalTypeMappingSource _typeMappingSource;

Expand Down Expand Up @@ -133,6 +141,17 @@ public SqlServerDateTimeMethodTranslator(
resultTypeMapping);
}

if (_methodInfoDateDiffMapping.TryGetValue(method, out var timePart))
{
return _sqlExpressionFactory.ApplyDefaultTypeMapping(
_sqlExpressionFactory.Function(
"DATEDIFF_BIG",
new[] { _sqlExpressionFactory.Fragment(timePart), UnixEpoch, instance! },
nullable: true,
argumentsPropagateNullability: new[] { false, false, true },
typeof(long)));
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class SqliteDateTimeAddTranslator : IMethodCallTranslator
public class SqliteDateTimeMethodTranslator : IMethodCallTranslator
{
private static readonly MethodInfo AddMilliseconds
= typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddMilliseconds), new[] { typeof(double) })!;

private static readonly MethodInfo AddTicks
= typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddTicks), new[] { typeof(long) })!;

private static readonly MethodInfo ToUnixTimeSeconds
= typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeSeconds), Type.EmptyTypes)!;

private static readonly MethodInfo ToUnixTimeMilliseconds
= typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeMilliseconds), Type.EmptyTypes)!;

private readonly Dictionary<MethodInfo, string> _methodInfoToUnitSuffix = new()
{
{ typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddYears), new[] { typeof(int) })!, " years" },
Expand All @@ -40,7 +46,7 @@ private static readonly MethodInfo AddTicks
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public SqliteDateTimeAddTranslator(SqliteSqlExpressionFactory sqlExpressionFactory)
public SqliteDateTimeMethodTranslator(SqliteSqlExpressionFactory sqlExpressionFactory)
{
_sqlExpressionFactory = sqlExpressionFactory;
}
Expand All @@ -60,7 +66,9 @@ public SqliteDateTimeAddTranslator(SqliteSqlExpressionFactory sqlExpressionFacto
? TranslateDateTime(instance, method, arguments)
: method.DeclaringType == typeof(DateOnly)
? TranslateDateOnly(instance, method, arguments)
: null;
: method.DeclaringType == typeof(DateTimeOffset)
? TranslateDateTimeOffset(instance, method, arguments)
: null;

private SqlExpression? TranslateDateTime(
SqlExpression? instance,
Expand Down Expand Up @@ -145,4 +153,40 @@ public SqliteDateTimeAddTranslator(SqliteSqlExpressionFactory sqlExpressionFacto

return null;
}

private SqlExpression? TranslateDateTimeOffset(
SqlExpression? instance,
MethodInfo method,
IReadOnlyList<SqlExpression> arguments)
{
if (ToUnixTimeSeconds.Equals(method))
{
return _sqlExpressionFactory.Function(
"unixepoch",
new[]
{
instance!
},
argumentsPropagateNullability: new[] { true, true },
nullable: true,
returnType: method.ReturnType);
}
else if (ToUnixTimeMilliseconds.Equals(method))
{
return _sqlExpressionFactory.Convert(
_sqlExpressionFactory.Multiply(
_sqlExpressionFactory.Subtract(
_sqlExpressionFactory.Function(
"julianday",
new[] { instance! },
nullable: true,
argumentsPropagateNullability: new[] { true },
typeof(double)),
_sqlExpressionFactory.Constant(2440587.5)), // NB: Result of julianday('1970-01-01 00:00:00')
_sqlExpressionFactory.Constant(TimeSpan.TicksPerDay)),
typeof(long));
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public SqliteMethodCallTranslatorProvider(RelationalMethodCallTranslatorProvider
{
new SqliteByteArrayMethodTranslator(sqlExpressionFactory),
new SqliteCharMethodTranslator(sqlExpressionFactory),
new SqliteDateTimeAddTranslator(sqlExpressionFactory),
new SqliteDateTimeMethodTranslator(sqlExpressionFactory),
new SqliteGlobMethodTranslator(sqlExpressionFactory),
new SqliteHexMethodTranslator(sqlExpressionFactory),
new SqliteMathTranslator(sqlExpressionFactory),
Expand Down
30 changes: 30 additions & 0 deletions test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8210,6 +8210,36 @@ public virtual Task Using_indexer_on_byte_array_and_string_in_projection(bool as
Assert.Equal(e.String, a.String);
});

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task DateTimeOffset_to_unix_time_milliseconds(bool async)
{
long unixEpochMilliseconds = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();

return AssertQuery(
async,
ss => ss.Set<Gear>()
.Include(g => g.Squad.Missions)
.Where(s => s.Squad.Missions
.Where(m => unixEpochMilliseconds == m.Mission.Timeline.ToUnixTimeMilliseconds())
.FirstOrDefault() == null));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task DateTimeOffset_to_unix_time_seconds(bool async)
{
long unixEpochSeconds = DateTimeOffset.UnixEpoch.ToUnixTimeSeconds();

return AssertQuery(
async,
ss => ss.Set<Gear>()
.Include(g => g.Squad.Missions)
.Where(s => s.Squad.Missions
.Where(m => unixEpochSeconds == m.Mission.Timeline.ToUnixTimeSeconds())
.FirstOrDefault() == null));
}

protected GearsOfWarContext CreateContext()
=> Fixture.CreateContext();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9978,6 +9978,44 @@ FROM [Squads] AS [s]
""");
}

public override async Task DateTimeOffset_to_unix_time_milliseconds(bool async)
{
await base.DateTimeOffset_to_unix_time_milliseconds(async);

AssertSql(
@"@__unixEpochMilliseconds_0='0'
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [s].[Id], [s].[Banner], [s].[Banner5], [s].[InternalNumber], [s].[Name], [s1].[SquadId], [s1].[MissionId]
FROM [Gears] AS [g]
INNER JOIN [Squads] AS [s] ON [g].[SquadId] = [s].[Id]
LEFT JOIN [SquadMissions] AS [s1] ON [s].[Id] = [s1].[SquadId]
WHERE NOT (EXISTS (
SELECT 1
FROM [SquadMissions] AS [s0]
INNER JOIN [Missions] AS [m] ON [s0].[MissionId] = [m].[Id]
WHERE [s].[Id] = [s0].[SquadId] AND @__unixEpochMilliseconds_0 = DATEDIFF_BIG(millisecond, '1970-01-01T00:00:00.0000000+00:00', [m].[Timeline])))
ORDER BY [g].[Nickname], [g].[SquadId], [s].[Id], [s1].[SquadId]");
}

public override async Task DateTimeOffset_to_unix_time_seconds(bool async)
{
await base.DateTimeOffset_to_unix_time_seconds(async);

AssertSql(
@"@__unixEpochSeconds_0='0'
SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], [s].[Id], [s].[Banner], [s].[Banner5], [s].[InternalNumber], [s].[Name], [s1].[SquadId], [s1].[MissionId]
FROM [Gears] AS [g]
INNER JOIN [Squads] AS [s] ON [g].[SquadId] = [s].[Id]
LEFT JOIN [SquadMissions] AS [s1] ON [s].[Id] = [s1].[SquadId]
WHERE NOT (EXISTS (
SELECT 1
FROM [SquadMissions] AS [s0]
INNER JOIN [Missions] AS [m] ON [s0].[MissionId] = [m].[Id]
WHERE [s].[Id] = [s0].[SquadId] AND @__unixEpochSeconds_0 = DATEDIFF_BIG(second, '1970-01-01T00:00:00.0000000+00:00', [m].[Timeline])))
ORDER BY [g].[Nickname], [g].[SquadId], [s].[Id], [s1].[SquadId]");
}

private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9396,6 +9396,44 @@ public override async Task Using_indexer_on_byte_array_and_string_in_projection(
""");
}

public override async Task DateTimeOffset_to_unix_time_milliseconds(bool async)
{
await base.DateTimeOffset_to_unix_time_milliseconds(async);

AssertSql(
@"@__unixEpochMilliseconds_0='0'
SELECT ""g"".""Nickname"", ""g"".""SquadId"", ""g"".""AssignedCityName"", ""g"".""CityOfBirthName"", ""g"".""Discriminator"", ""g"".""FullName"", ""g"".""HasSoulPatch"", ""g"".""LeaderNickname"", ""g"".""LeaderSquadId"", ""g"".""Rank"", ""s"".""Id"", ""s"".""Banner"", ""s"".""Banner5"", ""s"".""InternalNumber"", ""s"".""Name"", ""s1"".""SquadId"", ""s1"".""MissionId""
FROM ""Gears"" AS ""g""
INNER JOIN ""Squads"" AS ""s"" ON ""g"".""SquadId"" = ""s"".""Id""
LEFT JOIN ""SquadMissions"" AS ""s1"" ON ""s"".""Id"" = ""s1"".""SquadId""
WHERE NOT (EXISTS (
SELECT 1
FROM ""SquadMissions"" AS ""s0""
INNER JOIN ""Missions"" AS ""m"" ON ""s0"".""MissionId"" = ""m"".""Id""
WHERE ""s"".""Id"" = ""s0"".""SquadId"" AND @__unixEpochMilliseconds_0 = CAST(((julianday(""m"".""Timeline"") - 2440587.5) * 864000000000.0) AS INTEGER)))
ORDER BY ""g"".""Nickname"", ""g"".""SquadId"", ""s"".""Id"", ""s1"".""SquadId""");
}

public override async Task DateTimeOffset_to_unix_time_seconds(bool async)
{
await base.DateTimeOffset_to_unix_time_seconds(async);

AssertSql(
@"@__unixEpochSeconds_0='0'
SELECT ""g"".""Nickname"", ""g"".""SquadId"", ""g"".""AssignedCityName"", ""g"".""CityOfBirthName"", ""g"".""Discriminator"", ""g"".""FullName"", ""g"".""HasSoulPatch"", ""g"".""LeaderNickname"", ""g"".""LeaderSquadId"", ""g"".""Rank"", ""s"".""Id"", ""s"".""Banner"", ""s"".""Banner5"", ""s"".""InternalNumber"", ""s"".""Name"", ""s1"".""SquadId"", ""s1"".""MissionId""
FROM ""Gears"" AS ""g""
INNER JOIN ""Squads"" AS ""s"" ON ""g"".""SquadId"" = ""s"".""Id""
LEFT JOIN ""SquadMissions"" AS ""s1"" ON ""s"".""Id"" = ""s1"".""SquadId""
WHERE NOT (EXISTS (
SELECT 1
FROM ""SquadMissions"" AS ""s0""
INNER JOIN ""Missions"" AS ""m"" ON ""s0"".""MissionId"" = ""m"".""Id""
WHERE ""s"".""Id"" = ""s0"".""SquadId"" AND @__unixEpochSeconds_0 = unixepoch(""m"".""Timeline"")))
ORDER BY ""g"".""Nickname"", ""g"".""SquadId"", ""s"".""Id"", ""s1"".""SquadId""");
}

private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}

0 comments on commit c747489

Please sign in to comment.