From b56475744fdd902c2af730b3a27cfc1d5a1f1bb5 Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Sun, 5 Jul 2026 18:32:07 -0400 Subject: [PATCH] fix arrays of domains defined over arrays --- lib/postgrex/extensions/array.ex | 30 ++++++++++++++++++---------- test/query_test.exs | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/lib/postgrex/extensions/array.ex b/lib/postgrex/extensions/array.ex index da3df290..a8aed60d 100644 --- a/lib/postgrex/extensions/array.ex +++ b/lib/postgrex/extensions/array.ex @@ -17,9 +17,9 @@ defmodule Postgrex.Extensions.Array do def encode(_) do quote location: :keep do list, [oid], [type] when is_list(list) -> + recurse_nested? = unquote(__MODULE__).recurse_nested_arrays(type) # encode_list/2 defined by TypeModule - encoder = &encode_list(&1, type) - unquote(__MODULE__).encode(list, oid, encoder) + unquote(__MODULE__).encode(list, recurse_nested?, oid, &encode_list(&1, type)) other, _, _ -> raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "a list") @@ -50,29 +50,29 @@ defmodule Postgrex.Extensions.Array do # Special case for empty lists. This treats an empty list as an empty 1-dim array. # While libpq will decode a payload encoded for a 0-dim array, CockroachDB will not. # Also, this is how libpq actually encodes 0-dim arrays. - def encode([], elem_oid, _encoder) do + def encode([], _, elem_oid, _encoder) do <<20::int32(), 1::int32(), 0::int32(), elem_oid::uint32(), 0::int32(), 1::int32()>> end - def encode(list, elem_oid, encoder) do - {data, ndims, lengths} = encode(list, 0, [], encoder) + def encode(list, recurse_nested?, elem_oid, encoder) do + {data, ndims, lengths} = encode(list, recurse_nested?, 0, [], encoder) lengths = for len <- Enum.reverse(lengths), do: <> iodata = [<>, lengths, data] [<> | iodata] end - defp encode([], ndims, lengths, _encoder) do + defp encode([], _, ndims, lengths, _encoder) do {"", ndims, lengths} end - defp encode([head | tail] = list, ndims, lengths, encoder) when is_list(head) do + defp encode([head | tail] = list, true, ndims, lengths, encoder) when is_list(head) do lengths = [length(list) | lengths] - {data, ndims, lengths} = encode(head, ndims, lengths, encoder) + {data, ndims, lengths} = encode(head, true, ndims, lengths, encoder) [dimlength | _] = lengths rest = Enum.reduce(tail, [], fn sublist, acc -> - {data, _, [len | _]} = encode(sublist, ndims, lengths, encoder) + {data, _, [len | _]} = encode(sublist, true, ndims, lengths, encoder) if len != dimlength do raise ArgumentError, "nested lists must have lists with matching lengths" @@ -84,7 +84,7 @@ defmodule Postgrex.Extensions.Array do {[data | rest], ndims + 1, lengths} end - defp encode(list, ndims, lengths, encoder) do + defp encode(list, _, ndims, lengths, encoder) do {encoder.(list), ndims + 1, [length(list) | lengths]} end @@ -110,6 +110,16 @@ defmodule Postgrex.Extensions.Array do Enum.reverse(acc) end + # A nested array is normally handled by recursing through the dimensions + # until you hit the base element. However, there is a special case + # where a parameter may look like a nested array but we cannot treat it + # this way. An array whose element type is a domain that is defined + # over an array will look like a nested array from Elixir, but the + # Postgres protocol must treat it as a single dimensional array over + # the domain type + def recurse_nested_arrays({__MODULE__, _, _}), do: false + def recurse_nested_arrays(_), do: true + # elems and lengths in reverse order defp nest(elems, [len]) do nest_inner(elems, len, []) diff --git a/test/query_test.exs b/test/query_test.exs index 337bef87..1b08f613 100644 --- a/test/query_test.exs +++ b/test/query_test.exs @@ -94,6 +94,15 @@ defmodule QueryTest do assert [[[{"a", 1}]]] = query("SELECT ARRAY['(a, 1)']::composite_array_domain", []) end + @tag min_pg_version: "11.0" + test "decode array of composite array domain", context do + assert [[[[{"a", 1}]]]] = + query( + "SELECT ARRAY[ARRAY['(a, 1)']::composite_array_domain]::composite_array_domain[]", + [] + ) + end + @tag min_pg_version: "11.0" test "decode array of composite domain", context do assert [[[{"a", 1}]]] = query("SELECT ARRAY['(a, 1)']::composite_domain[]", []) @@ -114,6 +123,15 @@ defmodule QueryTest do assert [[[{"a", 1}]]] = query("SELECT ARRAY['(a, 1)']::nested_composite_array_domain", []) end + @tag min_pg_version: "11.0" + test "decode array of nested composite array domain", context do + assert [[[[{"a", 1}]]]] = + query( + "SELECT ARRAY[ARRAY['(a, 1)']::nested_composite_array_domain]::nested_composite_array_domain[]", + [] + ) + end + test "decode arrays", context do assert [[[]]] = query("SELECT ARRAY[]::integer[]", []) assert [[[1]]] = query("SELECT ARRAY[1]", []) @@ -161,6 +179,14 @@ defmodule QueryTest do assert [[[{"a", 1}]]] = query("SELECT $1::composite_array_domain", [[{"a", 1}]]) end + @tag min_pg_version: "11.0" + test "encode array of composite array domain", context do + nrow = 3 + ncol = 2 + params = Enum.map(1..nrow, fn _ -> Enum.map(1..ncol, fn _ -> {"a", 1} end) end) + assert [[^params]] = query("SELECT $1::composite_array_domain[]", [params]) + end + @tag min_pg_version: "11.0" test "encode array of composite domain", context do assert [[[{"a", 1}]]] = query("SELECT $1::composite_domain[]", [[{"a", 1}]]) @@ -181,6 +207,14 @@ defmodule QueryTest do assert [[[{"a", 1}]]] = query("SELECT $1::nested_composite_array_domain", [[{"a", 1}]]) end + @tag min_pg_version: "11.0" + test "encode array of nested composite array domain", context do + nrow = 3 + ncol = 2 + params = Enum.map(1..nrow, fn _ -> Enum.map(1..ncol, fn _ -> {"a", 1} end) end) + assert [[^params]] = query("SELECT $1::nested_composite_array_domain[]", [params]) + end + test "decode interval", context do assert [[%Postgrex.Interval{months: 0, days: 0, secs: 0, microsecs: 0}]] = query("SELECT interval '0'", [])