From a3bcce97ca11dbe7135dacb732695ec18d40885c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Gro=C3=9F?= Date: Sun, 25 Oct 2026 20:11:46 +0100 Subject: [PATCH] test: a second calendar seed for the refusals, and a join on a period (#451) - calendar-iso-week-53-of-a-52-week-year-rolls-over.patch lets the week check accept one week too many, so IsoWeek(2025, 53) rolls over to 2026-W01. It fails CalendarFactories_OverAnArgumentMemoryRefuses_RaiseInPostgres alone: the half of CalendarFactoryParityTests no seed proved. - CalendarFactories_AsAJoinKey_PairTheRowsMemoryPairs joins a date form against an integer form (Month, Quarter, Year) and requires the first and last periods to find a partner. E76's claim names the join; the docs already promised it. - The existing seed lost its doubled diff --git line, and its Fails line names the join, which it now also fails. `dotnet run tools/oracles.cs seeds calendar`: every seed is caught. Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 23d943949030e22ec69622dc9262b6af546c303e) --- .../CalendarFactoryParityTests.cs | 30 +++++++++++++++++++ test/oracles/claims.json | 2 +- ...week-53-of-a-52-week-year-rolls-over.patch | 18 +++++++++++ ...st-period-ends-on-a-day-not-infinity.patch | 3 +- 4 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 test/oracles/seeds/calendar-iso-week-53-of-a-52-week-year-rolls-over.patch diff --git a/test/CodoMetis.ValueRanges.EFCore.PostgreSQL.IntegrationTests/CalendarFactoryParityTests.cs b/test/CodoMetis.ValueRanges.EFCore.PostgreSQL.IntegrationTests/CalendarFactoryParityTests.cs index ed8226d..5e56df7 100644 --- a/test/CodoMetis.ValueRanges.EFCore.PostgreSQL.IntegrationTests/CalendarFactoryParityTests.cs +++ b/test/CodoMetis.ValueRanges.EFCore.PostgreSQL.IntegrationTests/CalendarFactoryParityTests.cs @@ -121,6 +121,36 @@ async Task Agree(string label, Expression> key, bo await Agree("OrderBy(IsoWeek(WeekYear, Week))", r => DateRange.IsoWeek(r.WeekYear, r.Week), descending: false); } + /// + /// "Filter, join and sort by it" (#451): a join on a period pairs the rows memory pairs, the first and last + /// periods included, where a date form meets an integer form. + /// + [TestMethod] + public async Task CalendarFactories_AsAJoinKey_PairTheRowsMemoryPairs() + { + ContainerLifecycle.RequireServer(); + var connectionString = await Seed(); + + await using var context = new CalendarContext(connectionString); + var loaded = await context.Rows.AsNoTracking().ToListAsync(); + var (minimum, maximum) = (loaded.Single(r => r.Day == DateOnly.MinValue).Id, loaded.Single(r => r.Day == DateOnly.MaxValue).Id); + + async Task Agree(string label, Expression> outer, Expression> inner) + { + var server = (await context.Rows.Join(context.Rows, outer, inner, (x, y) => new { X = x.Id, Y = y.Id }).ToListAsync()) + .Select(pair => (pair.X, pair.Y)).Order().ToList(); + var memory = loaded.Join(loaded, outer.Compile(), inner.Compile(), (x, y) => (x.Id, y.Id)).Order().ToList(); + + CollectionAssert.AreEqual(memory, server, $"{label}: server {server.Count} pairs, memory {memory.Count}"); + Assert.IsTrue(memory.Any(pair => pair.Item1 == minimum) && memory.Any(pair => pair.Item1 == maximum), + $"{label}: the first and last periods must find a partner, or the extremes are never joined."); + } + + await Agree("Month(x.Day) == Month(y.Year, y.Month)", x => DateRange.Month(x.Day), y => DateRange.Month(y.Year, y.Month)); + await Agree("Quarter(x.Day) == Quarter(y.Year, y.Quarter)", x => DateRange.Quarter(x.Day), y => DateRange.Quarter(y.Year, y.Quarter)); + await Agree("Year(x.Day) == Year(y.Year)", x => DateRange.Year(x.Day), y => DateRange.Year(y.Year)); + } + [TestMethod] public async Task CalendarFactories_OverAnArgumentMemoryRefuses_RaiseInPostgres() { diff --git a/test/oracles/claims.json b/test/oracles/claims.json index 9d31f3b..825bda6 100644 --- a/test/oracles/claims.json +++ b/test/oracles/claims.json @@ -1229,7 +1229,7 @@ "id": "E76-calendar-factories-over-a-column-run-as-sql", "source": "docs/efcore.md#what-runs-where", "quote": "**`DateRange.Month`, `Quarter`, `Year` and `IsoWeek` over a column build the same range in the database as in memory.** `DateRange.Month(b.Day)` gives the month that holds each row's day, so you can filter, join and sort by it. The first period starts at `-infinity` and the last ends at `infinity`, as Npgsql writes `DateOnly.MinValue` and `MaxValue` (the days themselves with `Npgsql.DisableDateTimeInfinityConversions`), so they still equal a stored range.", - "claim": "DateRange.Month, Quarter, Year and IsoWeek over a date column or integer columns run as SQL (daterange(first, last, '[]') with memory's first and last periods as constants, -infinity/infinity under Npgsql's conversions and the days themselves without them) and match memory over every period boundary, the ISO weeks of the neighbouring year and the extremes, NULL arguments (rule (c)) and a missing outer-join child, in a filter, its negation, a null test, equality with a stored range, a compound predicate, an ordering and a final projection; an argument memory rejects raises a PostgreSQL error, never a rolled-over week.", + "claim": "DateRange.Month, Quarter, Year and IsoWeek over a date column or integer columns run as SQL (daterange(first, last, '[]') with memory's first and last periods as constants, -infinity/infinity under Npgsql's conversions and the days themselves without them) and match memory over every period boundary, the ISO weeks of the neighbouring year and the extremes, NULL arguments (rule (c)) and a missing outer-join child, in a filter, its negation, a null test, equality with a stored range, a compound predicate, an ordering, a join key (a date form against an integer form, the first and last periods included) and a final projection; an argument memory rejects raises a PostgreSQL error, never a rolled-over week.", "oracle": "differential", "surfaces": [ "DateRange.Month", diff --git a/test/oracles/seeds/calendar-iso-week-53-of-a-52-week-year-rolls-over.patch b/test/oracles/seeds/calendar-iso-week-53-of-a-52-week-year-rolls-over.patch new file mode 100644 index 0000000..69bed61 --- /dev/null +++ b/test/oracles/seeds/calendar-iso-week-53-of-a-52-week-year-rolls-over.patch @@ -0,0 +1,18 @@ +Seed: the ISO week check accepts one week more than the week-year has, so week 53 of a 52-week year rolls over to the next year's week 1 where memory throws +Oracles: CalendarFactoryParityTests +Fails: CalendarFactories_OverAnArgumentMemoryRefuses_RaiseInPostgres alone: IsoWeek(2025, 53) Select and Where, the server answered, where memory throws +History: never shipped; the class of #191's to_date rollover, kept out by the week guard (#451) + +diff --git a/src/CodoMetis.ValueRanges.EFCore.PostgreSQL/Query/ValueRangesMethodCallTranslator.cs b/src/CodoMetis.ValueRanges.EFCore.PostgreSQL/Query/ValueRangesMethodCallTranslator.cs +index a9a151d..e1dc3a0 100644 +--- a/src/CodoMetis.ValueRanges.EFCore.PostgreSQL/Query/ValueRangesMethodCallTranslator.cs ++++ b/src/CodoMetis.ValueRanges.EFCore.PostgreSQL/Query/ValueRangesMethodCallTranslator.cs +@@ -841,7 +841,7 @@ internal sealed class ValueRangesMethodCallTranslator( + var checkedWeek = sqlExpressionFactory.Case( + [ + new CaseWhenClause( +- sqlExpressionFactory.AndAlso(sqlExpressionFactory.GreaterThanOrEqual(week, Int(1)), sqlExpressionFactory.LessThanOrEqual(week, weeks)), ++ sqlExpressionFactory.AndAlso(sqlExpressionFactory.GreaterThanOrEqual(week, Int(1)), sqlExpressionFactory.LessThanOrEqual(week, sqlExpressionFactory.Add(weeks, Int(1)))), + week) + ], + Raise([$"{name}: the week-year ", Text(year), " has no week ", Text(week)], typeof(int), IntMapping)); diff --git a/test/oracles/seeds/calendar-last-period-ends-on-a-day-not-infinity.patch b/test/oracles/seeds/calendar-last-period-ends-on-a-day-not-infinity.patch index 51abb59..c59b654 100644 --- a/test/oracles/seeds/calendar-last-period-ends-on-a-day-not-infinity.patch +++ b/test/oracles/seeds/calendar-last-period-ends-on-a-day-not-infinity.patch @@ -1,7 +1,6 @@ Seed: the calendar factories write the last period's end as the day 9999-12-31 rather than the infinity Npgsql binds for DateOnly.MaxValue Oracles: CalendarFactoryParityTests -Fails: Month(r.Day) == r.Period and Month(r.Day).Contains(DateOnly.MaxValue) on the rows in 9999-12: server drops them, rule (c) selects them; Month(r.Day) in a Select does not load ([9999-12-01,10000-01-01)) -diff --git a/src/CodoMetis.ValueRanges.EFCore.PostgreSQL/Query/ValueRangesMethodCallTranslator.cs b/src/CodoMetis.ValueRanges.EFCore.PostgreSQL/Query/ValueRangesMethodCallTranslator.cs +Fails: Month(r.Day) == r.Period and Month(r.Day).Contains(DateOnly.MaxValue) on the rows in 9999-12: server drops them, rule (c) selects them; Month(r.Day) in a Select does not load ([9999-12-01,10000-01-01)); the join on Month(x.Day) == Month(y.Year, y.Month) loses the pairs of the rows in 9999-12 diff --git a/src/CodoMetis.ValueRanges.EFCore.PostgreSQL/Query/ValueRangesMethodCallTranslator.cs b/src/CodoMetis.ValueRanges.EFCore.PostgreSQL/Query/ValueRangesMethodCallTranslator.cs index a9a151d..317b133 100644 --- a/src/CodoMetis.ValueRanges.EFCore.PostgreSQL/Query/ValueRangesMethodCallTranslator.cs