1

I'm learning Elixir and came across such situation:
I have an Ecto schema and I want to make a function like "get_by" that takes a column name and it's value as an arguments like this: get_by(:id, 7) So the working version of the function would be like this:

def get_by(column, value) do
  Repo.all(
    from(
      r in __MODULE__,
      where: field(r, ^column) == ^value,
    )
  )
end

I know this is fully functional but I was wondering how the field macro works.
The original code is too hard to read for me. I was trying to play with AST in macro, but nothing seems to work. The best I had was this:

defmacro magic(var, {:^, _, [{column, _, _}]}) do
  dot = {:., [], [var, column]}
  {dot, [], []}
end

But this returns r.column instead of the atom bound to column variable.
How the macro should be written to return r.id?

m.cichacz
  • 749
  • 9
  • 21
  • If you look at the source code for `Ecto.Query.API.field/2`, it's just a placeholder: https://github.com/elixir-ecto/ecto/blob/v3.2.5/lib/ecto/query/api.ex#L353 The query macro looks for calls to it and treats them as desired. – Brett Beatty Nov 15 '19 at 17:32

1 Answers1

1

If you check the source code for Ecto.Query.API.field/2, you’ll see that the explicit call to this function (it’s not a macro btw) raises.

That is because it makes sense only inside Ecto.Query.from/2 macro.

What you want is still possible to some extent; not with a dot notation (AFAICT, but with an Access)

defmodule M do
  defmacro magic(a1, {:^, _, [a2]}) do
    quote do: unquote(a1)[unquote(a2)]
  end
end
import M
{r, column} = {%{id: 42}, :id}
magic(r, ^column)
#⇒ 42

I was unable to get quote do: unquote(a1).unquote(a2) work without absolutely nasty tricks like inplace eval.

To better understand macros, you probably should clarify for yourself what AST is available where.


I highly recommend Metaprogramming Elixir by Chris McCord.

Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
  • Actually what I am looking for is the way to do exactly what `field` does, so to insert into where clause a condition "where r.id = 7" while `id` is stored as atom in some variable. Of course outside `from` it wouldn't make sense to use it. – m.cichacz Nov 15 '19 at 21:05
  • `field` does not do anything, it is handled as a plain atom in the AST by `from/2` as I said. – Aleksei Matiushkin Nov 15 '19 at 21:07
  • Ok I think I understand now. So basically the `from/2` is looking for `:field` and handling it, right? – m.cichacz Nov 15 '19 at 21:50
  • Yes, same for `fragment/2`. `Ecto.Query.API.field/2`is just there so they can attach documentation to it – Jhon Pedroza Nov 16 '19 at 05:42