Dynamically accessing named bindings and fields from parent queries

741 views
Skip to first unread message

Arno Dirlam

unread,
Jul 2, 2021, 6:12:17 AM7/2/21
to elixir-ecto
Hi everyone!

I'm developing an inference engine based on Ecto schemas, that should be able to translate conditions on deeply nested associations to Ecto queries recursively. For example:

SELECT * FROM x WHERE
    EXISTS (SELECT * FROM y WHERE y.x_id = x.id AND y.date < x.date) AND
    NOT EXISTS (SELECT * FROM z WHERE z.x_id = x.id AND z.role = x.role)

Assuming these are has_many associations, the only approach I could come up with so far requires dynamically accessing named bindings and fields of the parent queries.

The docs only mention dynamically referencing named bindings from the same query (see Named bindings docs, last paragraph):

You can also match on a specific binding when building queries. For example, let's suppose you want to create a generic sort function that will order by a given field with a given as in query:

    # Knowing the name of the binding
    def sort(query, as, field) do
      from [{^as, x}] in query, order_by: field(x, ^field)
    end


When I try to do it with a named binding from the parent query, I get:
    ** (Ecto.Query.CompileError) `from` in query expression specified 2 binds but query contains 1 binds

When I try it using parent_as(^as).id I get:
    ** (Ecto.Query.CompileError) cannot fetch field `id` from `parent_as(^as)`. Can only fetch fields from:
    * sources, such as `p` in `from p in Post`
    * named bindings, such as `as(:post)` in `from Post, as: :post`
    * parent named bindings, such as `parent_as(:post)` in a subquery

Is there any way to achieve this?

If not, would you be interested in adding it, for example using this syntax?

    # Knowing the name of the parent binding
    def sort(query, parent_as, field) do
      from [{:parent, ^parent_as, x}] in query, order_by: field(x, ^field)
    end


Thanks a lot for any pointers and/or consideration.

José Valim

unread,
Jul 2, 2021, 6:41:11 AM7/2/21
to elixi...@googlegroups.com
It probably makes more sense to support both parent_as(^as) and as(^as). PRs are welcome!

--
You received this message because you are subscribed to the Google Groups "elixir-ecto" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-ecto...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-ecto/cb82d5d8-a080-400e-87de-837fe2ad10f7n%40googlegroups.com.

Arno Dirlam

unread,
Jul 2, 2021, 7:13:19 AM7/2/21
to elixir-ecto
Okay cool.

To summarize, what would be possible then:

1. Positional binding for current query: [x] (unchanged)
2. Named binding for current query: [name: x] (unchanged)
3. Named reference for current query: as(:name) (unchanged)
4. Named reference for parent query: parent_as(:name) (unchanged)

5. Dynamic named reference for current query: as(^as) (new)
6. Dynamic named binding for current query: [{^as, x}] (unchanged)
7. Dynamic named reference for parent query: parent_as(^as) (new)

What would not be possible:
8. Dynamic named binding for parent query: [{:parent, ^as, x}] (unsupported)

I see a minor inconsistency by having 3. and not having 8., but I don't think it's a problem; I imagine dynamic named references (5. and 7.) to become preferred over a dynamic named binding (6.) once they're in, as it seems more natural to express it like this, at least to me.
So adding 8. is not needed (my proposed syntax seems slightly clumsy anyway), neither is removing 6. because this change would be backward-incompatible.

I'll try to come up with a PR and link it here 👍

Arno Dirlam

unread,
Jul 3, 2021, 1:53:14 PM7/3/21
to elixir-ecto
That was easier than I thought :)

PR is open.
Reply all
Reply to author
Forward
0 new messages