7.9.24. SQL-23 — Foreign keys

DDL-only foreign-key support: a child column names its parent struct, optional on_delete / on_update actions pick the integrity behavior. dasSQLITE deliberately ships no navigation properties — to query across a relationship use _join (SQL-15 — _join (inner equi-join)), _left_join (SQL-16 — _left_join with Option<TB>), or _any / _in correlated subqueries (SQL-17 — Subqueries).

7.9.24.1. Annotation shape

Annotation

Meaning

@sql_references = "ParentTypeName"

parent struct (must have @sql_primary_key)

@sql_on_delete = "<action>"

CASCADE / SET NULL / SET DEFAULT / RESTRICT / NO ACTION

@sql_on_update = "<action>"

same value set

Each decorator is a separate @name = value line — daslang field annotations are scalar key/value pairs, not function-call shapes. @sql_references resolves at [sql_table] expansion time: it finds the named struct in the same module, reads its [sql_table(name = ...)], and finds the @sql_primary_key field. Defaults when omitted: on_delete = "no_action", on_update = "no_action".

7.9.24.2. PRAGMA foreign_keys = ON

with_sqlite enables the pragma on every connection. SQLite ships it OFF for back-compat with pre-3.6.19 databases; without it FK clauses are parsed and stored but never enforced — orphans silently appear and CASCADE silently no-ops. dasSQLITE turns it on always.

7.9.24.3. CASCADE delete

[sql_table(name = "Users")]
struct User {
    @sql_primary_key Id : int
    Name : string
}

[sql_table(name = "Orders")]
struct Order {
    @sql_primary_key Id : int

    @sql_references = "User"
    @sql_on_delete = "cascade"
    UserId : int

    Total : float
}

db |> _sql_delete(type<User>, _.Id == 1)
// DELETE FROM "Users" WHERE "Id" = ?
// (SQLite cascades internally to DELETE FROM "Orders"
//  WHERE "UserId" = 1)

7.9.24.4. SET NULL requires Option<T>

The FK column must be Option<T> so SQLite can write NULL when the parent goes away. [sql_table] rejects the combination at compile time if the column is plain T.

[sql_table(name = "Comments")]
struct Comment {
    @sql_primary_key Id : int

    @sql_references = "User"
    @sql_on_delete = "set_null"
    @safe_when_uninitialized AuthorId : Option<int>

    Body : string
}

7.9.24.5. Action reference

Annotation value

ON DELETE / ON UPDATE clause

"cascade"

CASCADE — propagate the change to children

"set_null"

SET NULL — requires Option<T>

"set_default"

SET DEFAULT — requires field-init or @sql_default_fn

"restrict"

RESTRICT — reject the change immediately

"no_action"

NO ACTION — the SQL standard default

7.9.24.6. FK violations through try_insert

Constraint violations surface as SQLite errors at insert/update time. try_insert wraps them as Result.

let res = db |> try_insert(Order(Id = 999, UserId = 9999, Total = 1.0f))
if (res |> is_err) {
    // "FOREIGN KEY constraint failed"
}

7.9.24.7. Drop / create order

Parent before child on create; child before parent on drop. The FK clause names the parent table by name, so the parent must exist when the child is created.

See also

Full source: tutorials/sql/23-foreign_keys.das

Previous tutorial: SQL-22 — Transactions

Next tutorial: SQL-24 — Indexes