Skip to content

A predicate over a NULL range, RangeSet or value set column is NULL in SQL, so its negation drops the row EF Core's own translations keep #355

Closed

Description

@CaffeinatedCoder

Problem

A Boolean predicate over a NULL range, RangeSet or value set column is NULL in SQL, so a filter drops the row whether the predicate is negated or not. EF Core's own translations treat a predicate over NULL as false and keep the row under a negation. Measured 2026-10-24 with ToQueryString() on main (EF Core 11 RC, Npgsql 11 RC) and on release/10.x (EF Core 10.0.0, and 10.0.13 with Npgsql 10.0.3), with identical SQL on all three:

LINQ over a nullable column SQL NULL row
!r.Name.StartsWith("a") (EF Core) r."Name" NOT LIKE 'a%' OR r."Name" IS NULL kept
!r.Names.Contains("x") (string[], EF Core) NOT ('x' = ANY (r."Names") AND 'x' = ANY (r."Names") IS NOT NULL) kept
!r.Names.Any() (string[], EF Core) CASE WHEN cardinality(r."Names") > 0 THEN FALSE ELSE TRUE END kept
!r.Maybe.IsEmpty() NOT (isempty(r."Maybe")) dropped
!r.Maybe.Overlaps(p) NOT (r."Maybe" && @p) dropped
r.Maybe.Contains(DateRange.Empty) r."Maybe" @> @Empty dropped (memory: true)
!r.MaybeSet.Overlaps(p) (RangeSet) NOT (r."MaybeSet" && @p) dropped
!r.MaybeSet.IsEmpty() (RangeSet) NOT (isempty(r."MaybeSet")) dropped
!r.MaybeTags.Contains("x") (StringSet) NOT (r."MaybeTags" @> ARRAY['x']::text[]) dropped
!r.MaybeTags.IsEmpty (StringSet) cardinality(r."MaybeTags") <> 0 OR r."MaybeTags" IS NULL kept

So the package also disagrees with itself: a value set's !IsEmpty keeps the NULL row (it is a comparison, which EF Core compensates) and its !Contains drops it. In a projection, Select(r => r.Maybe!.IsEmpty()) is isempty(r."Maybe"), NULL for a NULL row; what reading that into bool does was not measured.

For a range, the in-memory side is #350: most extensions answer false on a null range, so the negated forms disagree with memory on the NULL row. For RangeSet and the value sets the members are instance members, so memory throws NullReferenceException and the only reference is EF Core's convention.

Change

Rule (c) from #350: a Boolean predicate with a NULL range, RangeSet or value set on either side is false, and its negation true. The translators write each operand that can be NULL as x IS NOT NULL AND <predicate>, and EF Core's nullability processing does the rest. Measured with that shape written by hand in LINQ (r.X != null && r.X.IsEmpty()), identical on both EF Core lines:

Operand Positive Negated
required column isempty(r."Period") (check removed) NOT (isempty(r."Period"))
nullable column r."Maybe" IS NOT NULL AND isempty(r."Maybe") r."Maybe" IS NULL OR NOT (isempty(r."Maybe"))
nullable column, && r."Maybe" IS NOT NULL AND r."Maybe" && @p r."Maybe" IS NULL OR NOT (r."Maybe" && @p)
two nullable columns r."Maybe" IS NULL OR r."Other" IS NULL OR NOT (r."Maybe" && r."Other")
non-null captured parameter NOT (r."Period" && @p) (check removed)
projection, nullable column r."Maybe" IS NOT NULL AND isempty(r."Maybe")

The SQL over required columns and non-null parameters is unchanged. A positive filter over a nullable column keeps the operator a GiST index serves, beside an IS NOT NULL (not checked with EXPLAIN). This covers every Boolean predicate the translators emit for ranges, RangeSet and value sets, with two exceptions that keep their documented rules. CompareTo against 0 or ±1 follows Comparer<T>.Default (docs/ranges.md#sort-ranges; #220, #253, #256, #261): a NULL column equals a null range and sorts before every other range, so its rewrite stays as it is. Equality (==, !=) is EF Core's own null semantics. A null test is already allowed on a value derived from array_cat (ValueSetSqlGuard), so Union operands keep working.

Done when

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
    area/rangesRange unions, variants and the shape engines
    area/range-setRangeSet and multirange algebra
    area/value-setsValue set families and wrapper arities
    package/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQL
    package/efcore-nodatimeAffects CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime
    severity/mediumInconsistency or failure in a narrow scenario
    on Oct 24, 2026
  3. CaffeinatedCoder commented on Oct 24, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Measured 2026-10-24 on PostgreSQL 16, for the projection row of the table above: today Select(r => r.Maybe!.IsEmpty()), Select(r => r.Maybe!.Overlaps(DateRange.Infinite)) and Select(r => r.MaybeTags!.Count) over a NULL row throw InvalidOperationException: Cannot read the Value property of a Nullable object that has no value when EF Core reads the NULL into bool or int. Memory returns false for IsEmpty() (the range extension) and throws NullReferenceException for the value set's Count (an instance member). With the x IS NOT NULL AND … shape the two predicates read false. Count has no rule yet; under (c) it is a value, not a predicate.

  4. added
    breakingChanges an answer or a signature — needs a major
    package/coreAffects CodoMetis.ValueRanges
    on Oct 24, 2026
  5. added 2 commits that reference 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 enginesarea/value-setsValue set families and wrapper aritiesbreakingChanges an answer or a signature — needs a majorbugSomething isn't workingpackage/coreAffects CodoMetis.ValueRangespackage/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQLpackage/efcore-nodatimeAffects CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTimeseverity/mediumInconsistency or failure in a narrow scenariosilentAnswers 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