Skip to content

Translate RangeSet.Count as a counted unnest of the multirange, built after the alias pass as #478 builds its subqueries (split from #94) #484

Closed

Description

@CaffeinatedCoder

Problem

RangeSet.Count doesn't translate. bookings.Where(b => b.Blocked.Count > 2) ("how fragmented is this availability?") throws, and in a projection EF Core loads the multirange and counts it in memory.

#94's spike (2026-10-18) stopped here. PostgreSQL has no multirange cardinality function, so Count needs a scalar subquery, (SELECT count(*) FROM unnest(m)), and a member translator has no alias manager to build one.

#478 removed that obstacle. ValueSetSqlGuard builds its canonical subqueries after EF Core's alias pass (SqlAliasManager.PostprocessAliases), under vrN aliases that can't collide, and the same code works on EF Core 10 and 11. The count can be built the same way.

Change

  • The translator emits a placeholder function for RangeSet<TRange, T>.Count. The processor that runs the guard expands it into (SELECT count(*)::integer FROM unnest(m) AS vrN). count(*) is bigint and Count is an int.
  • NULL needs the NULL-over-NULL wrapper. Measured on PostgreSQL 16.15 and 18.1: (SELECT count(*) FROM unnest(NULL::int4multirange)) is 0, not NULL. Value set Count over a NULL column is cardinality(NULL), which is NULL, so the count goes inside Translate value set Intersect, Except, Add and a canonical Union for the non-text families with composed SQL #478's CASE (NullOverNullOperand).
  • The placeholder fails closed like Translate value set Intersect, Except, Add and a canonical Union for the non-text families with composed SQL #478's. Without the decorated processor, PostgreSQL rejects an unknown function instead of counting.
  • No canonical-form caveat, unlike array_cat. PostgreSQL stores a multirange canonical: {[1,3),[3,8),[10,12)} counts 2. That is the number of ranges memory holds after reading the row.
  • Decide where the expansion lives. ValueSetSqlGuard is about value sets, and the multirange count would be its first non-set expression. A shared expander that both use may read better.

Out of scope:

Does it change an answer?

No. A predicate that throws today gets the in-memory answer. A projection that counted in memory counts in SQL, with the same result. It's an addition: it lands on main first and is cherry-picked with -x into release/10.x as a 10.x minor.

Done when

Split out of #94.

Activity

  1. added this to the 10.4.0 milestone on Oct 26, 2026
  2. added a commit that references this issue on Oct 26, 2026
  3. added a commit that references this issue on Oct 26, 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 algebraenhancementNew feature or requestpackage/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQLpackage/efcore-nodatimeAffects CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTime

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions