Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

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:

  • count is the number of matching rows, not the database’s affected-row total. (Scalar-only updateMany still 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 updateMany still 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 }
});

Was this page helpful?