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
16 changes: 14 additions & 2 deletions lib/postgrex/type_info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ defmodule Postgrex.TypeInfo do
* `input` - The name of the "input" function (the function postgres uses
to convert the type from its text format);
* `array_elem` - If this is an array, the array elements' oid;
* `base_type` - If this is a range type, the base type's oid;
* `base_type` - If this is a range type, the base type's oid. If this is a domain, the underlying oid is pointed to;
* `comp_elems` - If this is a composite type (record), the tuple
elements' oid;
"""
Expand All @@ -25,6 +25,7 @@ defmodule Postgrex.TypeInfo do
@type t :: %__MODULE__{
oid: Types.oid(),
type: String.t(),
category: atom(),
send: String.t(),
receive: String.t(),
output: String.t(),
Expand All @@ -34,5 +35,16 @@ defmodule Postgrex.TypeInfo do
comp_elems: [Types.oid()]
}

defstruct [:oid, :type, :send, :receive, :output, :input, :array_elem, :base_type, :comp_elems]
defstruct [
:oid,
:type,
:category,
:send,
:receive,
:output,
:input,
:array_elem,
:base_type,
:comp_elems
]
end
86 changes: 61 additions & 25 deletions lib/postgrex/types.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ defmodule Postgrex.Types do
alias Postgrex.TypeInfo
import Postgrex.BinaryUtils

@type_categories %{
"b" => :base,
"c" => :composite,
"d" => :domain,
"e" => :enum,
"p" => :pseudo,
"r" => :range,
"m" => :multirange
}

@typedoc """
PostgreSQL internal identifier that maps to a type. See
<https://www.postgresql.org/docs/9.4/static/datatype-oid.html>.
Expand Down Expand Up @@ -64,25 +74,18 @@ defmodule Postgrex.Types do
end

defp build_bootstrap_query(version, filter_oids, s) do
{typelem, join_domain} =
if version >= {9, 0, 0} do
{"coalesce(d.typelem, t.typelem)", "LEFT JOIN pg_type AS d ON t.typbasetype = d.oid"}
else
{"t.typelem", ""}
end

{rngsubtype, join_range} =
{base_oid, join_range} =
cond do
version >= {14, 0, 0} ->
{"coalesce(r.rngsubtype, 0)",
"LEFT JOIN pg_range AS r ON r.rngtypid = t.oid OR r.rngmultitypid = t.oid OR (t.typbasetype <> 0 AND r.rngtypid = t.typbasetype)"}
{"coalesce(r.rngsubtype, t.typbasetype)",
"LEFT JOIN pg_range AS r ON r.rngtypid = t.oid OR r.rngmultitypid = t.oid"}

version >= {9, 2, 0} ->
{"coalesce(r.rngsubtype, 0)",
"LEFT JOIN pg_range AS r ON r.rngtypid = t.oid OR (t.typbasetype <> 0 AND r.rngtypid = t.typbasetype)"}
{"coalesce(r.rngsubtype, t.typbasetype)",
"LEFT JOIN pg_range AS r ON r.rngtypid = t.oid"}

true ->
{"0", ""}
{"t.typbasetype", ""}
end

comp_oids =
Expand All @@ -93,17 +96,16 @@ defmodule Postgrex.Types do
ARRAY (
SELECT a.atttypid
FROM pg_attribute AS a
WHERE a.attrelid = coalesce(d.typrelid, t.typrelid) AND a.attnum > 0 AND NOT a.attisdropped
WHERE a.attrelid = t.typrelid AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum
)
"""
end

"""
SELECT t.oid, t.typname, t.typsend, t.typreceive, t.typoutput, t.typinput,
#{typelem}, #{rngsubtype}, #{comp_oids}
SELECT t.oid, t.typname, t.typtype, t.typsend, t.typreceive, t.typoutput, t.typinput,
t.typelem, #{base_oid}, #{comp_oids}
FROM pg_type AS t
#{join_domain}
#{join_range}
#{filter_oids}
"""
Expand All @@ -125,7 +127,10 @@ defmodule Postgrex.Types do
@doc false
@spec build_type_info(binary) :: TypeInfo.t()
def build_type_info(row) do
[oid, type, send, receive, output, input, array_oid, base_oid, comp_oids] = row_decode(row)
[oid, type, category, send, receive, output, input, array_oid, base_oid, comp_oids] =
row_decode(row)

category = Map.get(@type_categories, category, :unknown)
oid = String.to_integer(oid)
array_oid = String.to_integer(array_oid)
base_oid = String.to_integer(base_oid)
Expand All @@ -134,6 +139,7 @@ defmodule Postgrex.Types do
%TypeInfo{
oid: oid,
type: :binary.copy(type),
category: category,
send: unqualify_proc(send),
receive: :binary.copy(receive),
output: :binary.copy(output),
Expand All @@ -157,13 +163,30 @@ defmodule Postgrex.Types do

_ =
for %TypeInfo{oid: oid} = type_info <- type_infos do
info = find(type_info, :any, module, table)
true = :ets.update_element(table, oid, {3, info})
case find(type_info, :any, module, table) do
:missing_type ->
# Did not bootstrap necessary subtypes i.e domain over composites
# so we remove and rely on reload behaviour
:ets.delete(table, oid)

info ->
true = :ets.update_element(table, oid, {3, info})
end
end

:ok
end

defp find(%{category: :domain, base_type: base_oid}, formats, module, table) do
case resolve_domain(base_oid, table) do
:missing_type ->
:missing_type

info ->
find(info, formats, module, table)
end
end

defp find(type_info, formats, module, table) do
case apply(module, :find, [type_info, formats]) do
{:super_binary, extension, nil} ->
Expand All @@ -184,13 +207,26 @@ defmodule Postgrex.Types do
end
end

defp resolve_domain(oid, table) do
case :ets.lookup(table, oid) do
[{_, %{category: :domain, base_type: base_oid}, _}] ->
resolve_domain(base_oid, table)

[{_, type_info, _}] ->
type_info

[] ->
:missing_type
end
end

defp super_find(sub_oids, extension, module, table) do
case sub_find(sub_oids, module, table, []) do
{:ok, sub_types} ->
{:binary, {extension, sub_oids, sub_types}}

:error ->
nil
nil_or_missing_type ->
nil_or_missing_type
end
end

Expand All @@ -204,12 +240,12 @@ defmodule Postgrex.Types do
{:binary, types} ->
sub_find(oids, module, table, [types | acc])

nil ->
:error
nil_or_missing_type ->
nil_or_missing_type
end

[] ->
:error
:missing_type
end
end

Expand Down
56 changes: 54 additions & 2 deletions test/query_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,30 @@ defmodule QueryTest do
end

@tag min_pg_version: "11.0"
test "decode arrays of composite domain", context do
test "decode composite array domain", context do
assert [[[{"a", 1}]]] = query("SELECT ARRAY['(a, 1)']::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[]", [])
end

@tag min_pg_version: "11.0"
test "decode nested composite domain", context do
assert [[{"a", 1}]] = query("SELECT '(a, 1)'::nested_composite_domain", [])
end

@tag min_pg_version: "11.0"
test "decode array of nested composite domain", context do
assert [[[{"a", 1}]]] = query("SELECT ARRAY['(a, 1)']::nested_composite_domain[]", [])
end

@tag min_pg_version: "11.0"
test "decode nested composite array domain", context do
assert [[[{"a", 1}]]] = query("SELECT ARRAY['(a, 1)']::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 @@ -137,10 +157,30 @@ defmodule QueryTest do
end

@tag min_pg_version: "11.0"
test "encode arrays of composite domain", context do
test "encode composite array domain", context do
assert [[[{"a", 1}]]] = query("SELECT $1::composite_array_domain", [[{"a", 1}]])
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}]])
end

@tag min_pg_version: "11.0"
test "encode nested composite domain", context do
assert [[{"a", 1}]] = query("SELECT $1::nested_composite_domain", [{"a", 1}])
end

@tag min_pg_version: "11.0"
test "encode array of nested composite domain", context do
assert [[[{"a", 1}]]] = query("SELECT $1::nested_composite_domain[]", [[{"a", 1}]])
end

@tag min_pg_version: "11.0"
test "encode nested composite array domain", context do
assert [[[{"a", 1}]]] = query("SELECT $1::nested_composite_array_domain", [[{"a", 1}]])
end

test "decode interval", context do
assert [[%Postgrex.Interval{months: 0, days: 0, secs: 0, microsecs: 0}]] =
query("SELECT interval '0'", [])
Expand Down Expand Up @@ -596,6 +636,12 @@ defmodule QueryTest do
query("SELECT ARRAY['[2014-1-1,2014-12-31)'::tsrange, '[2014-1-1,2014-12-31)'::tsrange]", [])
end

@tag min_pg_version: "9.2"
test "decode range over composite type", context do
[[%Postgrex.Range{lower: {1, 0, 0}, upper: {2, 0, 0}}]] =
query("SELECT version_range('(1,0,0)', '(2,0,0)', '[)')", [])
end

@tag min_pg_version: "14.0"
test "decode multirange", context do
# empty ranges
Expand Down Expand Up @@ -1331,6 +1377,12 @@ defmodule QueryTest do
])
end

@tag min_pg_version: "9.2"
test "encode range over composite type", context do
[[%Postgrex.Range{lower: {1, 0, 0}, upper: {2, 0, 0}}]] =
query("SELECT $1::version_range", [%Postgrex.Range{lower: {1, 0, 0}, upper: {2, 0, 0}}])
end

@tag min_pg_version: "9.2"
test "encode enforces bounds on integer ranges", context do
# int4's range is -2147483648 to +2147483647
Expand Down
29 changes: 29 additions & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,26 @@ DROP DOMAIN IF EXISTS floats_domain;
CREATE DOMAIN floats_domain AS float[] CONSTRAINT is_populated CHECK (COALESCE(array_length(VALUE, 1), 0) >= 1);
"""

sql_test =
if pg_version >= {9, 2} do
sql_test <>
"""
DROP TYPE IF EXISTS version_number;
CREATE TYPE version_number AS (
major integer,
minor integer,
patch integer
);

DROP TYPE IF EXISTS version_range;
CREATE TYPE version_range AS RANGE (
subtype = version_number
);
"""
else
sql_test
end

sql_test =
if pg_version >= {10, 0} do
sql_test <>
Expand All @@ -149,6 +169,15 @@ sql_test =

DROP DOMAIN IF EXISTS composite_domain;
CREATE DOMAIN composite_domain AS composite_test CHECK ((VALUE).a is NOT NULL);

DROP DOMAIN IF EXISTS composite_array_domain;
CREATE DOMAIN composite_array_domain AS composite_test[] CHECK (array_length(VALUE, 1) > 0);

DROP DOMAIN IF EXISTS nested_composite_domain;
CREATE DOMAIN nested_composite_domain AS composite_domain;

DROP DOMAIN IF EXISTS nested_composite_array_domain;
CREATE DOMAIN nested_composite_array_domain AS composite_array_domain;
"""
else
sql_test
Expand Down
Loading