upsert_all
upsert_all(&self, values: Vec<T>) where T derives #[derive(SqlxInsert)] (see
insert) — the batch form of upsert, one round-trip for the whole
batch. Also requires #[unique(col)]:
#[unique(username)]
async fn upsert_all(&self, values: Vec<UserValues>) -> sqlx::Result<Vec<UserRow>>;
Unlike single-row upsert, this does support a ()-returning form — declare -> sqlx::Result<()> to skip fetching the written rows back. An empty Vec short-circuits without
touching the database.
Same requirement as single-row upsert: #[unique(col)] only checks that col is a real field
on T at compile time, not that the database column is actually UNIQUE/PRIMARY KEY. See
upsert for what happens per-dialect if that constraint is missing — it's the same
failure mode here, just hit on the first batch with a real conflict.
Per-dialect SQL
Postgres/SQLite: built with sqlx::QueryBuilder::push_values, same as
insert_all:
INSERT INTO "table" (cols...) VALUES ($1,$2),($3,$4),...
ON CONFLICT (conflict_col) DO UPDATE SET other_col = EXCLUDED.other_col, ...
RETURNING *
Rebuilt per call (row count varies), so — unlike single-row upsert — there's no cached SQL
string here.
MySQL batch-inserts with ON DUPLICATE KEY UPDATE, then selects the written rows back by
WHERE {conflict_col} IN (v1, v2, ...):
INSERT INTO `table` (cols...) VALUES (?, ?),(?, ?),...
ON DUPLICATE KEY UPDATE col = VALUES(col), ...
-- then:
SELECT * FROM `table` WHERE {conflict_col} IN (?, ?, ...)
This differs from insert_all's MySQL strategy: insert_all
selects back by a contiguous auto-increment ID range, which only works because every row is a
fresh insert. A batch upsert mixes fresh inserts with in-place updates, so there's no such ID
range — but the conflict column's values are already known before the write (they're struct
fields on T, not DB-assigned), so selecting back by IN (...) on that column works regardless
of which rows were inserted vs. updated.