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
13 changes: 8 additions & 5 deletions docs/efcore.md
Original file line number Diff line number Diff line change
Expand Up @@ -919,7 +919,7 @@ translation, and in the final projection EF runs them in memory instead:
| `Values()` and `Values(step)` on a discrete range, `Split` on any range | Not translated. Each returns a sequence, which in SQL would be `generate_series` wrapped in `ARRAY(SELECT …)`. A long range produces one element per step. |
| `ToRangeSet()` / `ToInt32Set()` and the other bridge conversions | Not translated. An array becomes a multirange through `range_agg` over one-element ranges, which needs care at the element type's extremes: `[2147483647,2147483647]` can't be built as an `int4range`. A multirange becomes an array only by enumerating it, as `Values` does. |
| `Shift`, `Expand` on any range | Not translated. Rebuilding the range from `lower(x) + d` differs from memory unless each case is handled: the empty range becomes `(,)`, `time` wraps at midnight where `TimeRange` throws, and a bound moved past the last date .NET holds throws in memory where PostgreSQL returns a later date. A bound at `infinity` stays there in both. |
| `LocalDateRange.Month`, `Quarter`, `Year` and `IsoWeek`, and `YearMonthRange.Quarter` and `Year`, with an argument read from a column | Not translated. A `LocalDate` column holding `-infinity` stands for -9998-01-01, and neither its month nor any year before 4713 BC has a PostgreSQL date, so the database would fail where memory returns a range. Built from values your code passes in, the range is computed in memory and sent as one constant, so comparing a column against a month runs as SQL. On `DateRange` these run as SQL. |
| `LocalDateRange.Month`, `Quarter`, `Year` and `IsoWeek`, and `YearMonthRange.Quarter` and `Year`, with an argument read from a column | Not translated. PostgreSQL has no dates before 4714-11-24 BC, so the month and the year that hold its first day have no start, and the database would fail where memory returns a range. That holds with Npgsql's infinity conversions on or off. Built from values your code passes in, the range is computed in memory and sent as one constant, so comparing a column against a month runs as SQL. On `DateRange` these run as SQL. |
| `Clamp(value)` on the other range types | Not translated. The same `LEAST(GREATEST(…))` form would apply, but it has only been checked against memory on `Int32Range`, `Int64Range` and `DecimalRange`, where it runs as SQL. |
| `Intersect`, `Except` on value sets | Not translated. Filtering the left array with `unnest … WITH ORDINALITY` keeps its order, but the result is canonical only when the stored arrays are, as with `Union`. |
| `Add` on value sets | Inserting in canonical order means sorting in SQL, where `text` follows the database collation rather than ordinal order. |
Expand All @@ -940,12 +940,15 @@ Good to know:
passes, even an exclusive one. The empty range gives `null`, and so does a `null` range or value.
- **`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 and last periods end at `-infinity` and `infinity`,
as Npgsql writes `DateOnly.MinValue` and `MaxValue`, so they still equal a stored range.
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.
- **An argument out of range fails in the database too.** A month outside 1 to 12, a year outside 1
to 9999, a week its week-year doesn't have, and the ISO week that runs past 9999-12-31 throw
`ArgumentOutOfRangeException` in memory. In a query, PostgreSQL raises an error that names the
argument (`PostgresException`).
`ArgumentOutOfRangeException` in memory. In a query, PostgreSQL raises an error
(`PostgresException`). For a week, or a year other than 0, the message names the argument. For a
month, a quarter or the year 0, it shows the date PostgreSQL was asked to build: `Quarter(2026, 5)`
reports `2026-13-01`.
- **An in-memory operation inside a `Where` fails loudly.** EF throws rather than silently fetching
the whole table, and that is the intended behaviour.
- **In the final `Select` it runs in memory.** EF Core evaluates the outermost projection on the
Expand Down
2 changes: 1 addition & 1 deletion src/CodoMetis.ValueRanges.EFCore.PostgreSQL/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ table. In the final `Select`, EF Core computes them in memory after it loads the
| `Length` on the range types other than `Int32Range` and `Int64Range` | not translated: a temporal range open to `infinity` fails in PostgreSQL |
| `Values()` and `Values(step)` on a discrete range, `Split` on any range | not translated: each would be `generate_series` collected into an array |
| `Shift`, `Expand` on any range | not translated: rebuilding the range from `lower(x) + d` answers differently for the empty range, `time` past midnight and a bound moved past the last date .NET holds |
| `LocalDateRange.Month`, `Quarter`, `Year` and `IsoWeek`, and `YearMonthRange.Quarter` and `Year`, with an argument read from a column | not translated: a `LocalDate` at `-infinity` and the years before 4713 BC have no PostgreSQL date. Built from values your code passes in, the range is sent as one constant, so comparing a column against a month runs as SQL. On `DateRange` they run as SQL |
| `LocalDateRange.Month`, `Quarter`, `Year` and `IsoWeek`, and `YearMonthRange.Quarter` and `Year`, with an argument read from a column | not translated: PostgreSQL has no dates before 4714-11-24 BC, so the month and the year that hold its first day have no start, with Npgsql's infinity conversions on or off. Built from values your code passes in, the range is sent as one constant, so comparing a column against a month runs as SQL. On `DateRange` they run as SQL |
| `ToRangeSet()` / `ToInt32Set()` and the other bridge conversions | not translated: an array becomes a multirange by aggregating one-element ranges, and the reverse is enumeration |
| `Clamp(value)` on the range types other than `Int32Range`, `Int64Range` and `DecimalRange` | not translated: the same `LEAST`/`GREATEST` form has only been checked against memory on those three |
| the value set indexer, `set[0]` | a row another tool wrote may not be canonical, arrays start at 1, and an index past the end is `NULL` where memory throws |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ namespace CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime.Tests;

/// <summary>
/// The calendar factories of <see cref="LocalDateRange"/> and <see cref="YearMonthRange"/> over a column stay in
/// memory, where <c>DateRange</c>'s run as SQL (#191): a column holding <c>-infinity</c> stands for
/// <see cref="LocalDate.MinIsoValue"/>, -9998-01-01, and neither its month nor any year before 4713 BC has a
/// PostgreSQL date, so the server would raise where memory answers. Inside a <c>Where</c> they fail
/// memory, where <c>DateRange</c>'s run as SQL (#191): PostgreSQL has no dates before 4714-11-24 BC, so the
/// month and the year that hold its first day have no start, and the server would raise where memory answers.
/// That holds with Npgsql's infinity conversions on or off (#450). Inside a <c>Where</c> they fail
/// translation; built from values the query does not read, they are one constant and run as SQL.
/// </summary>
[TestClass]
Expand Down
2 changes: 1 addition & 1 deletion test/oracles/claims.json
Original file line number Diff line number Diff line change
Expand Up @@ -1321,7 +1321,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 and last periods end at `-infinity` and `infinity`, as Npgsql writes `DateOnly.MinValue` and `MaxValue`, so they still equal a stored range.",
"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.",
"oracle": "differential",
"surfaces": [
Expand Down
8 changes: 4 additions & 4 deletions test/oracles/translation-support.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,31 +72,31 @@
{
"members": "*Range.Month(*",
"support": "client",
"reason": "docs/efcore.md#what-runs-where: with an argument read from a column, daterange(first, last, '[]') on DateRange, with memory's first and last periods as constants and an error where memory throws; on LocalDateRange and YearMonthRange a LocalDate at -infinity (-9998-01-01) and the years before 4713 BC have no PostgreSQL date, so the database would fail where memory returns a range. Over values the code passes in, the range is computed in memory and sent as a constant",
"reason": "docs/efcore.md#what-runs-where: with an argument read from a column, daterange(first, last, '[]') on DateRange, with memory's first and last periods as constants and an error where memory throws; on LocalDateRange and YearMonthRange PostgreSQL has no dates before 4714-11-24 BC, so the month and the year that hold its first day have no start (with Npgsql's infinity conversions on or off), and the database would fail where memory returns a range. Over values the code passes in, the range is computed in memory and sent as a constant",
"except": {
"DateRange": "server"
}
},
{
"members": "*Range.Quarter(*",
"support": "client",
"reason": "docs/efcore.md#what-runs-where: with an argument read from a column, daterange(first, last, '[]') on DateRange, with memory's first and last periods as constants and an error where memory throws; on LocalDateRange and YearMonthRange a LocalDate at -infinity (-9998-01-01) and the years before 4713 BC have no PostgreSQL date, so the database would fail where memory returns a range. Over values the code passes in, the range is computed in memory and sent as a constant",
"reason": "docs/efcore.md#what-runs-where: with an argument read from a column, daterange(first, last, '[]') on DateRange, with memory's first and last periods as constants and an error where memory throws; on LocalDateRange and YearMonthRange PostgreSQL has no dates before 4714-11-24 BC, so the month and the year that hold its first day have no start (with Npgsql's infinity conversions on or off), and the database would fail where memory returns a range. Over values the code passes in, the range is computed in memory and sent as a constant",
"except": {
"DateRange": "server"
}
},
{
"members": "*Range.Year(*",
"support": "client",
"reason": "docs/efcore.md#what-runs-where: with an argument read from a column, daterange(first, last, '[]') on DateRange, with memory's first and last periods as constants and an error where memory throws; on LocalDateRange and YearMonthRange a LocalDate at -infinity (-9998-01-01) and the years before 4713 BC have no PostgreSQL date, so the database would fail where memory returns a range. Over values the code passes in, the range is computed in memory and sent as a constant",
"reason": "docs/efcore.md#what-runs-where: with an argument read from a column, daterange(first, last, '[]') on DateRange, with memory's first and last periods as constants and an error where memory throws; on LocalDateRange and YearMonthRange PostgreSQL has no dates before 4714-11-24 BC, so the month and the year that hold its first day have no start (with Npgsql's infinity conversions on or off), and the database would fail where memory returns a range. Over values the code passes in, the range is computed in memory and sent as a constant",
"except": {
"DateRange": "server"
}
},
{
"members": "*Range.IsoWeek(*",
"support": "client",
"reason": "docs/efcore.md#what-runs-where: with an argument read from a column, daterange(first, last, '[]') on DateRange, with memory's first and last periods as constants and an error where memory throws; on LocalDateRange and YearMonthRange a LocalDate at -infinity (-9998-01-01) and the years before 4713 BC have no PostgreSQL date, so the database would fail where memory returns a range. Over values the code passes in, the range is computed in memory and sent as a constant",
"reason": "docs/efcore.md#what-runs-where: with an argument read from a column, daterange(first, last, '[]') on DateRange, with memory's first and last periods as constants and an error where memory throws; on LocalDateRange and YearMonthRange PostgreSQL has no dates before 4714-11-24 BC, so the month and the year that hold its first day have no start (with Npgsql's infinity conversions on or off), and the database would fail where memory returns a range. Over values the code passes in, the range is computed in memory and sent as a constant",
"except": {
"DateRange": "server"
}
Expand Down
Loading