A model is a named persisted row shape. It resembles a struct declaration,
but its fields also become database columns and query-scope names.
model Booking {
@id
id: int
@unique
reference: string
passengers: int = 1
notes: string?
}Constructing a model value does not write it to a database. SAVE, INSERT,
and UPSERT are the persistence operations; ordinary struct-like construction
only creates an Atoll value.
@id marks a primary-key field. When no field is decorated, a field named
id is the conventional key. Composite keys use more than one @id field.
@unique on one field becomes a single-column unique index.
Primary-key metadata controls GET, REMOVE, upsert conflict targets, and DDL.
Keep key fields stable after persistence; changing an in-memory key does not
implicitly move a stored row.
Names
Table names default to the lowercased model name; column names default to the
Atoll field name. @sql.name overrides either mapping:
@sql.name("bookings_v2")
model Booking {
@id
id: int
@sql.name("booking_no")
booking_number: string
}Result binding still uses booking_number; the override changes generated SQL
identifiers, not the Atoll API.
Generated identifiers remain dialect-quoted by the renderer. An override is an identifier, not arbitrary SQL, and should remain stable once migrations have created the table or column.
Column types
DDL generation supports booleans, integer widths, floats, strings, temporal
built-ins, nullability, and selected native arrays. Atoll int maps to a
64-bit database integer. DateTime, Date, Time, and Duration currently
use their integer wire representation in generated DDL rather than semantic
database timestamp types.
Native []int, []string, and []float columns are available only on engines
with array support. Unsupported model-to-engine mappings are compile errors.
Defaults in model source participate in construction, but generated database
DEFAULT clauses are not complete. Do not assume omitting a column in raw
database writes applies its Atoll expression.
Optional fields map to nullable columns. A required Atoll field is expected in decoded rows; schema drift that removes it or changes its storage type becomes a query encoding or engine error.
Aliases preserve their underlying storage mapping. They improve source-level domain meaning but do not automatically create a distinct database type.
Relations
Relation clauses describe associations:
model Booking {
@id
id: int
customer_id: int
BELONGS TO customer: Customer VIA customer_id
HAS MANY legs: Leg VIA booking_id
}BELONGS TO, HAS ONE, HAS MANY, and MANY TO MANY parse. An explicit
single-column BELONGS TO ... VIA can generate a foreign key and can supply an
implicit join condition. Composite foreign keys, referential actions, typed
navigation fields, and many-to-many join-table synthesis are not complete.
Relation declarations are metadata, not automatically loaded object fields.
Use a checked join to retrieve related data. An ambiguous implicit relation
requires an explicit ON.
Indexes
Indexes are clauses, not @indexed decorators:
model Event {
@id
id: int
tenant_id: int
created_at: DateTime
INDEX (tenant_id, created_at DESC)
UNIQUE INDEX event_key (tenant_id, id)
}The grammar also accepts USING, WITH (...), INCLUDE (...), and a partial
WHERE predicate. Core index DDL and migrations ship; covering columns and
partial predicates remain incomplete in some paths. Index types are validated
against the selected dialect.
Index order and direction affect query planning but do not change the model’s
Atoll field order. Unique indexes add a database constraint and can produce a
portable UniqueViolation classification at runtime.
DDL
The compiler can turn registered models into a desired schema snapshot and render tables, foreign keys, and indexes for a selected dialect. Migration diffing compares that snapshot with introspected database state.
Review generated migrations before applying them. A source rename with no explicit migration intent can appear as a destructive drop-and-add, and engine-specific type changes may not be reversible.
Boundaries
Models do not provide the inheritance, behavior blocks, validation DSL, or abstract/data model hierarchy described in older design documents. Use normal types and functions for application behavior.
Model validation belongs in constructors, functions, or database constraints that callers invoke explicitly. The compiler checks query and storage shape; it does not infer business invariants from field names.