Ecto.Query.update dynamic field name

I am working on a library that allows re-ordering rows of an Ecto schema based on an integer column.

Right now, the column is hard-coded to always be :position. Of course, this should become configureable.

However, the code contains statements such as the following:

    |> Ecto.Multi.update_all(:decremented_higher_siblings_1, from( p in later_siblings, update: [set: [position: fragment("- (? - 1)", p.position)]]), []).

Note here the update: [set: ...] part (see the documentation of Ecto.Query.update.

I know Ecto.Query.field exists to set a dynamic field name, but as update uses a keyword list, I have no idea how to properly set the keyword list key there (as the line still is supposed to be read as macro, with the p variable available).

How to do this?

2 Likes

I could do that like this:

     update_args = Keyword.new([{f, new}])

     from(
       a in table,
       update: [set: ^update_args],
       where: field(a, ^f) == ^old
     )
     |> MyApp.Repo.update_all([])
4 Likes

Wonderful! Thank you very much for solving this problem. :slight_smile:

1 Like

For people, like me, who need this to be sort-of generalized, the following MOSTLY works. It turns out that I needed to do {:replace, columns} rather than :replace_all_except_primary_key because my input data was not including that, so there’s some behind the scenes stuff coming in Ecto 3.0 which will make this very clean, but this will generalize this in the short term.

defmodule ConflictUpdate do
  @moduledoc """
  A function to create an update query for an `on_conflict:` clause in
  `insert_all`.
  """

  @doc "A function to create an update query for a schema."
  @spec conflict_update(Ecto.Schema.t(), :replace_all | :replace_all_except_primary_key |
                               {:replace, list(atom)}) :: Macro.t()
  defmacro conflict_update(schema, pattern) do
    e_schema = Macro.expand_once(schema, __CALLER__)
    e_pattern = Macro.expand_once(pattern, __CALLER__)

    update_args =
      e_schema
      |> columns(e_pattern)
      |> Enum.map(& {&1, {:fragment, [], ["EXCLUDED.#{&1}"]}})
      |> Keyword.new

    {:update, [context: Elixir, import: Ecto.Query],
      [
        schema,
        [
          set: update_args
        ]
      ]
    }
  end

  defp columns(schema, :replace_all), do: schema.__schema__(:fields)

  defp columns(schema, :replace_all_except_primary_key),
  do: schema.__schema__(:fields) -- schema.__schema__(:primary_key)

  defp columns(_schema, {:replace, fields}), do: fields
end
2 Likes