Skip to content

Translate Clamp on the remaining range types, each once it agrees with memory as Int32Range, Int64Range and DecimalRange do #486

Closed

Description

@CaffeinatedCoder

Problem

Clamp(value) translates on Int32Range, Int64Range and DecimalRange (#406, 10.3.0). On every other range type it runs in memory only. docs/efcore.md#what-runs-where gives a reason about verification, not a known disagreement: "The same LEAST(GREATEST(…)) form would apply, but it has only been checked against memory on Int32Range, Int64Range and DecimalRange."

That's where #478's ten set families were before #478 measured them. Measure each remaining type, translate those that agree, and name the disagreement for those that don't.

Change

Extend the translation one type at a time, each after ClampParityTests agrees with memory on PostgreSQL 16 and 18:

  • Float64Range, Float32Range. Continuous. To measure: which zero comes back when the value is -0 and a bound is 0 (memory and LEAST/GREATEST may pick different ones), and the real casts.
  • DateRange, LocalDateRange. Discrete: one day back from an exclusive upper bound, as the integer types step back one. To measure: an exclusive upper bound at DateOnly.MaxValue. Npgsql writes it as infinity, and 'infinity'::date - 1 is still infinity, while memory steps back to 9999-12-30. Measure with Npgsql's infinity conversions on and off (the InfinityDisabled integration project).
  • YearMonthRange. Stored as a month-aligned daterange, so the step back is a month, not a day.
  • DateTimeRange, DateTimeOffsetRange, LocalDateTimeRange, InstantRange. Continuous. To measure: a value with sub-microsecond ticks inside the range, which memory returns as it is and the database truncates on bind, and the bounds at the extremes, which Npgsql maps to ±infinity.
  • TimeRange. To measure: time allows 24:00:00, which TimeOnly doesn't.

A type whose disagreement can't be handled in the SQL (for example by a CASE at the extreme) stays in memory, with the reason in what-runs-where, as Length does for temporal ranges.

Does it change an answer?

No. For each widened type, a predicate that throws today gets the in-memory answer, and a projection that clamped in memory gives the same result in SQL. A type that would answer differently is not widened. It lands on main first and is cherry-picked with -x into release/10.x as a 10.x minor.

Done when

  • each widened type has its SQL asserted in ClampTranslationTests and its answers in ClampParityTests, on PostgreSQL 16 and 18, including bounds at the extremes and, for temporal types, the InfinityDisabled project
  • docs/efcore.md#what-runs-where: the two Clamp rows say which types run where and why
  • the seed clamp-reads-the-stored-exclusive-upper-bound is still caught, and ShapeMatrixCoverageTests and DiscreteDomainBoundaryTests cover the new types

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/rangesRange unions, variants and the shape enginesenhancementNew 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