kaffy icon indicating copy to clipboard operation
kaffy copied to clipboard

[BUG] Listing for schemas with `primary_key: false` in migration causes kaffy traceback

Open ivanov opened this issue 11 months ago • 0 comments

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.

ivanov avatar Jan 31 '25 01:01 ivanov