Skip to content

Commit c747489

Browse files
Marusykbricelam
authored andcommitted
SqlServer: Translate DateTimeOffset.ToUnixTime[Seconds|Milliseconds]
Fixes #28925
1 parent bbf4d95 commit c747489

File tree

6 files changed

+173
-4
lines changed

6 files changed

+173
-4
lines changed

src/EFCore.SqlServer/Query/Internal/SqlServerDateTimeMethodTranslator.cs

+19
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ public class SqlServerDateTimeMethodTranslator : IMethodCallTranslator
3131
{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.AddMilliseconds), new[] { typeof(double) })!, "millisecond" }
3232
};
3333

34+
private static readonly Dictionary<MethodInfo, string> _methodInfoDateDiffMapping = new()
35+
{
36+
{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeSeconds), Type.EmptyTypes)!, "second" },
37+
{ typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeMilliseconds), Type.EmptyTypes)!, "millisecond" }
38+
};
39+
3440
private static readonly MethodInfo AtTimeZoneDateTimeOffsetMethodInfo = typeof(SqlServerDbFunctionsExtensions)
3541
.GetRuntimeMethod(
3642
nameof(SqlServerDbFunctionsExtensions.AtTimeZone), new[] { typeof(DbFunctions), typeof(DateTimeOffset), typeof(string) })!;
@@ -39,6 +45,8 @@ public class SqlServerDateTimeMethodTranslator : IMethodCallTranslator
3945
.GetRuntimeMethod(
4046
nameof(SqlServerDbFunctionsExtensions.AtTimeZone), new[] { typeof(DbFunctions), typeof(DateTime), typeof(string) })!;
4147

48+
private static readonly SqlConstantExpression UnixEpoch = new (Expression.Constant(DateTimeOffset.UnixEpoch), null);
49+
4250
private readonly ISqlExpressionFactory _sqlExpressionFactory;
4351
private readonly IRelationalTypeMappingSource _typeMappingSource;
4452

@@ -133,6 +141,17 @@ public SqlServerDateTimeMethodTranslator(
133141
resultTypeMapping);
134142
}
135143

144+
if (_methodInfoDateDiffMapping.TryGetValue(method, out var timePart))
145+
{
146+
return _sqlExpressionFactory.ApplyDefaultTypeMapping(
147+
_sqlExpressionFactory.Function(
148+
"DATEDIFF_BIG",
149+
new[] { _sqlExpressionFactory.Fragment(timePart), UnixEpoch, instance! },
150+
nullable: true,
151+
argumentsPropagateNullability: new[] { false, false, true },
152+
typeof(long)));
153+
}
154+
136155
return null;
137156
}
138157
}

src/EFCore.Sqlite.Core/Query/Internal/SqliteDateTimeAddTranslator.cs renamed to src/EFCore.Sqlite.Core/Query/Internal/SqliteDateTimeMethodTranslator.cs

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

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

22+
private static readonly MethodInfo ToUnixTimeSeconds
23+
= typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeSeconds), Type.EmptyTypes)!;
24+
25+
private static readonly MethodInfo ToUnixTimeMilliseconds
26+
= typeof(DateTimeOffset).GetRuntimeMethod(nameof(DateTimeOffset.ToUnixTimeMilliseconds), Type.EmptyTypes)!;
27+
2228
private readonly Dictionary<MethodInfo, string> _methodInfoToUnitSuffix = new()
2329
{
2430
{ typeof(DateTime).GetRuntimeMethod(nameof(DateTime.AddYears), new[] { typeof(int) })!, " years" },
@@ -40,7 +46,7 @@ private static readonly MethodInfo AddTicks
4046
/// any release. You should only use it directly in your code with extreme caution and knowing that
4147
/// doing so can result in application failures when updating to a new Entity Framework Core release.
4248
/// </summary>
43-
public SqliteDateTimeAddTranslator(SqliteSqlExpressionFactory sqlExpressionFactory)
49+
public SqliteDateTimeMethodTranslator(SqliteSqlExpressionFactory sqlExpressionFactory)
4450
{
4551
_sqlExpressionFactory = sqlExpressionFactory;
4652
}
@@ -60,7 +66,9 @@ public SqliteDateTimeAddTranslator(SqliteSqlExpressionFactory sqlExpressionFacto
6066
? TranslateDateTime(instance, method, arguments)
6167
: method.DeclaringType == typeof(DateOnly)
6268
? TranslateDateOnly(instance, method, arguments)
63-
: null;
69+
: method.DeclaringType == typeof(DateTimeOffset)
70+
? TranslateDateTimeOffset(instance, method, arguments)
71+
: null;
6472

6573
private SqlExpression? TranslateDateTime(
6674
SqlExpression? instance,
@@ -145,4 +153,40 @@ public SqliteDateTimeAddTranslator(SqliteSqlExpressionFactory sqlExpressionFacto
145153

146154
return null;
147155
}
156+
157+
private SqlExpression? TranslateDateTimeOffset(
158+
SqlExpression? instance,
159+
MethodInfo method,
160+
IReadOnlyList<SqlExpression> arguments)
161+
{
162+
if (ToUnixTimeSeconds.Equals(method))
163+
{
164+
return _sqlExpressionFactory.Function(
165+
"unixepoch",
166+
new[]
167+
{
168+
instance!
169+
},
170+
argumentsPropagateNullability: new[] { true, true },
171+
nullable: true,
172+
returnType: method.ReturnType);
173+
}
174+
else if (ToUnixTimeMilliseconds.Equals(method))
175+
{
176+
return _sqlExpressionFactory.Convert(
177+
_sqlExpressionFactory.Multiply(
178+
_sqlExpressionFactory.Subtract(
179+
_sqlExpressionFactory.Function(
180+
"julianday",
181+
new[] { instance! },
182+
nullable: true,
183+
argumentsPropagateNullability: new[] { true },
184+
typeof(double)),
185+
_sqlExpressionFactory.Constant(2440587.5)), // NB: Result of julianday('1970-01-01 00:00:00')
186+
_sqlExpressionFactory.Constant(TimeSpan.TicksPerDay)),
187+
typeof(long));
188+
}
189+
190+
return null;
191+
}
148192
}

src/EFCore.Sqlite.Core/Query/Internal/SqliteMethodCallTranslatorProvider.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public SqliteMethodCallTranslatorProvider(RelationalMethodCallTranslatorProvider
2727
{
2828
new SqliteByteArrayMethodTranslator(sqlExpressionFactory),
2929
new SqliteCharMethodTranslator(sqlExpressionFactory),
30-
new SqliteDateTimeAddTranslator(sqlExpressionFactory),
30+
new SqliteDateTimeMethodTranslator(sqlExpressionFactory),
3131
new SqliteGlobMethodTranslator(sqlExpressionFactory),
3232
new SqliteHexMethodTranslator(sqlExpressionFactory),
3333
new SqliteMathTranslator(sqlExpressionFactory),

test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs

+30
Original file line numberDiff line numberDiff line change
@@ -8210,6 +8210,36 @@ public virtual Task Using_indexer_on_byte_array_and_string_in_projection(bool as
82108210
Assert.Equal(e.String, a.String);
82118211
});
82128212

8213+
[ConditionalTheory]
8214+
[MemberData(nameof(IsAsyncData))]
8215+
public virtual Task DateTimeOffset_to_unix_time_milliseconds(bool async)
8216+
{
8217+
long unixEpochMilliseconds = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
8218+
8219+
return AssertQuery(
8220+
async,
8221+
ss => ss.Set<Gear>()
8222+
.Include(g => g.Squad.Missions)
8223+
.Where(s => s.Squad.Missions
8224+
.Where(m => unixEpochMilliseconds == m.Mission.Timeline.ToUnixTimeMilliseconds())
8225+
.FirstOrDefault() == null));
8226+
}
8227+
8228+
[ConditionalTheory]
8229+
[MemberData(nameof(IsAsyncData))]
8230+
public virtual Task DateTimeOffset_to_unix_time_seconds(bool async)
8231+
{
8232+
long unixEpochSeconds = DateTimeOffset.UnixEpoch.ToUnixTimeSeconds();
8233+
8234+
return AssertQuery(
8235+
async,
8236+
ss => ss.Set<Gear>()
8237+
.Include(g => g.Squad.Missions)
8238+
.Where(s => s.Squad.Missions
8239+
.Where(m => unixEpochSeconds == m.Mission.Timeline.ToUnixTimeSeconds())
8240+
.FirstOrDefault() == null));
8241+
}
8242+
82138243
protected GearsOfWarContext CreateContext()
82148244
=> Fixture.CreateContext();
82158245

test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs

+38
Original file line numberDiff line numberDiff line change
@@ -9978,6 +9978,44 @@ FROM [Squads] AS [s]
99789978
""");
99799979
}
99809980

9981+
public override async Task DateTimeOffset_to_unix_time_milliseconds(bool async)
9982+
{
9983+
await base.DateTimeOffset_to_unix_time_milliseconds(async);
9984+
9985+
AssertSql(
9986+
@"@__unixEpochMilliseconds_0='0'
9987+
9988+
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]
9989+
FROM [Gears] AS [g]
9990+
INNER JOIN [Squads] AS [s] ON [g].[SquadId] = [s].[Id]
9991+
LEFT JOIN [SquadMissions] AS [s1] ON [s].[Id] = [s1].[SquadId]
9992+
WHERE NOT (EXISTS (
9993+
SELECT 1
9994+
FROM [SquadMissions] AS [s0]
9995+
INNER JOIN [Missions] AS [m] ON [s0].[MissionId] = [m].[Id]
9996+
WHERE [s].[Id] = [s0].[SquadId] AND @__unixEpochMilliseconds_0 = DATEDIFF_BIG(millisecond, '1970-01-01T00:00:00.0000000+00:00', [m].[Timeline])))
9997+
ORDER BY [g].[Nickname], [g].[SquadId], [s].[Id], [s1].[SquadId]");
9998+
}
9999+
10000+
public override async Task DateTimeOffset_to_unix_time_seconds(bool async)
10001+
{
10002+
await base.DateTimeOffset_to_unix_time_seconds(async);
10003+
10004+
AssertSql(
10005+
@"@__unixEpochSeconds_0='0'
10006+
10007+
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]
10008+
FROM [Gears] AS [g]
10009+
INNER JOIN [Squads] AS [s] ON [g].[SquadId] = [s].[Id]
10010+
LEFT JOIN [SquadMissions] AS [s1] ON [s].[Id] = [s1].[SquadId]
10011+
WHERE NOT (EXISTS (
10012+
SELECT 1
10013+
FROM [SquadMissions] AS [s0]
10014+
INNER JOIN [Missions] AS [m] ON [s0].[MissionId] = [m].[Id]
10015+
WHERE [s].[Id] = [s0].[SquadId] AND @__unixEpochSeconds_0 = DATEDIFF_BIG(second, '1970-01-01T00:00:00.0000000+00:00', [m].[Timeline])))
10016+
ORDER BY [g].[Nickname], [g].[SquadId], [s].[Id], [s1].[SquadId]");
10017+
}
10018+
998110019
private void AssertSql(params string[] expected)
998210020
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
998310021
}

test/EFCore.Sqlite.FunctionalTests/Query/GearsOfWarQuerySqliteTest.cs

+38
Original file line numberDiff line numberDiff line change
@@ -9396,6 +9396,44 @@ public override async Task Using_indexer_on_byte_array_and_string_in_projection(
93969396
""");
93979397
}
93989398

9399+
public override async Task DateTimeOffset_to_unix_time_milliseconds(bool async)
9400+
{
9401+
await base.DateTimeOffset_to_unix_time_milliseconds(async);
9402+
9403+
AssertSql(
9404+
@"@__unixEpochMilliseconds_0='0'
9405+
9406+
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""
9407+
FROM ""Gears"" AS ""g""
9408+
INNER JOIN ""Squads"" AS ""s"" ON ""g"".""SquadId"" = ""s"".""Id""
9409+
LEFT JOIN ""SquadMissions"" AS ""s1"" ON ""s"".""Id"" = ""s1"".""SquadId""
9410+
WHERE NOT (EXISTS (
9411+
SELECT 1
9412+
FROM ""SquadMissions"" AS ""s0""
9413+
INNER JOIN ""Missions"" AS ""m"" ON ""s0"".""MissionId"" = ""m"".""Id""
9414+
WHERE ""s"".""Id"" = ""s0"".""SquadId"" AND @__unixEpochMilliseconds_0 = CAST(((julianday(""m"".""Timeline"") - 2440587.5) * 864000000000.0) AS INTEGER)))
9415+
ORDER BY ""g"".""Nickname"", ""g"".""SquadId"", ""s"".""Id"", ""s1"".""SquadId""");
9416+
}
9417+
9418+
public override async Task DateTimeOffset_to_unix_time_seconds(bool async)
9419+
{
9420+
await base.DateTimeOffset_to_unix_time_seconds(async);
9421+
9422+
AssertSql(
9423+
@"@__unixEpochSeconds_0='0'
9424+
9425+
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""
9426+
FROM ""Gears"" AS ""g""
9427+
INNER JOIN ""Squads"" AS ""s"" ON ""g"".""SquadId"" = ""s"".""Id""
9428+
LEFT JOIN ""SquadMissions"" AS ""s1"" ON ""s"".""Id"" = ""s1"".""SquadId""
9429+
WHERE NOT (EXISTS (
9430+
SELECT 1
9431+
FROM ""SquadMissions"" AS ""s0""
9432+
INNER JOIN ""Missions"" AS ""m"" ON ""s0"".""MissionId"" = ""m"".""Id""
9433+
WHERE ""s"".""Id"" = ""s0"".""SquadId"" AND @__unixEpochSeconds_0 = unixepoch(""m"".""Timeline"")))
9434+
ORDER BY ""g"".""Nickname"", ""g"".""SquadId"", ""s"".""Id"", ""s1"".""SquadId""");
9435+
}
9436+
93999437
private void AssertSql(params string[] expected)
94009438
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
94019439
}

0 commit comments

Comments
 (0)