array.ex (3517B)
1 defmodule Postgrex.Extensions.Array do 2 @moduledoc false 3 import Postgrex.BinaryUtils, warn: false 4 @behaviour Postgrex.SuperExtension 5 6 def init(_), do: nil 7 8 def matching(_), 9 do: [send: "array_send"] 10 11 def format(_), 12 do: :super_binary 13 14 def oids(%Postgrex.TypeInfo{array_elem: elem_oid}, _), 15 do: [elem_oid] 16 17 def encode(_) do 18 quote location: :keep do 19 list, [oid], [type] when is_list(list) -> 20 # encode_list/2 defined by TypeModule 21 encoder = &encode_list(&1, type) 22 unquote(__MODULE__).encode(list, oid, encoder) 23 24 other, _, _ -> 25 raise DBConnection.EncodeError, Postgrex.Utils.encode_msg(other, "a list") 26 end 27 end 28 29 def decode(_) do 30 quote location: :keep do 31 <<len::int32(), binary::binary-size(len)>>, [oid], [type] -> 32 <<ndims::int32(), _has_null::int32(), ^oid::uint32(), dims::size(ndims)-binary-unit(64), 33 data::binary>> = binary 34 35 # decode_list/2 defined by TypeModule 36 flat = decode_list(data, type) 37 38 unquote(__MODULE__).decode(dims, flat) 39 end 40 end 41 42 ## Helpers 43 44 # Special case for empty lists. This treats an empty list as an empty 1-dim array. 45 # While libpq will decode an payload encoded for a 0-dim array, CockroachDB will not. 46 # Also, this is how libpq actually encodes 0-dim arrays. 47 def encode([], elem_oid, _encoder) do 48 <<20::int32(), 1::int32(), 0::int32(), elem_oid::uint32(), 0::int32(), 1::int32()>> 49 end 50 51 def encode(list, elem_oid, encoder) do 52 {data, ndims, lengths} = encode(list, 0, [], encoder) 53 lengths = for len <- Enum.reverse(lengths), do: <<len::int32(), 1::int32()>> 54 iodata = [<<ndims::int32(), 0::int32(), elem_oid::uint32()>>, lengths, data] 55 [<<IO.iodata_length(iodata)::int32()>> | iodata] 56 end 57 58 defp encode([], ndims, lengths, _encoder) do 59 {"", ndims, lengths} 60 end 61 62 defp encode([head | tail] = list, ndims, lengths, encoder) when is_list(head) do 63 lengths = [length(list) | lengths] 64 {data, ndims, lengths} = encode(head, ndims, lengths, encoder) 65 [dimlength | _] = lengths 66 67 rest = 68 Enum.reduce(tail, [], fn sublist, acc -> 69 {data, _, [len | _]} = encode(sublist, ndims, lengths, encoder) 70 71 if len != dimlength do 72 raise ArgumentError, "nested lists must have lists with matching lengths" 73 end 74 75 [acc | data] 76 end) 77 78 {[data | rest], ndims + 1, lengths} 79 end 80 81 defp encode(list, ndims, lengths, encoder) do 82 {encoder.(list), ndims + 1, [length(list) | lengths]} 83 end 84 85 def decode(dims, elems) do 86 case decode_dims(dims, []) do 87 [] when elems == [] -> 88 [] 89 90 [length] when length(elems) == length -> 91 Enum.reverse(elems) 92 93 lengths -> 94 {array, []} = nest(elems, lengths) 95 array 96 end 97 end 98 99 defp decode_dims(<<len::int32(), _lbound::int32(), rest::binary>>, acc) do 100 decode_dims(rest, [len | acc]) 101 end 102 103 defp decode_dims(<<>>, acc) do 104 Enum.reverse(acc) 105 end 106 107 # elems and lengths in reverse order 108 defp nest(elems, [len]) do 109 nest_inner(elems, len, []) 110 end 111 112 defp nest(elems, [len | lengths]) do 113 nest(elems, len, lengths, []) 114 end 115 116 defp nest(elems, 0, _, acc) do 117 {acc, elems} 118 end 119 120 defp nest(elems, n, lengths, acc) do 121 {row, elems} = nest(elems, lengths) 122 nest(elems, n - 1, lengths, [row | acc]) 123 end 124 125 defp nest_inner(elems, 0, acc) do 126 {acc, elems} 127 end 128 129 defp nest_inner([elem | elems], n, acc) do 130 nest_inner(elems, n - 1, [elem | acc]) 131 end 132 end