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.

SymbolNamePositionMeaningValid on
+auto-generateprefixFill on create when omitted (all four types)u32, u64, uuid, timestamp only
&uniqueprefixValue must be unique (enforced)any type
^indexedprefixBuild an index for fast lookupsany type
?nullablepostfix (or prefix)Value may be absentany 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/zero

Each 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: ^OrderStatus

Combine & 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 nullable

On 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.

Search documentation

Find pages across the ForgeDB docs