Skip to content

Models

Declare SQL-backed row shapes, keys, relations, and indexes.

Updated View as Markdown

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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close