Repository navigation
Translate ToRangeSet() from Int32Set, Int64Set, DateSet, LocalDateSet and YearMonthSet (split from #407) #487
Description
Problem
ToRangeSet() on Int32Set, Int64Set, DateSet, LocalDateSet and YearMonthSet runs in memory only. #407 holds both bridge directions together with enumeration, but the two directions aren't alike:
- Multirange to set has to enumerate. Its input may be unbounded, and it yields one element per step.
- Set to multirange doesn't. Its input is finite, and its output is never larger.
Translating this direction also makes cross-family questions translate through operators that already run as SQL: b.Dates.ToRangeSet().IsContainedBy(b.Stay) ("are all the booked days inside the stay?") or b.Dates.ToRangeSet().Overlaps(closedPeriods).
Measured (PostgreSQL 16.15 and 18.1)
(SELECT range_agg(int4range(x, x, '[]')) FROM unnest('{1,2,3,5}'::int[]) x)is{[1,4),[5,6)}.int.MaxValuefails:integer out of range, because the canonical form needsx + 1.long.MaxValueis the same case.- The empty set gives NULL (
range_aggover no rows), where memory givesRangeSet.Empty. It needsCOALESCE(…, '{}'). - A NULL element in a row another tool wrote gives
int4range(NULL, NULL, '[]'), which is(,). So{1,NULL}bridges to{(,)}, the whole domain. It must be filtered withWHERE x IS NOT NULL, as Translate value set Intersect, Except, Add and a canonical Union for the non-text families with composed SQL #478's canonical form does. DateOnly.MaxValue, which Npgsql writes asinfinity, aggregates to[infinity,infinity];9999-12-31itself gives[9999-12-31,10000-01-01). Both modes of Npgsql's infinity conversions need a test.YearMonthSetis stored as month-aligned dates, so its one-element range spans a month, not a day. Not measured yet.
Change
Translate as COALESCE((SELECT range_agg(<range>(vrN.x, vrN.x, '[]')) FROM unnest(set) AS vrN(x) WHERE vrN.x IS NOT NULL), '{}'), inside #478's NULL-over-NULL wrapper. Build it after EF Core's alias pass, as #478 builds its expressions, behind a placeholder that fails closed. At int.MaxValue and long.MaxValue, compare with what memory returns and what storing that result does (the domain-boundary table in docs/architecture.md). Then decide whether the server error is the documented answer.
Does it change an answer?
No. A predicate that throws today gets the in-memory answer.
Done when
ToRangeSet()translates for all five set types, core and NodaTime- a live test on PostgreSQL 16 and 18 agrees with memory, including the empty set, a NULL column, a NULL element written by another tool, and the extremes in both infinity modes
docs/efcore.md#what-runs-wheremoves this direction out of the bridge row, and Translate enumeration and the bridge conversions — a batch after 10.3.0 #407 narrows to enumeration and the reverse bridge
Split out of #407.
Activity
CaffeinatedCoder commentedon Oct 26, 2026
Deferred to 10.5.0 (decided 2026-10-26). It only adds a translation and changes no answer, so it doesn't hold up the 10.4.0 tag. It stays under the backlog epic #83 until the 10.5.0 epic is opened after
v10.4.0.