Skip to content

The range factories over a NULL element build a range: CreateFinite gives empty and CreateUnboundedStart/End give (,), where EF Core answers NULL #382

Closed

Description

@CaffeinatedCoder

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

  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)
    area/rangesRange unions, variants and the shape engines
    package/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQL
    severity/mediumInconsistency or failure in a narrow scenario
    on Oct 24, 2026
  3. CaffeinatedCoder commented on Oct 24, 2026

    @CaffeinatedCoder
    OwnerAuthor

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

  4. CaffeinatedCoder commented on Oct 24, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Fixed in PR #392 (stacked on PR #391). The six candidates reported candidate fixed and are removed. The 10.x port is PR #394.

  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/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/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