Modifiers
The .forge field modifiers — + auto-generate, & unique, ^ indexed, and ? nullable — with placement and type rules.
Modifiers are sigils on a field's type. Three are prefix modifiers (+, &,
^) placed before the type name; one is the nullable modifier (?) placed after.
| Symbol | Name | Position | Meaning | Valid on |
|---|---|---|---|---|
+ | auto-generate | prefix | Fill on create when omitted (all four types) | u32, u64, uuid, timestamp only |
& | unique | prefix | Value must be unique (enforced) | any type |
^ | indexed | prefix | Build an index for fast lookups | any type |
? | nullable | postfix (or prefix) | Value may be absent | any type |
+ — auto-generate#
+ marks a field ForgeDB fills for you. On create (db.create_<model>, and the REST
POST /api/<model> that routes through it), a field left unset gets a generated value, so
callers never supply it. Valid only on u32, u64, uuid, and timestamp:
id: +u64 // next number in the sequence when omitted/0
ref_id: +uuid // fresh UUID on create when omitted/nil
created_at: +timestamp // current time on create when omitted/zeroEach type has its own "unset" sentinel: a nil UUID, a zero timestamp, and — for
integers — 0.
A `+timestamp` is a stamp unless it is named `id`
Under any name but id, + on a timestamp means "stamp it now" — the value is now(),
floored to the field's declared precision. Named id it becomes an allocated identity and
must be declared us (id: +timestamp(us)), because uniqueness comes from the monotonic
allocator rather than from the clock. This is deliberately asymmetric with +u32/+u64: the
only reason to write an auto integer is to get an allocated sequence, so an auto integer is
unambiguously key-ish. See scalar types.
`+` is type-restricted
+ on any other type — e.g. name: +string — is a fatal parse error. Only the four
generatable types above are allowed.
`0` is the allocate sentinel
You cannot insert 0 explicitly into a +u32/+u64 field — supplying it means "allocate one
for me". Any other value is honoured verbatim and advances the counter past it, so
restoring a backup or importing existing rows does not collide on the next insert.
Integer sequences are monotonic, not gapless
A rolled-back transaction — or an attempt the multi-process commit coordinator rejects — burns
its number. +u32/+u64 guarantee values that are unique and increasing, not contiguous.
This is the same contract Postgres and MySQL offer; do not use the id as a count.
The sequence also refuses to wrap: a +u32 that reaches u32::MAX fails the create rather
than rolling back to 0 and colliding with every id already issued.
Every integer-auto shape is safe across writer processes
The counter lives in each writer process, so two processes coordinated through
forgedb coordinate can allocate the same number. What makes that detected instead of
silent is the write-set the coordinator compares — and every shape puts a claim there: the
identity field via its row key, a &unique field via its unique claim, and a bare field via
its own sequence claim.
Invoice {
id: +uuid
number: +u64 // bare integer auto — claims its value, no `&` needed
}Adding & is still meaningful when you want uniqueness enforced against every row ever
written: that index is durable, while a sequence claim only detects collisions for as long
as the current coordinator process has been running.
& — unique#
& marks a column whose values must be unique across all rows. Uniqueness is enforced:
on insert/update the generated code probes the field's index and rejects a duplicate
(HTTP 409). Applies to any type.
email: &string @email
slug: &string& builds an index too, so a unique field is also fast to look up.
^ — indexed#
^ builds a secondary index on the field for fast find_by_* lookups and for
index-served REST filters. Applies to any indexable type (not json).
slug: ^string
views: ^u64
status: ^OrderStatusCombine & and ^ freely; since & already indexes, ^& mainly documents intent.
See indexes & projections for hash-vs-ordered
index behavior.
Combining prefix modifiers#
Any combination of +, &, ^ is allowed, in any order, all before the type —
subject to the rule that + is valid only on u32/u64/uuid/timestamp:
ref_id: &+uuid // auto-generated, and unique across rows
seen_at: ^+timestamp // auto-generated, and indexed
email: ^&string @email // indexed + unique`&`/`^` on the identity field does nothing
Your model's identity — the field named id, or the first + field if there is
no id — is already unique: the generated code keys its primary map by it. So &
and ^ on that field have no effect, no secondary index is built, and validation
warns and suggests dropping the modifier.
id: &+uuid // ⚠ '&' has no effect — id is already the primary key
id: +uuid // ✓The schema is still valid — this is advisory, not an error. And it applies only
to the identity: on any other auto field, & and ^ are fully enforced
(that is what ref_id/seen_at above rely on).
? — nullable#
? makes a field optional (may be absent / NULL). It is a postfix modifier, placed
after the type, and this is the idiomatic form. A prefix form also parses:
bio: string? // nullable string (idiomatic)
age: i32? // nullable int
avatar: ?string // prefix form — also parses
published_at: timestamp?? composes with the prefix modifiers:
external_id: +uuid? // auto-generate and nullableOn a relation, prefix ? declares an optional foreign key
(editor: ?User). See relations.
Nullable storage keeps absent distinct from zero
A nullable fixed-width field carries a 1-byte presence tag, so None and a stored 0
(or Value::Null, or an empty string) round-trip as different values.