Macros / Ecto fragment : error when using list as an argument

Hello everyone !

I am struggling with macros in order to dynamically create a SQL query to search for various topics in multiple languages. To provide some context, I have a full text search query and I would like to perform queries based on the languages selected on the search bar.

For example, If a user selects english and italian, the query should look like this :

      """
      SELECT topic_id, topic_title, topic_description
          FROM (SELECT topics.id as topic_id, topics.title as topic_title, topics.description as topic_description
             setweight(topics.title_tsvector, 'A') ||
             setweight(topics.description_tsvector, 'B') as document
        FROM topics) topics_search
        WHERE topics_search.document @@ websearch_to_tsquery(english::text::regconfig, 'query') OR
        topics_search.document @@ websearch_to_tsquery(italian::text::regconfig, 'query')
      """

I took inspiration from this article and implemented the following :

topics.exs

defmacro multilang_search_topics(languages) do
    quote bind_quoted: [languages: languages] do
      sql =
        Enum.map(
          languages,
          fn _lang ->
            "topics_search.document @@ websearch_to_tsquery(?::text::regconfig, ?)"
          end
        )
        |> Enum.join(" OR ")

      """
      SELECT topic_id, topic_title, topic_description
          FROM (SELECT topics.id as topic_id, topics.title as topic_title, topics.description as topic_description,
             setweight(topics.title_tsvector, 'A') ||
             setweight(topics.description_tsvector, 'B') as document
        FROM topics) topics_search
        WHERE #{sql}
      """
    end
  end
defmacro multilang_search_topics(languages, search_query) do
    languages = quote(do: unquote(languages))

    arguments =
      languages
      |> Enum.map(
           &[
             quote(do: unquote(&1)),
             quote(do: ^unquote(search_query)),
           ]
         )
      |> List.flatten()
      |> List.insert_at(0, multilang_search_topics(languages))

    quote do
      fragment(unquote_splicing(arguments))
    end
  end
  def search(query \\ Topic, search_query, languages) do
    from topic in query,
         join: search in multilang_search_topics(languages, search_query),
         on: search.topic_id == topic.id
  end

I got tricked into thinking my solution was working because I hardcoded the languages argument like so : join: search in multilang_search_topics(["english"], search_query)

However, when I switched it back the search method triggers the following error : ** (Protocol.UndefinedError) protocol Enumerable not implemented for {:languages, [line: 121], nil} of type Tuple

Now, I get that macros are receiving representation of arguments. I was wondering if there is a way to handle list argument without triggering the error ?

Thanks in advance to whoever might read and help. :sunny:

As you noticed, the approach you’ve selected works only with hardcoded values.
You have to use Ecto.Query.dynamic/2 in order to build dynamic where clauses.

languages = ["english", "italian"]

search_query = "SEARCH QUERY"

where =
  Enum.reduce(languages, false, fn language, dynamic ->
    dynamic(
      [topics_search],
      fragment("? @@ websearch_to_tsquery(?::text::regconfig, ?)", topics_search.document, ^language, ^search_query) or
        ^dynamic
    )
  end)

from topics_search in subquery(
       from topics in "topics",
         select: %{
           id: topics.id,
           title: topics.title,
           description: topics.description,
           document:
             fragment("setweight(?, 'A') || setweight(?, 'B')", topics.title_tsvector, topics.description_tsvector)
         }
     ),
     where: ^where,
     select: map(topics_search, [:id, :title, :description])
3 Likes

Amazing. Works like a charm, thanks a lot for your help. :pray:t5: