Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Translate DateTime.TimeOfDay and NodaTime LocalDateTime.Time #2802

Merged
merged 1 commit into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -282,74 +282,42 @@ SqlExpression Upper()
}

private SqlExpression? TranslateDateTime(SqlExpression instance, MemberInfo member, Type returnType)
{
switch (member.Name)
=> member.Name switch
{
case "Year":
case "Years":
return GetDatePartExpression(instance, "year");

case "Month":
case "Months":
return GetDatePartExpression(instance, "month");

case "DayOfYear":
return GetDatePartExpression(instance, "doy");

case "Day":
case "Days":
return GetDatePartExpression(instance, "day");

case "Hour":
case "Hours":
return GetDatePartExpression(instance, "hour");

case "Minute":
case "Minutes":
return GetDatePartExpression(instance, "minute");

case "Second":
case "Seconds":
return GetDatePartExpression(instance, "second", true);

case "Millisecond":
case "Milliseconds":
return null; // Too annoying

case "DayOfWeek":
// Unlike DateTime.DayOfWeek, NodaTime's IsoDayOfWeek enum doesn't exactly correspond to PostgreSQL's
// values returned by date_part('dow', ...): in NodaTime Sunday is 7 and not 0, which is None.
// So we generate a CASE WHEN expression to translate PostgreSQL's 0 to 7.
var getValueExpression = GetDatePartExpression(instance, "dow", true);
// TODO: Can be simplified once https://github.com/aspnet/EntityFrameworkCore/pull/16726 is in
return
_sqlExpressionFactory.Case(
new[]
{
new CaseWhenClause(
_sqlExpressionFactory.Equal(getValueExpression, _sqlExpressionFactory.Constant(0)),
_sqlExpressionFactory.Constant(7))
},
getValueExpression
);
"Year" or "Years" => GetDatePartExpression(instance, "year"),
"Month" or "Months" => GetDatePartExpression(instance, "month"),
"DayOfYear" => GetDatePartExpression(instance, "doy"),
"Day" or "Days" => GetDatePartExpression(instance, "day"),
"Hour" or "Hours" => GetDatePartExpression(instance, "hour"),
"Minute" or "Minutes" => GetDatePartExpression(instance, "minute"),
"Second" or "Seconds" => GetDatePartExpression(instance, "second", true),
"Millisecond" or "Milliseconds" => null, // Too annoying

// Unlike DateTime.DayOfWeek, NodaTime's IsoDayOfWeek enum doesn't exactly correspond to PostgreSQL's
// values returned by date_part('dow', ...): in NodaTime Sunday is 7 and not 0, which is None.
// So we generate a CASE WHEN expression to translate PostgreSQL's 0 to 7.
"DayOfWeek" when GetDatePartExpression(instance, "dow", true) is var getValueExpression
=> _sqlExpressionFactory.Case(
getValueExpression,
new[]
{
new CaseWhenClause(_sqlExpressionFactory.Constant(0), _sqlExpressionFactory.Constant(7))
},
getValueExpression),

// PG allows converting a timestamp directly to date, truncating the time; but given a timestamptz, it performs a time zone
// conversion (based on TimeZone), which we don't want (so avoid translating except on timestamp).
// The translation for ZonedDateTime.Date converts to timestamp before ending up here.
case "Date" when instance.TypeMapping is TimestampLocalDateTimeMapping or LegacyTimestampInstantMapping:
return _sqlExpressionFactory.Convert(instance, typeof(LocalDate), _typeMappingSource.FindMapping(typeof(LocalDate))!);

case "TimeOfDay":
// TODO: Technically possible simply via casting to PG time,
// but ExplicitCastExpression only allows casting to PG types that
// are default-mapped from CLR types (timespan maps to interval,
// which timestamp cannot be cast into)
return null;

default:
return null;
}
}
"Date" when instance.TypeMapping is TimestampLocalDateTimeMapping or LegacyTimestampInstantMapping
=> _sqlExpressionFactory.Convert(instance, typeof(LocalDate), _typeMappingSource.FindMapping(typeof(LocalDate))!),

"TimeOfDay" => _sqlExpressionFactory.Convert(
instance,
typeof(LocalTime),
_typeMappingSource.FindMapping(typeof(LocalTime), storeTypeName: "time")),

_ => null
};

/// <summary>
/// Constructs the date_part expression.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Inte
/// </remarks>
public class NpgsqlDateTimeMemberTranslator : IMemberTranslator
{
private readonly IRelationalTypeMappingSource _typeMappingSource;
private readonly NpgsqlSqlExpressionFactory _sqlExpressionFactory;
private readonly RelationalTypeMapping _timestampMapping;
private readonly RelationalTypeMapping _timestampTzMapping;
Expand All @@ -26,6 +27,7 @@ public NpgsqlDateTimeMemberTranslator(
IRelationalTypeMappingSource typeMappingSource,
NpgsqlSqlExpressionFactory sqlExpressionFactory)
{
_typeMappingSource = typeMappingSource;
_timestampMapping = typeMappingSource.FindMapping("timestamp without time zone")!;
_timestampTzMapping = typeMappingSource.FindMapping("timestamp with time zone")!;
_sqlExpressionFactory = sqlExpressionFactory;
Expand Down Expand Up @@ -128,11 +130,10 @@ public NpgsqlDateTimeMemberTranslator(
// .NET's DayOfWeek is an enum, but its int values happen to correspond to PostgreSQL
nameof(DateTime.DayOfWeek) => GetDatePartExpression(instance!, "dow", floor: true),

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

// TODO: Should be possible
nameof(DateTime.Ticks) => null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ WHERE date_trunc('day', now()::timestamp) = date_trunc('day', now()::timestamp)
""");
}

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

AssertSql(
"""
SELECT o."OrderDate"::time
FROM "Orders" AS o
""");
}

public override async Task Where_datetime_date_component(bool async)
{
await base.Where_datetime_date_component(async);
Expand Down
25 changes: 21 additions & 4 deletions test/EFCore.PG.NodaTime.FunctionalTests/NodaTimeQueryNpgsqlTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,23 @@ await AssertQuery(
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task LocalDateTime_Time(bool async)
{
await AssertQuery(
async,
ss => ss.Set<NodaTimeTypes>().Where(t => t.LocalDateTime.TimeOfDay == new LocalTime(10, 31, 33, 666)),
entryCount: 1);

AssertSql(
"""
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"
FROM "NodaTimeTypes" AS n
WHERE n."LocalDateTime"::time = TIME '10:31:33.666'
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public async Task LocalDateTime_DayOfWeek(bool async)
Expand All @@ -324,8 +341,8 @@ await AssertQuery(
"""
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"
FROM "NodaTimeTypes" AS n
WHERE CASE
WHEN floor(date_part('dow', n."LocalDateTime"))::int = 0 THEN 7
WHERE CASE floor(date_part('dow', n."LocalDateTime"))::int
WHEN 0 THEN 7
ELSE floor(date_part('dow', n."LocalDateTime"))::int
END = 5
""");
Expand Down Expand Up @@ -1812,8 +1829,8 @@ await AssertQuery(
"""
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"
FROM "NodaTimeTypes" AS n
WHERE CASE
WHEN floor(date_part('dow', n."ZonedDateTime" AT TIME ZONE 'UTC'))::int = 0 THEN 7
WHERE CASE floor(date_part('dow', n."ZonedDateTime" AT TIME ZONE 'UTC'))::int
WHEN 0 THEN 7
ELSE floor(date_part('dow', n."ZonedDateTime" AT TIME ZONE 'UTC'))::int
END = 5
""");
Expand Down
Loading