Expected a keyword list with set/push/pop as keys with field-value pairs as values

I’m going through the Ecto Guide and working through the “Composing with Data Structure”
I adjusted the example so it would work with the earlier “Friends” project. So this example is slightly different but the concept is the same.

I’m stuck on an error and looking for feedback.

Step 1
I have a Person with the age column and I want to increment the age by +1

iex> john_update = from Friends.Person, where: [id: 1], update: [inc: [age: +1]]
#Ecto.Query<from p0 in Friends.Person, where: p0.id == 1, update: [inc: [age: 1]]>

Step 2
Now create an Ecto.Multi as in the tutorial and I only pipe in 1 Ecto.Multi.update_all/4 just for demo purposes.

iex> multi = Ecto.Multi.new() |> Ecto.Multi.update_all(:john_birthday, Friends.Person, john_update)
%Ecto.Multi{
  names: #MapSet<[:john_birthday]>,
  operations: [
    john_birthday: {:update_all, #Ecto.Query<from p0 in Friends.Person>,
     #Ecto.Query<from p0 in Friends.Person, where: p0.id == 1,
      update: [inc: [age: 1]]>, []}
  ]
}

Step 3

Now when I pass it into the Repo I get an error

iex> Friends.Repo.transaction(multi)

** (ArgumentError) malformed update #Ecto.Query
<from p0 in Friends.Person, where: p0.id == 1, update: [inc: [age: 1]]>
in query expression, 
expected a keyword list with set/push/pop as keys with field-value pairs as values

What does this error mean?

I was looking at the Ecto.Query.update/3 guide and it looks like I’m using the correct operators so I think.

$ iex -S mix
Erlang/OTP 22 [erts-10.5.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe] [dtrace]

Interactive Elixir (1.9.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> alias Friends.{Repo,Person}
[Friends.Repo, Friends.Person]
iex(2)> import Ecto.Query
Ecto.Query
iex(3)> query = from(Person, where: [id: 1])
#Ecto.Query<from p0 in Friends.Person, where: p0.id == 1>
iex(4)> update = from(query, update: [inc: [age: +1]])
#Ecto.Query<from p0 in Friends.Person, where: p0.id == 1,
 update: [inc: [age: 1]]>
iex(5)> multi = Ecto.Multi.new() |> Ecto.Multi.update_all(:birthday, update, [])
%Ecto.Multi{
  names: #MapSet<[:birthday]>,
  operations: [
    birthday: {:update_all,
     #Ecto.Query<from p0 in Friends.Person, where: p0.id == 1,
      update: [inc: [age: 1]]>, [], []}
  ]
}
iex(6)> Repo.all(query)

16:23:51.903 [debug] QUERY OK source="people" db=0.7ms decode=0.6ms queue=1.0ms
SELECT p0."id", p0."first_name", p0."last_name", p0."age" FROM "people" AS p0 WHERE (p0."id" = 1) []
[
  %Friends.Person{
    __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
    age: 30,
    first_name: "Ryan",
    id: 1,
    last_name: "Bigg"
  }
]
iex(7)> Repo.transaction(multi)

16:23:51.908 [debug] QUERY OK db=0.3ms
begin []
 
16:23:51.910 [debug] QUERY OK source="people" db=0.7ms
UPDATE "people" AS p0 SET "age" = p0."age" + 1 WHERE (p0."id" = 1) []
 
16:23:51.916 [debug] QUERY OK db=6.2ms
commit []
{:ok, %{birthday: {1, nil}}}
iex(8)> Repo.all(query)

16:23:51.919 [debug] QUERY OK source="people" db=2.3ms
SELECT p0."id", p0."first_name", p0."last_name", p0."age" FROM "people" AS p0 WHERE (p0."id" = 1) []
[
  %Friends.Person{
    __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
    age: 31,
    first_name: "Ryan",
    id: 1,
    last_name: "Bigg"
  }
]
iex(9)> 

So

multi = Ecto.Multi.new() |> Ecto.Multi.update_all(:john_birthday, john_update, [])

should fix your example. Your queryable already contains the updates - so the updates argument needs to be an empty list (the opts argument defaults to an empty list).


# file: friends/priv/repo/playground.exs
#
# pg_ctl -D /usr/local/var/postgres start
# mix format ./priv/repo/playground.exs
# mix run ./priv/repo/playground.exs
#

defmodule AppInfo do
  def string() do
    Application.loaded_applications()
    |> Enum.map(&to_app_keyword/1)
    |> Enum.sort_by(&map_app_name/1)
    |> Enum.map_join(", ", &app_keyword_to_string/1)
  end

  defp to_app_keyword({app, _, vsn}),
    do: {app, vsn}

  defp app_keyword_to_string({app, vsn}),
    do: "#{app}: #{vsn}"

  defp map_app_name({app, _}),
    do: app
end

defmodule Playground do
  import Ecto.Query
  alias Ecto.Multi, as: EM

  alias Friends.{Repo, Person}

  def play do
    query = from(p in Person, where: p.id == 1)
    update = query |> update([p], inc: [age: 1])

    multi =
      EM.new()
      |> EM.update_all(:birthday, update, [])

    r0 = Repo.one(query)
    r1 = Repo.transaction(multi)
    r2 = Repo.one(query)

    [r0: r0, r1: r1, r2: r2]
  end
end

IO.puts(AppInfo.string())
IO.puts("#{inspect(Playground.play(), pretty: true)}")
$ mix run ./priv/repo/playground.exs
asn1: 5.0.9, compiler: 7.4.7, connection: 1.0.4, crypto: 4.6.1, db_connection: 2.1.1, decimal: 1.8.0, ecto: 3.2.3, ecto_sql: 3.2.0, elixir: 1.9.2, friends: 0.1.0, hex: 0.20.1, inets: 7.1.1, kernel: 6.5, logger: 1.9.2, mix: 1.9.2, postgrex: 0.15.1, public_key: 1.7, ssl: 9.4, stdlib: 3.10, telemetry: 0.4.0

18:46:51.903 [debug] QUERY OK source="people" db=0.6ms decode=0.6ms queue=0.8ms
SELECT p0."id", p0."first_name", p0."last_name", p0."age" 
FROM "people" AS p0 WHERE (p0."id" = 1) []

18:46:51.907 [debug] QUERY OK db=0.2ms
begin []

18:46:51.909 [debug] QUERY OK source="people" db=0.8ms
UPDATE "people" AS p0 SET "age" = p0."age" + 1 
WHERE (p0."id" = 1) []

18:46:51.915 [debug] QUERY OK db=6.2ms
commit []

18:46:51.918 [debug] QUERY OK source="people" db=2.6ms
SELECT p0."id", p0."first_name", p0."last_name", p0."age" 
FROM "people" AS p0 WHERE (p0."id" = 1) []
[
  r0: %Friends.Person{
    __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
    age: 28,
    first_name: "Ryan",
    id: 1,
    last_name: "Bigg"
  },
  r1: {:ok, %{birthday: {1, nil}}},
  r2: %Friends.Person{
    __meta__: #Ecto.Schema.Metadata<:loaded, "people">,
    age: 29,
    first_name: "Ryan",
    id: 1,
    last_name: "Bigg"
  }
]
$
1 Like

Thank you for this. I can see the error:

My error:

Ecto.Multi.update_all(:john_birthday, Friends.Person, john_update)

Your correct example:

Ecto.Multi.update_all(:john_birthday, john_update, [])

Don’t need to pass in Friends.Person, because john_update already contains the queryable.

After thought
When I read the documentation I thought I was providing the exact same parameters. The documentation says to provide an Ecto.Queryable.t() I followed the same convention by giving it Friends.Person but now I can see it’s already implied in the john_update that gets passed in.