Skip to content

YearMonthRange Clamp and UpperBound() in a filter compare as the month's last day, so == against the month is false #506

Closed

Description

@CaffeinatedCoder

Found by a pre-release review of release/10.x @ a958bef (an outside review); the UpperBound() sibling was found by the sweep while verifying it. Reproduced against PostgreSQL 16 in a scratch test. Also on main @ 2949415.

Problem

A YearMonthRange is stored as a month-aligned daterange, and a YearMonth value binds as the first day of its month. In SQL, UpperBoundFunction reads the upper bound as upper(x) - 1, which is the last day of the last month (src/CodoMetis.ValueRanges.EFCore.PostgreSQL/Query/ValueRangesMethodCallTranslator.cs:1055). ClampFunction (#486/#494) builds on it (ValueRangesMethodCallTranslator.cs:1133).

Loading converts both dates to their month, so a projection agrees with memory. That is all the existing tests check (TimeAndYearMonthIntegrationTests.YearMonthRange_ServerBoundAccessors_AgreeWithInMemory).

A comparison in a filter does not agree: 2026-01-31 is not = 2026-01-01. The remark at ValueRangesMethodCallTranslator.cs:1126 says the stepped-back bound "compares as memory compares months". That holds for < against a later month and fails for =, <>, > and <= against the same month.

Scenario

period = [2026-01, 2026-01]. In memory, period.Clamp(2026-02) == 2026-01 and period.UpperBound() == 2026-01 are both true.

WHERE r."Id" = 990021 AND CASE WHEN isempty(r."BillingPeriod") THEN NULL
      ELSE LEAST(GREATEST(@february, lower(r."BillingPeriod")), upper(r."BillingPeriod") - 1) END = @january
-- no rows
WHERE r."Id" = 990031 AND upper(r."BillingPeriod") - 1 = @january
-- no rows

UpperBound() > january is true on the server and false in memory (traced). The same UpperBoundFunction serves RangeSet<YearMonthRange, YearMonth>.UpperBound() (ValueRangesMethodCallTranslator.cs:511; traced, not run).

Clamp on YearMonthRange is new in 10.4.0. The UpperBound() half is older.

Change

On a type whose elements are coarser than their store type, read the upper bound as the first day of its month, e.g. date_trunc('month', upper(x) - 1)::date, or upper(x) - interval '1 month' for a finite bound. That makes UpperBound() and Clamp compare as memory does. Keep the extremes and #489's exclusive infinity in the measurement. A definition hook (as ExclusiveInfinity is) keeps this out of the other date types.

Test

YearMonthRange and its range set in ClampDomainParityTests and the bound-reader parity tests, compared in a filter (==, >, <= against the same month), not only projected. Revert to prove each fails, and add a seed.

Release

silent: ### Changed answers in 10.4.0, main first, then a port.

Activity

  1. added this to the 10.4.0 milestone on Oct 26, 2026
  2. added
    bugSomething isn't working
    silentAnswers wrongly without throwing — fixes go in the next major, never deferred
    area/rangesRange unions, variants and the shape engines
    area/range-setRangeSet and multirange algebra
    package/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQL
    on Oct 26, 2026
  3. CaffeinatedCoder commented on Oct 26, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Fix on main: #509 (adds CoarseUpperBound, YearMonthBoundParityTests, claim E88, two seeds). Port to release/10.x: #511 → PR #515, under ### Changed answers in 10.4.0.

  4. added a commit that references this issue on Oct 26, 2026
  5. 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 algebraarea/rangesRange unions, variants and the shape enginesbugSomething isn't workingpackage/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQLpackage/efcore-nodatimeAffects CodoMetis.ValueRanges.EFCore.PostgreSQL.NodaTimeseverity/mediumInconsistency or failure in a narrow scenariosilentAnswers wrongly without throwing — fixes go in the next major, never deferred

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions