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 PascalCaseUser, OrderItem. user or order_item fail 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 is id: +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 typeNotes
uuidThe 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
+timestampOnly when the field is named id, and only at us precision
string(N) string(N!)A Copy InlineStr<N>, not a heap String
*ModelAn 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, not createdAt). 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 + nullable

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

Search documentation

Find pages across the ForgeDB docs