Skip to content

Commit 0f72c54

Browse files
authored
Translate DateTime.TimeOfDay and NodaTime LocalDateTime.Time (#2802)
Closes #2801
1 parent 555f972 commit 0f72c54

File tree

4 files changed

+69
-72
lines changed

4 files changed

+69
-72
lines changed

src/EFCore.PG.NodaTime/Query/Internal/NpgsqlNodaTimeMemberTranslatorPlugin.cs

Lines changed: 31 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -282,74 +282,42 @@ SqlExpression Upper()
282282
}
283283

284284
private SqlExpression? TranslateDateTime(SqlExpression instance, MemberInfo member, Type returnType)
285-
{
286-
switch (member.Name)
285+
=> member.Name switch
287286
{
288-
case "Year":
289-
case "Years":
290-
return GetDatePartExpression(instance, "year");
291-
292-
case "Month":
293-
case "Months":
294-
return GetDatePartExpression(instance, "month");
295-
296-
case "DayOfYear":
297-
return GetDatePartExpression(instance, "doy");
298-
299-
case "Day":
300-
case "Days":
301-
return GetDatePartExpression(instance, "day");
302-
303-
case "Hour":
304-
case "Hours":
305-
return GetDatePartExpression(instance, "hour");
306-
307-
case "Minute":
308-
case "Minutes":
309-
return GetDatePartExpression(instance, "minute");
310-
311-
case "Second":
312-
case "Seconds":
313-
return GetDatePartExpression(instance, "second", true);
314-
315-
case "Millisecond":
316-
case "Milliseconds":
317-
return null; // Too annoying
318-
319-
case "DayOfWeek":
320-
// Unlike DateTime.DayOfWeek, NodaTime's IsoDayOfWeek enum doesn't exactly correspond to PostgreSQL's
321-
// values returned by date_part('dow', ...): in NodaTime Sunday is 7 and not 0, which is None.
322-
// So we generate a CASE WHEN expression to translate PostgreSQL's 0 to 7.
323-
var getValueExpression = GetDatePartExpression(instance, "dow", true);
324-
// TODO: Can be simplified once https://github.com/aspnet/EntityFrameworkCore/pull/16726 is in
325-
return
326-
_sqlExpressionFactory.Case(
327-
new[]
328-
{
329-
new CaseWhenClause(
330-
_sqlExpressionFactory.Equal(getValueExpression, _sqlExpressionFactory.Constant(0)),
331-
_sqlExpressionFactory.Constant(7))
332-
},
333-
getValueExpression
334-
);
287+
"Year" or "Years" => GetDatePartExpression(instance, "year"),
288+
"Month" or "Months" => GetDatePartExpression(instance, "month"),
289+
"DayOfYear" => GetDatePartExpression(instance, "doy"),
290+
"Day" or "Days" => GetDatePartExpression(instance, "day"),
291+
"Hour" or "Hours" => GetDatePartExpression(instance, "hour"),
292+
"Minute" or "Minutes" => GetDatePartExpression(instance, "minute"),
293+
"Second" or "Seconds" => GetDatePartExpression(instance, "second", true),
294+
"Millisecond" or "Milliseconds" => null, // Too annoying
295+
296+
// Unlike DateTime.DayOfWeek, NodaTime's IsoDayOfWeek enum doesn't exactly correspond to PostgreSQL's
297+
// values returned by date_part('dow', ...): in NodaTime Sunday is 7 and not 0, which is None.
298+
// So we generate a CASE WHEN expression to translate PostgreSQL's 0 to 7.
299+
"DayOfWeek" when GetDatePartExpression(instance, "dow", true) is var getValueExpression
300+
=> _sqlExpressionFactory.Case(
301+
getValueExpression,
302+
new[]
303+
{
304+
new CaseWhenClause(_sqlExpressionFactory.Constant(0), _sqlExpressionFactory.Constant(7))
305+
},
306+
getValueExpression),
335307

336308
// PG allows converting a timestamp directly to date, truncating the time; but given a timestamptz, it performs a time zone
337309
// conversion (based on TimeZone), which we don't want (so avoid translating except on timestamp).
338310
// The translation for ZonedDateTime.Date converts to timestamp before ending up here.
339-
case "Date" when instance.TypeMapping is TimestampLocalDateTimeMapping or LegacyTimestampInstantMapping:
340-
return _sqlExpressionFactory.Convert(instance, typeof(LocalDate), _typeMappingSource.FindMapping(typeof(LocalDate))!);
341-
342-
case "TimeOfDay":
343-
// TODO: Technically possible simply via casting to PG time,
344-
// but ExplicitCastExpression only allows casting to PG types that
345-
// are default-mapped from CLR types (timespan maps to interval,
346-
// which timestamp cannot be cast into)
347-
return null;
348-
349-
default:
350-
return null;
351-
}
352-
}
311+
"Date" when instance.TypeMapping is TimestampLocalDateTimeMapping or LegacyTimestampInstantMapping
312+
=> _sqlExpressionFactory.Convert(instance, typeof(LocalDate), _typeMappingSource.FindMapping(typeof(LocalDate))!),
313+
314+
"TimeOfDay" => _sqlExpressionFactory.Convert(
315+
instance,
316+
typeof(LocalTime),
317+
_typeMappingSource.FindMapping(typeof(LocalTime), storeTypeName: "time")),
318+
319+
_ => null
320+
};
353321

354322
/// <summary>
355323
/// Constructs the date_part expression.

src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlDateTimeMemberTranslator.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Inte
1212
/// </remarks>
1313
public class NpgsqlDateTimeMemberTranslator : IMemberTranslator
1414
{
15+
private readonly IRelationalTypeMappingSource _typeMappingSource;
1516
private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
1617
private readonly RelationalTypeMapping _timestampMapping;
1718
private readonly RelationalTypeMapping _timestampTzMapping;
@@ -26,6 +27,7 @@ public NpgsqlDateTimeMemberTranslator(
2627
IRelationalTypeMappingSource typeMappingSource,
2728
NpgsqlSqlExpressionFactory sqlExpressionFactory)
2829
{
30+
_typeMappingSource = typeMappingSource;
2931
_timestampMapping = typeMappingSource.FindMapping("timestamp without time zone")!;
3032
_timestampTzMapping = typeMappingSource.FindMapping("timestamp with time zone")!;
3133
_sqlExpressionFactory = sqlExpressionFactory;
@@ -128,11 +130,10 @@ public NpgsqlDateTimeMemberTranslator(
128130
// .NET's DayOfWeek is an enum, but its int values happen to correspond to PostgreSQL
129131
nameof(DateTime.DayOfWeek) => GetDatePartExpression(instance!, "dow", floor: true),
130132

131-
// TODO: Technically possible simply via casting to PG time, should be better in EF Core 3.0
132-
// but ExplicitCastExpression only allows casting to PG types that
133-
// are default-mapped from CLR types (timespan maps to interval,
134-
// which timestamp cannot be cast into)
135-
nameof(DateTime.TimeOfDay) => null,
133+
nameof(DateTime.TimeOfDay) => _sqlExpressionFactory.Convert(
134+
instance!,
135+
typeof(TimeSpan),
136+
_typeMappingSource.FindMapping(typeof(TimeSpan), storeTypeName: "time")),
136137

137138
// TODO: Should be possible
138139
nameof(DateTime.Ticks) => null,

test/EFCore.PG.FunctionalTests/Query/NorthwindWhereQueryNpgsqlTest.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ WHERE date_trunc('day', now()::timestamp) = date_trunc('day', now()::timestamp)
3333
""");
3434
}
3535

36+
public override async Task Time_of_day_datetime(bool async)
37+
{
38+
await base.Time_of_day_datetime(async);
39+
40+
AssertSql(
41+
"""
42+
SELECT o."OrderDate"::time
43+
FROM "Orders" AS o
44+
""");
45+
}
46+
3647
public override async Task Where_datetime_date_component(bool async)
3748
{
3849
await base.Where_datetime_date_component(async);

test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,23 @@ await AssertQuery(
311311
""");
312312
}
313313

314+
[ConditionalTheory]
315+
[MemberData(nameof(IsAsyncData))]
316+
public async Task LocalDateTime_Time(bool async)
317+
{
318+
await AssertQuery(
319+
async,
320+
ss => ss.Set<NodaTimeTypes>().Where(t => t.LocalDateTime.TimeOfDay == new LocalTime(10, 31, 33, 666)),
321+
entryCount: 1);
322+
323+
AssertSql(
324+
"""
325+
SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime"
326+
FROM "NodaTimeTypes" AS n
327+
WHERE n."LocalDateTime"::time = TIME '10:31:33.666'
328+
""");
329+
}
330+
314331
[ConditionalTheory]
315332
[MemberData(nameof(IsAsyncData))]
316333
public async Task LocalDateTime_DayOfWeek(bool async)
@@ -324,8 +341,8 @@ await AssertQuery(
324341
"""
325342
SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime"
326343
FROM "NodaTimeTypes" AS n
327-
WHERE CASE
328-
WHEN floor(date_part('dow', n."LocalDateTime"))::int = 0 THEN 7
344+
WHERE CASE floor(date_part('dow', n."LocalDateTime"))::int
345+
WHEN 0 THEN 7
329346
ELSE floor(date_part('dow', n."LocalDateTime"))::int
330347
END = 5
331348
""");
@@ -1812,8 +1829,8 @@ await AssertQuery(
18121829
"""
18131830
SELECT n."Id", n."DateInterval", n."Duration", n."Instant", n."InstantRange", n."Interval", n."LocalDate", n."LocalDate2", n."LocalDateRange", n."LocalDateTime", n."LocalTime", n."Long", n."OffsetTime", n."Period", n."TimeZoneId", n."ZonedDateTime"
18141831
FROM "NodaTimeTypes" AS n
1815-
WHERE CASE
1816-
WHEN floor(date_part('dow', n."ZonedDateTime" AT TIME ZONE 'UTC'))::int = 0 THEN 7
1832+
WHERE CASE floor(date_part('dow', n."ZonedDateTime" AT TIME ZONE 'UTC'))::int
1833+
WHEN 0 THEN 7
18171834
ELSE floor(date_part('dow', n."ZonedDateTime" AT TIME ZONE 'UTC'))::int
18181835
END = 5
18191836
""");

0 commit comments

Comments
 (0)