Repository navigation
Any/Count over a captured list of ranges silently merge overlapping ranges and drop empty ones #76
Description
Any, Count and other LINQ over a captured list of ranges run in PostgreSQL and return a different answer from the same expression in memory, without throwing:
var wanted = new List<Int32Range> { Int32Range.CreateFinite(1, 5), Int32Range.CreateFinite(3, 8) };
// row: Seats = [1,5]
db.Reservations.Select(r => wanted.Count(w => w.Overlaps(r.Seats))); // database 1, memory 2
db.Reservations.Select(r => wanted.Count(w => w == r.Seats)); // database 0, memory 1
var withEmpty = new List<Int32Range> { Int32Range.Empty, Int32Range.CreateFinite(1, 5) };
db.Reservations.Select(r => withEmpty.Any(w => w.IsEmpty())); // database false, memory trueMeasured against live PostgreSQL 17 while pinning down #71.
Cause: EF Core sends the captured list as one parameter, which Npgsql receives as an untyped NpgsqlRange<T>[]. Npgsql infers that type as a multirange, not an array of ranges, so the SQL becomes FROM unnest(@wanted). unnest of a multirange returns its ranges after the multirange has merged overlapping and adjacent ones and dropped empty ones. So the query sees {[1,8]} where the list holds [1,5] and [3,8]. This is the same mistyping that makes wanted.Contains(r.Seats) fail loudly with 42809 (#71).
Fix (ready): the interceptor change for #71 already sends a captured range collection one parameter per range (VALUES (@wanted1), (@wanted2)), which gives the in-memory answer for all of these. Measured: 2/2, 1/1, true/true. For 9.1.0 it is limited to collections a Contains reads, because those queries failed before. For every other use it would change an answer that queries return today, so by the release rule it goes in 10.0.0: drop that limit (rangeContainsSources in ValueRangesQueryExpressionInterceptor). A test should cover overlapping, adjacent and empty ranges in the list.
Activity
CaffeinatedCoder commentedon Oct 17, 2026
Oracle check (main at 3ecfa10): no oracle on main catches this (pending). Verified by a run: every class in
oracles.jsonpasses on 3ecfa10 with the defect present, and no candidate is registered for it.Gap: the same as #46 and #71. The EF Core Operand position axis in
axes.mdhas no captured collection value, andDifferentialFuzzParityTestsbinds operands only as a column, a constant or a parameter. Worse, claim E38 states today'sunnestbehaviour as the documented workaround, so the catalogue pins the divergence instead of the parity. The fix has to restate E38 as "equals memory". The missing pair is captured collection × Any/Count × every range type, outside route B steps 2 and 3.CaffeinatedCoder commentedon Oct 17, 2026
Would be caught by
CapturedCollectionProbeTests(#138, closes #136). That's the probe sweep the gap above called for. It registers this issue as candidate row, alone:(methodRanges_AnswerAsInMemory), and the row will fail ascandidate fixedonce the fix lands.Measured wider than filed: 1,710 lines across all 11 range types and every collection form (array,
List,HashSet,IEnumerable), whenever noContainsin the query reads the collection.Any(IsEmpty),Count(Overlaps)andCount(IsAdjacentTo)answer differently for the overlapping, adjacent, duplicate, with-an-empty-range and every-shape lists.Any(w => w == c)is rewritten to= ANY (@p)and fails with42809, the list.Contains(rangeColumn) fails with 42809 'op ANY/ALL (array) requires array on right side' #71 error, because the interceptor only looks for aContainscall. ForTimeRange, every operation throwsInvalidCastException, since an untypedNpgsqlRange<TimeOnly>[]can't be written.For the fix: dropping the
rangeContainsSourceslimit makes an empty range collection throw "Empty collections are not supported as inline query roots", as value sets do today. That is #137, so the two need to be fixed together.test: captured collection probe sweep over every range and set type