diff --git a/EFCore.PG.sln.DotSettings b/EFCore.PG.sln.DotSettings index 9055198e9..fa933ce4d 100644 --- a/EFCore.PG.sln.DotSettings +++ b/EFCore.PG.sln.DotSettings @@ -188,5 +188,6 @@ True True True + True True \ No newline at end of file diff --git a/test/EFCore.PG.FunctionalTests/Query/CharacterQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/CharacterQueryNpgsqlTest.cs index 25e3de80f..cc248e83a 100644 --- a/test/EFCore.PG.FunctionalTests/Query/CharacterQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/CharacterQueryNpgsqlTest.cs @@ -14,8 +14,6 @@ public CharacterQueryNpgsqlTest(CharacterQueryNpgsqlFixture fixture, ITestOutput Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - #region Tests - [Fact] public void Find_in_database() { @@ -145,8 +143,6 @@ public void Test_change_tracking_key_sizes() } } - #endregion - #region Fixture // ReSharper disable once ClassNeverInstantiated.Global diff --git a/test/EFCore.PG.FunctionalTests/Query/NodaTimeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/NodaTimeQueryNpgsqlTest.cs deleted file mode 100644 index d35187c54..000000000 --- a/test/EFCore.PG.FunctionalTests/Query/NodaTimeQueryNpgsqlTest.cs +++ /dev/null @@ -1,2028 +0,0 @@ -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; - -public class NodaTimeQueryNpgsqlTest : QueryTestBase -{ - public NodaTimeQueryNpgsqlTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) - { - Fixture.TestSqlLoggerFactory.Clear(); - Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Operator(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDate < new LocalDate(2018, 4, 21))); - - 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."LocalDate" < DATE '2018-04-21' -"""); - } - - #region Addition and subtraction - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Add_LocalDate_Period(bool async) - { - // Note: requires some special type inference logic because we're adding things of different types - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDate + Period.FromMonths(1) > t.LocalDate)); - - 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."LocalDate" + INTERVAL 'P1M' > n."LocalDate" -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Subtract_Instant(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Instant + Duration.FromDays(1) - t.Instant == Duration.FromDays(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."Instant" + INTERVAL '1 00:00:00') - n."Instant" = INTERVAL '1 00:00:00' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Subtract_LocalDateTime(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDateTime + Period.FromDays(1) - t.LocalDateTime == Period.FromDays(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" + INTERVAL 'P1D') - n."LocalDateTime" = INTERVAL 'P1D' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Subtract_ZonedDateTime(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.ZonedDateTime + Duration.FromDays(1) - t.ZonedDateTime == Duration.FromDays(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."ZonedDateTime" + INTERVAL '1 00:00:00') - n."ZonedDateTime" = INTERVAL '1 00:00:00' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Subtract_LocalDate(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDate2 - t.LocalDate == Period.FromDays(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 make_interval(days => n."LocalDate2" - n."LocalDate") = INTERVAL 'P1D' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Subtract_LocalDate_parameter(bool async) - { - var date = new LocalDate(2018, 4, 20); - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDate2 - date == Period.FromDays(1))); - - AssertSql( - """ -@date='Friday, 20 April 2018' (DbType = Date) - -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 make_interval(days => n."LocalDate2" - @date) = INTERVAL 'P1D' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Subtract_LocalDate_constant(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDate2 - new LocalDate(2018, 4, 20) == Period.FromDays(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 make_interval(days => n."LocalDate2" - DATE '2018-04-20') = INTERVAL 'P1D' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Subtract_LocalTime(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalTime + Period.FromHours(1) - t.LocalTime == Period.FromHours(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."LocalTime" + INTERVAL 'PT1H') - n."LocalTime" = INTERVAL 'PT1H' -"""); - } - - #endregion - - #region LocalDateTime - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDateTime_Year(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDateTime.Year == 2018)); - - 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 date_part('year', n."LocalDateTime")::int = 2018 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDateTime_Month(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDateTime.Month == 4)); - - 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 date_part('month', n."LocalDateTime")::int = 4 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDateTime_DayOfYear(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDateTime.DayOfYear == 110)); - - 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 date_part('doy', n."LocalDateTime")::int = 110 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDateTime_Day(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDateTime.Day == 20)); - - 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 date_part('day', n."LocalDateTime")::int = 20 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDateTime_Hour(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDateTime.Hour == 10)); - - 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 date_part('hour', n."LocalDateTime")::int = 10 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDateTime_Minute(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDateTime.Minute == 31)); - - 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 date_part('minute', n."LocalDateTime")::int = 31 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDateTime_Second(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDateTime.Second == 33)); - - 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 floor(date_part('second', n."LocalDateTime"))::int = 33 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDateTime_Date(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDateTime.Date == new LocalDate(2018, 4, 20))); - - 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"::date = DATE '2018-04-20' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDateTime_Time(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDateTime.TimeOfDay == new LocalTime(10, 31, 33, 666))); - - 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) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDateTime.DayOfWeek == IsoDayOfWeek.Friday)); - - 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 CASE floor(date_part('dow', n."LocalDateTime"))::int - WHEN 0 THEN 7 - ELSE floor(date_part('dow', n."LocalDateTime"))::int -END = 5 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDateTime_InZoneLeniently_ToInstant(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where( - t => t.LocalDateTime.InZoneLeniently(DateTimeZoneProviders.Tzdb["Europe/Berlin"]).ToInstant() - == new ZonedDateTime(new LocalDateTime(2018, 4, 20, 8, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); - - AssertSql( - """ -@ToInstant='2018-04-20T08:31:33Z' (DbType = DateTime) - -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" AT TIME ZONE 'Europe/Berlin' = @ToInstant -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDateTime_InZoneLeniently_ToInstant_with_column_time_zone(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where( - t => t.LocalDateTime.InZoneLeniently(DateTimeZoneProviders.Tzdb[t.TimeZoneId]).ToInstant() - == new ZonedDateTime( - new LocalDateTime(2018, 4, 20, 8, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); - - AssertSql( - """ -@ToInstant='2018-04-20T08:31:33Z' (DbType = DateTime) - -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" AT TIME ZONE n."TimeZoneId" = @ToInstant -"""); - } - - [ConditionalFact] - public async Task LocalDateTime_Distance() - { - await using var context = CreateContext(); - var closest = await context.NodaTimeTypes - .OrderBy(t => EF.Functions.Distance(t.LocalDateTime, new LocalDateTime(2018, 4, 1, 0, 0, 0))).FirstAsync(); - - Assert.Equal(1, closest.Id); - - 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 -ORDER BY n."LocalDateTime" <-> TIMESTAMP '2018-04-01T00:00:00' NULLS FIRST -LIMIT 1 -"""); - } - - #endregion LocalDateTime - - #region LocalDate - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDate_Year(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDate.Year == 2018)); - - 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 date_part('year', n."LocalDate")::int = 2018 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDate_Month(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDate.Month == 4)); - - 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 date_part('month', n."LocalDate")::int = 4 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDate_DayOrYear(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDate.DayOfYear == 110)); - - 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 date_part('doy', n."LocalDate")::int = 110 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalDate_Day(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalDate.Day == 20)); - - 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 date_part('day', n."LocalDate")::int = 20 -"""); - } - - [ConditionalFact] - public async Task LocalDate_Distance() - { - await using var context = CreateContext(); - var closest = await context.NodaTimeTypes.OrderBy(t => EF.Functions.Distance(t.LocalDate, new LocalDate(2018, 4, 1))).FirstAsync(); - - Assert.Equal(1, closest.Id); - - 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 -ORDER BY n."LocalDate" <-> DATE '2018-04-01' NULLS FIRST -LIMIT 1 -"""); - } - - #endregion LocalDate - - #region LocalTime - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalTime_Hour(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalTime.Hour == 10)); - - 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 date_part('hour', n."LocalTime")::int = 10 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalTime_Minute(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalTime.Minute == 31)); - - 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 date_part('minute', n."LocalTime")::int = 31 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task LocalTime_Second(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.LocalTime.Second == 33)); - - 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 floor(date_part('second', n."LocalTime"))::int = 33 -"""); - } - - #endregion LocalTime - - #region Period - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_Years(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Period.Years == 2018)); - - 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 date_part('year', n."Period")::int = 2018 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_Months(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Period.Months == 4)); - - 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 date_part('month', n."Period")::int = 4 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_Days(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Period.Days == 20)); - - 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 date_part('day', n."Period")::int = 20 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_Hours(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Period.Hours == 10)); - - 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 date_part('hour', n."Period")::int = 10 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_Minutes(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Period.Minutes == 31)); - - 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 date_part('minute', n."Period")::int = 31 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_Seconds(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Period.Seconds == 23)); - - 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 floor(date_part('second', n."Period"))::int = 23 -"""); - } - - // PostgreSQL does not support extracting weeks from intervals - [ConditionalFact] - public Task Period_Weeks_is_not_translated() - { - using var ctx = CreateContext(); - - return AssertTranslationFailed( - () => ctx.Set().Where(t => t.Period.Weeks == 0).ToListAsync()); - } - - [ConditionalFact] - public Task Period_Milliseconds_is_not_translated() - { - using var ctx = CreateContext(); - - return AssertTranslationFailed( - () => ctx.Set().Where(t => t.Period.Nanoseconds == 0).ToListAsync()); - } - - [ConditionalFact] - public Task Period_Nanoseconds_is_not_translated() - { - using var ctx = CreateContext(); - - return AssertTranslationFailed( - () => ctx.Set().Where(t => t.Period.Nanoseconds == 0).ToListAsync()); - } - - [ConditionalFact] - public Task Period_Ticks_is_not_translated() - { - using var ctx = CreateContext(); - - return AssertTranslationFailed( - () => ctx.Set().Where(t => t.Period.Ticks == 0).ToListAsync()); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_FromYears(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => Period.FromYears(t.Id).Years == 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 date_part('year', make_interval(years => n."Id"))::int = 1 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_FromMonths(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => Period.FromMonths(t.Id).Months == 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 date_part('month', make_interval(months => n."Id"))::int = 1 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_FromWeeks(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => Period.FromWeeks(t.Id).Days == 7), - ss => ss.Set().Where(t => Period.FromWeeks(t.Id).Normalize().Days == 7)); - - 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 date_part('day', make_interval(weeks => n."Id"))::int = 7 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_FromDays(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => Period.FromDays(t.Id).Days == 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 date_part('day', make_interval(days => n."Id"))::int = 1 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_FromHours_int(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => Period.FromHours(t.Id).Hours == 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 date_part('hour', make_interval(hours => n."Id"))::int = 1 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_FromHours_long(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => Period.FromHours(t.Long).Hours == 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 date_part('hour', make_interval(hours => n."Long"::int))::int = 1 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_FromMinutes_int(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => Period.FromMinutes(t.Id).Minutes == 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 date_part('minute', make_interval(mins => n."Id"))::int = 1 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_FromMinutes_long(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => Period.FromMinutes(t.Long).Minutes == 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 date_part('minute', make_interval(mins => n."Long"::int))::int = 1 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_FromSeconds_int(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => Period.FromSeconds(t.Id).Seconds == 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 floor(date_part('second', make_interval(secs => n."Id"::bigint::double precision)))::int = 1 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Period_FromSeconds_long(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => Period.FromSeconds(t.Long).Seconds == 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 floor(date_part('second', make_interval(secs => n."Long"::double precision)))::int = 1 -"""); - } - - [ConditionalFact] - public Task Period_FromMilliseconds_is_not_translated() - { - using var ctx = CreateContext(); - - return AssertTranslationFailed( - () => ctx.Set().Where(t => Period.FromMilliseconds(t.Id).Seconds == 1).ToListAsync()); - } - - [ConditionalFact] - public Task Period_FromNanoseconds_is_not_translated() - { - using var ctx = CreateContext(); - - return AssertTranslationFailed( - () => ctx.Set().Where(t => Period.FromNanoseconds(t.Id).Seconds == 1).ToListAsync()); - } - - [ConditionalFact] - public Task Period_FromTicks_is_not_translated() - { - using var ctx = CreateContext(); - - return AssertTranslationFailed( - () => ctx.Set().Where(t => Period.FromNanoseconds(t.Id).Seconds == 1).ToListAsync()); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task GroupBy_Property_Select_Sum_over_Period(bool async) - { - await using var ctx = CreateContext(); - - // Note: Unlike Duration, Period can't be converted to total ticks (because its absolute time varies). - var query = ctx.Set() - .GroupBy(o => o.Id) - .Select(g => EF.Functions.Sum(g.Select(o => o.Period))); - - _ = async - ? await query.ToListAsync() - : query.ToList(); - - AssertSql( - """ -SELECT sum(n."Period") -FROM "NodaTimeTypes" AS n -GROUP BY n."Id" -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task GroupBy_Property_Select_Average_over_Period(bool async) - { - await using var ctx = CreateContext(); - - // Note: Unlike Duration, Period can't be converted to total ticks (because its absolute time varies). - var query = ctx.Set() - .GroupBy(o => o.Id) - .Select(g => EF.Functions.Average(g.Select(o => o.Period))); - - _ = async - ? await query.ToListAsync() - : query.ToList(); - - AssertSql( - """ -SELECT avg(n."Period") -FROM "NodaTimeTypes" AS n -GROUP BY n."Id" -"""); - } - - #endregion Period - - #region Duration - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Duration_TotalDays(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Duration.TotalDays > 27)); - - 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 date_part('epoch', n."Duration") / 86400.0 > 27.0 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Duration_TotalHours(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Duration.TotalHours < 700)); - - 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 date_part('epoch', n."Duration") / 3600.0 < 700.0 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Duration_TotalMinutes(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Duration.TotalMinutes < 40000)); - - 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 date_part('epoch', n."Duration") / 60.0 < 40000.0 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Duration_TotalSeconds(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Duration.TotalSeconds == 2365448.02)); - - 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 date_part('epoch', n."Duration") = 2365448.02 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Duration_TotalMilliseconds(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Duration.TotalMilliseconds == 2365448020)); - - 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 date_part('epoch', n."Duration") / 0.001 = 2365448020.0 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Duration_Days(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Duration.Days == 27)); - - 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 date_part('day', n."Duration")::int = 27 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Duration_Hours(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Duration.Hours == 9)); - - 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 date_part('hour', n."Duration")::int = 9 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Duration_Minutes(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Duration.Minutes == 4)); - - 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 date_part('minute', n."Duration")::int = 4 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Duration_Seconds(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Duration.Seconds == 8)); - - 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 floor(date_part('second', n."Duration"))::int = 8 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task GroupBy_Property_Select_Sum_over_Duration(bool async) - { - await AssertQueryScalar( - async, - ss => ss.Set() - .GroupBy(o => o.Id) - .Select(g => EF.Functions.Sum(g.Select(o => o.Duration))), - expectedQuery: ss => ss.Set() - .GroupBy(o => o.Id) - .Select(g => (Duration?)Duration.FromTicks(g.Sum(o => o.Duration.TotalTicks)))); - - AssertSql( - """ -SELECT sum(n."Duration") -FROM "NodaTimeTypes" AS n -GROUP BY n."Id" -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task GroupBy_Property_Select_Average_over_Duration(bool async) - { - await AssertQueryScalar( - async, - ss => ss.Set() - .GroupBy(o => o.Id) - .Select(g => EF.Functions.Average(g.Select(o => o.Duration))), - expectedQuery: ss => ss.Set() - .GroupBy(o => o.Id) - .Select(g => (Duration?)Duration.FromTicks((long)g.Average(o => o.Duration.TotalTicks)))); - - AssertSql( - """ -SELECT avg(n."Duration") -FROM "NodaTimeTypes" AS n -GROUP BY n."Id" -"""); - } - - #endregion - - #region Interval - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Interval_Start(bool async) - { - await AssertQuery( - async, - ss => ss.Set() - .Where(t => t.Interval.Start == new LocalDateTime(2018, 4, 20, 10, 31, 33, 666).InUtc().ToInstant())); - - 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 lower(n."Interval") = TIMESTAMPTZ '2018-04-20T10:31:33.666Z' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Interval_End(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Interval.End == new LocalDateTime(2018, 4, 25, 10, 31, 33, 666).InUtc().ToInstant())); - - 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 upper(n."Interval") = TIMESTAMPTZ '2018-04-25T10:31:33.666Z' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Interval_HasStart(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Interval.HasStart)); - - 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 NOT (lower_inf(n."Interval")) -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Interval_HasEnd(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Interval.HasEnd)); - - 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 NOT (upper_inf(n."Interval")) -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Interval_Duration(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Interval.Duration == Duration.FromDays(5))); - - 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 upper(n."Interval") - lower(n."Interval") = INTERVAL '5 00:00:00' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Interval_Contains_Instant(bool async) - { - var interval = new Interval( - new LocalDateTime(2018, 01, 01, 0, 0, 0).InUtc().ToInstant(), - new LocalDateTime(2020, 12, 25, 0, 0, 0).InUtc().ToInstant()); - - await AssertQuery( - async, - ss => ss.Set().Where(t => interval.Contains(t.Instant))); - - AssertSql( - """ -@interval='2018-01-01T00:00:00Z/2020-12-25T00:00:00Z' (DbType = Object) - -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 @interval @> n."Instant" -"""); - } - - [ConditionalTheory] - [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14 - [MemberData(nameof(IsAsyncData))] - public async Task Interval_RangeAgg(bool async) - { - await using var context = CreateContext(); - - var query = context.NodaTimeTypes - .GroupBy(x => true) - .Select(g => EF.Functions.RangeAgg(g.Select(x => x.Interval))); - - var union = async - ? await query.SingleAsync() - : query.Single(); - - var start = Instant.FromUtc(2018, 4, 20, 10, 31, 33).Plus(Duration.FromMilliseconds(666)); - Assert.Equal([new(start, start + Duration.FromDays(5))], union); - - AssertSql( - """ -SELECT range_agg(n0."Interval") -FROM ( - SELECT n."Interval", TRUE AS "Key" - FROM "NodaTimeTypes" AS n -) AS n0 -GROUP BY n0."Key" -LIMIT 2 -"""); - } - - [ConditionalTheory] - [MinimumPostgresVersion(14, 0)] // range_intersect_agg was introduced in PostgreSQL 14 - [MemberData(nameof(IsAsyncData))] - public async Task Interval_Intersect_aggregate(bool async) - { - await using var context = CreateContext(); - - var query = context.NodaTimeTypes - .GroupBy(x => true) - .Select(g => EF.Functions.RangeIntersectAgg(g.Select(x => x.Interval))); - - var intersection = async - ? await query.SingleAsync() - : query.Single(); - - var start = Instant.FromUtc(2018, 4, 20, 10, 31, 33).Plus(Duration.FromMilliseconds(666)); - Assert.Equal(new Interval(start, start + Duration.FromDays(5)), intersection); - - AssertSql( - """ -SELECT range_intersect_agg(n0."Interval") -FROM ( - SELECT n."Interval", TRUE AS "Key" - FROM "NodaTimeTypes" AS n -) AS n0 -GROUP BY n0."Key" -LIMIT 2 -"""); - } - - #endregion Interval - - #region DateInterval - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task DateInterval_Length(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.DateInterval.Length == 5)); - - 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 upper(n."DateInterval") - lower(n."DateInterval") = 5 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task DateInterval_Start(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.DateInterval.Start == new LocalDate(2018, 4, 20))); - - 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 lower(n."DateInterval") = DATE '2018-04-20' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task DateInterval_End(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.DateInterval.End == new LocalDate(2018, 4, 24))); - - 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 CAST(upper(n."DateInterval") - INTERVAL 'P1D' AS date) = DATE '2018-04-24' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task DateInterval_End_Select(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Select(t => t.DateInterval.End)); - - AssertSql( - """ -SELECT CAST(upper(n."DateInterval") - INTERVAL 'P1D' AS date) -FROM "NodaTimeTypes" AS n -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task DateInterval_Contains_LocalDate(bool async) - { - var dateInterval = new DateInterval(new LocalDate(2018, 01, 01), new LocalDate(2020, 12, 25)); - - await AssertQuery( - async, - ss => ss.Set().Where(t => dateInterval.Contains(t.LocalDate))); - - AssertSql( - """ -@dateInterval='[2018-01-01, 2020-12-25]' (DbType = Object) - -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 @dateInterval @> n."LocalDate" -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task DateInterval_Contains_DateInterval(bool async) - { - var dateInterval = new DateInterval(new LocalDate(2018, 4, 22), new LocalDate(2018, 4, 24)); - - await AssertQuery( - async, - ss => ss.Set().Where(t => t.DateInterval.Contains(dateInterval))); - - AssertSql( - """ -@dateInterval='[2018-04-22, 2018-04-24]' (DbType = Object) - -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."DateInterval" @> @dateInterval -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task DateInterval_Intersection(bool async) - { - var dateInterval = new DateInterval(new LocalDate(2018, 4, 22), new LocalDate(2018, 4, 26)); - - await AssertQuery( - async, - ss => ss.Set().Where( - t => t.DateInterval.Intersection(dateInterval) == new DateInterval(new LocalDate(2018, 4, 22), new LocalDate(2018, 4, 24)))); - - AssertSql( - """ -@dateInterval='[2018-04-22, 2018-04-26]' (DbType = Object) - -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."DateInterval" * @dateInterval = '[2018-04-22,2018-04-24]'::daterange -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task DateInterval_Union(bool async) - { - var dateInterval = new DateInterval(new LocalDate(2018, 4, 22), new LocalDate(2018, 4, 26)); - - await AssertQuery( - async, - ss => ss.Set().Where( - t => t.DateInterval.Union(dateInterval) == new DateInterval(new LocalDate(2018, 4, 20), new LocalDate(2018, 4, 26)))); - - AssertSql( - """ -@dateInterval='[2018-04-22, 2018-04-26]' (DbType = Object) - -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."DateInterval" + @dateInterval = '[2018-04-20,2018-04-26]'::daterange -"""); - } - - [ConditionalTheory] - [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14 - [MemberData(nameof(IsAsyncData))] - public async Task DateInterval_RangeAgg(bool async) - { - await using var context = CreateContext(); - - var query = context.NodaTimeTypes - .GroupBy(x => true) - .Select(g => EF.Functions.RangeAgg(g.Select(x => x.DateInterval))); - - var union = async - ? await query.SingleAsync() - : query.Single(); - - Assert.Equal([new(new LocalDate(2018, 4, 20), new LocalDate(2018, 4, 24))], union); - - AssertSql( - """ -SELECT range_agg(n0."DateInterval") -FROM ( - SELECT n."DateInterval", TRUE AS "Key" - FROM "NodaTimeTypes" AS n -) AS n0 -GROUP BY n0."Key" -LIMIT 2 -"""); - } - - [ConditionalTheory] - [MinimumPostgresVersion(14, 0)] // range_intersect_agg was introduced in PostgreSQL 14 - [MemberData(nameof(IsAsyncData))] - public async Task DateInterval_Intersect_aggregate(bool async) - { - await using var context = CreateContext(); - - var query = context.NodaTimeTypes - .GroupBy(x => true) - .Select(g => EF.Functions.RangeIntersectAgg(g.Select(x => x.DateInterval))); - - var intersection = async - ? await query.SingleAsync() - : query.Single(); - - Assert.Equal(new DateInterval(new LocalDate(2018, 4, 20), new LocalDate(2018, 4, 24)), intersection); - - AssertSql( - """ -SELECT range_intersect_agg(n0."DateInterval") -FROM ( - SELECT n."DateInterval", TRUE AS "Key" - FROM "NodaTimeTypes" AS n -) AS n0 -GROUP BY n0."Key" -LIMIT 2 -"""); - } - - #endregion DateInterval - - #region Range - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task DateRange_Contains(bool async) - { - var dateRange = new DateInterval(new LocalDate(2018, 01, 01), new LocalDate(2020, 12, 26)); - - await AssertQuery( - async, - ss => ss.Set().Where(t => dateRange.Contains(t.LocalDate))); - - AssertSql( - """ -@dateRange='[2018-01-01, 2020-12-26]' (DbType = Object) - -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 @dateRange @> n."LocalDate" -"""); - } - - #endregion Range - - #region Instant - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Instance_InUtc(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where( - t => t.Instant.InUtc() - == new ZonedDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33, 666), DateTimeZone.Utc, Offset.Zero))); - - AssertSql( - """ -@p='2018-04-20T10:31:33 UTC (+00)' (DbType = DateTime) - -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."Instant" = @p -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Instance_InZone_constant_LocalDateTime(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where( - t => t.Instant.InZone(DateTimeZoneProviders.Tzdb["Europe/Berlin"]).LocalDateTime - == new LocalDateTime(2018, 4, 20, 12, 31, 33, 666))); - - 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."Instant" AT TIME ZONE 'Europe/Berlin' = TIMESTAMP '2018-04-20T12:31:33.666' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Instance_InZone_constant_Date(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where( - t => t.Instant.InZone(DateTimeZoneProviders.Tzdb["Europe/Berlin"]).Date - == new LocalDate(2018, 4, 20))); - - 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 CAST(n."Instant" AT TIME ZONE 'Europe/Berlin' AS date) = DATE '2018-04-20' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Instance_InZone_parameter_LocalDateTime(bool async) - { - var timeZone = DateTimeZoneProviders.Tzdb["Europe/Berlin"]; - - await AssertQuery( - async, - ss => ss.Set().Where( - t => t.Instant.InZone(timeZone).LocalDateTime - == new LocalDateTime(2018, 4, 20, 12, 31, 33, 666))); - - AssertSql( - """ -@timeZone='Europe/Berlin' - -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."Instant" AT TIME ZONE @timeZone = TIMESTAMP '2018-04-20T12:31:33.666' -"""); - } - - [ConditionalFact] - public async Task Instance_InZone_without_LocalDateTime_fails() - { - await using var ctx = CreateContext(); - - await Assert.ThrowsAsync( - () => ctx.Set().Where(t => t.Instant.InZone(DateTimeZoneProviders.Tzdb["Europe/Berlin"]) == default) - .ToListAsync()); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task Instance_ToDateTimeUtc(bool async) - { - await AssertQuery( - async, - ss => ss.Set() - .Where(t => t.Instant.ToDateTimeUtc() == new DateTime(2018, 4, 20, 10, 31, 33, 666, DateTimeKind.Utc))); - - 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."Instant"::timestamptz = TIMESTAMPTZ '2018-04-20T10:31:33.666Z' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task GetCurrentInstant_from_Instance(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Instant < SystemClock.Instance.GetCurrentInstant())); - - 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."Instant" < NOW() -"""); - } - - [ConditionalFact] - public async Task Instant_Distance() - { - await using var context = CreateContext(); - var closest = await context.NodaTimeTypes - .OrderBy(t => EF.Functions.Distance(t.Instant, new LocalDateTime(2018, 4, 1, 0, 0, 0).InUtc().ToInstant())).FirstAsync(); - - Assert.Equal(1, closest.Id); - - 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 -ORDER BY n."Instant" <-> TIMESTAMPTZ '2018-04-01T00:00:00Z' NULLS FIRST -LIMIT 1 -"""); - } - - #endregion - - #region ZonedDateTime - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task ZonedDateTime_Year(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.ZonedDateTime.Year == 2018)); - - 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 date_part('year', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 2018 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task ZonedDateTime_Month(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.ZonedDateTime.Month == 4)); - - 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 date_part('month', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 4 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task ZonedDateTime_DayOfYear(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.ZonedDateTime.DayOfYear == 110)); - - 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 date_part('doy', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 110 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task ZonedDateTime_Day(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.ZonedDateTime.Day == 20)); - - 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 date_part('day', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 20 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task ZonedDateTime_Hour(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.ZonedDateTime.Hour == 10)); - - 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 date_part('hour', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 10 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task ZonedDateTime_Minute(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.ZonedDateTime.Minute == 31)); - - 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 date_part('minute', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 31 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task ZonedDateTime_Second(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.ZonedDateTime.Second == 33)); - - 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 floor(date_part('second', n."ZonedDateTime" AT TIME ZONE 'UTC'))::int = 33 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task ZonedDateTime_Date(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.ZonedDateTime.Date == new LocalDate(2018, 4, 20))); - - 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 CAST(n."ZonedDateTime" AT TIME ZONE 'UTC' AS date) = DATE '2018-04-20' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task ZonedDateTime_DayOfWeek(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.ZonedDateTime.DayOfWeek == IsoDayOfWeek.Friday)); - - 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 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 -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task ZonedDateTime_LocalDateTime(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where(t => t.Instant.InUtc().LocalDateTime == new LocalDateTime(2018, 4, 20, 10, 31, 33, 666))); - - 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."Instant" AT TIME ZONE 'UTC' = TIMESTAMP '2018-04-20T10:31:33.666' -"""); - } - - [ConditionalTheory] - [MemberData(nameof(IsAsyncData))] - public async Task ZonedDateTime_ToInstant(bool async) - { - await AssertQuery( - async, - ss => ss.Set().Where( - t => t.ZonedDateTime.ToInstant() - == new ZonedDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); - - AssertSql( - """ -@ToInstant='2018-04-20T10:31:33Z' (DbType = DateTime) - -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."ZonedDateTime" = @ToInstant -"""); - } - - [ConditionalFact] - public async Task ZonedDateTime_Distance() - { - await using var context = CreateContext(); - - var closest = await context.NodaTimeTypes - .OrderBy( - t => EF.Functions.Distance( - t.ZonedDateTime, - new ZonedDateTime(new LocalDateTime(2018, 4, 1, 0, 0, 0), DateTimeZone.Utc, Offset.Zero))).FirstAsync(); - Assert.Equal(1, closest.Id); - - AssertSql( - """ -@p='2018-04-01T00:00:00 UTC (+00)' (DbType = DateTime) - -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 -ORDER BY n."ZonedDateTime" <-> @p NULLS FIRST -LIMIT 1 -"""); - } - - #endregion ZonedDateTime - - #region Support - - private NodaTimeContext CreateContext() - => Fixture.CreateContext(); - - private static readonly Period _defaultPeriod = Period.FromYears(2018) - + Period.FromMonths(4) - + Period.FromDays(20) - + Period.FromHours(10) - + Period.FromMinutes(31) - + Period.FromSeconds(23) - + Period.FromMilliseconds(666); - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - public class NodaTimeContext(DbContextOptions options) : PoolableDbContext(options) - { - // ReSharper disable once MemberHidesStaticFromOuterClass - // ReSharper disable once UnusedAutoPropertyAccessor.Global - public DbSet NodaTimeTypes { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.HasPostgresExtension("btree_gist"); - } - - public static async Task SeedAsync(NodaTimeContext context) - { - context.AddRange(NodaTimeData.CreateNodaTimeTypes()); - await context.SaveChangesAsync(); - } - } - - public class NodaTimeTypes - { - // ReSharper disable UnusedAutoPropertyAccessor.Global - public int Id { get; set; } - public Instant Instant { get; set; } - public LocalDateTime LocalDateTime { get; set; } - public ZonedDateTime ZonedDateTime { get; set; } - public LocalDate LocalDate { get; set; } - public LocalDate LocalDate2 { get; set; } - public LocalTime LocalTime { get; set; } - public OffsetTime OffsetTime { get; set; } - public Period Period { get; set; } - public Duration Duration { get; set; } - public DateInterval DateInterval { get; set; } - public NpgsqlRange LocalDateRange { get; set; } - public Interval Interval { get; set; } - public NpgsqlRange InstantRange { get; set; } - public long Long { get; set; } - - public string TimeZoneId { get; set; } - // ReSharper restore UnusedAutoPropertyAccessor.Global - } - - public class NodaTimeQueryNpgsqlFixture : SharedStoreFixtureBase, IQueryFixtureBase, ITestSqlLoggerFactory - { - protected override string StoreName - => "NodaTimeQueryTest"; - - // Set the PostgreSQL TimeZone parameter to something local, to ensure that operations which take TimeZone into account - // don't depend on the database's time zone, and also that operations which shouldn't take TimeZone into account indeed - // don't. - // We also instruct the test store to pass a connection string to UseNpgsql() instead of a DbConnection - that's required to allow - // EF's UseNodaTime() to function properly and instantiate an NpgsqlDataSource internally. - protected override ITestStoreFactory TestStoreFactory - => new NpgsqlTestStoreFactory(connectionStringOptions: "-c TimeZone=Europe/Berlin", useConnectionString: true); - - public TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - private NodaTimeData _expectedData; - - protected override IServiceCollection AddServices(IServiceCollection serviceCollection) - => base.AddServices(serviceCollection).AddEntityFrameworkNpgsqlNodaTime(); - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - var optionsBuilder = base.AddOptions(builder); - new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseNodaTime(); - - return optionsBuilder; - } - - protected override Task SeedAsync(NodaTimeContext context) - => NodaTimeContext.SeedAsync(context); - - public Func GetContextCreator() - => CreateContext; - - public ISetSource GetExpectedData() - => _expectedData ??= new NodaTimeData(); - - public IReadOnlyDictionary EntitySorters - => new Dictionary> { { typeof(NodaTimeTypes), e => ((NodaTimeTypes)e)?.Id } } - .ToDictionary(e => e.Key, e => (object)e.Value); - - public IReadOnlyDictionary EntityAsserters - => new Dictionary> - { - { - typeof(NodaTimeTypes), (e, a) => - { - Assert.Equal(e is null, a is null); - if (a is not null) - { - var ee = (NodaTimeTypes)e; - var aa = (NodaTimeTypes)a; - - Assert.Equal(ee.Id, aa.Id); - Assert.Equal(ee.LocalDateTime, aa.LocalDateTime); - Assert.Equal(ee.ZonedDateTime, aa.ZonedDateTime); - Assert.Equal(ee.Instant, aa.Instant); - Assert.Equal(ee.LocalDate, aa.LocalDate); - Assert.Equal(ee.LocalDate2, aa.LocalDate2); - Assert.Equal(ee.LocalTime, aa.LocalTime); - Assert.Equal(ee.OffsetTime, aa.OffsetTime); - Assert.Equal(ee.Period, aa.Period); - Assert.Equal(ee.Duration, aa.Duration); - Assert.Equal(ee.DateInterval, aa.DateInterval); - // Assert.Equal(ee.DateRange, aa.DateRange); - Assert.Equal(ee.Long, aa.Long); - Assert.Equal(ee.TimeZoneId, aa.TimeZoneId); - } - } - } - }.ToDictionary(e => e.Key, e => (object)e.Value); - } - - private class NodaTimeData : ISetSource - { - private IReadOnlyList NodaTimeTypes { get; } = CreateNodaTimeTypes(); - - public IQueryable Set() - where TEntity : class - { - if (typeof(TEntity) == typeof(NodaTimeTypes)) - { - return (IQueryable)NodaTimeTypes.AsQueryable(); - } - - throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity)); - } - - public static IReadOnlyList CreateNodaTimeTypes() - { - var localDateTime = new LocalDateTime(2018, 4, 20, 10, 31, 33, 666); - var zonedDateTime = localDateTime.InUtc(); - var instant = zonedDateTime.ToInstant(); - var duration = Duration.FromMilliseconds(20) - .Plus(Duration.FromSeconds(8)) - .Plus(Duration.FromMinutes(4)) - .Plus(Duration.FromHours(9)) - .Plus(Duration.FromDays(27)); - - return new List - { - new() - { - Id = 1, - LocalDateTime = localDateTime, - ZonedDateTime = zonedDateTime, - Instant = instant, - LocalDate = localDateTime.Date, - LocalDate2 = localDateTime.Date + Period.FromDays(1), - LocalTime = localDateTime.TimeOfDay, - OffsetTime = new OffsetTime(new LocalTime(10, 31, 33, 666), Offset.Zero), - Period = _defaultPeriod, - Duration = duration, - DateInterval = new DateInterval(localDateTime.Date, localDateTime.Date.PlusDays(4)), // inclusive - LocalDateRange = new NpgsqlRange(localDateTime.Date, localDateTime.Date.PlusDays(5)), // exclusive - Interval = new Interval(instant, instant + Duration.FromDays(5)), - InstantRange = new NpgsqlRange(instant, true, instant + Duration.FromDays(5), false), - Long = 1, - TimeZoneId = "Europe/Berlin" - } - }; - } - } - - #endregion Support -} diff --git a/test/EFCore.PG.FunctionalTests/Query/BigIntegerQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/BigIntegerTranslationsTest.cs similarity index 95% rename from test/EFCore.PG.FunctionalTests/Query/BigIntegerQueryTest.cs rename to test/EFCore.PG.FunctionalTests/Query/Translations/BigIntegerTranslationsTest.cs index 72950440a..550ab373f 100644 --- a/test/EFCore.PG.FunctionalTests/Query/BigIntegerQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/BigIntegerTranslationsTest.cs @@ -1,12 +1,12 @@ using System.Numerics; using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; +namespace Microsoft.EntityFrameworkCore.Query.Translations; -public class BigIntegerQueryTest : QueryTestBase +public class BigIntegerTranslationsTest : QueryTestBase { // ReSharper disable once UnusedParameter.Local - public BigIntegerQueryTest(BigIntegerQueryFixture fixture, ITestOutputHelper testOutputHelper) + public BigIntegerTranslationsTest(BigIntegerQueryFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.PG.FunctionalTests/Query/CitextQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/CitextTranslationsTest.cs similarity index 97% rename from test/EFCore.PG.FunctionalTests/Query/CitextQueryTest.cs rename to test/EFCore.PG.FunctionalTests/Query/Translations/CitextTranslationsTest.cs index b55d1d3d0..48b5bac58 100644 --- a/test/EFCore.PG.FunctionalTests/Query/CitextQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/CitextTranslationsTest.cs @@ -1,17 +1,17 @@ using System.ComponentModel.DataAnnotations.Schema; using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; +namespace Microsoft.EntityFrameworkCore.Query.Translations; /// /// Tests operations on the PostgreSQL citext type. /// -public class CitextQueryTest : IClassFixture +public class CitextTranslationsTest : IClassFixture { private CitextQueryFixture Fixture { get; } // ReSharper disable once UnusedParameter.Local - public CitextQueryTest(CitextQueryFixture fixture, ITestOutputHelper testOutputHelper) + public CitextTranslationsTest(CitextQueryFixture fixture, ITestOutputHelper testOutputHelper) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsTest.cs similarity index 98% rename from test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs rename to test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsTest.cs index d65e0487f..af5555c19 100644 --- a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/EnumTranslationsTest.cs @@ -1,12 +1,12 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; +namespace Microsoft.EntityFrameworkCore.Query.Translations; -public class EnumQueryTest : QueryTestBase +public class EnumTranslationsTest : QueryTestBase { // ReSharper disable once UnusedParameter.Local - public EnumQueryTest(EnumFixture fixture, ITestOutputHelper testOutputHelper) + public EnumTranslationsTest(EnumFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/LTreeTranslationsTest.cs similarity index 98% rename from test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs rename to test/EFCore.PG.FunctionalTests/Query/Translations/LTreeTranslationsTest.cs index 07a2fa682..f8a41fd21 100644 --- a/test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/LTreeTranslationsTest.cs @@ -2,15 +2,15 @@ using System.ComponentModel.DataAnnotations.Schema; using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; +namespace Microsoft.EntityFrameworkCore.Query.Translations; [MinimumPostgresVersion(13, 0)] -public class LTreeQueryTest : IClassFixture +public class LTreeTranslationsTest : IClassFixture { private LTreeQueryFixture Fixture { get; } // ReSharper disable once UnusedParameter.Local - public LTreeQueryTest(LTreeQueryFixture fixture, ITestOutputHelper testOutputHelper) + public LTreeTranslationsTest(LTreeQueryFixture fixture, ITestOutputHelper testOutputHelper) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.PG.FunctionalTests/Query/MultirangeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/MultirangeTranslationsTest.cs similarity index 98% rename from test/EFCore.PG.FunctionalTests/Query/MultirangeQueryNpgsqlTest.cs rename to test/EFCore.PG.FunctionalTests/Query/Translations/MultirangeTranslationsTest.cs index 4639d26d8..b6db39283 100644 --- a/test/EFCore.PG.FunctionalTests/Query/MultirangeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/MultirangeTranslationsTest.cs @@ -1,15 +1,15 @@ using System.ComponentModel.DataAnnotations.Schema; using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; +namespace Microsoft.EntityFrameworkCore.Query.Translations; [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14 -public class MultirangeQueryNpgsqlTest : IClassFixture +public class MultirangeTranslationsTest : IClassFixture { private MultirangeQueryNpgsqlFixture Fixture { get; } // ReSharper disable once UnusedParameter.Local - public MultirangeQueryNpgsqlTest(MultirangeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public MultirangeTranslationsTest(MultirangeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.PG.FunctionalTests/Query/NetworkQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/NetworkTranslationsNpgsqlTest.cs similarity index 99% rename from test/EFCore.PG.FunctionalTests/Query/NetworkQueryNpgsqlTest.cs rename to test/EFCore.PG.FunctionalTests/Query/Translations/NetworkTranslationsNpgsqlTest.cs index ef9b37c97..06bbb3373 100644 --- a/test/EFCore.PG.FunctionalTests/Query/NetworkQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/NetworkTranslationsNpgsqlTest.cs @@ -6,7 +6,7 @@ // ReSharper disable ConvertToConstant.Local -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; +namespace Microsoft.EntityFrameworkCore.Query.Translations; /// /// Provides unit tests for network address operator and function translations. @@ -14,12 +14,12 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; /// /// See: https://www.postgresql.org/docs/current/static/functions-net.html /// -public class NetworkQueryNpgsqlTest : IClassFixture +public class NetworkTranslationsNpgsqlTest : IClassFixture { private NetworkAddressQueryNpgsqlFixture Fixture { get; } // ReSharper disable once UnusedParameter.Local - public NetworkQueryNpgsqlTest(NetworkAddressQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public NetworkTranslationsNpgsqlTest(NetworkAddressQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/DateIntervalTranslationsTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/DateIntervalTranslationsTest.cs new file mode 100644 index 000000000..a7cbeed20 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/DateIntervalTranslationsTest.cs @@ -0,0 +1,224 @@ +using Microsoft.EntityFrameworkCore.TestModels.NodaTime; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; + +public class DateIntervalTranslationsTest : QueryTestBase +{ + public DateIntervalTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Length(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.DateInterval.Length == 5)); + + 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 upper(n."DateInterval") - lower(n."DateInterval") = 5 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Start(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.DateInterval.Start == new LocalDate(2018, 4, 20))); + + 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 lower(n."DateInterval") = DATE '2018-04-20' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task End(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.DateInterval.End == new LocalDate(2018, 4, 24))); + + 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 CAST(upper(n."DateInterval") - INTERVAL 'P1D' AS date) = DATE '2018-04-24' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task End_Select(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Select(t => t.DateInterval.End)); + + AssertSql( + """ +SELECT CAST(upper(n."DateInterval") - INTERVAL 'P1D' AS date) +FROM "NodaTimeTypes" AS n +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Contains_LocalDate(bool async) + { + var dateInterval = new DateInterval(new LocalDate(2018, 01, 01), new LocalDate(2020, 12, 25)); + + await AssertQuery( + async, + ss => ss.Set().Where(t => dateInterval.Contains(t.LocalDate))); + + AssertSql( + """ +@dateInterval='[2018-01-01, 2020-12-25]' (DbType = Object) + +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 @dateInterval @> n."LocalDate" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Contains_DateInterval(bool async) + { + var dateInterval = new DateInterval(new LocalDate(2018, 4, 22), new LocalDate(2018, 4, 24)); + + await AssertQuery( + async, + ss => ss.Set().Where(t => t.DateInterval.Contains(dateInterval))); + + AssertSql( + """ +@dateInterval='[2018-04-22, 2018-04-24]' (DbType = Object) + +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."DateInterval" @> @dateInterval +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Intersection(bool async) + { + var dateInterval = new DateInterval(new LocalDate(2018, 4, 22), new LocalDate(2018, 4, 26)); + + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.DateInterval.Intersection(dateInterval) == new DateInterval(new LocalDate(2018, 4, 22), new LocalDate(2018, 4, 24)))); + + AssertSql( + """ +@dateInterval='[2018-04-22, 2018-04-26]' (DbType = Object) + +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."DateInterval" * @dateInterval = '[2018-04-22,2018-04-24]'::daterange +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Union(bool async) + { + var dateInterval = new DateInterval(new LocalDate(2018, 4, 22), new LocalDate(2018, 4, 26)); + + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.DateInterval.Union(dateInterval) == new DateInterval(new LocalDate(2018, 4, 20), new LocalDate(2018, 4, 26)))); + + AssertSql( + """ +@dateInterval='[2018-04-22, 2018-04-26]' (DbType = Object) + +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."DateInterval" + @dateInterval = '[2018-04-20,2018-04-26]'::daterange +"""); + } + + [ConditionalTheory] + [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14 + [MemberData(nameof(IsAsyncData))] + public async Task RangeAgg(bool async) + { + await using var context = CreateContext(); + + var query = context.NodaTimeTypes + .GroupBy(x => true) + .Select(g => EF.Functions.RangeAgg(g.Select(x => x.DateInterval))); + + var union = async + ? await query.SingleAsync() + : query.Single(); + + Assert.Equal([new(new LocalDate(2018, 4, 20), new LocalDate(2018, 4, 24))], union); + + AssertSql( + """ +SELECT range_agg(n0."DateInterval") +FROM ( + SELECT n."DateInterval", TRUE AS "Key" + FROM "NodaTimeTypes" AS n +) AS n0 +GROUP BY n0."Key" +LIMIT 2 +"""); + } + + [ConditionalTheory] + [MinimumPostgresVersion(14, 0)] // range_intersect_agg was introduced in PostgreSQL 14 + [MemberData(nameof(IsAsyncData))] + public async Task Intersect_aggregate(bool async) + { + await using var context = CreateContext(); + + var query = context.NodaTimeTypes + .GroupBy(x => true) + .Select(g => EF.Functions.RangeIntersectAgg(g.Select(x => x.DateInterval))); + + var intersection = async + ? await query.SingleAsync() + : query.Single(); + + Assert.Equal(new DateInterval(new LocalDate(2018, 4, 20), new LocalDate(2018, 4, 24)), intersection); + + AssertSql( + """ +SELECT range_intersect_agg(n0."DateInterval") +FROM ( + SELECT n."DateInterval", TRUE AS "Key" + FROM "NodaTimeTypes" AS n +) AS n0 +GROUP BY n0."Key" +LIMIT 2 +"""); + } + + private NodaTimeContext CreateContext() + => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/DurationTranslationsTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/DurationTranslationsTest.cs new file mode 100644 index 000000000..57c1e06de --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/DurationTranslationsTest.cs @@ -0,0 +1,206 @@ +using Microsoft.EntityFrameworkCore.TestModels.NodaTime; +using NodaTime; + +namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; + +public class DurationTranslationsTest : QueryTestBase +{ + public DurationTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task TotalDays(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.TotalDays > 27)); + + 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 date_part('epoch', n."Duration") / 86400.0 > 27.0 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task TotalHours(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.TotalHours < 700)); + + 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 date_part('epoch', n."Duration") / 3600.0 < 700.0 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task TotalMinutes(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.TotalMinutes < 40000)); + + 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 date_part('epoch', n."Duration") / 60.0 < 40000.0 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task TotalSeconds(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.TotalSeconds == 2365448.02)); + + 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 date_part('epoch', n."Duration") = 2365448.02 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task TotalMilliseconds(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.TotalMilliseconds == 2365448020)); + + 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 date_part('epoch', n."Duration") / 0.001 = 2365448020.0 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Days(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.Days == 27)); + + 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 date_part('day', n."Duration")::int = 27 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Hours(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.Hours == 9)); + + 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 date_part('hour', n."Duration")::int = 9 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Minutes(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.Minutes == 4)); + + 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 date_part('minute', n."Duration")::int = 4 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Seconds(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Duration.Seconds == 8)); + + 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 floor(date_part('second', n."Duration"))::int = 8 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task GroupBy_Property_Select_Sum_over_Duration(bool async) + { + await AssertQueryScalar( + async, + ss => ss.Set() + .GroupBy(o => o.Id) + .Select(g => EF.Functions.Sum(g.Select(o => o.Duration))), + expectedQuery: ss => ss.Set() + .GroupBy(o => o.Id) + .Select(g => (Duration?)Duration.FromTicks(g.Sum(o => o.Duration.TotalTicks)))); + + AssertSql( + """ +SELECT sum(n."Duration") +FROM "NodaTimeTypes" AS n +GROUP BY n."Id" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task GroupBy_Property_Select_Average_over_Duration(bool async) + { + await AssertQueryScalar( + async, + ss => ss.Set() + .GroupBy(o => o.Id) + .Select(g => EF.Functions.Average(g.Select(o => o.Duration))), + expectedQuery: ss => ss.Set() + .GroupBy(o => o.Id) + .Select(g => (Duration?)Duration.FromTicks((long)g.Average(o => o.Duration.TotalTicks)))); + + AssertSql( + """ +SELECT avg(n."Duration") +FROM "NodaTimeTypes" AS n +GROUP BY n."Id" +"""); + } + + private NodaTimeContext CreateContext() + => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/InstantTranslationsTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/InstantTranslationsTest.cs new file mode 100644 index 000000000..8081f36d8 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/InstantTranslationsTest.cs @@ -0,0 +1,173 @@ +using Microsoft.EntityFrameworkCore.TestModels.NodaTime; +using NodaTime; + +namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; + +public class InstantTranslationsTest : QueryTestBase +{ + public InstantTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Subtract_Duration(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Instant + Duration.FromDays(1) - t.Instant == Duration.FromDays(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."Instant" + INTERVAL '1 00:00:00') - n."Instant" = INTERVAL '1 00:00:00' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task InUtc(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.Instant.InUtc() + == new ZonedDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33, 666), DateTimeZone.Utc, Offset.Zero))); + + AssertSql( + """ +@p='2018-04-20T10:31:33 UTC (+00)' (DbType = DateTime) + +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."Instant" = @p +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task InZone_constant_LocalDateTime(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.Instant.InZone(DateTimeZoneProviders.Tzdb["Europe/Berlin"]).LocalDateTime + == new LocalDateTime(2018, 4, 20, 12, 31, 33, 666))); + + 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."Instant" AT TIME ZONE 'Europe/Berlin' = TIMESTAMP '2018-04-20T12:31:33.666' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task InZone_constant_Date(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.Instant.InZone(DateTimeZoneProviders.Tzdb["Europe/Berlin"]).Date == new LocalDate(2018, 4, 20))); + + 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 CAST(n."Instant" AT TIME ZONE 'Europe/Berlin' AS date) = DATE '2018-04-20' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task InZone_parameter_LocalDateTime(bool async) + { + var timeZone = DateTimeZoneProviders.Tzdb["Europe/Berlin"]; + + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.Instant.InZone(timeZone).LocalDateTime == new LocalDateTime(2018, 4, 20, 12, 31, 33, 666))); + + AssertSql( + """ +@timeZone='Europe/Berlin' + +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."Instant" AT TIME ZONE @timeZone = TIMESTAMP '2018-04-20T12:31:33.666' +"""); + } + + [ConditionalFact] + public async Task InZone_without_LocalDateTime_fails() + { + await using var ctx = CreateContext(); + + await Assert.ThrowsAsync( + () => ctx.Set().Where(t => t.Instant.InZone(DateTimeZoneProviders.Tzdb["Europe/Berlin"]) == default) + .ToListAsync()); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task ToDateTimeUtc(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(t => t.Instant.ToDateTimeUtc() == new DateTime(2018, 4, 20, 10, 31, 33, 666, DateTimeKind.Utc))); + + 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."Instant"::timestamptz = TIMESTAMPTZ '2018-04-20T10:31:33.666Z' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task GetCurrentInstant_from_Instance(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Instant < SystemClock.Instance.GetCurrentInstant())); + + 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."Instant" < NOW() +"""); + } + + [ConditionalFact] + public async Task Distance() + { + await using var context = CreateContext(); + var closest = await context.NodaTimeTypes + .OrderBy(t => EF.Functions.Distance(t.Instant, new LocalDateTime(2018, 4, 1, 0, 0, 0).InUtc().ToInstant())).FirstAsync(); + + Assert.Equal(1, closest.Id); + + 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 +ORDER BY n."Instant" <-> TIMESTAMPTZ '2018-04-01T00:00:00Z' NULLS FIRST +LIMIT 1 +"""); + } + + private NodaTimeContext CreateContext() + => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/IntervalTranslationsTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/IntervalTranslationsTest.cs new file mode 100644 index 000000000..e794a036d --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/IntervalTranslationsTest.cs @@ -0,0 +1,184 @@ +using Microsoft.EntityFrameworkCore.TestModels.NodaTime; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; + +public class IntervalTranslationsTest : QueryTestBase +{ + public IntervalTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Interval_Start(bool async) + { + await AssertQuery( + async, + ss => ss.Set() + .Where(t => t.Interval.Start == new LocalDateTime(2018, 4, 20, 10, 31, 33, 666).InUtc().ToInstant())); + + 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 lower(n."Interval") = TIMESTAMPTZ '2018-04-20T10:31:33.666Z' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Interval_End(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Interval.End == new LocalDateTime(2018, 4, 25, 10, 31, 33, 666).InUtc().ToInstant())); + + 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 upper(n."Interval") = TIMESTAMPTZ '2018-04-25T10:31:33.666Z' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Interval_HasStart(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Interval.HasStart)); + + 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 NOT (lower_inf(n."Interval")) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Interval_HasEnd(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Interval.HasEnd)); + + 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 NOT (upper_inf(n."Interval")) +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Interval_Duration(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Interval.Duration == Duration.FromDays(5))); + + 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 upper(n."Interval") - lower(n."Interval") = INTERVAL '5 00:00:00' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Interval_Contains_Instant(bool async) + { + var interval = new Interval( + new LocalDateTime(2018, 01, 01, 0, 0, 0).InUtc().ToInstant(), + new LocalDateTime(2020, 12, 25, 0, 0, 0).InUtc().ToInstant()); + + await AssertQuery( + async, + ss => ss.Set().Where(t => interval.Contains(t.Instant))); + + AssertSql( + """ +@interval='2018-01-01T00:00:00Z/2020-12-25T00:00:00Z' (DbType = Object) + +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 @interval @> n."Instant" +"""); + } + + [ConditionalTheory] + [MinimumPostgresVersion(14, 0)] // Multiranges were introduced in PostgreSQL 14 + [MemberData(nameof(IsAsyncData))] + public async Task Interval_RangeAgg(bool async) + { + await using var context = CreateContext(); + + var query = context.NodaTimeTypes + .GroupBy(x => true) + .Select(g => EF.Functions.RangeAgg(g.Select(x => x.Interval))); + + var union = async + ? await query.SingleAsync() + : query.Single(); + + var start = Instant.FromUtc(2018, 4, 20, 10, 31, 33).Plus(Duration.FromMilliseconds(666)); + Assert.Equal([new(start, start + Duration.FromDays(5))], union); + + AssertSql( + """ +SELECT range_agg(n0."Interval") +FROM ( + SELECT n."Interval", TRUE AS "Key" + FROM "NodaTimeTypes" AS n +) AS n0 +GROUP BY n0."Key" +LIMIT 2 +"""); + } + + [ConditionalTheory] + [MinimumPostgresVersion(14, 0)] // range_intersect_agg was introduced in PostgreSQL 14 + [MemberData(nameof(IsAsyncData))] + public async Task Interval_Intersect_aggregate(bool async) + { + await using var context = CreateContext(); + + var query = context.NodaTimeTypes + .GroupBy(x => true) + .Select(g => EF.Functions.RangeIntersectAgg(g.Select(x => x.Interval))); + + var intersection = async + ? await query.SingleAsync() + : query.Single(); + + var start = Instant.FromUtc(2018, 4, 20, 10, 31, 33).Plus(Duration.FromMilliseconds(666)); + Assert.Equal(new Interval(start, start + Duration.FromDays(5)), intersection); + + AssertSql( + """ +SELECT range_intersect_agg(n0."Interval") +FROM ( + SELECT n."Interval", TRUE AS "Key" + FROM "NodaTimeTypes" AS n +) AS n0 +GROUP BY n0."Key" +LIMIT 2 +"""); + } + + private NodaTimeContext CreateContext() + => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalDateTimeTranslationsTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalDateTimeTranslationsTest.cs new file mode 100644 index 000000000..212c0a558 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalDateTimeTranslationsTest.cs @@ -0,0 +1,258 @@ +using Microsoft.EntityFrameworkCore.TestModels.NodaTime; +using NodaTime; + +namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; + +public class LocalDateTimeTranslationsTest : QueryTestBase +{ + public LocalDateTimeTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Subtract_LocalDateTime(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime + Period.FromDays(1) - t.LocalDateTime == Period.FromDays(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" + INTERVAL 'P1D') - n."LocalDateTime" = INTERVAL 'P1D' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Year(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.Year == 2018)); + + 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 date_part('year', n."LocalDateTime")::int = 2018 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Month(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.Month == 4)); + + 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 date_part('month', n."LocalDateTime")::int = 4 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task DayOfYear(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.DayOfYear == 110)); + + 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 date_part('doy', n."LocalDateTime")::int = 110 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Day(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.Day == 20)); + + 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 date_part('day', n."LocalDateTime")::int = 20 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Hour(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.Hour == 10)); + + 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 date_part('hour', n."LocalDateTime")::int = 10 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Minute(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.Minute == 31)); + + 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 date_part('minute', n."LocalDateTime")::int = 31 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Second(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.Second == 33)); + + 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 floor(date_part('second', n."LocalDateTime"))::int = 33 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Date(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.Date == new LocalDate(2018, 4, 20))); + + 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"::date = DATE '2018-04-20' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Time(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.TimeOfDay == new LocalTime(10, 31, 33, 666))); + + 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 DayOfWeek(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDateTime.DayOfWeek == IsoDayOfWeek.Friday)); + + 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 CASE floor(date_part('dow', n."LocalDateTime"))::int + WHEN 0 THEN 7 + ELSE floor(date_part('dow', n."LocalDateTime"))::int +END = 5 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task InZoneLeniently_ToInstant(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.LocalDateTime.InZoneLeniently(DateTimeZoneProviders.Tzdb["Europe/Berlin"]).ToInstant() + == new ZonedDateTime(new LocalDateTime(2018, 4, 20, 8, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); + + AssertSql( + """ +@ToInstant='2018-04-20T08:31:33Z' (DbType = DateTime) + +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" AT TIME ZONE 'Europe/Berlin' = @ToInstant +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task InZoneLeniently_ToInstant_with_column_time_zone(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.LocalDateTime.InZoneLeniently(DateTimeZoneProviders.Tzdb[t.TimeZoneId]).ToInstant() + == new ZonedDateTime( + new LocalDateTime(2018, 4, 20, 8, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); + + AssertSql( + """ +@ToInstant='2018-04-20T08:31:33Z' (DbType = DateTime) + +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" AT TIME ZONE n."TimeZoneId" = @ToInstant +"""); + } + + [ConditionalFact] + public async Task Distance() + { + await using var context = CreateContext(); + var closest = await context.NodaTimeTypes + .OrderBy(t => EF.Functions.Distance(t.LocalDateTime, new LocalDateTime(2018, 4, 1, 0, 0, 0))).FirstAsync(); + + Assert.Equal(1, closest.Id); + + 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 +ORDER BY n."LocalDateTime" <-> TIMESTAMP '2018-04-01T00:00:00' NULLS FIRST +LIMIT 1 +"""); + } + + private NodaTimeContext CreateContext() + => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalDateTranslationsTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalDateTranslationsTest.cs new file mode 100644 index 000000000..b38cc4250 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalDateTranslationsTest.cs @@ -0,0 +1,185 @@ +using Microsoft.EntityFrameworkCore.TestModels.NodaTime; +using NodaTime; + +namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; + +public class LocalDateTranslationsTest : QueryTestBase +{ + public LocalDateTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Add_Period(bool async) + { + // Note: requires some special type inference logic because we're adding things of different types + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate + Period.FromMonths(1) > t.LocalDate)); + + 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."LocalDate" + INTERVAL 'P1M' > n."LocalDate" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Subtract_LocalDate_column(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate2 - t.LocalDate == Period.FromDays(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 make_interval(days => n."LocalDate2" - n."LocalDate") = INTERVAL 'P1D' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Subtract_LocalDate_parameter(bool async) + { + var date = new LocalDate(2018, 4, 20); + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate2 - date == Period.FromDays(1))); + + AssertSql( + """ +@date='Friday, 20 April 2018' (DbType = Date) + +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 make_interval(days => n."LocalDate2" - @date) = INTERVAL 'P1D' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LessThan(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate < new LocalDate(2018, 4, 21))); + + 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."LocalDate" < DATE '2018-04-21' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Subtract_LocalDate_constant(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate2 - new LocalDate(2018, 4, 20) == Period.FromDays(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 make_interval(days => n."LocalDate2" - DATE '2018-04-20') = INTERVAL 'P1D' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Year(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate.Year == 2018)); + + 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 date_part('year', n."LocalDate")::int = 2018 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Month(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate.Month == 4)); + + 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 date_part('month', n."LocalDate")::int = 4 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task DayOrYear(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate.DayOfYear == 110)); + + 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 date_part('doy', n."LocalDate")::int = 110 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Day(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalDate.Day == 20)); + + 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 date_part('day', n."LocalDate")::int = 20 +"""); + } + + [ConditionalFact] + public async Task Distance() + { + await using var context = CreateContext(); + var closest = await context.NodaTimeTypes.OrderBy(t => EF.Functions.Distance(t.LocalDate, new LocalDate(2018, 4, 1))).FirstAsync(); + + Assert.Equal(1, closest.Id); + + 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 +ORDER BY n."LocalDate" <-> DATE '2018-04-01' NULLS FIRST +LIMIT 1 +"""); + } + + private NodaTimeContext CreateContext() + => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalTimeTranslationsTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalTimeTranslationsTest.cs new file mode 100644 index 000000000..f5399b1c5 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/LocalTimeTranslationsTest.cs @@ -0,0 +1,81 @@ +using Microsoft.EntityFrameworkCore.TestModels.NodaTime; +using NodaTime; + +namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; + +public class LocalTimeTranslationsTest : QueryTestBase +{ + public LocalTimeTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Subtract_LocalTime(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalTime + Period.FromHours(1) - t.LocalTime == Period.FromHours(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."LocalTime" + INTERVAL 'PT1H') - n."LocalTime" = INTERVAL 'PT1H' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Hour(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalTime.Hour == 10)); + + 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 date_part('hour', n."LocalTime")::int = 10 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Minute(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalTime.Minute == 31)); + + 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 date_part('minute', n."LocalTime")::int = 31 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Second(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.LocalTime.Second == 33)); + + 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 floor(date_part('second', n."LocalTime"))::int = 33 +"""); + } + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/NodaTimeQueryNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/NodaTimeQueryNpgsqlFixture.cs new file mode 100644 index 000000000..cb4a2edf6 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/NodaTimeQueryNpgsqlFixture.cs @@ -0,0 +1,80 @@ +using Microsoft.EntityFrameworkCore.TestModels.NodaTime; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; + +public class NodaTimeQueryNpgsqlFixture : SharedStoreFixtureBase, IQueryFixtureBase, ITestSqlLoggerFactory +{ + protected override string StoreName + => "NodaTimeQueryTest"; + + // Set the PostgreSQL TimeZone parameter to something local, to ensure that operations which take TimeZone into account + // don't depend on the database's time zone, and also that operations which shouldn't take TimeZone into account indeed + // don't. + // We also instruct the test store to pass a connection string to UseNpgsql() instead of a DbConnection - that's required to allow + // EF's UseNodaTime() to function properly and instantiate an NpgsqlDataSource internally. + protected override ITestStoreFactory TestStoreFactory + => new NpgsqlTestStoreFactory(connectionStringOptions: "-c TimeZone=Europe/Berlin", useConnectionString: true); + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + private NodaTimeData _expectedData; + + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + => base.AddServices(serviceCollection).AddEntityFrameworkNpgsqlNodaTime(); + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + var optionsBuilder = base.AddOptions(builder); + new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseNodaTime(); + + return optionsBuilder; + } + + protected override Task SeedAsync(NodaTimeContext context) + => NodaTimeContext.SeedAsync(context); + + public Func GetContextCreator() + => CreateContext; + + public ISetSource GetExpectedData() + => _expectedData ??= new NodaTimeData(); + + public IReadOnlyDictionary EntitySorters + => new Dictionary> { { typeof(NodaTimeTypes), e => ((NodaTimeTypes)e)?.Id } } + .ToDictionary(e => e.Key, e => (object)e.Value); + + public IReadOnlyDictionary EntityAsserters + => new Dictionary> + { + { + typeof(NodaTimeTypes), (e, a) => + { + Assert.Equal(e is null, a is null); + if (a is not null) + { + var ee = (NodaTimeTypes)e; + var aa = (NodaTimeTypes)a; + + Assert.Equal(ee.Id, aa.Id); + Assert.Equal(ee.LocalDateTime, aa.LocalDateTime); + Assert.Equal(ee.ZonedDateTime, aa.ZonedDateTime); + Assert.Equal(ee.Instant, aa.Instant); + Assert.Equal(ee.LocalDate, aa.LocalDate); + Assert.Equal(ee.LocalDate2, aa.LocalDate2); + Assert.Equal(ee.LocalTime, aa.LocalTime); + Assert.Equal(ee.OffsetTime, aa.OffsetTime); + Assert.Equal(ee.Period, aa.Period); + Assert.Equal(ee.Duration, aa.Duration); + Assert.Equal(ee.DateInterval, aa.DateInterval); + // Assert.Equal(ee.DateRange, aa.DateRange); + Assert.Equal(ee.Long, aa.Long); + Assert.Equal(ee.TimeZoneId, aa.TimeZoneId); + } + } + } + }.ToDictionary(e => e.Key, e => (object)e.Value); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/PeriodTranslationsTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/PeriodTranslationsTest.cs new file mode 100644 index 000000000..dda9e79a1 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/PeriodTranslationsTest.cs @@ -0,0 +1,387 @@ +using Microsoft.EntityFrameworkCore.TestModels.NodaTime; +using NodaTime; + +namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; + +public class PeriodTranslationsTest : QueryTestBase +{ + public PeriodTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Years(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Period.Years == 2018)); + + 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 date_part('year', n."Period")::int = 2018 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Months(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Period.Months == 4)); + + 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 date_part('month', n."Period")::int = 4 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Days(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Period.Days == 20)); + + 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 date_part('day', n."Period")::int = 20 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Hours(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Period.Hours == 10)); + + 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 date_part('hour', n."Period")::int = 10 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Minutes(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Period.Minutes == 31)); + + 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 date_part('minute', n."Period")::int = 31 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Seconds(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Period.Seconds == 23)); + + 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 floor(date_part('second', n."Period"))::int = 23 +"""); + } + + // PostgreSQL does not support extracting weeks from intervals + [ConditionalFact] + public Task Weeks_is_not_translated() + { + using var ctx = CreateContext(); + + return AssertTranslationFailed( + () => ctx.Set().Where(t => t.Period.Weeks == 0).ToListAsync()); + } + + [ConditionalFact] + public Task Milliseconds_is_not_translated() + { + using var ctx = CreateContext(); + + return AssertTranslationFailed( + () => ctx.Set().Where(t => t.Period.Nanoseconds == 0).ToListAsync()); + } + + [ConditionalFact] + public Task Nanoseconds_is_not_translated() + { + using var ctx = CreateContext(); + + return AssertTranslationFailed( + () => ctx.Set().Where(t => t.Period.Nanoseconds == 0).ToListAsync()); + } + + [ConditionalFact] + public Task Ticks_is_not_translated() + { + using var ctx = CreateContext(); + + return AssertTranslationFailed( + () => ctx.Set().Where(t => t.Period.Ticks == 0).ToListAsync()); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task FromYears(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromYears(t.Id).Years == 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 date_part('year', make_interval(years => n."Id"))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task FromMonths(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromMonths(t.Id).Months == 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 date_part('month', make_interval(months => n."Id"))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task FromWeeks(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromWeeks(t.Id).Days == 7), + ss => ss.Set().Where(t => Period.FromWeeks(t.Id).Normalize().Days == 7)); + + 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 date_part('day', make_interval(weeks => n."Id"))::int = 7 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task FromDays(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromDays(t.Id).Days == 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 date_part('day', make_interval(days => n."Id"))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task FromHours_int(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromHours(t.Id).Hours == 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 date_part('hour', make_interval(hours => n."Id"))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task FromHours_long(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromHours(t.Long).Hours == 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 date_part('hour', make_interval(hours => n."Long"::int))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task FromMinutes_int(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromMinutes(t.Id).Minutes == 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 date_part('minute', make_interval(mins => n."Id"))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task FromMinutes_long(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromMinutes(t.Long).Minutes == 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 date_part('minute', make_interval(mins => n."Long"::int))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task FromSeconds_int(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromSeconds(t.Id).Seconds == 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 floor(date_part('second', make_interval(secs => n."Id"::bigint::double precision)))::int = 1 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task FromSeconds_long(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => Period.FromSeconds(t.Long).Seconds == 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 floor(date_part('second', make_interval(secs => n."Long"::double precision)))::int = 1 +"""); + } + + [ConditionalFact] + public Task FromMilliseconds_is_not_translated() + { + using var ctx = CreateContext(); + + return AssertTranslationFailed( + () => ctx.Set().Where(t => Period.FromMilliseconds(t.Id).Seconds == 1).ToListAsync()); + } + + [ConditionalFact] + public Task FromNanoseconds_is_not_translated() + { + using var ctx = CreateContext(); + + return AssertTranslationFailed( + () => ctx.Set().Where(t => Period.FromNanoseconds(t.Id).Seconds == 1).ToListAsync()); + } + + [ConditionalFact] + public Task FromTicks_is_not_translated() + { + using var ctx = CreateContext(); + + return AssertTranslationFailed( + () => ctx.Set().Where(t => Period.FromNanoseconds(t.Id).Seconds == 1).ToListAsync()); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task GroupBy_Property_Select_Sum_over_Period(bool async) + { + await using var ctx = CreateContext(); + + // Note: Unlike Duration, Period can't be converted to total ticks (because its absolute time varies). + var query = ctx.Set() + .GroupBy(o => o.Id) + .Select(g => EF.Functions.Sum(g.Select(o => o.Period))); + + _ = async + ? await query.ToListAsync() + : query.ToList(); + + AssertSql( + """ +SELECT sum(n."Period") +FROM "NodaTimeTypes" AS n +GROUP BY n."Id" +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task GroupBy_Property_Select_Average_over_Period(bool async) + { + await using var ctx = CreateContext(); + + // Note: Unlike Duration, Period can't be converted to total ticks (because its absolute time varies). + var query = ctx.Set() + .GroupBy(o => o.Id) + .Select(g => EF.Functions.Average(g.Select(o => o.Period))); + + _ = async + ? await query.ToListAsync() + : query.ToList(); + + AssertSql( + """ +SELECT avg(n."Period") +FROM "NodaTimeTypes" AS n +GROUP BY n."Id" +"""); + } + + private NodaTimeContext CreateContext() + => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/ZonedDateTimeTranslationsTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/ZonedDateTimeTranslationsTest.cs new file mode 100644 index 000000000..6811b15bc --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/NodaTime/ZonedDateTimeTranslationsTest.cs @@ -0,0 +1,242 @@ +using Microsoft.EntityFrameworkCore.TestModels.NodaTime; +using NodaTime; + +namespace Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; + +public class ZonedDateTimeTranslationsTest : QueryTestBase +{ + public ZonedDateTimeTranslationsTest(NodaTimeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Subtract_ZonedDateTime(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime + Duration.FromDays(1) - t.ZonedDateTime == Duration.FromDays(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."ZonedDateTime" + INTERVAL '1 00:00:00') - n."ZonedDateTime" = INTERVAL '1 00:00:00' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Year(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.Year == 2018)); + + 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 date_part('year', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 2018 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Month(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.Month == 4)); + + 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 date_part('month', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 4 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task DayOfYear(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.DayOfYear == 110)); + + 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 date_part('doy', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 110 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Day(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.Day == 20)); + + 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 date_part('day', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 20 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Hour(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.Hour == 10)); + + 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 date_part('hour', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 10 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Minute(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.Minute == 31)); + + 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 date_part('minute', n."ZonedDateTime" AT TIME ZONE 'UTC')::int = 31 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Second(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.Second == 33)); + + 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 floor(date_part('second', n."ZonedDateTime" AT TIME ZONE 'UTC'))::int = 33 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task Date(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.Date == new LocalDate(2018, 4, 20))); + + 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 CAST(n."ZonedDateTime" AT TIME ZONE 'UTC' AS date) = DATE '2018-04-20' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task DayOfWeek(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.ZonedDateTime.DayOfWeek == IsoDayOfWeek.Friday)); + + 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 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 +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task LocalDateTime(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where(t => t.Instant.InUtc().LocalDateTime == new LocalDateTime(2018, 4, 20, 10, 31, 33, 666))); + + 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."Instant" AT TIME ZONE 'UTC' = TIMESTAMP '2018-04-20T10:31:33.666' +"""); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public async Task ToInstant(bool async) + { + await AssertQuery( + async, + ss => ss.Set().Where( + t => t.ZonedDateTime.ToInstant() + == new ZonedDateTime(new LocalDateTime(2018, 4, 20, 10, 31, 33, 666), DateTimeZone.Utc, Offset.Zero).ToInstant())); + + AssertSql( + """ +@ToInstant='2018-04-20T10:31:33Z' (DbType = DateTime) + +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."ZonedDateTime" = @ToInstant +"""); + } + + [ConditionalFact] + public async Task Distance() + { + await using var context = CreateContext(); + + var closest = await context.NodaTimeTypes + .OrderBy( + t => EF.Functions.Distance( + t.ZonedDateTime, + new ZonedDateTime(new LocalDateTime(2018, 4, 1, 0, 0, 0), DateTimeZone.Utc, Offset.Zero))).FirstAsync(); + Assert.Equal(1, closest.Id); + + AssertSql( + """ +@p='2018-04-01T00:00:00 UTC (+00)' (DbType = DateTime) + +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 +ORDER BY n."ZonedDateTime" <-> @p NULLS FIRST +LIMIT 1 +"""); + } + + private NodaTimeContext CreateContext() + => Fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +} diff --git a/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/RangeTranslationsTest.cs similarity index 98% rename from test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs rename to test/EFCore.PG.FunctionalTests/Query/Translations/RangeTranslationsTest.cs index 6fe12c0f5..e50c963e4 100644 --- a/test/EFCore.PG.FunctionalTests/Query/RangeQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/RangeTranslationsTest.cs @@ -4,15 +4,15 @@ // ReSharper disable InconsistentNaming -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; +namespace Microsoft.EntityFrameworkCore.Query.Translations; // Note: timestamp range tests are in TimestampQueryTest -public class RangeQueryNpgsqlTest : IClassFixture +public class RangeTranslationsTest : IClassFixture { private RangeQueryNpgsqlFixture Fixture { get; } // ReSharper disable once UnusedParameter.Local - public RangeQueryNpgsqlTest(RangeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public RangeTranslationsTest(RangeQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/TimestampTranslationsTest.cs similarity index 99% rename from test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs rename to test/EFCore.PG.FunctionalTests/Query/Translations/TimestampTranslationsTest.cs index 8bb5d10b9..83392d987 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TimestampQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/TimestampTranslationsTest.cs @@ -9,12 +9,12 @@ // But there's no DateTime mapped to 'timestamp with time zone' (so Utc). // This test suite checks this and various PG-specific things. -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; +namespace Microsoft.EntityFrameworkCore.Query.Translations; -public class TimestampQueryTest : QueryTestBase +public class TimestampTranslationsTest : QueryTestBase { // ReSharper disable once UnusedParameter.Local - public TimestampQueryTest(TimestampQueryFixture fixture, ITestOutputHelper testOutputHelper) + public TimestampTranslationsTest(TimestampQueryFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) { Fixture.TestSqlLoggerFactory.Clear(); diff --git a/test/EFCore.PG.FunctionalTests/Query/TrigramsQueryNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Query/Translations/TrigramsTranslationsTest.cs similarity index 96% rename from test/EFCore.PG.FunctionalTests/Query/TrigramsQueryNpgsqlTest.cs rename to test/EFCore.PG.FunctionalTests/Query/Translations/TrigramsTranslationsTest.cs index 12f3668e0..64d2591d5 100644 --- a/test/EFCore.PG.FunctionalTests/Query/TrigramsQueryNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/Translations/TrigramsTranslationsTest.cs @@ -1,7 +1,7 @@ using System.ComponentModel.DataAnnotations; using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; +namespace Microsoft.EntityFrameworkCore.Query.Translations; /// /// Provides unit tests for the pg_trgm module operator and function translations. @@ -9,20 +9,18 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query; /// /// See: https://www.postgresql.org/docs/current/pgtrgm.html /// -public class TrigramsQueryNpgsqlTest : IClassFixture +public class TrigramsTranslationsTest : IClassFixture { private TrigramsQueryNpgsqlFixture Fixture { get; } // ReSharper disable once UnusedParameter.Local - public TrigramsQueryNpgsqlTest(TrigramsQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) + public TrigramsTranslationsTest(TrigramsQueryNpgsqlFixture fixture, ITestOutputHelper testOutputHelper) { Fixture = fixture; Fixture.TestSqlLoggerFactory.Clear(); Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); } - #region FunctionTests - [Fact] public void TrigramsShow() { @@ -214,8 +212,6 @@ public void PgUnknownBinary_operator_precedence() """); } - #endregion - #region Fixtures /// diff --git a/test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeContext.cs b/test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeContext.cs new file mode 100644 index 000000000..44a00329f --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeContext.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; + +namespace Microsoft.EntityFrameworkCore.TestModels.NodaTime; + +public class NodaTimeContext(DbContextOptions options) : PoolableDbContext(options) +{ + // ReSharper disable once MemberHidesStaticFromOuterClass + // ReSharper disable once UnusedAutoPropertyAccessor.Global + public DbSet NodaTimeTypes { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.HasPostgresExtension("btree_gist"); + } + + public static async Task SeedAsync(NodaTimeContext context) + { + context.AddRange(NodaTimeData.CreateNodaTimeTypes()); + await context.SaveChangesAsync(); + } +} diff --git a/test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeData.cs b/test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeData.cs new file mode 100644 index 000000000..e39fa2578 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeData.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore.Query.Translations.NodaTime; +using NodaTime; + +namespace Microsoft.EntityFrameworkCore.TestModels.NodaTime; + +internal class NodaTimeData : ISetSource +{ + private static readonly Period DefaultPeriod = Period.FromYears(2018) + + Period.FromMonths(4) + + Period.FromDays(20) + + Period.FromHours(10) + + Period.FromMinutes(31) + + Period.FromSeconds(23) + + Period.FromMilliseconds(666); + + private IReadOnlyList NodaTimeTypes { get; } = CreateNodaTimeTypes(); + + public IQueryable Set() + where TEntity : class + { + if (typeof(TEntity) == typeof(NodaTimeTypes)) + { + return (IQueryable)NodaTimeTypes.AsQueryable(); + } + + throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity)); + } + + public static IReadOnlyList CreateNodaTimeTypes() + { + var localDateTime = new LocalDateTime(2018, 4, 20, 10, 31, 33, 666); + var zonedDateTime = localDateTime.InUtc(); + var instant = zonedDateTime.ToInstant(); + var duration = Duration.FromMilliseconds(20) + .Plus(Duration.FromSeconds(8)) + .Plus(Duration.FromMinutes(4)) + .Plus(Duration.FromHours(9)) + .Plus(Duration.FromDays(27)); + + return new List + { + new() + { + Id = 1, + LocalDateTime = localDateTime, + ZonedDateTime = zonedDateTime, + Instant = instant, + LocalDate = localDateTime.Date, + LocalDate2 = localDateTime.Date + Period.FromDays(1), + LocalTime = localDateTime.TimeOfDay, + OffsetTime = new OffsetTime(new LocalTime(10, 31, 33, 666), Offset.Zero), + Period = DefaultPeriod, + Duration = duration, + DateInterval = new DateInterval(localDateTime.Date, localDateTime.Date.PlusDays(4)), // inclusive + LocalDateRange = new NpgsqlRange(localDateTime.Date, localDateTime.Date.PlusDays(5)), // exclusive + Interval = new Interval(instant, instant + Duration.FromDays(5)), + InstantRange = new NpgsqlRange(instant, true, instant + Duration.FromDays(5), false), + Long = 1, + TimeZoneId = "Europe/Berlin" + } + }; + } +} diff --git a/test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeTypes.cs b/test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeTypes.cs new file mode 100644 index 000000000..2a1087d8a --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/TestModels/NodaTime/NodaTimeTypes.cs @@ -0,0 +1,27 @@ +using NodaTime; + +namespace Microsoft.EntityFrameworkCore.TestModels.NodaTime; + +public class NodaTimeTypes +{ + // ReSharper disable UnusedAutoPropertyAccessor.Global + public int Id { get; set; } + public Instant Instant { get; set; } + public LocalDateTime LocalDateTime { get; set; } + public ZonedDateTime ZonedDateTime { get; set; } + public LocalDate LocalDate { get; set; } + public LocalDate LocalDate2 { get; set; } + public LocalTime LocalTime { get; set; } + public OffsetTime OffsetTime { get; set; } + public Period Period { get; set; } + public Duration Duration { get; set; } + public DateInterval DateInterval { get; set; } + public NpgsqlRange LocalDateRange { get; set; } + public Interval Interval { get; set; } + public NpgsqlRange InstantRange { get; set; } + public long Long { get; set; } + + public string TimeZoneId { get; set; } + // ReSharper restore UnusedAutoPropertyAccessor.Global +} +