Overview
1:1, 1:N, and N:N relations with nested operations and cascade behavior.
Cerial provides a relation system that maps SurrealDB record references to fully typed, queryable relationships between models. Relations are defined using Relation fields with @field() and @model() decorators in your .cerial schema files.
How Relations Work
Every relation in Cerial has two conceptual sides:
- Forward relation (PK side) — The model that stores the foreign key. It has a
Recordstorage field paired with aRelationfield using@field(recordField)and@model(TargetModel). - Reverse relation (non-PK side) — The model that is referenced by the foreign key. It has a
Relationfield with only@model(SourceModel)— no@fielddecorator. These are resolved at query time by looking up the source table.
The forward relation owns the data. The reverse relation is a convenience for querying in the opposite direction and is optional to define.
Anatomy of a Relation
model Post {
id Record @id
title String
authorId Record # FK storage field (persisted to DB)
author Relation @field(authorId) @model(User) # Forward relation (virtual)
}
model User {
id Record @id
name String
posts Relation[] @model(Post) # Reverse relation (virtual)
}In this example:
Post.authorIdis the storage field — aRecordreference stored in SurrealDB.Post.authoris the forward relation — a virtual field that resolvesauthorIdinto a fullUserobject when included.User.postsis the reverse relation — a virtual field that queries theposttable for records whereauthorIdmatches the user's ID.
Only authorId is persisted to the database. Both author and posts are resolved at query time.
Relation Types
| Type | PK Side | Non-PK Side |
|---|---|---|
| One-to-One | Record + Relation @field | Relation @model |
| One-to-Many | Record + Relation @field | Relation[] @model |
| Many-to-Many | Record[] + Relation[] @field (both sides) | Both sides are PK |
Side Capabilities
Both sides of a relation support a rich set of operations:
| Capability | PK Side (Forward) | Non-PK Side (Reverse) |
|---|---|---|
| Stores FK | Yes (Record field) | No (resolved at query time) |
| Nested create | ✓ | ✓ |
| Connect | ✓ | ✓ |
| Disconnect | ✓ | ✓ |
| Set (replace all) | ✓ (array relations) | — |
| Include | ✓ | ✓ |
| Optional to define | Required for FK storage | Optional |
Include options
The include option supports orderBy, limit, and offset for controlling included results. While select and where appear in the TypeScript types for future use, they are not processed at runtime — included relations always return full objects.
Nested Operations
Relations support creating and linking records inline, without separate queries.
Nested Create
Create related records as part of a parent operation:
const user = await client.db.User.create({
data: {
name: 'Alice',
posts: {
create: [{ title: 'First Post' }, { title: 'Second Post' }],
},
},
});See Nested Create for full details.
Connect and Disconnect
Link existing records or remove links:
// Connect an existing tag
await client.db.User.updateMany({
where: { id: userId },
data: { tags: { connect: [tagId] } },
});
// Disconnect a tag
await client.db.User.updateMany({
where: { id: userId },
data: { tags: { disconnect: [tagId] } },
});See Connect & Disconnect for full details.
Set (Array Relations)
The set operation replaces the entire contents of an array FK field:
// Replace all tags with a new set
await client.db.User.updateMany({
where: { id: userId },
data: { tags: { set: [tagId1, tagId2] } },
});This removes all existing items and sets the array to exactly the provided IDs. For N
relations, the reverse side is synced automatically.The @key Decorator
When a model has multiple relations to the same target model, Cerial needs a way to pair forward and reverse relations correctly. The @key decorator provides this disambiguation:
model Document {
id Record @id
title String
authorId Record
author Relation @field(authorId) @model(User) @key(author)
reviewerId Record?
reviewer Relation? @field(reviewerId) @model(User) @key(reviewer)
}
model User {
id Record @id
name String
authored Relation[] @model(Document) @key(author)
reviewing Relation[] @model(Document) @key(reviewer)
}The @key value must match between the forward and reverse sides of each pair. See Multiple Relations and Self-Referential for detailed usage.
The @readonly Interaction
When a PK Record field has @readonly, the relation's nested update operations (connect, disconnect, set) are excluded from the UpdateInput type. The FK can be set on create but cannot be changed afterward:
model Post {
id Record @id
title String
authorId Record @readonly
author Relation @field(authorId) @model(User)
}// ✓ Works — set on create
const post = await client.db.Post.create({
data: { title: 'Hello', author: { connect: userId } },
});
// ✗ Type error — cannot update a @readonly relation
await client.db.Post.updateMany({
where: { id: post.id },
data: { author: { connect: otherUserId } }, // compile error
});Delete Behavior
How related records are handled when a parent is deleted depends on whether the FK is required, optional, or nullable:
| FK Type | Default Behavior | Configurable |
|---|---|---|
Required (Record) | Cascade — deletes related | No (always cascade) |
Optional (Record?) | SetNone — clears FK | Yes, via @onDelete |
Optional + @nullable | SetNull — sets FK to null | Yes, via @onDelete |
Array (Record[]) | Auto-cleanup — removes ID | No (automatic) |
See Delete Behavior for all @onDelete options including Cascade, SetNull, SetNone, Restrict, and NoAction.
Sections
- One-to-One — Single record on each side
- One-to-Many — One parent, many children
- Many-to-Many — Bidirectional arrays with atomic sync
- Self-Referential — Models that reference themselves
- Single-Sided Relations — Forward-only relations with no reverse
- Multiple Relations — Multiple relations between the same models using
@key - Nested Create — Creating related records inline
- Connect & Disconnect — Linking and unlinking existing records
- Delete Behavior — Cascade, SetNull, SetNone, Restrict, and NoAction