Skip to content

Commit

Permalink
Add support for Elixir Durations (#696)
Browse files Browse the repository at this point in the history
  • Loading branch information
greg-rychlewski authored Jul 26, 2024
1 parent 85bac2d commit 3e9ea98
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 16 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
- pg:
version: 14
pair:
elixir: 1.16.2
elixir: 1.17.1
otp: 25.3
lint: lint
env:
Expand Down
100 changes: 85 additions & 15 deletions lib/postgrex/extensions/interval.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,93 @@ defmodule Postgrex.Extensions.Interval do
import Postgrex.BinaryUtils, warn: false
use Postgrex.BinaryExtension, send: "interval_send"

def encode(_) do
quote location: :keep do
%Postgrex.Interval{months: months, days: days, secs: secs, microsecs: microsecs} ->
microsecs = secs * 1_000_000 + microsecs
<<16::int32(), microsecs::int64(), days::int32(), months::int32()>>

other ->
raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.Interval)
def init(opts), do: Keyword.get(opts, :interval_decode_type, Postgrex.Interval)

if Code.ensure_loaded?(Duration) do
def encode(_) do
quote location: :keep do
%Postgrex.Interval{months: months, days: days, secs: seconds, microsecs: microseconds} ->
microseconds = 1_000_000 * seconds + microseconds
<<16::int32(), microseconds::int64(), days::int32(), months::int32()>>

%Duration{
year: years,
month: months,
week: weeks,
day: days,
hour: hours,
minute: minutes,
second: seconds,
microsecond: {microseconds, _precision}
} ->
months = 12 * years + months
days = 7 * weeks + days
microseconds = 1_000_000 * (3600 * hours + 60 * minutes + seconds) + microseconds
<<16::int32(), microseconds::int64(), days::int32(), months::int32()>>

other ->
raise DBConnection.EncodeError,
Postgrex.Utils.encode_msg(other, {Postgrex.Interval, Duration})
end
end

def decode(type) do
quote location: :keep do
<<16::int32(), microseconds::int64(), days::int32(), months::int32()>> ->
seconds = div(microseconds, 1_000_000)
microseconds = rem(microseconds, 1_000_000)

case unquote(type) do
Postgrex.Interval ->
%Postgrex.Interval{
months: months,
days: days,
secs: seconds,
microsecs: microseconds
}

Duration ->
years = div(months, 12)
months = rem(months, 12)
weeks = div(days, 7)
days = rem(days, 7)
minutes = div(seconds, 60)
seconds = rem(seconds, 60)
hours = div(minutes, 60)
minutes = rem(minutes, 60)

Duration.new!(
year: years,
month: months,
week: weeks,
day: days,
hour: hours,
minute: minutes,
second: seconds,
microsecond: {microseconds, 6}
)
end
end
end
else
def encode(_) do
quote location: :keep do
%Postgrex.Interval{months: months, days: days, secs: seconds, microsecs: microseconds} ->
microseconds = 1_000_000 * seconds + microseconds
<<16::int32(), microseconds::int64(), days::int32(), months::int32()>>

other ->
raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, Postgrex.Interval)
end
end
end

def decode(_) do
quote location: :keep do
<<16::int32(), microsecs::int64(), days::int32(), months::int32()>> ->
secs = div(microsecs, 1_000_000)
microsecs = rem(microsecs, 1_000_000)
%Postgrex.Interval{months: months, days: days, secs: secs, microsecs: microsecs}
def decode(_) do
quote location: :keep do
<<16::int32(), microseconds::int64(), days::int32(), months::int32()>> ->
seconds = div(microseconds, 1_000_000)
microseconds = rem(microseconds, 1_000_000)
%Postgrex.Interval{months: months, days: days, secs: seconds, microsecs: microseconds}
end
end
end
end
100 changes: 100 additions & 0 deletions test/query_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ defmodule QueryTest do
import ExUnit.CaptureLog
alias Postgrex, as: P

Postgrex.Types.define(Postgrex.ElixirDurationTypes, [], interval_decode_type: Duration)

setup context do
opts = [
database: "postgrex_test",
Expand Down Expand Up @@ -133,6 +135,66 @@ defmodule QueryTest do
query("SELECT interval '10240000 microseconds'", [])
end

if Version.match?(System.version(), ">= 1.17.0") do
test "decode interval with Elixir Duration" do
opts = [database: "postgrex_test", backoff_type: :stop, types: Postgrex.ElixirDurationTypes]
{:ok, pid} = P.start_link(opts)

assert [[%Duration{microsecond: {0, 6}}]] =
P.query!(pid, "SELECT interval '0'", []).rows

assert [[%Duration{year: 100, microsecond: {0, 6}}]] =
P.query!(pid, "SELECT interval '100 years'", []).rows

assert [[%Duration{month: 10, microsecond: {0, 6}}]] =
P.query!(pid, "SELECT interval '10 months'", []).rows

assert [[%Duration{week: 100, microsecond: {0, 6}}]] =
P.query!(pid, "SELECT interval '100 weeks'", []).rows

assert [[%Duration{day: 5, microsecond: {0, 6}}]] =
P.query!(pid, "SELECT interval '5 days'", []).rows

assert [[%Duration{hour: 100, microsecond: {0, 6}}]] =
P.query!(pid, "SELECT interval '100 hours'", []).rows

assert [[%Duration{minute: 10, microsecond: {0, 6}}]] =
P.query!(pid, "SELECT interval '10 minutes'", []).rows

assert [[%Duration{second: 10, microsecond: {0, 6}}]] =
P.query!(pid, "SELECT interval '10 secs'", []).rows

assert [
[
%Duration{
year: 1,
month: 2,
week: 5,
day: 5,
hour: 3,
minute: 2,
second: 1,
microsecond: {0, 6}
}
]
] =
P.query!(
pid,
"SELECT interval '1 year 2 months 40 days 3 hours 2 minutes 1 seconds'",
[]
).rows

assert [[%Duration{second: 53, microsecond: {204_800, 6}}]] =
P.query!(pid, "SELECT interval '53 secs 204800 microseconds'", []).rows

assert [[%Duration{second: 10, microsecond: {240_000, 6}}]] =
P.query!(pid, "SELECT interval '10240000 microseconds'", []).rows

assert [[[%Duration{second: 10, microsecond: {240_000, 6}}]]] =
P.query!(pid, "SELECT ARRAY[interval '10240000 microseconds']", []).rows
end
end

test "decode point", context do
assert [[%Postgrex.Point{x: -97.5, y: 100.1}]] ==
query("SELECT point(-97.5, 100.1)::point", [])
Expand Down Expand Up @@ -896,6 +958,44 @@ defmodule QueryTest do
])
end

if Version.match?(System.version(), ">= 1.17.0") do
test "encode interval with Elixir duration", context do
assert [[%Postgrex.Interval{months: 0, days: 0, secs: 0, microsecs: 0}]] =
query("SELECT $1::interval", [Duration.new!([])])

assert [[%Postgrex.Interval{months: 100, days: 0, secs: 0, microsecs: 0}]] =
query("SELECT $1::interval", [Duration.new!(month: 100)])

assert [[%Postgrex.Interval{months: 0, days: 100, secs: 0, microsecs: 0}]] =
query("SELECT $1::interval", [Duration.new!(day: 100)])

assert [[%Postgrex.Interval{months: 0, days: 0, secs: 100, microsecs: 0}]] =
query("SELECT $1::interval", [Duration.new!(second: 100)])

assert [[%Postgrex.Interval{months: 14, days: 40, secs: 10920, microsecs: 0}]] =
query("SELECT $1::interval", [Duration.new!(month: 14, day: 40, second: 10920)])

assert [[%Postgrex.Interval{months: 14, days: 40, secs: 10921, microsecs: 24000}]] =
query("SELECT $1::interval", [
Duration.new!(month: 14, day: 40, second: 10920, microsecond: {1_024_000, 0})
])

assert [[%Postgrex.Interval{months: 74, days: 54, secs: 46921, microsecs: 24000}]] =
query("SELECT $1::interval", [
Duration.new!(
year: 5,
month: 14,
week: 2,
day: 40,
hour: 9,
minute: 60,
second: 10920,
microsecond: {1_024_000, 0}
)
])
end
end

test "encode arrays", context do
assert [[[]]] = query("SELECT $1::integer[]", [[]])
assert [[[1]]] = query("SELECT $1::integer[]", [[1]])
Expand Down

0 comments on commit 3e9ea98

Please sign in to comment.