Repository navigation
A temporal literal with sub-microsecond digits is a different value than PostgreSQL reads from the same text #126
Description
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.000002Every 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
CaffeinatedCoder commentedon Oct 18, 2026
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.Nowprints 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) anddocs/serialization.md(claim J37) state the three values (library every digit, server rounded half-to-even, store truncated, at most 1 µs apart).DomainBoundaryDivergenceTestspins them against postgres:17 forDateTimeRangeandLocalDateTimeRange. The corpus probe counts the family as documented, and the 22 candidates are gone.