From db2e0a2e697da2a52cee6af50e295d177eb485ab Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Sat, 27 Jul 2024 18:25:51 -0400 Subject: [PATCH] Respect precision for intervals (#699) --- lib/postgrex/extensions/interval.ex | 16 +++++++++++++++- lib/postgrex/extensions/time.ex | 8 ++++---- lib/postgrex/extensions/timestamp.ex | 7 ++++--- lib/postgrex/extensions/timestamptz.ex | 7 ++++--- lib/postgrex/extensions/timetz.ex | 8 ++++---- test/query_test.exs | 22 ++++++++++++++++++++++ 6 files changed, 53 insertions(+), 15 deletions(-) diff --git a/lib/postgrex/extensions/interval.ex b/lib/postgrex/extensions/interval.ex index b7a16ef2..7860eb70 100644 --- a/lib/postgrex/extensions/interval.ex +++ b/lib/postgrex/extensions/interval.ex @@ -6,6 +6,13 @@ defmodule Postgrex.Extensions.Interval do def init(opts), do: Keyword.get(opts, :interval_decode_type, Postgrex.Interval) if Code.ensure_loaded?(Duration) do + import Bitwise, warn: false + @default_precision 6 + @precision_mask 0xFFFF + # 0xFFFF: user did not specify precision (2's complement version of -1) + # nil: coming from a super type that does not pass modifier for sub-type + @unspecified_precision [0xFFFF, nil] + def encode(_) do quote location: :keep do %Postgrex.Interval{months: months, days: days, secs: seconds, microsecs: microseconds} -> @@ -57,6 +64,13 @@ defmodule Postgrex.Extensions.Interval do seconds = rem(seconds, 60) hours = div(minutes, 60) minutes = rem(minutes, 60) + type_mod = var!(mod) + precision = if type_mod, do: type_mod &&& unquote(@precision_mask) + + precision = + if precision in unquote(@unspecified_precision), + do: unquote(@default_precision), + else: precision Duration.new!( year: years, @@ -66,7 +80,7 @@ defmodule Postgrex.Extensions.Interval do hour: hours, minute: minutes, second: seconds, - microsecond: {microseconds, 6} + microsecond: {microseconds, precision} ) end end diff --git a/lib/postgrex/extensions/time.ex b/lib/postgrex/extensions/time.ex index 3eba709f..c6c47678 100644 --- a/lib/postgrex/extensions/time.ex +++ b/lib/postgrex/extensions/time.ex @@ -4,6 +4,9 @@ defmodule Postgrex.Extensions.Time do use Postgrex.BinaryExtension, send: "time_send" @default_precision 6 + # -1: user did not specify precision + # nil: coming from a super type that does not pass modifier for sub-type + @unspecified_precision [-1, nil] def encode(_) do quote location: :keep do @@ -31,10 +34,7 @@ defmodule Postgrex.Extensions.Time do end def microsecond_to_elixir(microsec, precision) do - # use the default precision if the precision modifier from postgres is -1 (this means no precision specified) - # or if the precision is missing because we are in a super type which does not give us the sub-type's modifier - precision = if precision in [-1, nil], do: @default_precision, else: precision - + precision = if precision in @unspecified_precision, do: @default_precision, else: precision sec = div(microsec, 1_000_000) microsec = rem(microsec, 1_000_000) diff --git a/lib/postgrex/extensions/timestamp.ex b/lib/postgrex/extensions/timestamp.ex index 27184109..ee5da06d 100644 --- a/lib/postgrex/extensions/timestamp.ex +++ b/lib/postgrex/extensions/timestamp.ex @@ -9,6 +9,9 @@ defmodule Postgrex.Extensions.Timestamp do @plus_infinity 9_223_372_036_854_775_807 @minus_infinity -9_223_372_036_854_775_808 @default_precision 6 + # -1: user did not specify precision + # nil: coming from a super type that does not pass modifier for sub-type + @unspecified_precision [-1, nil] def init(opts), do: Keyword.get(opts, :allow_infinite_timestamps, false) @@ -77,9 +80,7 @@ defmodule Postgrex.Extensions.Timestamp do end defp split(secs, microsecs, precision) do - # use the default precision if the precision modifier from postgres is -1 (this means no precision specified) - # or if the precision is missing because we are in a super type which does not give us the sub-type's modifier - precision = if precision in [-1, nil], do: @default_precision, else: precision + precision = if precision in @unspecified_precision, do: @default_precision, else: precision NaiveDateTime.from_gregorian_seconds(secs + @gs_epoch, {microsecs, precision}) end diff --git a/lib/postgrex/extensions/timestamptz.ex b/lib/postgrex/extensions/timestamptz.ex index d2b7ece7..acd2d89c 100644 --- a/lib/postgrex/extensions/timestamptz.ex +++ b/lib/postgrex/extensions/timestamptz.ex @@ -10,6 +10,9 @@ defmodule Postgrex.Extensions.TimestampTZ do @plus_infinity 9_223_372_036_854_775_807 @minus_infinity -9_223_372_036_854_775_808 @default_precision 6 + # -1: user did not specify precision + # nil: coming from a super type that does not pass modifier for sub-type + @unspecified_precision [-1, nil] def init(opts), do: Keyword.get(opts, :allow_infinite_timestamps, false) @@ -67,9 +70,7 @@ defmodule Postgrex.Extensions.TimestampTZ do end defp split(secs, microsecs, precision) do - # use the default precision if the precision modifier from postgres is -1 (this means no precision specified) - # or if the precision is missing because we are in a super type which does not give us the sub-type's modifier - precision = if precision in [-1, nil], do: @default_precision, else: precision + precision = if precision in @unspecified_precision, do: @default_precision, else: precision DateTime.from_gregorian_seconds(secs + @gs_epoch, {microsecs, precision}) end diff --git a/lib/postgrex/extensions/timetz.ex b/lib/postgrex/extensions/timetz.ex index 9f8b34c8..b63a5a4b 100644 --- a/lib/postgrex/extensions/timetz.ex +++ b/lib/postgrex/extensions/timetz.ex @@ -5,6 +5,9 @@ defmodule Postgrex.Extensions.TimeTZ do @day (:calendar.time_to_seconds({23, 59, 59}) + 1) * 1_000_000 @default_precision 6 + # -1: user did not specify precision + # nil: coming from a super type that does not pass modifier for sub-type + @unspecified_precision [-1, nil] def encode(_) do quote location: :keep do @@ -51,10 +54,7 @@ defmodule Postgrex.Extensions.TimeTZ do end defp microsecond_to_elixir(microsec, precision) do - # use the default precision if the precision modifier from postgres is -1 (this means no precision specified) - # or if the precision is missing because we are in a super type which does not give us the sub-type's modifier - precision = if precision in [-1, nil], do: @default_precision, else: precision - + precision = if precision in @unspecified_precision, do: @default_precision, else: precision sec = div(microsec, 1_000_000) microsec = rem(microsec, 1_000_000) diff --git a/test/query_test.exs b/test/query_test.exs index ff2b7d66..eb811db5 100644 --- a/test/query_test.exs +++ b/test/query_test.exs @@ -193,6 +193,28 @@ defmodule QueryTest do assert [[[%Duration{second: 10, microsecond: {240_000, 6}}]]] = P.query!(pid, "SELECT ARRAY[interval '10240000 microseconds']", []).rows end + + test "decode interval with Elixir Duration: precision is given" do + opts = [database: "postgrex_test", backoff_type: :stop, types: Postgrex.ElixirDurationTypes] + {:ok, pid} = P.start_link(opts) + + assert [[%Duration{second: 10, microsecond: {240_000, 2}}]] = + P.query!(pid, "SELECT interval(2) '10240000 microseconds'", []).rows + + assert [[[%Duration{second: 10, microsecond: {0, 0}}]]] = + P.query!(pid, "SELECT ARRAY[interval(0) '10240000 microseconds']", []).rows + end + + test "decode interval with Elixir Duration: field is given but not precision" do + opts = [database: "postgrex_test", backoff_type: :stop, types: Postgrex.ElixirDurationTypes] + {:ok, pid} = P.start_link(opts) + + assert [[%Duration{week: 1, day: 3, microsecond: {0, 6}}]] = + P.query!(pid, "SELECT interval '10' DAY", []).rows + + assert [[[%Duration{week: 1, day: 3, microsecond: {0, 6}}]]] = + P.query!(pid, "SELECT ARRAY[interval '10' DAY]", []).rows + end end test "decode point", context do