Macros can certainly make things more difficult to grok, but I think they’ll somewhat “fade away” as you gain a better mental model of Ecto queries. Here’s some info that might hopefully help:
First, Ecto queries are just data structures. This might seem obvious, but it helps to remember. There’s no real magic going on – every time we add a new clause to a query, Ecto’s just updating and returning a new data structure that includes that added information. It’s only when you pass that query to a Repo
function that it gets converted into whatever database-speak your db requires (SQL, for instance).
So, somewhere in this data structure that is a query, there’s a field that’s tracking sources – the tables/subqueries/joins/etc. your query is selecting from. Using your example: from(c in schema)
. Note that c
doesn’t actually matter here and is equivalent to from(schema)
. That’s because the c
binding is local to that particular from
. But now we have a query and schema
is a source, so you can imagine that there might be an internal field in the query:
query = from(schema)
#=> %Ecto.Query{sources: [schema], ...}
So at this point, our query has a single source. Let’s add another clause:
query
|> where([c], c.attr == "foo")
We need to reference the source somehow, so we’re using the positional binding c
. It could have been anything, though, and it doesn’t have to be the same for later clauses:
query
|> where([c], c.attr == "foo")
|> or_where([d], d.attr == "bar")
|> or_where([e], e.attr == "baz")
You’d obviously never write this, but in the above, c
, d
, and e
all represent the same source – the first one.
Let’s add some joins:
query =
from(c in query, join: assoc(c, :first_assoc), join: assoc(c, :second_assoc)
We’ve joined in two associations – by using assoc(c, :first_assoc)
, Ecto will know to find the source of the joins by looking at the associations defined on our first source, whatever schema
was. The query data structure might now look like this:
%Ecto.Query{sources: [schema, first_assoc, second_assoc])
At this point, you can probably guess which positional binding would relate to which source if we added more to the query:
from([a, b, c] in query, ...)
So positional bindings are just based on the order that the sources were added. But what if you don’t know how many sources were added, or the order they were added in? Ecto has two ways (that I know of) to handle this. The first is a special bit of syntax for selecting whatever the last source was:
from([..., last] in query, ...)
# you can also bind the original source and the last source
from([first, ..., last] in query, ...)
# or the first two and the last
from([first, second, ..., last] in query, ...)
# etc.
The second is to use named bindings. When we added our joins in, we could have done this instead:
from(c in query,
join: assoc(c, :first_assoc), as: :first,
join: assoc(c, :second_assoc), as: :second
)
When we do this, you can imagine that the query data structure might look more like this:
%Ecto.Query{sources: [schema, first: first_assoc, second: second_assoc]}
And now we can use those names to refer to those sources:
from([first: f] in query, ...)
# can also use `as`
from(query, where: as(:first).attr == "whatever")
So when we use the various Ecto macros to refer to previous sources, there’s no real magic happening – you’re just giving more convenient names to sources using their position or :as
name. Hopefully this helps to demystify it a bit!