Custom type receiving expression to cast

In my system, I have the following custom UUID V7 type which I use for ids:

defmodule UUIDV7.Type do
  @moduledoc """
  UUID V7 type
  """

  alias UUIDV7.Encoder

  use Ash.Type

  @impl true
  def storage_type, do: :uuid

  @impl true
  def generator(_constraints) do
    {:ok, term} = Uniq.UUID.uuid7() |> process(:raw, :encoded)

    term
  end

  @impl true
  def cast_input(term, _constraints), do: process(term, identify_format(term), :encoded)

  @impl true
  def cast_stored(term, _constraints), do: process(term, identify_format(term), :encoded)

  @impl true
  def dump_to_native(term, _constraints), do: process(term, identify_format(term), :integer)

  @impl true
  def dump_to_embedded(term, _constraints), do: process(term, identify_format(term), :raw)

  @impl true
  def equal?(lhs, rhs),
    do: process(lhs, identify_format(lhs), :raw) == process(rhs, identify_format(rhs), :raw)

  defp process(term, initial_format, requested_format) do
    term
    |> validate(initial_format)
    |> restore(initial_format, requested_format)
    |> decode(initial_format, requested_format)
    |> encode(requested_format)
    |> dump(initial_format, requested_format)
  end

  defp validate(term, initial_format)

  defp validate(term, initial_format) when initial_format in [:integer, :raw] do
    if Uniq.UUID.valid?(term), do: {:ok, term}, else: {:error, "got invalid term"}
  end

  defp validate(term, :encoded), do: {:ok, term}
  defp validate(nil, _initial_format), do: {:ok, nil}
  defp validate(_term, _initial_format), do: {:error, "got invalid term"}

  defp restore(result, initial_format, requested_format)

  defp restore({:ok, term}, :integer, requested_format) when requested_format in [:raw, :encoded],
    do: {:ok, Uniq.UUID.to_string(term)}

  defp restore({:ok, term}, _initial_format, _requested_format), do: {:ok, term}

  defp decode(result, initial_format, requested_format)
  defp decode({:ok, nil}, nil, _requested_format), do: {:ok, nil}
  defp decode({:ok, term}, :encoded, _requested_format), do: Encoder.decode(term)
  defp decode({:ok, term}, _initial_format, _requested_format), do: {:ok, term}

  defp encode(result, requested_format)
  defp encode({:ok, nil}, _requested_format), do: {:ok, nil}
  defp encode({:ok, term}, :encoded), do: Encoder.encode(term)
  defp encode({:ok, term}, _requested_format), do: {:ok, term}

  defp dump(result, initial_format, requested_format)

  defp dump({:ok, term}, initial_format, :integer) when initial_format in [:raw, :encoded] do
    {:ok, Uniq.UUID.string_to_binary!(term)}
  rescue
    _error in ArgumentError -> {:error, "can not dump term"}
  end

  defp dump({:ok, term}, _initial_format, _requested_format), do: {:ok, term}

  defp identify_format(
         <<_::binary-size(8), ?-, _::binary-size(4), ?-, _::binary-size(4), ?-, _::binary-size(4),
           ?-, _::binary-size(12)>>
       ),
       do: :raw

  defp identify_format(<<_::binary-size(22)>>), do: :encoded
  defp identify_format(<<_::128>>), do: :integer

  defp identify_format(string) when is_binary(string) do
    case String.split(string, "_") do
      [<<_::binary-size(32)>>] -> :hex
      _ -> :unknown
    end
  end

  defp identify_format(nil), do: nil
  defp identify_format(_term), do: :unknown
end

Inside one of my resources, I have the following relationship:

    has_one :similar_entity, Entity do
      no_attributes? true

      filter expr(
               (address_normalized == parent(address_normalized) or full_name == parent(full_name)) and id < parent(id)
             )
    end

When I try to load it, I get the following error:

** (FunctionClauseError) no function clause matching in UUIDV7.Type.dump/3    
    
    The following arguments were given to UUIDV7.Type.dump/3:
    
        # 1
        {:error, "got invalid uuid string; parent(id)"}
    
        # 2
        :unknown
    
        # 3
        :encoded
    
    Attempted function clauses (showing 2 out of 2):
    
        defp dump({:ok, term}, initial_format, :integer) when initial_format === :raw or initial_format === :encoded
        defp dump({:ok, term}, _initial_format, _requested_format)
    
    (uuid_v7 0.1.0) lib/uuid_v7/type.ex:79: UUIDV7.Type.dump/3
    (ash 2.18.1) lib/ash/type/type.ex:576: Ash.Type.cast_input/3
    (ash 2.18.1) lib/ash/query/type.ex:45: Ash.Query.Type.try_cast/3

If I add a dbg call to my process/3 function to check what the term variable contains, I will get this:

term #=> parent(id)

Is this correct? How am I supposed to convert this in my custom type?

That is definitely a bug in Ash core, not in your type. We should never send an expression into a type for casting.

EDIT: can I see the full stack trace? I’ll look into it.

There you go:

** (FunctionClauseError) no function clause matching in UUIDV7.Type.restore/3    
    
    The following arguments were given to UUIDV7.Type.restore/3:
    
        # 1
        {:error, "got invalid term"}
    
        # 2
        :unknown
    
        # 3
        :encoded
    
    Attempted function clauses (showing 2 out of 2):
    
        defp restore({:ok, term}, :integer, requested_format) when requested_format === :raw or requested_format === :encoded
        defp restore({:ok, term}, _initial_format, _requested_format)
    
    (uuid_v7 0.1.0) lib/uuid_v7/type.ex:57: UUIDV7.Type.restore/3
    (uuid_v7 0.1.0) lib/uuid_v7/type.ex:39: UUIDV7.Type.process/3
    (ash 2.18.1) lib/ash/type/type.ex:576: Ash.Type.cast_input/3
    (ash 2.18.1) lib/ash/query/type.ex:45: Ash.Query.Type.try_cast/3
    (ash 2.18.1) lib/ash/query/operator/operator.ex:191: Ash.Query.Operator.try_cast/3
    (elixir 1.16.0) lib/enum.ex:4319: Enum.find_value_list/3
    (ash 2.18.1) lib/ash/query/operator/operator.ex:167: Ash.Query.Operator.try_cast_with_ref/3
    (ash 2.18.1) lib/ash/filter/filter.ex:2961: Ash.Filter.resolve_call/2
    (ash 2.18.1) lib/ash/filter/filter.ex:2422: Ash.Filter.add_expression_part/3
    (ash 2.18.1) lib/ash/filter/filter.ex:2360: anonymous fn/3 in Ash.Filter.parse_expression/2
    (elixir 1.16.0) lib/enum.ex:4842: Enumerable.List.reduce/3
    (elixir 1.16.0) lib/enum.ex:2582: Enum.reduce_while/3
    (ash 2.18.1) lib/ash/filter/filter.ex:3597: anonymous fn/4 in Ash.Filter.parse_and_join/3
    (elixir 1.16.0) lib/enum.ex:4842: Enumerable.List.reduce/3
    (elixir 1.16.0) lib/enum.ex:2582: Enum.reduce_while/3
    (ash 2.18.1) lib/ash/filter/filter.ex:2412: Ash.Filter.add_expression_part/3
    (ash 2.18.1) lib/ash/filter/filter.ex:2360: anonymous fn/3 in Ash.Filter.parse_expression/2
    (elixir 1.16.0) lib/enum.ex:4842: Enumerable.List.reduce/3
    (elixir 1.16.0) lib/enum.ex:2582: Enum.reduce_while/3
    (ash 2.18.1) lib/ash/filter/filter.ex:327: Ash.Filter.parse/5
    (ash 2.18.1) lib/ash/query/query.ex:2363: Ash.Query.do_filter/3
    (ash 2.18.1) lib/ash/actions/load.ex:1741: anonymous fn/10 in Ash.Actions.Load.run_actual_query/11
    (ash 2.18.1) lib/ash/actions/load.ex:1136: anonymous fn/10 in Ash.Actions.Load.data/10
    (ash 2.18.1) lib/ash/engine/engine.ex:493: anonymous fn/2 in Ash.Engine.run_iteration/1
    (ash 2.18.1) lib/ash/engine/engine.ex:514: anonymous fn/4 in Ash.Engine.async/2
    (elixir 1.16.0) lib/task/supervised.ex:101: Task.Supervised.invoke_mfa/2
    (elixir 1.16.0) lib/task/supervised.ex:36: Task.Supervised.reply/4
    (ash 2.18.1) lib/ash/engine/engine.ex:508: Ash.Engine.async/2
    (elixir 1.16.0) lib/enum.ex:1700: Enum."-map/2-lists^map/1-1-"/2
    (ash 2.18.1) lib/ash/engine/engine.ex:658: Ash.Engine.start_pending_tasks/1
    (ash 2.18.1) lib/ash/engine/engine.ex:279: Ash.Engine.run_to_completion/1
    (ash 2.18.1) lib/ash/engine/engine.ex:208: Ash.Engine.do_run/2
    (ash 2.18.1) lib/ash/engine/engine.ex:104: Ash.Engine.run/2
    (ash 2.18.1) lib/ash/actions/read/read.ex:195: Ash.Actions.Read.do_run/3
    (ash 2.18.1) lib/ash/actions/read/read.ex:93: anonymous fn/3 in Ash.Actions.Read.run/3
    (ash 2.18.1) lib/ash/actions/read/read.ex:92: Ash.Actions.Read.run/3
    (ash 2.18.1) lib/ash/api/api.ex:2259: Ash.Api.read!/3

okay, I will try to get a fix in tomorrow. For now, you can add a fallback case that fails to parse that value and it should work.