Can't get the right AST when conditionally selecting fields

I have a translate_field/2 macro that receives the current locale and a list of available locale/field mappings (this is a legacy database and we have some bizarre names for translation columns).

current_locale = "en"

query = 
  from t in Table,
  select: %{
    id: t.id,
    description: translate_field(^current_locale, en: t.description_en, pt: t.descricao, es: t.description_es),
    ...
  }

But I don’t know how to evaluate locale parameter, so it obviously fail:

== Compilation error on file web/models/table.ex ==
** (ArgumentError) argument error
    :erlang.binary_to_atom({:^, [line: 120], [{:current_locale, [line: 120], nil}]}, :utf8)

translate_field/2 definition:

  defmacro translate_field(locale, field_map) do
    quote do
      unquote(Keyword.fetch!(field_map,  String.to_atom(locale)))
    end
  end

If I call it with a hardcoded locale translate_field("en", en: t.description_en, pt: t.descricao, es: t.description_es) it works as expected.

I tried other quote/unquote definition variations, but Ecto’s never happy with the returned AST.

So, what I’m doing wrong? How can I make this work?

If it’s not possible, any other approach I can follow to achieve this?

1 Like

I managed to get it working returning a fragment with hardcoded possible locales:

defmacro translate_field(current_locale, field_map) do
  quote do
    fragment(
      """
      CASE ?
      WHEN 'pt' then ?
      WHEN 'en' then ?
      WHEN 'es' then ?
      END
      """,
      unquote(current_locale),
      unquote(Keyword.fetch!(field_map, :pt)),
      unquote(Keyword.fetch!(field_map, :en)),
      unquote(Keyword.fetch!(field_map, :es))
    )
  end
end

Not optimal, but works for now.

Still trying to figure out how to return the correct field directly.

2 Likes

You are misinterpreting the Ecto’s ^ operator :slight_smile:

You could just use translate_field(current_locale, en: t.description_en, ...).

The reason is that the ^ operator is only used to tell Ecto that a value in the query is dynamic, and should not be compiled to SQL, but rather escaped and used as a parameter. In your case, the current_locale is not being used in the query, but in your macro instead. You would only need the operator in this case if you would do something like this:

dynamic_value = "some description"

translate_field("en", en: ^dynamic_value)

That said, you do realize that using this as a macro and returning the AST to the query, the location will be hardcoded at compile time, and I doubt that this is what you wanted :smiley:

You should probably take a look at the Ecto’s field/2 query API function, so you could do something like:

description_field =
  case locale do
    "en" -> :description_en
    "pt" -> :descricao
    ...
  end

from t in Table, select: field(t, ^description_field)

This way the locale based field selection will work at runtime :smiley:

PS: Avoid using String.to_atom for values that might come from outside your application, as atoms aren’t garbage collected and creating a lot of different ones can bring your application down (Look here at the item 10.2).

1 Like