Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Reads and writes

Reads and writes

When a write is refused

A write can be refused for two reasons, and a lifecycle transition for a third, when its when: doesn't hold (Transitions, above):

  • The record isn't there. That reports the entity's not-found failure, TodoNotFound. It includes a reference that names a missing record: a bad project_id is ProjectNotFound.
  • A database rule refuses it: a unique rule, or a restricted reference blocking a delete.

Map each rule a write can break in its conflict: block, keyed by the rule: Project.name_per_owner (a named unique rule), User.email (a unique field) or Todo.project_id (a reference that blocks deleting a project). The compiler knows which rules every write can break, so a missing mapping is a compile error, and there is no catch-all. The database's own error is kept as the failure's internal cause (Failures).

Here creating a project can break the unique rule on its owner and name, and closing one is blocked while todos still point at it. owner.id comes from a loaded record, so the owner is surely there and the create can't fail with UserNotFound:

example
action start_project(owner: User, name: Project.name) fails ProjectNameTaken -> Project {    return attempt Project.create {        set: {            owner_id: owner.id  // a loaded record's id: the owner is there            name: name        }        conflict: {            Project.name_per_owner: ProjectNameTaken        }    }}
example
action close_project(project: Project) fails ProjectHasTodos -> Unit {    attempt Project.delete(project) {        conflict: {            Todo.project_id: ProjectHasTodos        }    }}

What a write can break follows from what it writes:

  • a create: every unique rule of its entity;
  • an update: the unique rules over the fields it sets;
  • a delete: each restricted reference to the record, or to a record its delete cascades to.

A reference's value surely names a record that is there when it is read from a record loaded earlier in the action (owner.id, or a loaded record's own reference to that entity), or when it is none. Any other value may name a record that isn't there, so the write can fail with that entity's not-found failure, which attempt passes on. A loaded record is surely there because the transaction's snapshot holds it, and a concurrent delete of it is a concurrent change (All-or-nothing actions and concurrent changes).

The runtime maps the store's constraint name back to the rule, since Jadpo names every constraint itself (Data).

A failed action inside another

An action whose failure its caller handles with match runs inside a savepoint, a point in the transaction it can be rolled back to. When it fails, everything it wrote is undone, a refused write among them, and the caller carries on in a transaction the failure didn't end. An action is all-or-nothing whoever calls it.

An action that passes its failure on with attempt needs no savepoint: the failure ends its caller too, up to a match that undoes both, or up to the outermost action, which rolls back.

Markdown for agents: /docs/when-a-write-is-refused.md.Built from Jadpo main at a707efb.