Yes, those are the two biggest issues with Ecto today. Joins and dynamic expressions (inside a where, for example).
Before I go into those, I would like to say that it is important that where([e], ...) does not raise in case of more bindings because it would negatively impact query composition. Imagine you would have to write where([e], e.public) for one binding, where([e, _], e.public) for two, etc.
Anyway, this ties to the problem of joins we mentioned above. There are two ways we can consider joins and both ways may be available at compile-time based or runtime.
1. The first mechanism is positional. That's how joins work today. Your solution of doing tables[0] and tables[1] is also based on position. The issue with position is that you may not know how many joins you had in the past and that's why we introduced "..."
2. The second mechanism is based on names. For example, whenever I use join: c in "comments", we could store the name of the join as "c" and allow a position match later on using maps: where(%{c: c}, c.private)
In any case, the solutions above are still limited if they are restricted to compilation time. And Ecto joins today *are* compile time limited.
I believe your tables suggestion is dangerous because you are considering where([a, b], ...) or where(tables, ...) to be an assignment on the first argument and that wouldn't work with Ecto. It makes sense for Python and Ruby, because the tables could be an object with entries being able to collect state at runtime, but that won't work with Elixir.
In other words, your tables suggestion is mixing runtime with compile time, which is a feature we absolutely need in some shape in Ecto, but it can be quite tricky to get right. And that's directly related with the inability to write complex and/or. Those require the ability the shape queries at runtime but we can't do it.
Sorry, that's a long answer to agree that the issue with Ecto is the inability to build queries at runtime. That reflects joins and wheres (and others). Being able to write tables[0] or write joins based on names may be an improvement but it won't be enough to solve the main problem of writing dynamic queries.
We should definitely support this feature but we never figured out exactly how to write it. Maybe one option is to actually expose Ecto's query expressions, allowing you to convert complex logic into trees, which could even be done using Elixir's quote/unquote.
One idea is to allow expressions to be composed and added dynamically. For example:
part_a = dynamic([p, q], p.field == q.field)
part_b = dynamic([p], p.other == ^value or ^part_a)
from query, where: ^part_b
I am not convinced about the name "dynamic" though but I hope you get the idea. "fragment" would be a good one but it is already taken. :)