Infinite loop when reading query inside ManualRead action

I have a read action like this:

    read :get_by_name do
      get? true

      argument :name, :string, allow_nil?: false

      filter expr(name == ^arg(:name))

      prepare build(limit: 1, sort: {:updated_at, :desc}, load: :columns)
    end

I want to make it so that if no results are returned, the action will create a new row for it and return that.

I think I can do that with a manual read action, so I changed the action to:

    read :get_by_name do
      get? true

      argument :name, :string, allow_nil?: false

      manual Actions.GetByName
    end

And implemented GetByName as:

defmodule Ui.Resources.TableManagement.Actions.GetByName do
  @moduledoc false

  use Ash.Resource.ManualRead

  require Ash.Query

  import Ash.Expr

  def read(query, _data_layer_query, _opts, _context) do
    with {:ok, nil} <- read(query),
         {:ok, result} <- create(query) do
      {:ok, [result]}
    end
  end

  defp read(query) do
    query
    |> Ash.Query.filter(name == ^arg(:name))
    |> Ash.Query.limit(1)
    |> Ash.Query.sort(updated_at: :desc)
    |> Ash.Query.load(:columns)
    |> Ash.read_one()
  end

  defp create(query) do
    name = Ash.Query.get_argument(query, :name)
      
    TableManagement
    |> Ash.Changeset.for_create(:create, %{name: name})
    |> Ash.create()
    |> Ash.load(:columns)
  end
end

This will not work however, basically the Ash.read_one call will recursively call the same action in an infinite loop.

I though about adding a Ash.Query.for_read(:read) to my query in the read function, but that will make the query return the following error:

{:error,
 %Ash.Error.Invalid{
   query: "#Query<>", 
   errors: [
     %Ash.Error.Invalid.TimeoutNotSupported{
       resource: Ui.Resources.TableManagement,
       splode: Ash.Error,
       bread_crumbs: [],
       vars: [],
       path: [],
       stacktrace: #Splode.Stacktrace<>,
       class: :invalid
     }
   ]
 }}

I guess I can simply create a new query from scratch, but I was wondering if there is some way to use the existing query or if I’m not supposed to use that.

Btw, I did found a better solution with preparations:

    read :get_by_name do
      get? true
      transaction? true

      argument :name, :string, allow_nil?: false

      prepare fn query, _context ->
        Ash.Query.after_action(query, fn
          query, [] ->
            read = fn query ->
              name = Ash.Query.get_argument(query, :name)

              TableManagement
              |> Ash.Changeset.for_create(:create, %{name: name})
              |> Ash.create()
              |> Ash.load(:columns)
            end

            with {:ok, result} <- read.(query), do: {:ok, [result]}

          _query, results ->
            {:ok, results}
        end)
      end

      filter expr(name == ^arg(:name))

      prepare build(limit: 1, sort: {:updated_at, :desc}, load: :columns)
    end

That works great, but I still would like o understand why the original solution didn’t work

The query that you provided has an action set on it, and so calling Ash.read_one is reusing that (so its calling the same action infinitely). You should add Ash.Query.for_action to change the action before calling Ash.read_one.

I can’t find a Ash.Query.for_action function, only Ash.Query.for_read

Right, sorry that’s what I meant to say :slight_smile:

Ah, but I tried that, it will give me the error I posted in the first post (Ash.Error.Invalid.TimeoutNotSupported)

Is there a stacktrace? :thinking: are you setting a timeout at any point? What is query.timeout in your hook there?