Skip to content

Writes

Insert, update, upsert, and delete SQL-backed rows.

Updated View as Markdown

Writes are fallible query expressions and require a writable datasource. They use the same checked model metadata, prepared parameters, route selection, and QueryError channel as reads. Values are never interpolated into generated SQL.

Results

The operation and its terminal determine the success value:

Form Success value
SAVE Model { ... } the saved Model
SAVE Model FROM rows []Model
REMOVE Model(key) bool
REMOVE Model(keys) affected-row int
INSERT, UPDATE, or DELETE without RETURNING affected-row int
single-key REMOVE ... RETURNING an optional projected row
batch REMOVE ... RETURNING a list of projected rows
other writes with RETURNING a list of projected rows

Every form is Result[success, QueryError]; ? propagates its error. A database constraint, connection failure, malformed host request, or failure to decode returned rows stays distinguishable through the QueryError variants.

Save

SAVE upserts one full model by primary key and returns the saved model:

saved := SAVE Booking {
    id: booking_id
    reference
    passengers
}?

All key fields must be present. The compiler generates an engine-specific conflict clause and returns the full row. A bare field such as reference is shorthand for reference: reference; field names are checked against the model.

SAVE Model FROM rows accepts a list of models, performs the same upsert for each item, and returns the saved models:

saved: []Booking = SAVE Booking FROM bookings?

The current batch path executes a prepared single-row statement repeatedly. It supports all-scalar model rows; nullable-column batch binding and some relationship-bearing shapes remain incomplete.

Remove

REMOVE deletes by primary key:

removed: bool = REMOVE Booking(booking_id)?
count: int = REMOVE Booking(ids)?

Single removal reports whether a row existed. Batch removal reports affected rows. RETURNING changes the result to an optional projected row for one key or a list for many keys.

Composite keys are supplied positionally in declaration order:

removed := REMOVE DailyStat(day, route_id)
    RETURNING { total }?

For a single key, absence is None, not an error. A malformed key shape is a compile-time type error.

Insert

created := INSERT INTO Booking
    VALUES {
        id: booking_id
        reference
        passengers: 1
    }
    RETURNING { id, reference }?

Single insert rendering and typing ship, but it has less end-to-end runtime coverage than SAVE and UPSERT. Batch INSERT INTO Model FROM rows without RETURNING executes and returns an affected-row count. Batch returning, BATCH SIZE, and several nullable-row paths remain incomplete.

Unlike SAVE, insert does not update an existing row. A duplicate key normally returns a constraint QueryError. Generated or defaulted columns may be omitted when the model and database schema permit it.

Update

count := UPDATE Booking
    SET passengers = passengers + 1
    WHERE id == booking_id?

Without RETURNING, update yields an affected-row count. With RETURNING, it yields a list of projection rows:

changed := UPDATE Booking
    SET status = new_status
    WHERE customer_id == wanted_customer
    RETURNING { id, status }?

Assignments may read the old row, as in passengers = passengers + 1, or bind ordinary Atoll values such as new_status. The compiler separates the two and binds guest values as statement parameters. An update matching no rows succeeds with 0 or an empty returned list.

Delete

count := DELETE FROM Booking
    WHERE cancelled_at < cutoff?

Delete returns affected rows unless RETURNING requests projected deleted rows.

DELETE FROM is predicate-based and can remove many rows. Prefer REMOVE when the caller already has a primary key: its single-key bool or optional-row result makes absence explicit.

Upsert

UPSERT states an explicit conflict target:

saved := UPSERT DailyStat ON day, route_id {
    day
    route_id
    total
}?

PostgreSQL/SQLite-style conflict clauses and MySQL duplicate-key syntax are rendered from the same query plan. Conflict fields must name model columns and must be present in the values block. Add RETURNING { ... } when the caller needs database-generated values; the result is a list because a general write terminal can affect more than one row.

Safety gates

The compiler rejects clauses it cannot safely lower. Unsupported LIMIT, GROUP BY, joins on writes, conditional SET, and incomplete batch forms do not silently disappear. Dialect gates also apply: DataFusion is read-only, and MySQL currently rejects RETURNING. A datasource configured with readOnly = true rejects every write before execution.

A successful statement is atomic, but several separate statements are not an atomic unit. Use Transactions when multiple writes must commit or roll back together. Inspect err.is_constraint() for data conflicts the caller must correct, and retry only errors for which err.is_retryable() is true.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close