NONE vs null
How SurrealDB's three-state field model (value, NONE, null) maps to TypeScript types, query operators, and update behavior.
SurrealDB distinguishes between three field states:
- Value — the field exists and has a typed value
- NONE — the field doesn't exist at all (absent from the record)
- null — the field exists and its value is explicitly
null
Cerial models these states using two independent schema modifiers:
?(optional) — enables NONE. Maps toundefinedin TypeScript.@nullable— enables null. Maps to| nullin TypeScript.
These modifiers are independent. You can use either one, both, or neither. The combination determines what states a field can hold.
The Three-State Model
| Schema | TypeScript Output | Allowed States | Migration |
|---|---|---|---|
name String | name: string | value | TYPE string |
bio String? | bio?: string | value, NONE | TYPE option<string> |
bio String @nullable | bio: string | null | value, null | TYPE string | null |
bio String? @nullable | bio?: string | null | value, null, NONE | TYPE option<string | null> |
Key observations:
?without@nullable— the field can be absent (NONE) but cannot be null. Passingnullis a validation error.@nullablewithout?— the field must always be present but can be null. The field is required on create (you must pass a value ornull).- Both
?and@nullable— the field can be any of: a typed value,null, or absent (NONE).
Runtime Behavior
Optional-Only (String?)
// Schema: bio String?
await db.User.create({ data: { name: 'Alice' } });
// bio is NONE (field absent in DB)
await db.User.create({ data: { name: 'Bob', bio: 'Hello' } });
// bio = 'Hello'
await db.User.create({ data: { name: 'Carol', bio: null } });
// Error: bio is not nullable — use undefined/omit to unsetThe field either has a string value or doesn't exist. There's no middle ground.
Nullable-Only (String @nullable)
// Schema: deletedAt Date @nullable
await db.User.create({ data: { name: 'Alice', deletedAt: null } });
// deletedAt = null (stored as null)
await db.User.create({ data: { name: 'Bob', deletedAt: new Date() } });
// deletedAt = current date
await db.User.create({ data: { name: 'Carol' } });
// Error: deletedAt is required (must be a Date or null)The field is always present. It holds either a typed value or null.
Optional + Nullable (String? @nullable)
// Schema: bio String? @nullable
await db.User.create({ data: { name: 'Alice' } });
// bio is NONE (field absent)
await db.User.create({ data: { name: 'Bob', bio: null } });
// bio = null (explicit null stored)
await db.User.create({ data: { name: 'Carol', bio: 'Hello' } });
// bio = 'Hello'All three states are available. This gives maximum flexibility when you need to distinguish "never set" from "explicitly cleared."
Optional Record Fields
// Schema: authorId Record? @nullable
await db.Post.create({ data: { title: 'Draft' } });
// authorId is NONE (field absent — no FK stored)
await db.Post.create({ data: { title: 'Orphan', authorId: null } });
// authorId = null (explicit null — "unassigned")
await db.Post.create({ data: { title: 'Post', authorId: userId } });
// authorId = record reference to userWithout @nullable, Record? only supports value or NONE. Passing null is rejected.
Query Operators
Cerial provides specific operators based on the field modifiers. These operators are only available on fields that support the corresponding state.
| Operator | Available On | Description |
|---|---|---|
isNull: true | @nullable fields | Field is null |
isNull: false | @nullable fields | Field is not null |
isNone: true | ? (optional) fields | Field is absent |
isNone: false | ? (optional) fields | Field is present |
isDefined: true | ? (optional) fields | Alias for isNone: false |
isDefined: false | ? (optional) fields | Alias for isNone: true |
See the special operators page for more details.
Finding null Values
// Only on @nullable fields
await db.User.findMany({
where: { deletedAt: { isNull: true } },
});Finding Absent Fields (NONE)
// Only on optional (?) fields
await db.User.findMany({
where: { bio: { isNone: true } },
});Combining on Optional + Nullable Fields
// Schema: bio String? @nullable — has all three operators
// Find users where bio doesn't exist at all
await db.User.findMany({ where: { bio: { isNone: true } } });
// Find users where bio is explicitly null
await db.User.findMany({ where: { bio: { isNull: true } } });
// Find users where bio has an actual value (not null, not NONE)
await db.User.findMany({
where: { bio: { isNone: false, isNull: false } },
});Update Behavior
Setting a Field to null
Only works on @nullable fields. Sets the field value to null in the database:
// Schema: bio String? @nullable
await db.User.updateMany({
where: { id: userId },
data: { bio: null },
});Removing a Field (NONE)
Only works on optional (?) fields. You can either use the NONE sentinel in data or the unset parameter:
import { NONE } from 'cerial';
// Option 1: NONE sentinel in data
await db.User.updateMany({
where: { id: userId },
data: { bio: NONE },
});
// Option 2: unset parameter (cleaner for multiple fields)
await db.User.updateMany({
where: { id: userId },
data: {},
unset: { bio: true },
});The unset parameter also supports nested object fields and tuple elements. See the updateMany, updateUnique, and upsert docs for details.
Disconnecting Optional Relations
How disconnect works depends on @nullable:
// Record? (no @nullable) — disconnect sets FK to NONE
await db.Post.updateUnique({
where: { id: postId },
data: { author: { disconnect: true } },
});
// FK becomes absent (NONE)
// Record? @nullable — disconnect sets FK to NULL
await db.Post.updateUnique({
where: { id: postId },
data: { author: { disconnect: true } },
});
// FK becomes nullThe distinction matters for queries: after a NONE disconnect, isNone: true matches. After a NULL disconnect, isNull: true matches.
The @default(null) Pattern
@default(null) requires @nullable on the field. It converts omitted values to null on create:
model User {
id Record @id
bio String? @nullable @default(null)
}// Without @default(null): omitting = NONE
// With @default(null): omitting = null (default applied)
await db.User.create({ data: { name: 'Alice' } });
// bio = null (not NONE)This is useful when you want omitted fields to be queryable with isNull: true instead of isNone: true.
Practical Guidelines
-
Use
?alone when the field is truly optional. It either has a value or doesn't exist. Most optional fields fall here. -
Use
@nullablealone for soft-delete patterns.deletedAt Date @nullableis required but can be null (not deleted) or a Date (deleted at timestamp). -
Use
? @nullablefor FK fields that need null queries.authorId Record? @nullablelets you query{ isNull: true }to find unassigned records, and{ isNone: true }to find records where the field was never set. -
Use
@default(null)with@nullableto ensure optional fields are always queryable by null instead of being absent.