Cerial

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 Record storage field paired with a Relation field using @field(recordField) and @model(TargetModel).
  • Reverse relation (non-PK side) — The model that is referenced by the foreign key. It has a Relation field with only @model(SourceModel) — no @field decorator. 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.authorId is the storage field — a Record reference stored in SurrealDB.
  • Post.author is the forward relation — a virtual field that resolves authorId into a full User object when included.
  • User.posts is the reverse relation — a virtual field that queries the post table for records where authorId matches the user's ID.

Only authorId is persisted to the database. Both author and posts are resolved at query time.

Relation Types

TypePK SideNon-PK Side
One-to-OneRecord + Relation @fieldRelation @model
One-to-ManyRecord + Relation @fieldRelation[] @model
Many-to-ManyRecord[] + Relation[] @field (both sides)Both sides are PK

Side Capabilities

Both sides of a relation support a rich set of operations:

CapabilityPK Side (Forward)Non-PK Side (Reverse)
Stores FKYes (Record field)No (resolved at query time)
Nested create
Connect
Disconnect
Set (replace all)✓ (array relations)
Include
Optional to defineRequired for FK storageOptional

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 TypeDefault BehaviorConfigurable
Required (Record)Cascade — deletes relatedNo (always cascade)
Optional (Record?)SetNone — clears FKYes, via @onDelete
Optional + @nullableSetNull — sets FK to nullYes, via @onDelete
Array (Record[])Auto-cleanup — removes IDNo (automatic)

See Delete Behavior for all @onDelete options including Cascade, SetNull, SetNone, Restrict, and NoAction.

Sections

On this page