From e96eca2ffe6f959dcf03f5a2ce67495a31e9a051 Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Tue, 23 Jun 2026 05:44:04 -0400 Subject: [PATCH 1/3] refactor subtype handling --- .github/workflows/ci.yml | 2 +- lib/postgrex/type_info.ex | 14 ++++++- lib/postgrex/types.ex | 85 +++++++++++++++++++++++++++------------ test/query_test.exs | 56 +++++++++++++++++++++++++- test/test_helper.exs | 29 +++++++++++++ 5 files changed, 157 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f02e83463..686ce254b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: branches: - - master + - fix_domain_2 pull_request: jobs: diff --git a/lib/postgrex/type_info.ex b/lib/postgrex/type_info.ex index 7d3603e3c..ac673e58b 100644 --- a/lib/postgrex/type_info.ex +++ b/lib/postgrex/type_info.ex @@ -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(), @@ -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 diff --git a/lib/postgrex/types.ex b/lib/postgrex/types.ex index 1fa13f306..ee853cbfb 100644 --- a/lib/postgrex/types.ex +++ b/lib/postgrex/types.ex @@ -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 . @@ -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 = @@ -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} """ @@ -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) @@ -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), @@ -157,13 +163,29 @@ 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 :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} -> @@ -184,13 +206,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 @@ -204,12 +239,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 diff --git a/test/query_test.exs b/test/query_test.exs index 4442866f3..33951fe6a 100644 --- a/test/query_test.exs +++ b/test/query_test.exs @@ -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]", []) @@ -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'", []) @@ -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 @@ -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 diff --git a/test/test_helper.exs b/test/test_helper.exs index 1277895b7..d6a17e04c 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -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 <> @@ -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 From 6a1814884f329cde345875d0c1fe1178ebc8002d Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Tue, 23 Jun 2026 05:47:07 -0400 Subject: [PATCH 2/3] missing end --- lib/postgrex/types.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/postgrex/types.ex b/lib/postgrex/types.ex index ee853cbfb..0edf4bb2d 100644 --- a/lib/postgrex/types.ex +++ b/lib/postgrex/types.ex @@ -171,6 +171,7 @@ defmodule Postgrex.Types do info -> true = :ets.update_element(table, oid, {3, info}) + end end :ok From cbf2fcaaad70a7cdc0b2a2f9ed1103352f0e20ea Mon Sep 17 00:00:00 2001 From: Greg Rychlewski Date: Tue, 23 Jun 2026 05:53:20 -0400 Subject: [PATCH 3/3] fix ci and docs --- .github/workflows/ci.yml | 2 +- lib/postgrex/type_info.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 686ce254b..f02e83463 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ name: CI on: push: branches: - - fix_domain_2 + - master pull_request: jobs: diff --git a/lib/postgrex/type_info.ex b/lib/postgrex/type_info.ex index ac673e58b..25946d722 100644 --- a/lib/postgrex/type_info.ex +++ b/lib/postgrex/type_info.ex @@ -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; """