Update
Update rows with Drizzle-style builders
Basic Update
import { z } from 'zod';
import { eq } from 'kitcn/orm';
import { publicMutation } from '../lib/crpc';
import { users } from '../schema';
export const renameUser = publicMutation
.input(z.object({ userId: z.string(), name: z.string() }))
.mutation(async ({ ctx, input }) => {
await ctx.orm
.update(users)
.set({ name: input.name })
.where(eq(users.id, input.userId));
});Important: update() without .where(...) throws unless you call .allowFullScan(). See Querying Data for details on allowFullScan.
Returning
Use .returning() to get back the updated rows. You can return all fields or pick specific columns:
const updated = await ctx.orm
.update(users)
.set({ name: 'Mr. Dan' })
.where(eq(users.id, userId))
.returning();
const ids = await ctx.orm
.update(users)
.set({ name: 'Mr. Dan' })
.where(eq(users.id, userId))
.returning({ id: users.id });The ORM collects matching rows in bounded pages before applying writes. See Mutations for safety limit defaults and override syntax.
Paginated Update Execution
For large workloads that exceed safety limits, you can process updates page-by-page. This follows Convex's batching pattern and avoids one large transaction.
Process updates across multiple pages (requires an index on the filtered field):
// Schema: index('by_role').on(t.role) on users tableconst page1 = await ctx.orm
.update(users)
.set({ role: 'member' })
.where(eq(users.role, 'pending'))
.paginate({ cursor: null, limit: 100 });
if (!page1.isDone) {
const page2 = await ctx.orm
.update(users)
.set({ role: 'member' })
.where(eq(users.role, 'pending'))
.paginate({ cursor: page1.continueCursor, limit: 100 });
}Each page returns:
continueCursor-- cursor for the next batchisDone--truewhen no more pages remainnumAffected-- rows updated in this pagepage-- returned rows (only when.returning()is used)
Note: paginate() currently supports single-range index plans. Multi-probe filters (inArray, some OR patterns, complement ranges) are not yet supported in paged mutation mode.
Async Batched Update
Updates run in async mode by default. The first batch runs in the current mutation, then remaining batches are scheduled automatically.
Customize batch size and delay per call:
const firstBatch = await ctx.orm
.update(users)
.set({ role: 'member' })
.where(eq(users.role, 'pending'))
.returning({ id: users.id })
.execute({ batchSize: 200, delayMs: 0 });See API Reference -- Async Batched Update Behaviors for resolution precedence details.
To force sync execution (all rows in a single transaction), use .execute({ mode: 'sync' }) or set defineSchema(..., { defaults: { mutationExecutionMode: 'sync' } }).
Drizzle Differences
A few SQL-only features from Drizzle are not applicable in Convex:
limit,orderBy,UPDATE ... FROM, andWITHclauses are not supportedundefinedvalues passed to.set(...)are ignored (treated as "not provided"). If everything isundefined, the update is a no-op.- to explicitly remove a field, use
unsetToken:.set({ nickname: unsetToken })(shallow: unsets the top-level field only)
Note: Unique constraints, foreign keys, and RLS policies are enforced at runtime for ORM mutations. Direct native Convex writes like ctx.db.patch(...) bypass these checks (and are intentionally not exposed on ctx.orm).
API Reference
Async Batched Update Behaviors
- With
.returning(), you get rows from the first batch only — remaining batches are scheduled - Async mode cannot be combined with
.paginate() batchSizeresolves as: per-callbatchSize>defaults.mutationBatchSize>400delayMsresolves as: per-calldelayMs>defaults.mutationAsyncDelayMs>0- Async FK update fan-out (
onUpdate: 'cascade',set null,set default) usesmutationLeafBatchSize