Repository navigation
IsUnboundedStart/IsUnboundedEnd on the infinite range answer false in memory and true through EF Core (lower_inf) #109
Description
IsUnboundedStart() and IsUnboundedEnd() on a single range answer false for the infinite range (,). EF Core translates them to lower_inf and upper_inf, which answer true for it. A query and the same predicate in memory therefore disagree on every row holding (,), for every range type, without an error.
Int32Range.Infinite.IsUnboundedStart(); // false
Int32Range.Infinite.IsUnboundedEnd(); // false
// The same value as a one-element set answers the other way:
RangeSet<Int32Range, int>.From(Int32Range.Infinite).IsUnboundedStart(); // true
// Through EF Core, a row whose Seats column holds (,) matches:
db.Reservations.Where(r => r.Seats.IsUnboundedStart()); // lower_inf("Seats") → trueSELECT lower_inf('(,)'::int4range), upper_inf('(,)'::int4range); -- t, tConfirmed against PostgreSQL 17 (postgres:17-alpine) on main (7f4dc47).
Where. src/CodoMetis.ValueRanges/RangeExtensions.cs: IsUnboundedStart() => range is IUnboundedStartRange<T> (and IsUnboundedEnd likewise) is a test for the variant. RangeSet.IsUnboundedStart() tests IUnboundedStartRange<T> or IInfinityRange<T>, which is what lower_inf means. The documents promise both: the XML doc says "false for all other shapes", while docs/efcore.md says "IsUnboundedStart() becomes lower_inf" and docs/ranges.md says the state checks "match PostgreSQL's isempty, lower_inf and upper_inf".
Scope. All 11 range types (Int32Range, Int64Range, DecimalRange, DateRange, DateTimeRange, DateTimeOffsetRange, TimeRange, LocalDateRange, LocalDateTimeRange, InstantRange, YearMonthRange), through a column, a constant and a parameter. RangeSet is unaffected.
Fix (10.0.0). Pick one meaning and make memory and SQL agree. Either the range methods answer true for (,), as lower_inf and RangeSet do, or the translation becomes lower_inf(x) AND NOT upper_inf(x). Both change an answer one side gives today, so this is breaking. Remove the DifferentialFuzzParityTests candidates for this family in the fix's PR.
Found by the differential fuzzing sweep (#104), on 12 to 22 of the 200 default rows per type (every row whose left operand is (,)).
Activity
CaffeinatedCoder commentedon Oct 17, 2026
Oracle check (main at 3ecfa10): would catch.
DifferentialFuzzParityTests(Ranges_AgreeWithPostgres) fails on rowsIsUnboundedStart Infinite×andIsUnboundedEnd Infinite×, e.g.YearMonthRange IsUnboundedStart Infinite×Finite [column]: (,) … → memory=false, EF=true, PostgreSQL=true.Verified by a run on 3ecfa10. At the default seed, 354 lines report
SKIPPED [known candidate, #109]across every range type. It was found by this sweep.CaffeinatedCoder commentedon Oct 18, 2026
RangeSetLiftParityTests(#139) finds this one again in memory alone:IsUnboundedStart()/IsUnboundedEnd()on(,)answerfalse, and the one-elementRangeSetholding(,)answerstrue(it readslower_inf/upper_inffrom its first and last element). That happens on all 11 range types. The rows are registered intest/oracles/candidates.jsonunder this issue (<Type> (,) IsUnboundedStart:/IsUnboundedEnd:), so the fix's PR removes them along with theDifferentialFuzzParityTestsrows.CaffeinatedCoder commentedon Oct 18, 2026
Fixed in #168 (
fix/infinite-range-is-unbounded, stacked on #167, 10.0.0). Decision: PostgreSQL is the authority, so memory moved. What is now true:IsUnboundedStart()andIsUnboundedEnd()on a range aretruefor(,), aslower_inf/upper_inf, the EF Core translation andRangeSetalready were. The XML docs say so and point atIUnboundedStartRange<T>/IUnboundedEndRange<T>for the exact shape test.IsInfinity()andIsFinite()are unchanged and follow from the three primitives in memory as in SQL.lower_inf/upper_inf;QueryTranslationTestsasserts it does not narrow to exclude(,).ExecutedQueryTests.RangeShapePredicates_MatchInMemoryruns the five checks over a row of each shape against the server and pins the infinite row totrue.RangeSetLiftParityTests) are removed; with the fix reverted both sweeps report them as unregistered rows again. Changelog Breaking row anddocs/migration.mdsection added.