Skip to main content

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.