Skip to content

Reject timestamptz values that do not fit the 64-bit wire format - #793

Merged
josevalim merged 3 commits into
elixir-ecto:masterfrom
dylanpulver:fix-timestamptz-int64-truncation
Sep 3, 2026
Merged

Reject timestamptz values that do not fit the 64-bit wire format#793
josevalim merged 3 commits into
elixir-ecto:masterfrom
dylanpulver:fix-timestamptz-int64-truncation

Conversation

@dylanpulver

Copy link
Copy Markdown
Contributor

TimestampTZ.encode_elixir/1 writes the microsecond value into an int64
field without checking that it fits. Erlang truncates silently, so PostgreSQL
receives a different timestamp and stores that instead.

#689 removed the year cap on the grounds that "Postgres will return an error if
the timestamp is out of range". That holds for values that survive the cast, but
not for ones that wrap back into the accepted range.

Measured against PostgreSQL 17.9, inserting into a timestamptz column:

input DateTime stored
294277-01-09 04:00:54.775807Z infinity (isfinite = false)
585706-01-01 1151-12-14 ...
587483-01-01 2928-12-13 ...
294277-01-01, 300000, 1000000 correctly rejected

The first is exact rather than lucky: it encodes to 9223372036854775807, which
is the infinity sentinel. Sampling years 294278..2000000, 477 of 960 land back
inside the accepted range.

Reachable from ordinary arithmetic, e.g. DateTime.add(dt, n, :microsecond)
with a large n.

The sibling timestamp extension already bounds its input and rejects the same
year. #689 only added a test for the negative bound, since at the time the upper
bound "will crash on most elixir versions" — on Elixir 1.20 it no longer does.

Reverting the guard fails the new test. Guarding only the two sentinels also
fails it, because the wrap cases still pass through.

Assisted by Claude (claude-opus-5); I reviewed and verified the change.

dylanpulver and others added 2 commits September 3, 2026 10:57
DateTime values beyond PostgreSQL's timestamp range were passed to the
binary construction unchecked, which truncates them to 64 bits. The
server then receives a different, in-range timestamp and stores it
instead of rejecting it. The two endpoints of the range are the infinity
sentinels, so a finite DateTime could also be stored as infinity.

The sibling timestamp extension already guards its input this way.

Co-authored-by: Claude <noreply@anthropic.com>
@josevalim josevalim closed this Sep 3, 2026
@josevalim josevalim reopened this Sep 3, 2026
Comment thread test/calendar_test.exs
@josevalim
josevalim merged commit 615d1aa into elixir-ecto:master Sep 3, 2026
1 of 13 checks passed
@josevalim

Copy link
Copy Markdown
Member

💚 💙 💜 💛 ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants