Skip to content

Commit b3e895a

Browse files
Support infinite intervals (#759)
1 parent 87ec645 commit b3e895a

File tree

4 files changed

+154
-18
lines changed

4 files changed

+154
-18
lines changed

lib/postgrex/extensions/interval.ex

Lines changed: 98 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@ defmodule Postgrex.Extensions.Interval do
33
import Postgrex.BinaryUtils, warn: false
44
use Postgrex.BinaryExtension, send: "interval_send"
55

6+
@int64_max 9_223_372_036_854_775_807
7+
@int64_min -9_223_372_036_854_775_808
8+
@int32_max 2_147_483_647
9+
@int32_min -2_147_483_648
10+
611
def init(opts) do
7-
case Keyword.get(opts, :interval_decode_type, Postgrex.Interval) do
8-
type when type in [Postgrex.Interval, Duration] ->
9-
type
12+
infinity? = Keyword.get(opts, :allow_infinite_intervals, false)
13+
type = Keyword.get(opts, :interval_decode_type, Postgrex.Interval)
1014

11-
other ->
12-
raise ArgumentError,
13-
"#{inspect(other)} is not valid for `:interval_decode_type`. Please use either `Postgrex.Interval` or `Duration`"
15+
if type not in [Postgrex.Interval, Duration] do
16+
raise ArgumentError,
17+
"#{inspect(type)} is not valid for `:interval_decode_type`. Please use either `Postgrex.Interval` or `Duration`"
1418
end
19+
20+
{type, infinity?}
1521
end
1622

1723
if Code.ensure_loaded?(Duration) do
@@ -22,8 +28,18 @@ defmodule Postgrex.Extensions.Interval do
2228
# nil: coming from a super type that does not pass modifier for sub-type
2329
@unspecified_precision [0xFFFF, nil]
2430

25-
def encode(_) do
31+
def encode({_type, infinity?}) do
2632
quote location: :keep do
33+
:inf ->
34+
if unquote(infinity?),
35+
do: unquote(__MODULE__).infinity_binary(:inf),
36+
else: unquote(__MODULE__).raise_encode_infinity(:inf)
37+
38+
:"-inf" ->
39+
if unquote(infinity?),
40+
do: unquote(__MODULE__).infinity_binary(:"-inf"),
41+
else: unquote(__MODULE__).raise_encode_infinity(:"-inf")
42+
2743
%Postgrex.Interval{months: months, days: days, secs: seconds, microsecs: microseconds} ->
2844
microseconds = 1_000_000 * seconds + microseconds
2945
<<16::int32(), microseconds::int64(), days::int32(), months::int32()>>
@@ -49,22 +65,31 @@ defmodule Postgrex.Extensions.Interval do
4965
end
5066
end
5167

52-
def decode(type) do
68+
def decode({type, infinity?}) do
5369
quote location: :keep, generated: true do
5470
<<16::int32(), microseconds::int64(), days::int32(), months::int32()>> ->
5571
unquote(__MODULE__).decode_interval(
5672
microseconds,
5773
days,
5874
months,
5975
var!(mod),
60-
unquote(type)
76+
unquote(type),
77+
unquote(infinity?)
6178
)
6279
end
6380
end
6481

6582
## Helpers
6683

67-
def decode_interval(microseconds, days, months, _type_mod, Postgrex.Interval) do
84+
def decode_interval(@int64_max, @int32_max, @int32_max, _type_mod, _struct, infinity?) do
85+
if infinity?, do: :inf, else: raise_decode_infinity("infinity")
86+
end
87+
88+
def decode_interval(@int64_min, @int32_min, @int32_min, _type_mod, _struct, infinity?) do
89+
if infinity?, do: :"-inf", else: raise_decode_infinity("-infinity")
90+
end
91+
92+
def decode_interval(microseconds, days, months, _type_mod, Postgrex.Interval, _infinity?) do
6893
seconds = div(microseconds, 1_000_000)
6994
microseconds = rem(microseconds, 1_000_000)
7095

@@ -76,7 +101,7 @@ defmodule Postgrex.Extensions.Interval do
76101
}
77102
end
78103

79-
def decode_interval(microseconds, days, months, type_mod, Duration) do
104+
def decode_interval(microseconds, days, months, type_mod, Duration, _infinity?) do
80105
seconds = div(microseconds, 1_000_000)
81106
microseconds = rem(microseconds, 1_000_000)
82107
precision = if type_mod, do: type_mod &&& unquote(@precision_mask)
@@ -94,8 +119,18 @@ defmodule Postgrex.Extensions.Interval do
94119
)
95120
end
96121
else
97-
def encode(_) do
122+
def encode({_type, infinity?}) do
98123
quote location: :keep do
124+
:inf ->
125+
if unquote(infinity?),
126+
do: unquote(__MODULE__).infinity_binary(:inf),
127+
else: unquote(__MODULE__).raise_encode_infinity(:inf)
128+
129+
:"-inf" ->
130+
if unquote(infinity?),
131+
do: unquote(__MODULE__).infinity_binary(:"-inf"),
132+
else: unquote(__MODULE__).raise_encode_infinity(:"-inf")
133+
99134
%Postgrex.Interval{months: months, days: days, secs: seconds, microsecs: microseconds} ->
100135
microseconds = 1_000_000 * seconds + microseconds
101136
<<16::int32(), microseconds::int64(), days::int32(), months::int32()>>
@@ -105,16 +140,30 @@ defmodule Postgrex.Extensions.Interval do
105140
end
106141
end
107142

108-
def decode(_) do
143+
def decode({_type, infinity?}) do
109144
quote location: :keep do
110145
<<16::int32(), microseconds::int64(), days::int32(), months::int32()>> ->
111-
unquote(__MODULE__).decode_interval(microseconds, days, months, Postgrex.Interval)
146+
unquote(__MODULE__).decode_interval(
147+
microseconds,
148+
days,
149+
months,
150+
Postgrex.Interval,
151+
unquote(infinity?)
152+
)
112153
end
113154
end
114155

115156
## Helpers
116157

117-
def decode_interval(microseconds, days, months, Postgrex.Interval) do
158+
def decode_interval(@int64_max, @int32_max, @int32_max, _struct, infinity?) do
159+
if infinity?, do: :inf, else: raise_decode_infinity("infinity")
160+
end
161+
162+
def decode_interval(@int64_min, @int32_min, @int32_min, _struct, infinity?) do
163+
if infinity?, do: :"-inf", else: raise_decode_infinity("-infinity")
164+
end
165+
166+
def decode_interval(microseconds, days, months, Postgrex.Interval, _infinity?) do
118167
seconds = div(microseconds, 1_000_000)
119168
microseconds = rem(microseconds, 1_000_000)
120169

@@ -126,4 +175,38 @@ defmodule Postgrex.Extensions.Interval do
126175
}
127176
end
128177
end
178+
179+
def infinity_binary(:inf) do
180+
<<16::int32(), @int64_max::int64(), @int32_max::int32(), @int32_max::int32()>>
181+
end
182+
183+
def infinity_binary(:"-inf") do
184+
<<16::int32(), @int64_min::int64(), @int32_min::int32(), @int32_min::int32()>>
185+
end
186+
187+
def raise_encode_infinity(type) do
188+
raise ArgumentError, """
189+
got query parameter value of `#{inspect(type)}`. If you want to support infinite intervals \
190+
in your application, you can enable them by defining your own types:
191+
192+
Postgrex.Types.define(MyApp.PostgrexTypes, [], allow_infinite_intervals: true)
193+
194+
And then configuring your database to use it:
195+
196+
types: MyApp.PostgrexTypes
197+
"""
198+
end
199+
200+
defp raise_decode_infinity(type) do
201+
raise ArgumentError, """
202+
got \"#{type}\" from PostgreSQL. If you want to support infinite intervals \
203+
in your application, you can enable them by defining your own types:
204+
205+
Postgrex.Types.define(MyApp.PostgrexTypes, [], allow_infinite_intervals: true)
206+
207+
And then configuring your database to use it:
208+
209+
types: MyApp.PostgrexTypes
210+
"""
211+
end
129212
end

lib/postgrex/types.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,11 @@ defmodule Postgrex.Types do
316316
317317
* `:allow_infinite_timestamps` - A boolean controlling whether or not
318318
the built-in extensions `timestamp` and `timestamptz` will allow
319-
a value of infinity to be decoded. Defaults to `false`.
319+
infinite values (`:inf`/`:"-inf"`) to be decoded. Defaults to `false`.
320+
321+
* `:allow_infinite_intervals` - A boolean controlling whether or not
322+
the built-in extension `interval` will allow infinite values (`:inf`/`:"-inf"`)
323+
to be encoded and decoded. Defaults to `false`.
320324
321325
* `:interval_decode_type` - The struct that intervals will be decoded
322326
into. Either `Postgrex.Interval` or `Duration` (Elixir 1.17.0+ only).

test/query_test.exs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ defmodule QueryTest do
44
import ExUnit.CaptureLog
55
alias Postgrex, as: P
66

7-
Postgrex.Types.define(Postgrex.ElixirDurationTypes, [], interval_decode_type: Duration)
7+
Postgrex.Types.define(Postgrex.ElixirDurationTypes, [],
8+
interval_decode_type: Duration,
9+
allow_infinite_intervals: true
10+
)
811

912
setup context do
1013
opts = [
@@ -141,6 +144,29 @@ defmodule QueryTest do
141144
query("SELECT interval '10240000 microseconds'", [])
142145
end
143146

147+
@tag min_pg_version: "17.0"
148+
test "decode infinite interval" do
149+
opts = [database: "postgrex_test", backoff_type: :stop, types: Postgrex.ElixirDurationTypes]
150+
{:ok, pid} = P.start_link(opts)
151+
152+
assert P.query!(pid, "SELECT 'infinity'::interval", []).rows == [[:inf]]
153+
assert P.query!(pid, "SELECT '-infinity'::interval", []).rows == [[:"-inf"]]
154+
end
155+
156+
@tag min_pg_version: "17.0"
157+
test "decode infinite interval raise when option not specified" do
158+
opts = [database: "postgrex_test", backoff_type: :stop]
159+
{:ok, pid} = P.start_link(opts)
160+
161+
assert_raise ArgumentError, ~r/got "infinity" from PostgreSQL/, fn ->
162+
P.query(pid, "SELECT 'infinity'::interval", [])
163+
end
164+
165+
assert_raise ArgumentError, ~r/got "-infinity" from PostgreSQL/, fn ->
166+
P.query(pid, "SELECT '-infinity'::interval", [])
167+
end
168+
end
169+
144170
if Version.match?(System.version(), ">= 1.17.0") do
145171
test "decode interval with Elixir Duration" do
146172
opts = [database: "postgrex_test", backoff_type: :stop, types: Postgrex.ElixirDurationTypes]
@@ -1009,6 +1035,29 @@ defmodule QueryTest do
10091035
])
10101036
end
10111037

1038+
@tag min_pg_version: "17.0"
1039+
test "encode infinite interval" do
1040+
opts = [database: "postgrex_test", backoff_type: :stop, types: Postgrex.ElixirDurationTypes]
1041+
{:ok, pid} = P.start_link(opts)
1042+
1043+
assert P.query!(pid, "SELECT $1::interval", [:inf]).rows == [[:inf]]
1044+
assert P.query!(pid, "SELECT $1::interval", [:"-inf"]).rows == [[:"-inf"]]
1045+
end
1046+
1047+
@tag min_pg_version: "17.0"
1048+
test "encode infinite interval raise when option not specified" do
1049+
opts = [database: "postgrex_test", backoff_type: :stop]
1050+
{:ok, pid} = P.start_link(opts)
1051+
1052+
assert_raise ArgumentError, ~r/got query parameter value of `:inf`/, fn ->
1053+
P.query(pid, "SELECT $1::interval", [:inf])
1054+
end
1055+
1056+
assert_raise ArgumentError, ~r/got query parameter value of `:"-inf"`/, fn ->
1057+
P.query(pid, "SELECT $1::interval", [:"-inf"])
1058+
end
1059+
end
1060+
10121061
if Version.match?(System.version(), ">= 1.17.0") do
10131062
test "encode interval with Elixir duration", context do
10141063
assert [[%Postgrex.Interval{months: 0, days: 0, secs: 0, microsecs: 0}]] =

test/test_helper.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ replication_exclude =
7272
end
7373

7474
version_exclude =
75-
[{8, 4}, {9, 0}, {9, 1}, {9, 2}, {9, 3}, {9, 4}, {9, 5}, {10, 0}, {13, 0}, {14, 0}]
75+
[{8, 4}, {9, 0}, {9, 1}, {9, 2}, {9, 3}, {9, 4}, {9, 5}, {10, 0}, {13, 0}, {14, 0}, {17, 0}]
7676
|> Enum.filter(fn x -> x > pg_version end)
7777
|> Enum.map(fn {major, minor} -> {:min_pg_version, "#{major}.#{minor}"} end)
7878

0 commit comments

Comments
 (0)