How to use ecto's named bindings for dinamicly built search filters

My models have a lot of associations and I’m trying to build search forms with filters to filter by not only fields of the entity’s schema but also by fields of it’s associations.

If I understood correctly, I can’t interpolate the association name into the :as key of the join and into the bindings of the where functions, I mean I can’t do something like that (correct me if I’m wrong!!):


def ensure_association(query, assoc) do
  if has_named_binding?(query, assoc) do
    query
  else
    join(query, [p], c in assoc(p, ^assoc), as: ^assoc)
  end
end

Nor I can do this:

def add_where(query, association, field_name, value) do
   where(query, [^association: q], field(q, ^field_name) == ^value)
end

So I tried to overcome this like this:

defmodule Jp.Search do
  import Ecto.query

  for association <- ~w(address skype phones events subscriptions notifications actions assignments notes company specialist agent vacancies offers contract contracts driving_licence general_skills materials work_experiences professional_schools professional_courses professional_certificates welder concreter machine_operator cnc_experiences turner_cutter)a do
  
    def add_where(query, unquote(association), field_name, value) do
      where(query, [unquote(association): q], field(q, ^field_name) == ^value)
    end


    def ensure_association(query, unquote(association)) do
      has_named_binding?(query, unquote(association)) && query || join(query, :inner, [q], a in assoc(p, unquote(association)), as: unquote(association))
    end
  end

end

But it doesn’t compile, giving me error:
unexpected token: ":"
on the line where I try to put association name into binding like [unquote(association): q]
I also tried to put it like [unquote(to_string(association)):q]
but the error persists.

So how to do it properly and what do I do wrong?

You can use ~a like ~w to get a list of atoms instead.

for association <- ~a(address ...) do

I’m not clear if you want a string or atom to match with in the function head, but I’m going to assume string, since it likely comes from url params. One thing to note is that keyword lists, [foo: f], is syntactic sugar for [{:foo, f}]. With that in mind:

    def add_where(query, unquote(to_string(association)), field_name, value) do
      where(query, [{unquote(association), q}], field(q, ^field_name) == ^value)
    end

Thanks, @blatyo, I have a long string so you haven’t noticed ~w(…)a at the end, I didn’t know about ~a, as for your advice to replace the syntactic sugar form of keyword to tuple, I’ll give it a try and let you know…

Ok, now it gives me:

== Compilation error in file lib/jp/search.ex ==
** (ArgumentError) you attempted to apply :type on {{{:., [], [{:__aliases__, [alias: false], [:Ecto, :Query, :Builder]}, :count_alias!]}, [], [{:query, [], Ecto.Query.Builder}, :address]}, {:field_name, [line: 169], nil}}. If you are using apply/3, make sure the module is an atom. If you are using the dot syntax, such as map.field or module.function, make sure the left side of the dot is an atom or a map
    :erlang.apply({{{:., [], [{:__aliases__, [alias: false], [:Ecto, :Query, :Builder]}, :count_alias!]}, [], [{:query, [],
Ecto.Query.Builder}, :address]}, {:field_name, [line: 169], nil}}, :type, [])

The line giving the issue is this:
"less_then_years_ago" -> where(query, [{unquote(association), q}], field(q, ^field_name) > ago(^value, "year"))

And it seems not connected with the first issue but can you translate it to me because I don’t really understand what that means?

So, the line:
where(query, [{unquote(association), q}], field(q, ^field_name) > ^value)
compiles
but the line:
where(query, [{unquote(association), q}], field(q, ^field_name) > ago(^value, "year"))
not.
Why is that?

Sorry, I misremembered this. ~w(...)a is correct.

I’m not sure. Is it perhaps a problem because address isn’t a date field? I’m going by the error above which mentions address.

No, address is just the association name, it’s the atom. The error tells me that I provide the piece of AST instead of the atom to apply function. I think it’s buried deeper in ecto’s Ecto.Query.Builder module. I found the issue for ecto project with error like mine:

But that was fixed, may be I have something like that…

I’ve made an issue on Ecto’s project about this bug and it was solved by Jose Valim in the commit: https://github.com/elixir-ecto/ecto/commit/26c1cb5591a27241067435aedec48c68d102fb8d

3 Likes

And today the support of dynamic binding was added in the new release of Ecto, so now it’s possible to pass the named binding to where like I was trying first and it was not working:

def add_where(query, association, field_name, value) do
   where(query, [{^association, q}], field(q, ^field_name) == ^value)
end
2 Likes