Skip to content

A temporal literal with sub-microsecond digits is a different value than PostgreSQL reads from the same text #126

Closed

Description

@CaffeinatedCoder

A temporal literal with more than six fraction digits is a different value in the library than in PostgreSQL. The server rounds the text half-to-even to the microsecond; the library keeps it to the 100 ns (BCL) or the nanosecond (NodaTime), and EF Core then stores it truncated to the microsecond. So the same text holds three values. Measured on main (3ecfa10) against PostgreSQL 17.10:

TimeRange.Parse("[09:00:00.1234567,)", null);                  // [09:00:00.1234567,)
DateTimeRange.Parse("[2026-01-01 10:00:00.1234567,)", null);   // 10:00:00.1234567
// written back by EF Core (SQL constant or JSON document): ["2026-01-01 10:00:00.123456",)
SELECT '[2026-01-01 10:00:00.1234567,)'::tsrange;   -- ["2026-01-01 10:00:00.123457",)
SELECT '09:00:00.0000015'::time;                    -- 09:00:00.000002

Every temporal range, multirange and value set type, BCL and NodaTime, through every entry point (Parse, TryParse, JSON, TypeConverter, the EF JSON document reader). Found by LiteralCorpusProbeTests (#123), family sub-microsecond.

Authority. docs/serialization.md says the text is "the PostgreSQL literal, the same text the server itself reads and prints", and that the fraction "is truncated to the 100 ns the BCL types hold". docs/efcore.md#put-a-range-or-set-inside-a-json-column says the store holds "the store's microsecond, truncated toward 2000-01-01 as Npgsql truncates a parameter". Each rule is documented; together they mean a document or a configuration value written by another tool with seven fraction digits reads 1 µs off from what the server reads out of the same text, silently.

Cause. IsoTemporal.TruncateFraction (BCL) and NodaPatterns (NodaTime) keep the extra digits; StoreTemporal / NodaStoreTemporal truncate on the way out. PostgreSQL's timestamp_in / time_in round.

Options. Round half-to-even to the microsecond when a literal has more than six fraction digits (changes in-memory values → breaking), or document the divergence next to the store rule and add it to docs/architecture.md#domain-width-and-the-boundary. Either way the candidate rows in test/oracles/candidates.json go when it lands.

Activity

  1. added this to the 10.0.0 milestone on Oct 17, 2026
  2. added
    bugSomething isn't working
    silentAnswers wrongly without throwing — fixes go in the next major, never deferred
    found-by/oracleFound by the generated-oracle machinery (claims × API surfaces)
    package/nodatimeAffects CodoMetis.ValueRanges.NodaTime
    package/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQL
    area/literalsParse, TryParse and formatting
    on Oct 17, 2026
  3. CaffeinatedCoder commented on Oct 18, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Resolved by documenting (#175, stacked on #173/#171), the issue's second option. Rounding on parse would break claim S08 (sub-second ticks survive the literal and JSON): a value built from DateTime.Now prints seven digits and would read back different. Planting six-digit parsing fails 12 round-trip unit tests. It would also disagree with every parameter, which Npgsql truncates. Now true: docs/architecture.md#domain-width-and-the-boundary (Precision) and docs/serialization.md (claim J37) state the three values (library every digit, server rounded half-to-even, store truncated, at most 1 µs apart). DomainBoundaryDivergenceTests pins them against postgres:17 for DateTimeRange and LocalDateTimeRange. The corpus probe counts the family as documented, and the 22 candidates are gone.

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/literalsParse, TryParse and formattingbugSomething isn't workingfound-by/oracleFound by the generated-oracle machinery (claims × API surfaces)package/coreAffects CodoMetis.ValueRangespackage/efcoreAffects CodoMetis.ValueRanges.EFCore.PostgreSQLpackage/nodatimeAffects CodoMetis.ValueRanges.NodaTimeseverity/lowCosmetic, message or documentation-level differencesilentAnswers 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