Writing
Create, update, upsert and delete rows, and how relations are enforced
Create
const user = await db.orm.user.create({ id: crypto.randomUUID(), name: "Alice", email: null });create() inserts one row and returns it. Fields with a @default can be left out, and so can nullable fields: an omitted nullable field is stored as null. Required fields must be set. create() fails if a row with the same primary key exists; use upsert() to update in that case.
createAll() inserts several rows in one transaction. If any row fails, none are written:
const todos = await db.orm.todo
.createAll([
{ id: crypto.randomUUID(), title: "One", done: false, userId },
{ id: crypto.randomUUID(), title: "Two", done: false, userId },
])
.toArray();Use createCount() when you only need the number of rows.
Update
update() changes the first row that matches the current filter, and returns the updated row, or null when nothing matched:
const todo = await db.orm.todo.where({ id: todoId }).update({ done: true });updateAll() changes every matching row in one transaction and returns them. updateCount() returns how many changed.
const done = await db.orm.todo.where({ userId }).updateAll({ done: true }).toArray();An update merges the patch into the stored row. Fields you leave out keep their values.
Upsert
upsert() updates the row that matches where, or creates one from create if none matches. The lookup and the write happen in one transaction.
const user = await db.orm.user.upsert({
where: { id: "user_1" },
create: { id: "user_1", name: "Alice", email: null },
update: { name: "Alice Smith" },
});Delete
delete() removes the row with a primary key. For a compound key, pass the values as an array:
await db.orm.todo.delete(todoId);deleteAll() removes every matching row and returns them. deleteCount() returns how many were removed.
const removed = await db.orm.todo.where({ done: true }).deleteAll().toArray();Write related rows
A relation field accepts a function that creates, connects or disconnects related rows. Everything runs in one transaction.
// Create a user and their first todos together.
await db.orm.user.create({
id: userId,
name: "Alice",
email: null,
todos: (todos) => todos.create([{ id: crypto.randomUUID(), title: "Unpack", done: false }]),
});
// Create a todo and point it at an existing user.
await db.orm.todo.create({
id: crypto.randomUUID(),
title: "Write docs",
done: false,
user: (user) => user.connect({ id: userId }),
});
// Detach a user's finished todos (their userId must be optional).
await db.orm.user.where({ id: userId }).update({
todos: (todos) => todos.disconnect([{ done: true }]),
});| Function | In create() | In update() | Does |
|---|---|---|---|
create(rows) | Yes | Yes | Inserts related rows, with their foreign keys filled in. |
connect(criteria) | Yes | Yes | On a to-one relation, points this row at an existing row, and fails if none matches. On a to-many relation, points every matching row at this one. |
disconnect(criteria?) | No | Yes | Sets the foreign key to null. With no criteria, disconnects every related row. |
Related rows can't have relation functions of their own: nesting is one level deep.
Foreign keys and referential actions
IndexedDB has no foreign keys, so the client enforces them the way a SQL database does. Every check and action runs in the write's transaction. If any of them fails, the whole write is rolled back.
Foreign keys are checked on every write
When a write sets a foreign key, the client checks that the parent row exists:
await db.orm.todo.create({ id: "t1", title: "Orphan", done: false, userId: "nobody" });
// Error: FK violation on relation 'user': no User with id='nobody'This applies to create, createAll, update, updateAll, both branches of upsert, and every row a relation function writes. It also applies to a foreign key filled in by a @default.
- A compound foreign key is checked as a whole. One parent row must match every field.
- A foreign key with any
nullfield isn't checked, as in SQL.
What happens to children when a parent changes
When you delete a row, or update a field that other rows refer to, each relation's onDelete or onUpdate action decides what happens to the rows that point at it:
| Action | Children |
|---|---|
Restrict (default) | The write fails while any child exists. |
NoAction | Same as Restrict. |
Cascade | Deleted too, or updated to the new value. Continues to their own children. |
SetNull | Their foreign key is set to null. The field must be optional. |
SetDefault | Their foreign key is set to its @default value, which must point at a row that exists. |
Declare the actions on the relation in your schema:
model Todo {
id String @id
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
}Without them, deleting a user who still has todos fails:
Cannot delete User 'user_1': child records exist on relation 'todos'. Delete those children first, or declare onDelete: Cascade, SetNull or SetDefault on the relation.Both actions default to Restrict, matching Postgres, where an undeclared action is NO ACTION. So a synced app's client and server refuse the same changes. An update only triggers onUpdate when a referenced value actually changes.