Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions lib/postgrex/extensions/timestamptz.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,19 @@ defmodule Postgrex.Extensions.TimestampTZ do
## Helpers

def encode_elixir(%DateTime{utc_offset: 0, std_offset: 0} = datetime) do
microsecs = DateTime.to_unix(datetime, :microsecond)
<<8::int32(), microsecs - @us_epoch::int64()>>
microsecs = DateTime.to_unix(datetime, :microsecond) - @us_epoch

# PostgreSQL rejects timestamps outside its own range, but only when it
# receives the value we meant to send. Anything that does not fit in a
# signed 64-bit integer is silently truncated by the binary construction
# below and arrives as a different, in-range timestamp; the two endpoints
# are the infinity sentinels.
if microsecs > @minus_infinity and microsecs < @plus_infinity do
<<8::int32(), microsecs::int64()>>
else
raise ArgumentError,
"#{inspect(datetime)} is beyond the range PostgreSQL can represent in a timestamptz"
end
end

def encode_elixir(%DateTime{} = datetime) do
Expand Down
21 changes: 21 additions & 0 deletions test/calendar_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,27 @@ defmodule CalendarTest do
assert message =~ "timestamp out of range"
end

if Version.match?(System.version(), ">= 1.19.0") do
test "encode timestamptz beyond the 64-bit wire range", context do
# These do not fit in the signed 64-bit microsecond wire format. Truncating
# them sends PostgreSQL a different, in-range timestamp that it accepts, so
# the value has to be rejected here instead.
sentinel =
DateTime.from_naive!(NaiveDateTime.new!(294_277, 1, 9, 4, 0, 54, {775_807, 6}), "Etc/UTC")

wraps_into_range =
DateTime.from_naive!(NaiveDateTime.new!(585_706, 1, 1, 0, 0, 0), "Etc/UTC")

assert_raise ArgumentError, ~r/beyond the range/, fn ->
query("SELECT $1::timestamptz", [sentinel])
end

assert_raise ArgumentError, ~r/beyond the range/, fn ->
Comment thread
josevalim marked this conversation as resolved.
query("SELECT $1::timestamptz", [wraps_into_range])
end
end
end

@tag :capture_log
test "decode infinity", context do
assert_raise ArgumentError, fn -> query("SELECT 'infinity'::timestamp", []) end
Expand Down
Loading