Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,36 @@ async Task Agree<TKey>(string label, Expression<Func<CalendarRow, TKey>> key, bo
await Agree("OrderBy(IsoWeek(WeekYear, Week))", r => DateRange.IsoWeek(r.WeekYear, r.Week), descending: false);
}

/// <summary>
/// "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.
/// </summary>
[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<Func<CalendarRow, DateRange>> outer, Expression<Func<CalendarRow, DateRange>> 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()
{
Expand Down
2 changes: 1 addition & 1 deletion test/oracles/claims.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading