Search code examples
macroselixirecto

Building macro for generating fragment that accesses json key


Suppose I have a jsonb field that I want to order based on one of its fields, it should also use dynamic since there is no way to tell the field name at compile-time. What I currently have is:

def order_by_dynamic([sort_direction, %{binding_name: binding_name, json_field: json_key, field: field}]) do
    [{sort_direction |> String.to_atom(), dynamic([{^binding_name, c}], create_fragment([{c.details, ^field}, json_key]))}]
  end

defmacro create_fragment(fields) do
    require Ecto.Query

    query = "?->>?"
    quote do
      fragment(unquote(query), unquote_splicing(fields))
    end
  end

The problem with this code is that it won't compile:

** (Ecto.Query.CompileError) Tuples can only be used in comparisons with literal tuples of the same size
    expanding macro: Ecto.Query.dynamic/2

I've also tried to interpolate the json field name into the string before passing it to the fragment, and the error was:

** (Ecto.Query.CompileError) to prevent SQL injection attacks, fragment(...) does not allow strings to be interpolated as the first argument via the `^` operator, got: `"?->>#{json_key}"`
    expanding macro: Ecto.Query.dynamic/2

Am I missing something or this is not achievable with current fragment macro?


Solution

  • Ok, it seems that I understood, you need to provide the first argument straight from binding to avoid sql injection:

    defmacro create_dynamic_fragment(binding_name, field, json_field) do
        require Ecto.Query
    
        quote do
          dynamic([{^unquote(binding_name), c}], fragment("?->>?", field(c, ^unquote(field)), ^unquote(json_field)))
        end
      end