Cerial

Delete Behavior (@onDelete)

Controlling what happens to related records when a referenced record is deleted

When you delete a record that other records reference via foreign keys, Cerial needs to decide what to do with those dependent records. Should they be deleted too? Should their FK be cleared? Should the deletion be blocked entirely?

Cerial handles this automatically based on the FK field type, and you can customize the behavior with the @onDelete decorator.

Default Behavior

The default action depends on the FK field's type and modifiers:

FK TypeDefault BehaviorConfigurable?
Required (Record)Cascade — deletes dependent recordsNo (always cascades)
Optional (Record?)SetNone — removes FK field (NONE)Yes, via @onDelete
Optional + nullable (Record? @nullable)SetNull — sets FK to nullYes, via @onDelete
Array (Record[])Auto cleanup — removes ID from arraysAutomatic

You don't need @onDelete for most cases — the defaults match the most common behavior for each FK type. Only add @onDelete when you want to override the default.

Required Relations (Auto-Cascade)

When a FK field is required (Record without ?), deleting the referenced record always cascades to delete all direct dependents. The @onDelete decorator is not allowed on required relations — they always cascade.

model User {
  id Record @id
  name String
  posts Relation[] @model(Post)
}

model Post {
  id Record @id
  title String
  authorId Record
  author Relation @field(authorId) @model(User)
}
// Deleting a user cascades to all their posts
await client.db.User.deleteMany({ where: { id: userId } });
// All Posts where authorId = userId are also deleted

The logic is straightforward: if a Post requires an author, it cannot exist without one. Deleting the author must delete the post.

Cascade depth

Cascade processes the direct dependents of the deleted records. If those deleted dependents have their own required relations, Cerial generates cascade statements for each level it can see during query building. However, this is not unlimited recursive traversal — Cerial processes each level of the dependency chain within a single transaction.

@onDelete Options

The @onDelete decorator overrides the default behavior for optional and nullable relations. There are five strategies:

Cascade

Deletes the dependent record when the referenced record is deleted.

model Profile {
  id Record @id
  bio String?
  userId Record?
  user Relation? @field(userId) @model(User) @onDelete(Cascade)
}
await client.db.User.deleteMany({ where: { id: userId } });
// All Profiles where userId = userId are DELETED

Use Cascade when the dependent record has no meaning without its parent — even though the FK is technically optional.

SetNull

Sets the FK to null on the dependent record. The record itself is preserved, and the FK field remains present but holds a null value. This is the default for optional relations where the FK has @nullable.

model Post {
  id Record @id
  title String
  authorId Record? @nullable
  author Relation? @field(authorId) @model(User) @onDelete(SetNull)
}
await client.db.User.deleteMany({ where: { id: userId } });
// All Posts where authorId = userId have authorId set to null
// The Posts themselves are preserved

Since SetNull is the default for @nullable FK fields, these two declarations are equivalent:

author Relation? @field(authorId) @model(User) @onDelete(SetNull)
# When authorId has @nullable, SetNull is the default
author Relation? @field(authorId) @model(User)

@onDelete(SetNull) requires the backing Record field to have @nullable. Without @nullable, the field cannot hold null — use SetNone instead.

SetNone

Removes the FK field entirely (sets to NONE) when the referenced record is deleted. The record is preserved, but the FK field becomes absent. This is the default for optional relations where the FK does not have @nullable.

model Post {
  id Record @id
  title String
  authorId Record?
  author Relation? @field(authorId) @model(User) @onDelete(SetNone)
}
await client.db.User.deleteMany({ where: { id: userId } });
// All Posts where authorId = userId have authorId removed (NONE)
// The Posts themselves are preserved

@onDelete(SetNone) requires the backing Record field to be optional (?). Without ?, the field cannot be absent.

Restrict

Prevents deletion of the referenced record if any dependents exist. The delete operation throws an error instead of proceeding.

model Order {
  id Record @id
  total Float
  customerId Record?
  customer Relation? @field(customerId) @model(Customer) @onDelete(Restrict)
}
// If the customer has any orders, this throws an error
await client.db.Customer.deleteMany({ where: { id: customerId } });
// Error: Cannot delete Customer because related Order records exist

Use Restrict to protect referential integrity when dependents should never become orphaned.

NoAction

Leaves the FK as-is, creating a dangling reference. The dependent record will have an FK pointing to a non-existent record.

model AuditLog {
  id Record @id
  action String
  actorId Record?
  actor Relation? @field(actorId) @model(User) @onDelete(NoAction)
}
await client.db.User.deleteMany({ where: { id: userId } });
// AuditLog records still have actorId = userId (dangling reference)
// Querying the actor relation will return null

Use NoAction sparingly — it creates dangling references that can cause unexpected null results when querying relations. It's appropriate for audit logs or historical records where you intentionally want to preserve the original reference even after the target is gone.

Array Relations (Auto Cleanup)

When a record is deleted, its ID is automatically removed from all Record[] arrays that reference it. This happens regardless of any @onDelete setting — array cleanup is always automatic.

model User {
  id Record @id
  tagIds Record[]
  tags Relation[] @field(tagIds) @model(Tag)
}

model Tag {
  id Record @id
  userIds Record[]
  users Relation[] @field(userIds) @model(User)
}
// Delete a tag
await client.db.Tag.deleteMany({ where: { name: 'deprecated' } });
// All Users who had this tag in their tagIds array
// will have the tag's ID removed automatically

For N

relations, both sides are cleaned up atomically — the deleted record's ID is removed from all arrays on both sides.

@onDelete is not allowed on array relations (Relation[]). Array cleanup is always automatic, so there's nothing to configure.

Cascade Chains

When multiple models form a chain of required relations, cascade operations propagate through the chain within a single transaction:

model Organization {
  id Record @id
  name String
  teams Relation[] @model(Team)
}

model Team {
  id Record @id
  name String
  orgId Record
  org Relation @field(orgId) @model(Organization)
  members Relation[] @model(Member)
}

model Member {
  id Record @id
  name String
  teamId Record
  team Relation @field(teamId) @model(Team)
}
// Deleting an Organization cascades through the chain:
await client.db.Organization.deleteMany({ where: { id: orgId } });
// 1. All Teams where orgId = orgId are deleted
// 2. All Members where teamId = (any deleted team) are deleted

Cerial generates the appropriate cascade statements for each level of the dependency chain, and they execute within a single transaction to ensure atomicity.

Rules and Restrictions

Where @onDelete is allowed

@onDelete is only valid on forward relations (those with @field) that are optional (?) or nullable (@nullable):

# ✅ Valid — optional forward relation
author Relation? @field(authorId) @model(User) @onDelete(Cascade)

# ✅ Valid — nullable (even without ?) forward relation
author Relation @nullable @field(authorId) @model(User) @onDelete(SetNull)

Where @onDelete is NOT allowed

# ❌ Required relations — always auto-cascade
author Relation @field(authorId) @model(User) @onDelete(Cascade)

# ❌ Reverse relations — no @field, so no FK to manage
posts Relation[] @model(Post) @onDelete(Cascade)

# ❌ Array relations — auto-cleanup is always automatic
tags Relation[] @field(tagIds) @model(Tag) @onDelete(Cascade)

Strategy-specific requirements

StrategyRequirement on backing Record field
SetNullMust have @nullable
SetNoneMust be optional (?)
CascadeNo special requirements
RestrictNo special requirements
NoActionNo special requirements

Transaction Guarantees

All delete operations — including cascades, SetNull/SetNone updates, Restrict checks, and array cleanup — execute within a transaction. This ensures:

  • Either all cascade operations complete, or none do
  • Restrict checks happen before any deletions
  • Array cleanup is atomic with the delete
  • No partial state if any operation fails

Delete operations with cascade behavior also work inside $transaction, covered by a single atomic transaction.

Summary

StrategyBehaviorDefault forUse case
(required FK)Cascade (always)Record (required)Child can't exist without parent
@onDelete(SetNone)Remove FK (NONE)Record? (optional)Preserve child, clear the reference
@onDelete(SetNull)Set FK to nullRecord? @nullablePreserve child, keep queryable null
@onDelete(Cascade)Delete dependents(must be explicit)Optional FK but child is meaningless without parent
@onDelete(Restrict)Block deletion(must be explicit)Protect referential integrity
@onDelete(NoAction)Leave dangling ref(must be explicit)Audit logs, historical records
(array FK)Remove ID from arrayRecord[] (always)Automatic for all array relations

On this page