Repository navigation
Contains(element) on a range or RangeSet is NULL in SQL for a NULL element: the negation drops the row EF Core keeps, and a projection into bool throws #381
Description
Problem
Contains(T) on a range or a RangeSet is NULL in SQL when its element is NULL, so a negated filter drops the row and a projection into bool throws. Rule (c) (#350, #355) guards a NULL range, RangeSet or value set operand (x IS NOT NULL AND …), but not the element. EF Core's own comparisons over the same NULL keep the row under a negation.
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 element column read with .Value. Measured 2026-10-24 on main (EF Core 11 RC2, Npgsql 11 RC), PostgreSQL 16. Booking 1 has a visit on 2026-03-07 and Day 2026-03-07; booking 2 has no visit and a NULL Day. Both have Period [2026-03-01,2026-03-10].
| LINQ | SQL | Booking 2 |
|---|---|---|
!b.Period.Contains(b.Visit!.On) |
WHERE NOT (b."Period" @> v."On") (LEFT JOIN "Visits") |
dropped |
!b.Blocks.Contains(b.Visit!.On) (RangeSet) |
WHERE NOT (b."Blocks" @> v."On") |
dropped |
!b.Period.Contains(b.Day!.Value) |
WHERE NOT (b."Period" @> b."Day") |
dropped |
Select(b => b.Period.Contains(b.Visit!.On)) |
SELECT b."Period" @> v."On" |
throws InvalidOperationException: Cannot read the Value property of a Nullable object that has no value |
Select(b => (bool?)b.Period.Contains(b.Visit!.On)) |
the same | NULL |
!(b.Visit!.On > v) (EF Core's own) |
WHERE CASE WHEN v."On" > @v THEN FALSE ELSE TRUE END |
kept |
!(b.Day!.Value > v) (EF Core's own) |
WHERE CASE WHEN b."Day" > @v THEN FALSE ELSE TRUE END |
kept |
In memory the call cannot be made: the missing child throws NullReferenceException, .Value throws InvalidOperationException. So the reference is EF Core's convention for its own members, which is what rule (c) adopted (docs/efcore.md#query-ranges-with-linq: "a predicate over a NULL value is false, and its negation true").
Documented-decision check. #350's decision table and #355 name a NULL range, RangeSet or value set on either side; neither mentions an element. docs/efcore.md states the rule for "a predicate over a NULL value" on a nullable column. No waiver, candidate or closed issue covers a NULL element. A value set's Contains(element) with a NULL element already answers false/true (@> ARRAY[NULL] is false), so only the range and RangeSet forms disagree.
Found by OuterJoinProbeTests (#375): p.Window.Contains(c.From) and p.WindowBlocks.Contains(c.From), through a navigation, DefaultIfEmpty and LeftJoin alike, on both parents without a child. And by NullableOperandPairProbeTests (#376): Contains(r.Day.Value) on a range or RangeSet column, a CASE over two of them and a COALESCE of them, on the five rows whose Day is NULL.
Change
Treat an element operand like a range operand in the predicate wrapper (FalseOverNull in ValueRangesMethodCallTranslator): e IS NOT NULL AND x @> e. EF Core's nullability processing then drops the check for a required element, and writes e IS NULL OR NOT (…) under a negation. That is SQL only; memory has no answer to change.
Done when
The candidates registered for OuterJoinProbeTests and NullableOperandPairProbeTests against this issue report candidate fixed and are removed.
Activity
CaffeinatedCoder commentedon Oct 24, 2026
Measured 2026-10-24 by
QuantifierProbeTests(#377), PostgreSQL 16: underAllthe NULL element also turns a positive filter wrong.list.All(x => x.Contains(r.Day!.Value)), withlista capturedList<DateRange>orList<RangeSet<DateRange, DateOnly>>that is not empty, selects every row whoseDayis NULL. EF Core writesAllasNOT EXISTS (… WHERE NOT (x @> r."Day")); for a NULLDaythe innerNOTis NULL, no row qualifies, andNOT EXISTSistrue.Selectanswerstruetoo, andWhere(!…)drops the row. Rule (c) answersfalse(the element is missing), so the row should be dropped and kept under the negation.Any(x => x.Contains(r.Day!.Value))andCount(x => x.Contains(r.Day!.Value)) == 0agree with rule (c), sinceEXISTSover a NULL predicate isfalse. With the wrapper this issue proposes (e IS NOT NULL AND x @> e),NOT (…)istruefor a NULL element andAllbecomesfalsefor a non-empty list. The candidates are registered against this issue.CaffeinatedCoder commentedon Oct 24, 2026
Re-evaluated 2026-10-24: this goes to 10.2.0 too. The port is #387 (epic #358). The
mainfix comes first, after PR #383, which registers the candidates; #387 cherry-picks it with-x.CaffeinatedCoder commentedon Oct 24, 2026
Fixed in PR #391. The 14 candidates reported
candidate fixedand are removed. The 10.x port is PR #393.Merge pull request #391 from CaffeinatedCoder/fix/381-null-element-co…
test: hold the 10.x NULL-element fixes where a predicate consumes them (