Models & fields
Model and field syntax in .forge — the field form, modifier positions, and the parser-enforced declaration rules.
A model is an entity that becomes a stored table. Declare it by name — no keyword — followed by a block of fields.
Model syntax#
ModelName {
field: type
field: type
}Rules the parser enforces (fatal on violation):
- The model name must be PascalCase —
User,OrderItem.userororder_itemfail to parse. - A model must contain at least one field. An empty
User { }is rejected. - Model names must be unique within the schema.
- A model must have an identity field — one named
id, or any field with the+auto-generate modifier. The convention isid: +uuid.
Identity is mandatory
A model without an identity field is rejected by forgedb validate and
forgedb generate. Identity is what create_* writes, what the row index is
keyed on, what relations point at, and what the record-level REST routes take as
their path parameter — there is almost no generated surface left without it.
User {
id: +uuid
email: &string @email
created_at: +timestamp
}The identity field#
A field named id is the identity. If there is no id, the first field carrying +
is. Name beats position, so id wins even over a + field declared above it:
Event {
seq: +u64 // allocated on every insert, but a column, not the key
id: u32 // the identity: what create returns, what get() takes
note: string
}An identity has to be fixed-width, Copy, hashable, and totally ordered. It lives in
the row index, in every foreign-key column pointing at the model, in a many-to-many
junction map, and in a fixed-width replication frame. That rules most types out, and
the admitted set is short:
| Identity type | Notes |
|---|---|
uuid | The convention. +uuid fills on create |
u32 u64 i32 i64 | +u32/+u64 allocate monotonically; i32/i64 admit a signed natural key |
timestamp timestamp(s|ms|us) | A written value is floored to the declared precision |
+timestamp | Only when the field is named id, and only at us precision |
string(N) string(N!) | A Copy InlineStr<N>, not a heap String |
*Model | An identity that is itself a required foreign key. It takes the target's key type, and the chain must terminate |
Any other identity type is a validation error
Rejected, each with the field and the allowed set named in the message: bare string
(no width, so not Copy), bytes(N), f64, bool, decimal, json, an enum, a
struct, a fixed array, any nullable type including ?Model, and a [Model]
collection. You get one error per bad key, not one per rule it breaks.
Field syntax#
Each field has the form:
name: [MODIFIER]type [@directive ...]name— required, snake_case (created_at, notcreatedAt). Non-snake_case names are a fatal parse error.type— required, immediately after the colon. One of the scalar types, an enum, a struct, or a relation.- modifiers — optional
+,&,^prefixes (before the type). - directives — optional
@...directives after the type.
Field names must be unique within a model — a duplicate id is rejected.
Modifier positions#
The prefix modifiers + (auto-generate), & (unique), and ^ (indexed) appear
between the colon and the type name. Any combination is allowed, in any order:
field: +type // auto-generate
field: &type // unique
field: ^type // indexed
field: +&^type // all three (any combination)The nullable modifier ? is different: it appears after the type (postfix), though a
prefix form also parses. See modifiers for the full rules.
field: string? // nullable string (postfix — the idiomatic form)
field: ?string // prefix nullable also parses
field: +uuid? // auto-generate + nullableRead the sigils left to right
username: &^string @length(3, 50) reads as: unique, indexed string, with a length
constraint. Prefix sigils modify the type; postfix ? and @-directives follow it. (+
would need an auto-gen-eligible type — u32/u64/uuid/timestamp — so it can't go on a
string.)
Worked field examples#
Post {
id: +uuid // auto-generated UUID PK
title: string @length(1, 200) // required string, length constraint
slug: ^&string @length(1, 200) // indexed + unique string
view_count: u32 @default(0) // default marker (semantic)
published: bool @default(false)
published_at: timestamp? // nullable timestamp
author: *User // required foreign key
created_at: +timestamp // auto-generated timestamp
}Terminators & layout#
- No semicolons. Fields and models are delimited by newlines and the
{/}block braces — newlines are significant. - A field's
@directives must follow its type on the same logical line (before the next newline or the next constraint). - Definitions may span multiple lines; horizontal whitespace is insignificant.
See comments & whitespace for the details.