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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The first preview of the 11 line: the same public API as 10.0.0, on .NET 11. Not

- All four packages target `net11.0` and need the .NET 11 runtime, one target framework per line; the EF Core packages reference EF Core 11 and `Npgsql.EntityFrameworkCore.PostgreSQL` 11 (release candidates while .NET 11 is in preview). PostgreSQL 16 or later, as in 10.x.
- EF Core: `EFCore.ComplexIndexes.PostgreSQL` 5.x stays on EF Core 10 and can't be installed beside the 11 packages. Its EF Core 11 successor, `CodoMetis.EFCore.SchemaKit.PostgreSQL`, isn't on NuGet yet ([CaffeinatedCoder/EFCore.ComplexIndexes#38](https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/38)). For exclusion constraints until then, stay on 10.x or add the constraint with explicit SQL. Temporal keys are Npgsql's own on EF Core 11 ([Exclusion constraints](https://github.com/CaffeinatedCoder/CodoMetis.ValueRanges/blob/main/docs/efcore.md#reject-overlapping-rows-with-an-exclusion-constraint)).
- EF Core: `LowerBound()` and `UpperBound()` in a query are `NULL` for an empty range or set and on an unbounded side, as in memory. So `== null` selects those rows, where it selected none on a required column, and `?? fallback` returns the fallback, where the row was missing ([Migration](https://github.com/CaffeinatedCoder/CodoMetis.ValueRanges/blob/main/docs/migration.md#check-bound-comparisons-with-null-in-queries)).

### Added

Expand Down
4 changes: 3 additions & 1 deletion docs/efcore.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ Good to know:
have a gap. PostgreSQL normalizes multiranges the same way this library does, so the equality is
exact ([verified against live PostgreSQL](../README.md#verified-against-postgresql)).
- **`LowerBound()` and `UpperBound()` return `T?`.** PostgreSQL's `lower` and `upper` return `NULL`
for an unbounded or empty range, and the in-memory methods match that.
for an unbounded or empty range, and the in-memory methods match that. So `== null`, `HasValue`
and `??` on a bound select the same rows in a query as in memory, on a range or a `RangeSet`
column.
- **Discrete upper bounds are adjusted for you.** For `int4range`, `int8range` and `daterange`,
PostgreSQL normalizes to the half-open form `[lower, upper)`. This library normalizes to the
closed form `[lower, upper]`. So `UpperBound()` becomes `upper(x) - 1`, and
Expand Down
43 changes: 43 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -785,3 +785,46 @@ runtimes. Look at the text instead:
- **Say "the end of the day" another way.** Use an unbounded end, `[22:00,)`, or an inclusive
`23:59:59.9999999` bound
([TimeRange and the custom timerange type](efcore.md#timerange-and-the-custom-timerange-type)).

## Migration from v10.x

Nothing was removed or renamed. Some queries return different rows, and the compiler can't show you
where, so check these:

- **EF Core: a bound tested for `null` in a query selects the rows memory selects.**
`LowerBound() == null` selected no rows of a required column, and `?? fallback` lost its fallback
([details](#check-bound-comparisons-with-null-in-queries)).

### Check bound comparisons with `null` in queries

Check EF Core queries that call `LowerBound()` or `UpperBound()` on a range or `RangeSet` column and
then test the result for `null`. That covers `== null`, `!= null`, `HasValue` and `??` with a
fallback, in a filter or a projection. It also covers a filter on a bound you projected first, which
is what a data grid sends for a "blank" filter.

PostgreSQL's `lower` and `upper` return `NULL` for an empty range and for an unbounded side. The
in-memory methods return `null` for the same values. Before 11.0.0, the EF Core package declared
that a bound is `NULL` only when the column is, and EF Core simplified the query on that basis. On a
required column:

| Query | Before 11.0.0 | Now |
|---|---|---|
| `Where(r => r.Period.LowerBound() == null)` | no rows | the empty rows and the rows with no lower bound |
| `Where(r => r.Period.UpperBound() != null)`, or `HasValue` | every row | the rows with an upper bound |
| `Where(r => (r.Period.UpperBound() ?? DateOnly.MaxValue) >= day)` | the empty rows and the rows with no upper bound are missing | those rows match through the fallback |
| `Select(r => r.Period.UpperBound() ?? DateOnly.MaxValue)` | throws (`InvalidOperationException`) at an empty row or a row with no upper bound | returns the fallback for it |
| `Select(r => new { To = r.Period.UpperBound() }).Where(x => x.To == null)` | no rows | the empty rows and the rows with no upper bound |

On a nullable column, `LowerBound() == null` ran as the column's own `IS NULL`. It found the `NULL`
rows and missed the empty and unbounded ones.

This holds for every range type and for `RangeSet`: the .NET and NodaTime ranges, `TimeRange`, the
float ranges and `YearMonthRange`. Code that runs in memory is unchanged.

- **Search your queries for `LowerBound()` and `UpperBound()`.** Each one followed by `== null`,
`!= null`, `HasValue` or `??` now returns the rows the same code returns in memory.
- **Keep or drop the workarounds you wrote for it.** A query that adds `IsEmpty()`,
`IsUnboundedStart()` or `IsUnboundedEnd()` beside the `null` test returns the same rows as before,
so the extra test is now optional.
- **Check reports built on the old rows.** On a required column, a count of rows with no end date
was 0, and a filter on a date with a `??` fallback left out every open-ended row.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The first preview of the 11 line: the same public API as 10.0.0, on .NET 11, EF
### Breaking

- Targets `net11.0` and references `Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime` 11 (a release candidate while .NET 11 is in preview). PostgreSQL 16 or later, as in 10.x.
- EF Core: `LowerBound()` and `UpperBound()` of a NodaTime range or multirange column, `YearMonthRange` included, are `NULL` in a query for an empty value and on an unbounded side, as in memory. So `== null` selects those rows, where it selected none on a required column, and `?? fallback` returns the fallback, where the row was missing ([Migration](https://github.com/CaffeinatedCoder/CodoMetis.ValueRanges/blob/main/docs/migration.md#check-bound-comparisons-with-null-in-queries)).

### Added

Expand Down
1 change: 1 addition & 0 deletions src/CodoMetis.ValueRanges.EFCore.PostgreSQL/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The first preview of the 11 line: the same public API as 10.0.0, on .NET 11, EF

- Targets `net11.0` and references EF Core 11 and `Npgsql.EntityFrameworkCore.PostgreSQL` 11 (release candidates while .NET 11 is in preview). PostgreSQL 16 or later, as in 10.x.
- EF Core: `EFCore.ComplexIndexes.PostgreSQL` 5.x stays on EF Core 10 and can't be installed beside the 11 packages. Its EF Core 11 successor, `CodoMetis.EFCore.SchemaKit.PostgreSQL`, isn't on NuGet yet ([CaffeinatedCoder/EFCore.ComplexIndexes#38](https://github.com/CaffeinatedCoder/EFCore.ComplexIndexes/issues/38)). For exclusion constraints until then, stay on 10.x or add the constraint with explicit SQL. Temporal keys are Npgsql's own on EF Core 11 ([Exclusion constraints](https://github.com/CaffeinatedCoder/CodoMetis.ValueRanges/blob/main/docs/efcore.md#reject-overlapping-rows-with-an-exclusion-constraint)).
- EF Core: `LowerBound()` and `UpperBound()` in a query are `NULL` for an empty range or set and on an unbounded side, as in memory. So `== null` selects those rows, where it selected none on a required column, and `?? fallback` returns the fallback, where the row was missing ([Migration](https://github.com/CaffeinatedCoder/CodoMetis.ValueRanges/blob/main/docs/migration.md#check-bound-comparisons-with-null-in-queries)).

### Added

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -634,14 +634,17 @@ private SqlExpression BoolFunction(string name, SqlExpression argument)
/// <summary>
/// A bound-extraction function (<c>lower</c>/<c>upper</c>) on a range or multirange.
/// PostgreSQL returns NULL for an empty or correspondingly unbounded operand, hence the
/// nullable CLR type.
/// nullable CLR type. That NULL comes from a non-null argument, so the argument must not be
/// declared as propagating nullability: EF Core would then take the bound of a required column
/// as never NULL, fold <c>IS NULL</c> to <c>FALSE</c> and drop the <c>COALESCE</c> of a
/// <c>??</c>, and test a nullable column's bound with the column's own <c>IS NULL</c>.
/// </summary>
private SqlExpression BoundFunction(string name, SqlExpression argument, IRangeTypeDefinition definition)
=> sqlExpressionFactory.Function(
name,
[argument],
nullable: true,
argumentsPropagateNullability: [true],
argumentsPropagateNullability: [false],
typeof(Nullable<>).MakeGenericType(definition.ElementClrType),
ElementMapping(definition));

Expand Down
Loading
Loading