Skip to content

Constraints — ASL

In addition to field-level constraints, a constraint <expr> on (.a, .b); declaration inside a type body applies a constraint across one or more columns. This is how you express composite constraints such as unique-together.

type Membership {
required user_id: uuid;
required org_id: uuid;
required code: str;
constraint exclusive on (.user_id, .org_id); # composite UNIQUE (unique together)
constraint min_length(4) on (.code); # CHECK on char_length
}

Supported expressions: exclusive → composite UNIQUE, pk → composite PRIMARY KEY, min_length(n) / max_length(n)char_length CHECK. Constraints are emitted with deterministic names (e.g. uq_membership_user_id_org_id) inside CREATE TABLE, and adding or removing one on an existing type generates an ALTER TABLE ... ADD/DROP CONSTRAINT in the migration SQL.

An exclusive constraint can carry a filter <predicate> clause, making it a partial unique constraint — the uniqueness only applies to rows matching the predicate. The predicate is a native AQL expression (the same language as policies and query filters), including Enum.Member references.

enum QueueStatus { Pending, Running, Done }
type Job {
required name: str;
required actor: str;
required status: QueueStatus;
# At most one Pending job per (name, actor); Running/Done rows are unconstrained.
constraint exclusive on (.name, .actor) filter .status = QueueStatus.Pending;
}

Postgres can’t put a WHERE on a table constraint, so a filtered exclusive lowers to a partial unique index rather than a CONSTRAINT … UNIQUE:

CREATE UNIQUE INDEX IF NOT EXISTS "uq_job_name_actor"
ON "job" ("name", "actor")
WHERE (status = 'Pending');

See the job queue example for the full pattern.