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
30 changes: 20 additions & 10 deletions lib/postgrex/extensions/array.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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: <<len::int32(), 1::int32()>>
iodata = [<<ndims::int32(), 0::int32(), elem_oid::uint32()>>, lengths, data]
[<<IO.iodata_length(iodata)::int32()>> | 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"
Expand All @@ -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

Expand All @@ -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
Comment on lines +120 to +121

@greg-rychlewski greg-rychlewski Jul 5, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason why this check works is because no matter how many dimensions the array is over a non-domain type, the sub type is still considered the base type. For example [[[[[int]]]]] will still just have a sub type of int. This is enforced by Postgres and not just a quirk of how we handle types.

But [domain_over_array] will have a sub type of array. This is also enforced by postgres or else the protocol rejects it. i.e. postgres doesn't see base_type[][] the same as domain_over_array_of_base_type[].


# elems and lengths in reverse order
defp nest(elems, [len]) do
nest_inner(elems, len, [])
Expand Down
34 changes: 34 additions & 0 deletions test/query_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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[]", [])
Expand All @@ -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]", [])
Expand Down Expand Up @@ -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}]])
Expand All @@ -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'", [])
Expand Down
Loading