Skip to content

A server-side predicate over a stored NaN or infinite float answers in PostgreSQL's order; only loading throws #232

Closed

Description

@CaffeinatedCoder

Problem

docs/why.md says loading a row with a NaN or infinite float "throws instead of returning a value that would sort differently in memory", and docs/efcore.md says "Only finite values load". Both suggest the model is protected from PostgreSQL's NaN order, but a server-side predicate over such a row answers silently in PostgreSQL's order. Found by the pre-tag analysis (#179), part B, with rows written directly in SQL:

Stored Query Answer
Score = '[0,NaN)' Where(r => r.Score.Contains(1e308)) the row is returned
Ratio = 'NaN' r.Score.Contains(r.Ratio) on [0,) 1 row
Weights = '{1,NaN}' Select(r => r.Weights.Count) 2

Only materializing the entity throws. The operand guards are complete: a NaN constant, a NaN parameter, and server-side constructors all throw before the query runs.

Change

Document it where the finite-only rule is stated, in docs/efcore.md (custom types / "stricter than the database") and docs/why.md. A query over rows the application did not write can answer in PostgreSQL's NaN order. To keep NaN and infinity out of a column, add a CHECK constraint (for example lower(x) <> 'NaN' AND upper(x) <> 'NaN', NOT ('NaN' = ANY(x)) for arrays, and the infinity equivalents), measured on a live server. Add a claim for the sentence.

Done when

The docs state the boundary. A live test shows the recommended constraints refuse NaN and ±Infinity in each column kind (range, multirange, array).

Activity

  1. added this to the 10.0.0 milestone on Oct 18, 2026
  2. added
    documentationImprovements or additions to documentation
    area/rangesRange unions, variants and the shape engines
    area/value-setsValue set families and wrapper arities
    package/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQL
    on Oct 18, 2026
  3. CaffeinatedCoder commented on Oct 18, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Measured on PostgreSQL 16.15 and 18.1 while fixing this (#241): the bounds check suggested above (lower(x) <> 'NaN' AND upper(x) <> 'NaN' and the infinity equivalents) is complete for a range but not for a multirange. A multirange's lower/upper are its outer bounds and are NULL beside an unbounded side, which a CHECK lets pass, so {(,-Infinity],[1,2)}, {(,-Infinity),[1,2)}, {[1,2),[Infinity,)}, {[1,2),(Infinity,)} and {(,0),[1,Infinity),[NaN,)} are all stored. The documented multirange constraint checks the text instead (m::text !~* '(nan|inf)'); arrays use NOT (a && '{NaN,Infinity,-Infinity}').

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 enginesarea/value-setsValue set families and wrapper aritiesdocumentationImprovements or additions to documentationpackage/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQL

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions