Repository navigation
The range factories over a NULL element build a range: CreateFinite gives empty and CreateUnboundedStart/End give (,), where EF Core answers NULL #382
Description
Problem
CreateFinite, CreateUnboundedStart and CreateUnboundedEnd translated over an element that is NULL build a range instead of answering NULL. CreateFinite falls into its inverted-bounds guard and gives empty. CreateUnboundedStart and CreateUnboundedEnd pass the NULL to PostgreSQL's range constructor, which reads a NULL bound as unbounded, so the result is (,). A predicate on that range then answers for a row that has no element: Contains(v) is true.
An element is NULL when it comes from the optional side of an outer join (a required column of a missing child) or from a nullable column read with .Value. Measured 2026-10-24 on main (EF Core 11 RC2, Npgsql 11 RC), PostgreSQL 16. Booking 1 has a visit From 2026-03-05 To 2026-03-08 and End 2026-03-10; booking 2 has no visit and a NULL End. v is 2026-03-07.
| LINQ | SQL | Booking 2 |
|---|---|---|
Select(b => DateRange.CreateFinite(b.Visit!.From, b.Visit!.To)) |
CASE WHEN v."From" <= v."To" THEN daterange(v."From", v."To", '[]') ELSE 'empty'::daterange END (LEFT JOIN "Visits") |
empty |
Select(b => DateRange.CreateUnboundedStart(b.Visit!.To)) |
daterange(NULL, v."To", '(]') |
(,) |
Where(b => DateRange.CreateUnboundedEnd(b.Visit!.From).Contains(v)) |
daterange(v."From", NULL, '[)') IS NOT NULL AND daterange(v."From", NULL, '[)') @> @v |
selected |
Where(b => DateRange.CreateFinite(b.Visit!.From, b.Visit!.To) == null) |
CASE … END IS NULL |
dropped |
Where(b => DateRange.CreateUnboundedStart(b.End!.Value).Contains(v)) |
daterange(NULL, b."End", '(]') IS NOT NULL AND … @> @v |
selected |
Where(b => DateRange.CreateFinite(b.Start, b.End!.Value).IsEmpty()) |
CASE WHEN b."Start" <= b."End" THEN … ELSE 'empty'::daterange END IS NOT NULL AND isempty(…) |
selected |
Select(b => (DateOnly?)b.Visit!.From.AddDays(1)) (EF Core's own) |
v."From" + 1 |
NULL |
In memory the call cannot be made: the missing child throws NullReferenceException, .Value throws InvalidOperationException. The reference is EF Core's null propagation for its own value-producing members (AddDays above), and #350's rule that a value-producing member over a null has no value. Rule (c)'s predicate wrapper cannot help here, because PostgreSQL's range constructor is not strict, so daterange(NULL, NULL, '[)') IS NOT NULL is true.
Documented-decision check. docs/efcore.md#query-ranges-with-linq says the factories "become range constructor calls inside a guard. As in memory, a start after the end gives you an empty range"; it says nothing about a NULL element. The comment on RangeConstructor ("A NULL bound makes the range unbounded on that side, it does not null the result") describes the NULL constant the translator writes for the unbounded side, not a column argument. No waiver, candidate or closed issue covers it.
Found by OuterJoinProbeTests (#375): every factory over the child's From/To, as a value, == null, IsEmpty() and Contains(v), through a navigation, DefaultIfEmpty and LeftJoin alike, on both parents without a child.
Change
Answer NULL when an element argument is NULL: CASE WHEN a IS NULL OR b IS NULL THEN NULL ELSE <today's translation> END around the factory when an argument can be NULL, leaving required arguments' SQL as it is. Declared nullable, so EF Core's == null and the predicate wrapper see it. SQL only; memory has no answer to change.
Done when
The factory candidates registered for OuterJoinProbeTests against this issue report candidate fixed and are removed.
Activity
CaffeinatedCoder commentedon Oct 24, 2026
Re-evaluated 2026-10-24: this goes to 10.2.0 too. The port is #388 (epic #358). The
mainfix comes first, after PR #383, which registers the candidates; #388 cherry-picks it with-x.CaffeinatedCoder commentedon Oct 24, 2026
Fixed in PR #392 (stacked on PR #391). The six candidates reported
candidate fixedand are removed. The 10.x port is PR #394.fix: a range factory over a NULL element is NULL in a query (#382)
Merge pull request #392 from CaffeinatedCoder/fix/382-factories-over-…
test: hold the 10.x NULL-element fixes where a predicate consumes them (