Skip to content

NOT IN query with custom type that uses atoms no longer working. #174

@Hermanverschooten

Description

@Hermanverschooten

Code of Conduct

  • I agree to follow this project's Code of Conduct

AI Policy

  • I agree to follow this project's AI Policy, or I agree that AI was not used while creating this issue.

Versions

  • ash 3.5.42
  • ash_postgres 2.6.19
  • ash_sql 0.2.93

Operating system

MacOS

Current Behavior

After upgrading to 0.2.93 my tests fail when there is a "NOT IN" query, apparently where before dump_to_native was called with the atom (:cancelled) now a string "cancelled" is used causing the postgres query to fail.

This is my custom type:

defmodule Admin.Resources.Types.TimerStatus do
  @moduledoc """
  Custom Ash type for timer status enum that maps atoms to integers
  """
  use Ash.Type

  @values [running: 0, complete: 1, cancelled: 2, invoiced: 3]
  @atom_to_int Map.new(@values)
  @int_to_atom Map.new(@values, fn {k, v} -> {v, k} end)

  @impl Ash.Type
  def storage_type(_), do: :integer

  @impl Ash.Type
  def cast_input(value, _) when is_atom(value) do
    if value in Map.keys(@atom_to_int) do
      {:ok, value}
    else
      :error
    end
  end

  def cast_input(value, _) when is_integer(value) do
    if Map.has_key?(@int_to_atom, value) do
      {:ok, Map.get(@int_to_atom, value)}
    else
      :error
    end
  end

  def cast_input(value, _) when is_binary(value) do
    case String.to_existing_atom(value) do
      atom when is_atom(atom) -> cast_input(atom, nil)
      _ -> :error
    end
  rescue
    _ -> :error
  end

  def cast_input(_, _), do: :error

  @impl Ash.Type
  def cast_stored(nil, _), do: {:ok, nil}

  def cast_stored(value, _) when is_integer(value) do
    case Map.get(@int_to_atom, value) do
      nil -> :error
      atom -> {:ok, atom}
    end
  end

  def cast_stored(_, _), do: :error

  @impl Ash.Type
  def dump_to_native(value, _) when is_atom(value) do
    case Map.get(@atom_to_int, value) do
      nil -> :error
      int -> {:ok, int}
    end
  end

  def dump_to_native(_, _), do: :error
end

and this is the now failing code:

  @spec list_active() :: [Timer.t()]
  def list_active do
    Timer
    |> Ash.Query.filter(status not in [:cancelled, :invoiced])
    |> Ash.Query.load([:customer])
    |> Ash.Query.sort(:id)
    |> Ash.read!()
  end

And the error:

29) test editing invoice details handles errors when updating detail (AdminWeb.InvoicesLive.EditTest)
     test/admin_web/live/invoices_live/edit_test.exs:214
     ** (Ash.Error.Unknown)
     Bread Crumbs:
       > Error returned from: Admin.Resources.Timer.read

     Unknown Error

     * ** (DBConnection.EncodeError) Postgrex expected an integer in -9223372036854775808..9223372036854775807, got "cancelled". Please make sure the value you are passing matches the definition in your table or in your query or convert the value accordingly.
       (postgrex 0.21.1) lib/postgrex/type_module.ex:1084: Postgrex.DefaultTypes.encode_list/3
       (postgrex 0.21.1) lib/postgrex/extensions/array.ex:88: Postgrex.Extensions.Array.encode/4
       (postgrex 0.21.1) lib/postgrex/extensions/array.ex:58: Postgrex.Extensions.Array.encode/3
       (postgrex 0.21.1) lib/postgrex/type_module.ex:1084: Postgrex.DefaultTypes.encode_params/3
       (postgrex 0.21.1) lib/postgrex/query.ex:75: DBConnection.Query.Postgrex.Query.encode/3
       (db_connection 2.8.1) lib/db_connection.ex:1446: DBConnection.encode/5
       (db_connection 2.8.1) lib/db_connection.ex:1546: DBConnection.run_prepare_execute/5
       (db_connection 2.8.1) lib/db_connection.ex:1650: DBConnection.run/6
       (db_connection 2.8.1) lib/db_connection.ex:769: DBConnection.parsed_prepare_execute/5
       (db_connection 2.8.1) lib/db_connection.ex:761: DBConnection.prepare_execute/4
       (ecto_sql 3.13.2) lib/ecto/adapters/postgres/connection.ex:102: Ecto.Adapters.Postgres.Connection.prepare_execute/5
       (ecto_sql 3.13.2) lib/ecto/adapters/sql.ex:1004: Ecto.Adapters.SQL.execute!/5
       (ecto_sql 3.13.2) lib/ecto/adapters/sql.ex:996: Ecto.Adapters.SQL.execute/6
       (ecto 3.13.3) lib/ecto/repo/queryable.ex:241: Ecto.Repo.Queryable.execute/4
       (ecto 3.13.3) lib/ecto/repo/queryable.ex:19: Ecto.Repo.Queryable.all/3
       (ash_postgres 2.6.19) lib/data_layer.ex:828: anonymous fn/3 in AshPostgres.DataLayer.run_query/2
       (ash_postgres 2.6.19) lib/data_layer.ex:827: AshPostgres.DataLayer.run_query/2
       (ash 3.5.41) lib/ash/actions/read/read.ex:3988: Ash.Actions.Read.run_query/4
       (ash 3.5.41) lib/ash/actions/read/read.ex:729: anonymous fn/9 in Ash.Actions.Read.do_read/5
       (ash 3.5.41) lib/ash/actions/read/read.ex:1495: Ash.Actions.Read.maybe_in_transaction/3
     stacktrace:
       (ash 3.5.41) lib/ash/error/unknown.ex:3: Ash.Error.Unknown."exception (overridable 2)"/1
       (ash 3.5.41) /Users/herman/Projects/admin/deps/splode/lib/splode.ex:264: Ash.Error.to_class/2
       (ash 3.5.41) lib/ash/error/error.ex:108: Ash.Error.to_error_class/2
       (ash 3.5.41) lib/ash/actions/read/read.ex:489: anonymous fn/3 in Ash.Actions.Read.do_run/3
       (ash 3.5.41) lib/ash/actions/read/read.ex:438: Ash.Actions.Read.do_run/3
       (ash 3.5.41) lib/ash/actions/read/read.ex:86: anonymous fn/3 in Ash.Actions.Read.run/3
       (ash 3.5.41) lib/ash/actions/read/read.ex:85: Ash.Actions.Read.run/3
       (ash 3.5.41) lib/ash.ex:2772: Ash.read/2
       (ash 3.5.41) lib/ash.ex:2707: Ash.read!/2
       (admin 0.1.0) lib/admin/timers.ex:86: Admin.Timers.list_for_customer/1
       (admin 0.1.0) lib/admin_web/live/invoices_live/edit.ex:30: AdminWeb.InvoicesLive.Edit.mount/3
       (phoenix_live_view 1.1.12) lib/phoenix_live_view/utils.ex:348: anonymous fn/6 in Phoenix.LiveView.Utils.maybe_call_live_view_mount!/5
       (telemetry 1.3.0) /Users/herman/Projects/admin/deps/telemetry/src/telemetry.erl:324: :telemetry.span/3
       (phoenix_live_view 1.1.12) lib/phoenix_live_view/static.ex:324: Phoenix.LiveView.Static.call_mount_and_handle_params!/5
       (phoenix_live_view 1.1.12) lib/phoenix_live_view/static.ex:155: Phoenix.LiveView.Static.do_render/4
       (phoenix_live_view 1.1.12) lib/phoenix_live_view/controller.ex:39: Phoenix.LiveView.Controller.live_render/3
       (phoenix 1.8.1) lib/phoenix/router.ex:416: Phoenix.Router.__call__/5
       (admin 0.1.0) lib/admin_web/endpoint.ex:1: AdminWeb.Endpoint.plug_builder_call/2
       (admin 0.1.0) lib/admin_web/endpoint.ex:1: AdminWeb.Endpoint.call/2

According to claude:
The regression is triggered at ash/lib/ash/type/atom.ex:115:

  def dump_tonative(value, ) when is_atom(value) do
    {:ok, to_string(value)}
  end

Root Cause Analysis:

The regression path:

  1. ash_sql 0.2.93 introduced evaluate_right() function in commit 3cad640
  2. This calls maybe_type_expr() at /Users/herman/Projects/_playground/ash_sql/lib/expr.ex:3411-3418
  3. Which wraps expressions in Ash.Query.Function.Type
  4. The Type function calls Ash.Type.coerce()
  5. Coerce determines the values are atoms and processes them with Ash.Type.Atom.dump_to_native/2
  6. This converts :cancelled → "cancelled" at line 115
  7. When strings reach TimerStatus.dump_to_native/2, it expects atoms but gets strings
  8. Results in Postgrex expected an integer...got "cancelled" error

The issue is that the new Type coercion layer in ash_sql 0.2.93 prematurely converts atoms to strings before they reach custom types that expect to handle the original atom values.

Reproduction

I made a reproduction repo: https://github.com/Hermanverschooten/ash_sql_reproduction

Expected Behavior

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions