From 31805bb9a91111af3b5e46527698cb3a754a0fc7 Mon Sep 17 00:00:00 2001 From: Cyril GHALI Date: Wed, 7 Oct 2026 22:15:45 +0200 Subject: [PATCH] Fix type errors while another connection loads types While a connection loads a batch of new types, other connections read the table without the type server lock. Each entry briefly holds nil, which means unsupported, so a query can fail with "type `x` can not be handled by the types module" for a type that works a moment later. Entries now hold :associating until they are resolved. fetch/2 treats that as unknown, so the caller reloads, waits on the type server for the batch to finish, and reads the finished entry. Types that end up unsupported still store nil and fail as before. The type server owns the table, so a crash mid-batch cannot leave the marker behind. Anonymous records decoded during the window still raise, now with "was not bootstrapped" instead of "can not be handled". Fixes https://github.com/elixir-ecto/postgrex/issues/801 --- lib/postgrex/types.ex | 5 ++++- test/type_server_test.exs | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/lib/postgrex/types.ex b/lib/postgrex/types.ex index 2108cfc22..6ec77236c 100644 --- a/lib/postgrex/types.ex +++ b/lib/postgrex/types.ex @@ -158,7 +158,7 @@ defmodule Postgrex.Types do def associate_type_infos(type_infos, {module, table}) do _ = for %TypeInfo{oid: oid} = type_info <- type_infos do - true = :ets.insert_new(table, {oid, type_info, nil}) + true = :ets.insert_new(table, {oid, type_info, :associating}) end _ = @@ -408,6 +408,9 @@ defmodule Postgrex.Types do {_, _} = info -> {:ok, info} + :associating -> + {:error, nil, mod} + nil -> fetch_type_info(oid, mod, table) end diff --git a/test/type_server_test.exs b/test/type_server_test.exs index 289983a9e..e163629f1 100644 --- a/test/type_server_test.exs +++ b/test/type_server_test.exs @@ -190,6 +190,50 @@ defmodule TypeServerTest do assert {:lock, _, ^types} = Task.await(task) end + defmodule PausingTypes do + def find(type_info, formats) do + if type_info.type == "int4" do + send(Process.get(:test_pid), {:finding, self()}) + receive do: (:continue -> :ok) + end + + Postgrex.DefaultTypes.find(type_info, formats) + end + end + + test "types being associated are unknown, not unsupported" do + int4 = %Postgrex.TypeInfo{oid: 23, type: "int4", send: "int4send", receive: "int4recv"} + + other = %Postgrex.TypeInfo{ + oid: 100_000, + type: "other", + send: "othersend", + receive: "otherrecv" + } + + top = self() + + {:ok, pid} = + Task.start_link(fn -> + Process.put(:test_pid, top) + types = Postgrex.Types.new(PausingTypes) + send(top, {:types, types}) + Postgrex.Types.associate_type_infos([int4, other], types) + send(top, :associated) + :timer.sleep(:infinity) + end) + + assert_receive {:types, types} + assert_receive {:finding, ^pid} + assert Postgrex.Types.fetch(23, types) == {:error, nil, PausingTypes} + assert Postgrex.Types.fetch(100_000, types) == {:error, nil, PausingTypes} + + send(pid, :continue) + assert_receive :associated + assert {:ok, {:binary, _}} = Postgrex.Types.fetch(23, types) + assert Postgrex.Types.fetch(100_000, types) == {:error, other, PausingTypes} + end + defp wait_until_dead(pid) do ref = Process.monitor(pid) receive do: ({:DOWN, ^ref, _, _, _} -> :ok)