Writing
update_all reads its hash the way Active Record does — update_all(likes: :likes)
sets the column to the symbol itself. The block reads a symbol as the column it
names, as every other block here does, which is what lets the new value be
worked out from the old:
Post.where { :published == true }.update_all { { likes: :likes + 1 } }
# UPDATE "posts" SET "likes" = ("posts"."likes" + 1) WHERE ...
Post.update_all { { title: upper(:title), likes: case_when { :likes < 0 }.then(0).else(:likes) } }
upsert_all takes one too, for the part that decides what happens to a row
that is already there. excluded is the row that could not be inserted:
Tally.upsert_all(rows, unique_by: :page) { { hits: :hits + excluded(:hits) } }
# ... ON CONFLICT ("page") DO UPDATE SET "hits"=("tallies"."hits" + "excluded"."hits")
PostgreSQL and SQLite name that row excluded; MySQL spells the same thing
VALUES(column), and the block comes out as whichever the adapter reads.
Active Record's own on_duplicate: takes SQL text and nothing else, so this is
the one place the DSL writes SQL out itself rather than handing Arel a tree —
and the two cannot both be given.
insert_all has no block: its values are literals by construction.
Active Record type-casts each one on the way into the VALUES list, so an
expression does not become SQL there — it becomes nothing, silently. Use
upsert_all where a row's value has to be worked out.