Prisma IDB FaviconPrisma IDB

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();

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 }]),
});
FunctionIn create()In update()Does
create(rows)YesYesInserts related rows, with their foreign keys filled in.
connect(criteria)YesYesOn 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?)NoYesSets 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 null field 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:

ActionChildren
Restrict (default)The write fails while any child exists.
NoActionSame as Restrict.
CascadeDeleted too, or updated to the new value. Continues to their own children.
SetNullTheir foreign key is set to null. The field must be optional.
SetDefaultTheir 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.

On this page