updateMany
Update every record matching a filter
Update every record matching the filter, in one call:
const result = await client.user.updateMany({
where: { role: "GUEST" },
data: { role: "USER" },
});
// Result: { count: 42 }
updateMany returns a { count }. where is optional — omit it to update every row.
Examples
// Archive old drafts
await client.post.updateMany({
where: {
createdAt: { lt: new Date("2023-01-01") },
published: false,
},
data: { archived: true },
});
// Apply a discount with an atomic update
await client.product.updateMany({
where: { category: "electronics" },
data: {
price: { multiply: 0.9 }, // 10% off
},
});
Writing relations
data takes the same relation surface a single update
takes — connect, disconnect, create, update, upsert, set, and the
rest — applied to every matching row:
// Move every guest onto the free plan, and log it on each one
await client.user.updateMany({
where: { role: "GUEST" },
data: {
role: "USER",
plan: { connect: { name: "free" } }, // parent-held: each row gets its own
events: { create: { kind: "downgraded" } }, // one fresh child per row
},
});
Three things change when data carries a relation, and all three are
consequences of the same fact — the call becomes one ordinary update per
matching row, run in deterministic primary-key order:
countis the number of matching rows, not the database’s affected-row total. (Scalar-onlyupdateManystill reports the provider’s number, which on MySQL is zero for an assignment that changes nothing.)- Transaction-capable drivers roll everything back together. If the fifth row’s update fails, the first four are undone.
- Drivers with native atomic batches execute safe root members as ordered segments. Without an interactive transaction, each normalized batch success makes the next member visible. If a later member fails, earlier segments can remain committed and the error reports committed or possibly committed progress. A driver’s strong committed-segment capability adds callback-before-decode attribution; it is not required for this route. See D1 transactions and batching for one provider-specific example.
- A driver with neither interactive transactions nor native atomic batches rejects
this relation-bearing form. Scalar-only
updateManystill uses its grouped statement there. Explicit$transaction([...])arrays remain indivisible and do not use progressive execution.
Getting the rows back
Add a select and updateMany returns the updated rows instead of a count. The
return type follows: with select it is an array of the projected rows, without
it, { count }.
const updated = await client.post.updateMany({
where: { published: false },
data: { published: true },
select: { id: true, title: true },
});
// Type: { id: string; title: string }[]
The rows are read after the update, so an atomic operation on the primary key returns the new identity, not the old one.
When data carries a relation, that read happens after every row has finished,
by each row’s final primary key, in the captured order. VibORM groups the final
lookup into bounded set queries, normally one query; large inputs are split only
to respect the driver’s parameter limit. If one of those rows is gone by then —
a later row’s nested effects deleted it, or moved its key — the call fails
rather than hand back a shorter list, because a silently shorter answer would
depend on nothing but capture order. The { count } form of the same payload
answers the captured row count and succeeds.
Capping how many rows are updated
limit caps the number of rows the update affects. The count you get back is
min(matching, limit):
// Retry at most 100 stuck jobs per tick
const result = await client.job.updateMany({
where: { status: "STUCK" },
data: { status: "PENDING" },
limit: 100,
});
// Result: { count: 100 } if at least 100 matched, fewer otherwise
limit: 0 is legal and means “update nothing”: you get { count: 0 } and no
write statement is sent at all. A negative or fractional limit is a validation
error.
limit also caps the returning form — select gives you back exactly the rows
that were updated, so at most limit of them.
What it costs
Scalar-only updateMany is unchanged: one UPDATE statement, or none at all
for limit: 0. A limit: 0 call stays on that path even when data carries a
relation — a cap of no rows writes nothing, so it needs no transaction and works
on every driver.
When data carries a relation the call issues one SELECT of the matching
primary keys, evaluating your where and limit once and locking where the
database supports it. An interactive driver keeps the capture and members in
one transaction. A no-transaction driver with native batching commits one
atomic batch per safe executable segment. Per matched row, the cost is exactly
what that row would have cost as a single
update: the same target probes and child writes, in the
same order. Ten matched rows with a connect issue ten probes. Adding a
select adds one or more bounded set reads at the end, normally one. Narrow the
where (or use limit) when the row count is large; this form is per-row work
by design, and it is the price of the relation semantics being the same ones a
single update gives you.
Options
await client.user.updateMany({
where: { ... }, // Optional: filter (all rows if empty)
data: { ... }, // Required: fields to update, and relations to write
limit: 100, // Optional: cap on how many rows are updated
select: { ... }, // Optional (scalar fields only): return the updated rows
// instead of { count }
});