Skip to content

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

Closed

Description

@CaffeinatedCoder

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

  1. added this to the 11.0.0 milestone on Oct 24, 2026
  2. added
    bugSomething isn't working
    silentAnswers wrongly without throwing — fixes go in the next major, never deferred
    found-by/oracleFound by the generated-oracle machinery (claims × API surfaces)
    on Oct 24, 2026
  3. added
    area/rangesRange unions, variants and the shape engines
    area/range-setRangeSet and multirange algebra
    package/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQL
    severity/lowCosmetic, message or documentation-level difference
    on Oct 24, 2026
  4. CaffeinatedCoder commented on Oct 24, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Measured 2026-10-24 by QuantifierProbeTests (#377), PostgreSQL 16: under All the NULL element also turns a positive filter wrong. list.All(x => x.Contains(r.Day!.Value)), with list a captured List<DateRange> or List<RangeSet<DateRange, DateOnly>> that is not empty, selects every row whose Day is NULL. EF Core writes All as NOT EXISTS (… WHERE NOT (x @> r."Day")); for a NULL Day the inner NOT is NULL, no row qualifies, and NOT EXISTS is true. Select answers true too, and Where(!…) drops the row. Rule (c) answers false (the element is missing), so the row should be dropped and kept under the negation.

    Any(x => x.Contains(r.Day!.Value)) and Count(x => x.Contains(r.Day!.Value)) == 0 agree with rule (c), since EXISTS over a NULL predicate is false. With the wrapper this issue proposes (e IS NOT NULL AND x @> e), NOT (…) is true for a NULL element and All becomes false for a non-empty list. The candidates are registered against this issue.

  5. CaffeinatedCoder commented on Oct 24, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Re-evaluated 2026-10-24: this goes to 10.2.0 too. The port is #387 (epic #358). The main fix comes first, after PR #383, which registers the candidates; #387 cherry-picks it with -x.

  6. CaffeinatedCoder commented on Oct 24, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Fixed in PR #391. The 14 candidates reported candidate fixed and are removed. The 10.x port is PR #393.

  7. added a commit that references this issue on Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/range-setRangeSet and multirange algebraarea/rangesRange unions, variants and the shape enginesbugSomething isn't workingfound-by/oracleFound by the generated-oracle machinery (claims × API surfaces)package/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQLpackage/efcore-nodatimeAffects CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTimeseverity/lowCosmetic, message or documentation-level differencesilentAnswers wrongly without throwing — fixes go in the next major, never deferred

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions