Extract main schema from an ecto query

I want to execute a function defined in a module using Ecto.Schema based on the main entity of a query. So I need to extract from a query its main schema. This is the solution I have so far (very simplified), I was wondering if there is a better way of doing so or if there are some cases I’m missing.

defmodule Foo do
  
  def foo(query) do
    case schema_module(query) do
      schema -> apply(schema, :bar, [query])
      nil -> query
    end
  end

  defp schema_module(%Ecto.Query{from: %{source: {_, module}}}), do: schema_module(module)

  defp schema_module(query) when is_atom(query) do
    cond do
      function_exported?(query, :__schema__, 1) -> query
      true -> nil
    end
  end
end

It can be used like

User |> Foo.foo() |> Repo.all()

or

User |> Repo.preload(:posts) |> Foo.foo() |> Repo.all()

In both cases User.bar/1 is called with the query as argument.

My main concern is about the Ecto.Query.FromExpr struct (the value of :from key in Ecto.Query struct). I think it is an ecto internal that can change without notification.

It probably pays to take a step back and review why would you need to extract a schema module as deeply in the code as you seem to need it. Is it not possible to use that info the moment you get the schema module, earlier in the code?

Furthermore, you shouldn’t assume the shape of Ecto’s data structures. Use Ecto.get_meta/2 and then check out the docs of the returned struct.

I have a similar use case, where I want to apply a set of filters to some Schema fields. I can do this in two ways:

  • Give an extra parameter to define the schema (explicit).
  • Use the Ecto.Query.FromExpr inside the query to extract the Schema (implicit).

Breaking changes would only be detected by Dialyzer. Since it isn’t a hassle to have a third argument, I will leave this explicit.