[BUG] Listing for schemas with `primary_key: false` in migration causes kaffy traceback
Versions Used Kaffy: 0.10.2 Phoenix: 1.17.14 Elixir: 1.17.3
What's actually happening?
I'm trying to make a many-to-many relation between users and groups, the migration looks something like this
create table(:user_group_rel, primary_key: false) do
add :user_id, references(:users, on_delete: :nothing), primary_key: true
add :group_id, references(:user_groups, on_delete: :nothing), primary_key: true
...
This currently does not work for listing such user_group relationships in the kaffy interace, and the workaround I'm rolling with for now is to not pass primary_key: false for the table creation, and keep around the id field.
I think this is related to #190, though that bug dealt with embedded_schema, and predates my time using Phoenix, so
I'm out of my depth if the issue I'm seeing is similar enough too what was previously reported there.
Here's the full stack trace of going to the kaffy page for this
Postgrex.Error at GET /admin/accounts/user_group_rel
Exception:
** (Postgrex.Error) ERROR 42703 (undefined_column) column u0.id does not exist
query: SELECT u0."id", u0."user_id", u0."group_id", u0."inserted_at", u0."updated_at" FROM "user_group_rel" AS u0 ORDER BY u0."id" DESC LIMIT $1 OFFSET $2
(ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:1096: Ecto.Adapters.SQL.raise_sql_call_error/1
(ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:994: Ecto.Adapters.SQL.execute/6
(ecto 3.12.4) lib/ecto/repo/queryable.ex:232: Ecto.Repo.Queryable.execute/4
(ecto 3.12.4) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
(kaffy 0.10.2) lib/kaffy/resource_query.ex:34: Kaffy.ResourceQuery.list_resource/3
(kaffy 0.10.2) lib/kaffy_web/controllers/resource_controller.ex:70: KaffyWeb.ResourceController.index/2
(kaffy 0.10.2) lib/kaffy_web/controllers/resource_controller.ex:1: KaffyWeb.ResourceController.action/2
(kaffy 0.10.2) lib/kaffy_web/controllers/resource_controller.ex:1: KaffyWeb.ResourceController.phoenix_controller_pipeline/2
(phoenix 1.7.14) lib/phoenix/router.ex:484: Phoenix.Router.__call__/5
(my 0.1.5) lib/my_web/endpoint.ex:1: MyWeb.Endpoint.plug_builder_call/2
(my 0.1.5) deps/plug/lib/plug/debugger.ex:136: MyWeb.Endpoint."call (overridable 3)"/2
(my 0.1.5) lib/my_web/endpoint.ex:1: MyWeb.Endpoint.call/2
(phoenix 1.7.14) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
(plug_cowboy 2.7.2) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
(cowboy 2.12.0) /home/pi/code/my/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
(cowboy 2.12.0) /home/pi/code/my/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
(cowboy 2.12.0) /home/pi/code/my/deps/cowboy/src/cowboy_stream_h.erl:295: :cowboy_stream_h.request_process/3
(stdlib 6.0) proc_lib.erl:323: :proc_lib.init_p_do_apply/3
Code:
lib/ecto/adapters/sql.ex
1091 defp raise_sql_call_error(%DBConnection.OwnershipError{} = err) do
1092 message = err.message <> "\nSee Ecto.Adapters.SQL.Sandbox docs for more information."
1093 raise %{err | message: message}
1094 end
1095
1096> defp raise_sql_call_error(err), do: raise(err)
1097
1098 @doc false
1099 def reduce(adapter_meta, statement, params, opts, acc, fun) do
1100 %{pid: pool, telemetry: telemetry, sql: sql, opts: default_opts} = adapter_meta
1101 opts = with_log(telemetry, params, opts ++ default_opts)
lib/ecto/adapters/sql.ex
989 end
990
991 @doc false
992 def execute(prepare, adapter_meta, query_meta, prepared, params, opts) do
993 %{num_rows: num, rows: rows} =
994> execute!(prepare, adapter_meta, prepared, params, put_source(opts, query_meta))
995
996 {num, rows}
997 end
998
999 defp execute!(prepare, adapter_meta, {:cache, update, {id, prepared}}, params, opts) do
lib/ecto/repo/queryable.ex
227 assocs: assocs,
228 from: from
229 } = select
230
231 preprocessor = preprocessor(from, preprocess, adapter)
232> {count, rows} = adapter.execute(adapter_meta, query_meta, prepared, dump_params, opts)
233 postprocessor = postprocessor(from, postprocess, take, adapter)
234
235 {count,
236 rows
237 |> Ecto.Repo.Assoc.query(assocs, sources, preprocessor)
lib/ecto/repo/queryable.ex
14 query =
15 queryable
16 |> Ecto.Queryable.to_query()
17 |> Ecto.Query.Planner.ensure_select(true)
18
19> execute(:all, name, query, tuplet) |> elem(1)
20 end
21
22 def stream(_name, queryable, {adapter_meta, opts}) do
23 %{adapter: adapter, cache: cache, repo: repo} = adapter_meta
24
lib/kaffy/resource_query.ex
29 case Kaffy.ResourceAdmin.custom_index_query(conn, resource, paged) do
30 {custom_query, opts} ->
31 {Kaffy.Utils.repo().all(custom_query, opts), opts}
32
33 custom_query ->
34> {Kaffy.Utils.repo().all(custom_query), []}
35 end
36
37 do_cache = if search == "" and Enum.empty?(filtered_fields), do: true, else: false
38 all_count = cached_total_count(schema, do_cache, all, opts)
39 {all_count, current_page}
lib/kaffy_web/controllers/resource_controller.ex
65 false ->
66 unauthorized_access(conn)
67
68 true ->
69 fields = Kaffy.ResourceAdmin.index(my_resource)
70> {filtered_count, entries} = Kaffy.ResourceQuery.list_resource(conn, my_resource, params)
71 items_per_page = Map.get(params, "limit", "100") |> String.to_integer()
72 page = Map.get(params, "page", "1") |> String.to_integer()
73 has_next = round(filtered_count / items_per_page) > page
74 next_class = if has_next, do: "", else: " disabled"
75 has_prev = page >= 2
lib/kaffy_web/controllers/resource_controller.ex
1> defmodule KaffyWeb.ResourceController do
2 @moduledoc false
3
4 use Phoenix.Controller, namespace: KaffyWeb
5 use Phoenix.HTML
6 alias Kaffy.Pagination
lib/kaffy_web/controllers/resource_controller.ex
1> defmodule KaffyWeb.ResourceController do
2 @moduledoc false
3
4 use Phoenix.Controller, namespace: KaffyWeb
5 use Phoenix.HTML
6 alias Kaffy.Pagination
lib/phoenix/router.ex
479 :telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata)
480 halted_conn
481
482 %Plug.Conn{} = piped_conn ->
483 try do
484> plug.call(piped_conn, plug.init(opts))
485 else
486 conn ->
487 measurements = %{duration: System.monotonic_time() - start}
488 metadata = %{metadata | conn: conn}
489 :telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata)
lib/my_web/endpoint.ex
1> defmodule MyWeb.Endpoint do
2 use Phoenix.Endpoint, otp_app: :my
3 # temporarily disable
4 #use ErrorTracker.Integrations.Plug
5
6
deps/plug/lib/plug/debugger.ex
131 case conn do
132 %Plug.Conn{path_info: ["__plug__", "debugger", "action"], method: "POST"} ->
133 Plug.Debugger.run_action(conn)
134
135 %Plug.Conn{} ->
136> super(conn, opts)
137 end
138 rescue
139 e in Plug.Conn.WrapperError ->
140 %{conn: conn, kind: kind, reason: reason, stack: stack} = e
141 Plug.Debugger.__catch__(conn, kind, reason, stack, @plug_debugger)
lib/my_web/endpoint.ex
1> defmodule MyWeb.Endpoint do
2 use Phoenix.Endpoint, otp_app: :my
3 # temporarily disable
4 #use ErrorTracker.Integrations.Plug
5
6
lib/phoenix/endpoint/sync_code_reload_plug.ex
17
18 def call(conn, {endpoint, opts}), do: do_call(conn, endpoint, opts, true)
19
20 defp do_call(conn, endpoint, opts, retry?) do
21 try do
22> endpoint.call(conn, opts)
23 rescue
24 exception in [UndefinedFunctionError] ->
25 case exception do
26 %UndefinedFunctionError{module: ^endpoint} when retry? ->
27 # Sync with the code reloader and retry once
lib/plug/cowboy/handler.ex
6 def init(req, {plug, opts}) do
7 conn = @connection.conn(req)
8
9 try do
10 conn
11> |> plug.call(opts)
12 |> maybe_send(plug)
13 |> case do
14 %Plug.Conn{adapter: {@connection, %{upgrade: {:websocket, websocket_args}} = req}} = conn ->
15 {handler, state, cowboy_opts} = websocket_args
16 {__MODULE__, copy_resp_headers(conn, req), {handler, state}, cowboy_opts}
/home/pi/code/my/deps/cowboy/src/cowboy_handler.erl
32 -optional_callbacks([terminate/3]).
33
34 -spec execute(Req, Env) -> {ok, Req, Env}
35 when Req::cowboy_req:req(), Env::cowboy_middleware:env().
36 execute(Req, Env=#{handler := Handler, handler_opts := HandlerOpts}) ->
37> try Handler:init(Req, HandlerOpts) of
38 {ok, Req2, State} ->
39 Result = terminate(normal, Req2, State, Handler),
40 {ok, Req2, Env#{result => Result}};
41 {Mod, Req2, State} ->
42 Mod:upgrade(Req2, Env, Handler, State);
/home/pi/code/my/deps/cowboy/src/cowboy_stream_h.erl
301 end.
302
303 execute(_, _, []) ->
304 ok;
305 execute(Req, Env, [Middleware|Tail]) ->
306> case Middleware:execute(Req, Env) of
307 {ok, Req2, Env2} ->
308 execute(Req2, Env2, Tail);
309 {suspend, Module, Function, Args} ->
310 proc_lib:hibernate(?MODULE, resume, [Env, Tail, Module, Function, Args]);
311 {stop, _Req2} ->
/home/pi/code/my/deps/cowboy/src/cowboy_stream_h.erl
290 %% to simplify the debugging of errors. The proc_lib library
291 %% already adds the stacktrace to other types of exceptions.
292 -spec request_process(cowboy_req:req(), cowboy_middleware:env(), [module()]) -> ok.
293 request_process(Req, Env, Middlewares) ->
294 try
295> execute(Req, Env, Middlewares)
296 catch
297 exit:Reason={shutdown, _}:Stacktrace ->
298 erlang:raise(exit, Reason, Stacktrace);
299 exit:Reason:Stacktrace when Reason =/= normal, Reason =/= shutdown ->
300 erlang:raise(exit, {Reason, Stacktrace}, Stacktrace)
proc_lib.erl
No code available.
Connection details
Params
%{"context" => "accounts", "resource" => "user_group_rel"}
Request info
- URI: http://redacted:80/admin/accounts/user_group_rel
- Query string:
What should happen instead?
It should be possible to show list items that do not have an id key.
Thank you for taking a look and for sharing kaffy with the world.