How are functions from Ecto.Query.API imported?

When I import Ecto.Query I also get access to functions from Ecto.Query.API like max, count.

How?

Also functions in Ecto.Query.API seem to be there just for documentation. Where do they actually come from?

I’m not an ecto user, so I’m not aware of that fact and can’t really believe it’s true. Can you give an example snippet that would work and uses the remote and not imported functions?

Sure, https://github.com/idi-ot/postgres-tutorial/blob/part-1/part-1/rdb/lib/rdb/ch2/ch2.ex#L251-L262

  @spec agg1 :: integer
  def agg1 do
    import Ecto.Query

    # want
    # SELECT max(temp_lo) FROM weather;

    query = from(w in Weather, select: max(w.temp_lo))
    Repo.one(query)

    # is actually
    # SELECT max(w0."temp_lo") FROM "weather" AS w0 []
  end

I’m not deep-diving code today, but from/2 is a macro, so it will probably rewrite the max.

From the Ecto.Query.API moduledocs

Note the functions in this module exist for documentation purposes and one should never need to invoke them directly.

If you look at the module code, none of the functions actually do anything. This is to say, the module isn’t actually used at all when evaluating queries. In reality the from macro just takes the AST of the query and handles everything. Ecot.Query.API is docs only.

1 Like

So where does max/1 come from? I’ve skimmed through Ecto.Query, Ecto.Query.Builder, Ecto.Query.Builder.From and couldn’t see any functions from Ecto.Query.API being mentioned there.

It, like everything else you pass to from, is just treated as AST and transformed into a query struct.

transformed into a query struct

This is what I want to know, how and where this transformation happens.

iex(1)> from(a in "asdf", select: %{sum: asdf(a.id)})
** (Ecto.Query.CompileError) `asdf(a.id())` is not a valid query expression.

* If you intended to call a database function, please check the documentation
  for Ecto.Query to see the supported database expressions

* If you intended to call an Elixir function or introduce a value,
  you need to explicitly interpolate it with ^

    (ecto) expanding macro: Ecto.Query.select/3
    iex:1: (file)
    (ecto) expanding macro: Ecto.Query.from/2
    iex:1: (file)

iex(1)> from(a in "asdf", select: %{sum: sum(a.id)})
#Ecto.Query<from a in "asdf", select: %{sum: sum(a.id)}>

They must be defined somewhere, otherwise how does ecto know what functions like max or sum are valid.

Well, your adventure begins here: ecto/lib/ecto/query.ex at v2.2.9 · elixir-ecto/ecto · GitHub
It’s pretty complicated. If I had to summarize the answer to the original question though which was

When I import Ecto.Query I also get access to functions from Ecto.Query.API like max, count.

The answer is "you don’t get access to functions like max or count`. The thing with quoting is that you can quote literally any valid elixir syntax without anything inside needing to be compilable yet. So for example just open IEX and do:

iex(4)> quote do: this_doesnt_exist(foo)
{:this_doesnt_exist, [], [{:foo, [], Elixir}]}

You haven’t really gained access to some this_doesnt_exist function, rather you’re simply grabbing the AST of that code. This is what happens with From, and then it takes that AST and does… a lot with it.

1 Like

Yeah, I have some understanding how this works, I just can’t find any mentions of functions from Ecto.Query.API elsewhere in code.

Nevermind, found them in lib/ecto/query/builder.ex, lib/ecto/query/builder/select.ex.

2 Likes