Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Contract and application authoring reference

RiffDB contract and RiffQL source accept // line comments. Block comments are not supported.

The following lowercase words are reserved by the contract lexer and cannot be used as field or declaration identifiers:

active aggregate allow approval as attempts binary_utf8_v1 bool bytes capability
checked_by child claim columnar_projection command conflict_key contract count create date decimal
default delete duration_seconds else emit entity enum event
exhausted exists expire expired expires_at fact false fence fencing_token field
from frontier i64 idempotency_key illegal import in include index input invalid
initialize init_or_mutate observe_or_initialize decide apply no_effect reject invariant is key lease list measure module money mutate not null on optional
overwrite owner partition_by policy presence principal projection query read reference
release renew require return revision root row service set source stale
state state_machine string sum text_index text_key timestamp to
transaction_time transactionally_ordered transition true u64 unavailable
unicode_fold_v1 unique update uuid uuid_v7 vector_field version when where workflow

For example, use origin, origin_label, or source_label instead of the reserved field name source.

The vector search words cosine, euclidean, dot_product, staleness_slo, ann_threshold, and recall_target_bps are contextual, not reserved: they are meaningful only inside a vector_field declaration and remain usable as ordinary field and declaration identifiers everywhere else. The same is true of nearest in RiffQL — a contract field named nearest stays queryable.

Names are unique in their semantic namespace, not globally across an entire contract. Enum variants belong to their enum, command outcomes belong to their command, and indexes belong to their owning entity. A duplicate diagnostic identifies the current declaration and includes the related span of the first declaration.

Each set assignment needs initialization, a narrow same-field arithmetic update, an explicit checked_by requirement_name association, or a caller-version fence. Reading another field on the binding is insufficient. Use overwrite for deliberate replacement without those proofs. The write evidence cookbook explains the distinctions and limits of each proof.

Command declaration order and relationship proofs

Grammar-v1 command clauses follow one visible order: inputs and service values, the idempotency declaration, every read/mutate/create binding, requirements, effects such as set and emit, then return. Bind everything a requirement can observe before writing the requirement:

command AddComment {
    input request_key: string<128>
    input site_id: uuid
    input post_id: uuid
    input comment_id: uuid
    service created_at: transaction_time
    idempotency_key request_key

    read Post(site_id, post_id) as post else PostMissing {}
    create Comment(site_id, post_id, comment_id) as comment
        else CommentExists {}
    require PublishedPost: post.state == PostState.Published
        else PostNotPublished {}
    set comment.created_at = created_at
    return Created { comment: comment }
}

Every required relationship affected by a create or mutation needs a dominating exact read of that relationship’s complete target key. Reading one aggregate root does not prove a separate author, route, project, or other target exists. This is intentional: the compiled command proves all reference integrity in the same transaction instead of relying on a racy application preflight.

Choosing a partition route

partition_by and conflict_key are separate decisions and are routinely confused. Getting the distinction wrong produces a contract that compiles and is quietly unusable, so it is worth stating plainly.

conflict_key is what writers contend on. Two commands whose conflict keys differ do not serialise against each other, whatever partition they are in.

partition_by is the locality and visibility unit. It decides three things:

  • what one index scan or bounded query can see, because a scan is scoped to one partition;
  • which entities a single command may write, since every create/mutate binding must derive the same route; and
  • what a capability may be scoped to.

The practical consequence is that a partition should be as wide as the widest thing you need to search or write atomically, and the conflict key should then be as narrow as the thing you actually update. Widening a partition does not cost concurrency; narrowing a conflict key is what buys it.

Two failure modes follow directly, and both are silent:

  • A route that identifies a single row makes that entity’s indexes useless. If Item keys (item_id) and the aggregate declares partition_by item_id, every item is alone in its partition, so index by_created (...) can only ever return that one item. There is no way to list items. If a collection has to be searched or ordered as a whole, partition it by the scope it is searched within — a tenant, an organization, or a fixed scope constant for a genuinely global collection — and keep the per-row identifier in the conflict key.
  • A unique key is enforced inside one partition, which is why RDB-C025 requires it to begin with the partition route. Prefixing the route makes a value unique per route, which is vacuous when the route already identifies one row. A value that must be unique across a wider scope needs the aggregate partitioned by that scope; see the entry under “Uniqueness” below.

A fixed scope constant is a legitimate route. An entity partitioned by scope: string<32> always set to "global" is one searchable collection whose writers still do not contend, provided the conflict key carries the row identifier.

Partition-local cross-aggregate writes

A command may write roots owned by different aggregates when every binding derives the same complete partition route. Keep the aggregates’ conflict keys independent; RiffDB acquires the union in its compiler-owned order and commits the mutations atomically. For example, inventory and an order may remain separate aggregates while one checkout writes both, provided both aggregate declarations say partition_by tenant_id and both bindings use the same tenant_id input.

Do not copy the partition value through a different expression or omit part of a composite route. RDB-C017 means the compiler could not prove the routes identical. Make each create and mutate key derive the complete same route, or split a genuinely cross-partition operation into independently idempotent commands. A cross-aggregate command does not make cross-partition transactions available.

Uniqueness

unique is enforced by a durable occupancy row inside the writing command’s partition, transaction-current, in the same commit. That makes it exact and race-free, and it is why the key must start with the partition route.

To make a value unique across every row of a collection, partition the aggregate by the scope the value is unique in and include that scope in the unique key:

entity Experiment {
  key (scope: string<32>, experiment_id: u64)
  field name: string<500>
  unique by_name (scope, name)
}
aggregate Experiments {
  root Experiment
  partition_by scope
  conflict_key (scope, experiment_id)
}

Creates and renames are then both checked atomically by the engine. Concurrent writes to different experiments do not contend, because the conflict key carries experiment_id.

Boolean row policies

A row-policy predicate can use a Boolean constant, row field, or declared principal fact directly, including under !, &&, and ||. For example, allow create when true permits creation when the other capability, scope, and command checks allow it. An update must satisfy its policy for both the current and proposed row. Missing fields, mistyped facts, and missing required relationship evidence still deny the operation; values are never coerced to Booleans.

Upgrade note: the row-policy evaluator corrects an earlier defect that denied direct Boolean predicates even though they compiled. Such policies now apply their declared true/false result and may admit rows that the defective evaluator denied.

Delete policy

Every deletable entity declares what happens to inbound relationships. Choose the policy from the relationship graph, not from the command that happens to delete the row:

  • delete_policy no_inbound is valid only when no declared relationship can target the entity.
  • delete_policy restrict Child.by_parent names the complete partition-local reverse index used to prove there are no referencing rows. The delete command also declares restrict Referenced {} so a reference produces a typed zero-mutation outcome.
  • delete_policy cascade { relationship Child.parent using Child.by_parent maximum 32 } enumerates every direct inbound relationship and its fixed fan-out maximum. Cascade is one bounded hop; recursive discovery is never substituted.

The reverse index starts with the target’s partition and key fields. Child entities reached by a cascade declare no_inbound, and the command must use the bounded collection-delete surface described in Bounded Commands. RDB-C045 means the declared policy is incomplete or inconsistent with that graph; add the missing reverse-index proof or choose the policy that matches the actual inbound relationships.

Composing two indexed predicates

RiffQL has no general join, but one ordered dependent batch composes two bounded index reads into an intersection: read the first predicate, then read the second restricted to the first’s keys. A candidate with no matching second row simply produces no row, which is the intersection.

The planner accepts this only in an exact shape, and RDB-QP007 reports the whole shape rather than the part that failed. The conditions most often missed:

  • Every comparison in the dependent binding must be on key fields. A predicate over a non-key field is a residual filter and is refused. If you need to match on a value that is not in the key, put a fixed-size digest of it in the key and compare that.
  • The dependent binding needs an absence outcome (else <Outcome>). For a many binding it never fires — an empty match is zero rows, not an outcome — but the planner requires it to be declared.
  • Both bindings order ascending by exactly the joining field, and the dependent binding’s take must not carry an after cursor and must be no larger than the source’s.

Operational query indexes

Indexes used by null/existence and text-prefix RiffQL predicates declare their physical semantics in the contract:

entity Document {
    key (organization_id: uuid, document_id: uuid)
    field deleted_at: optional<timestamp>
    field title: string<200>

    index by_deleted (organization_id, deleted_at, document_id)
        presence(deleted_at)
    index by_title (organization_id, title, document_id)
        text_key(title, binary_utf8_v1)
}

presence is valid only for an optional authoritative key scalar and preserves missing, explicit null, and non-null as distinct index states. text_key is valid only for a bounded string field. binary_utf8_v1 compares exact UTF-8 bytes; it is case-sensitive and performs no Unicode normalization. The unicode_fold_v1 profile applies the frozen Unicode 17.0.0 NFKC plus full non-Turkic case fold for folded-byte equality, membership, range, prefix, and ordering. Adding or changing either encoding changes durable index identity and requires the contract migration/rebuild path; RiffDB never silently changes a text profile during a software upgrade.

An optional field is not an ordinary index key. If a query filters or orders on an optional field, declare presence(field) as shown above so missing, explicit-null, and present values remain distinct and the compiler can prove the access shape.

Tokenized text indexes

An entity may declare a bounded, weighted tokenized-text index over required or optional bounded string fields:

entity Document {
    key (organization_id: uuid, document_id: uuid)
    field title: string<256>
    field body: optional<string<65536>>

    text_index search((title weight 4, body weight 1),
        analyzer standard_v1,
        staleness_slo 60,
        replay_age_seconds 86400,
        replay_bytes 1073741824,
        replay_backlog 100000,
        result boolean_v1,
        max_terms 16,
        max_candidates 10000,
        max_results 1000)
}

Every clause is required and compiler owned. Source names must be distinct; weights and all budgets must be positive and within the fixed format maxima; max_results cannot exceed max_candidates. Callers cannot replace these values, name a field or analyzer, provide boosts, request a scan, or choose a consistency fallback at runtime.

The v1 analyzer set is closed:

  • keyword_v1 emits the complete source value as exactly one term, including one empty term for an empty value. It performs no normalization.
  • standard_v1 first applies Unicode 17.0.0 UAX 29 word segmentation, assigns monotonically increasing positions, and then applies the deterministic unicode_fold_v1 compatibility fold to each term. Analyzer behavior is pinned by checked-in conformance tests rather than changing with the host.

Two consequences of UAX 29 segmentation are worth knowing before you choose standard_v1, because both are pinned behaviour rather than defects:

  • Scripts without spacing segment per character. UAX 29 carries no dictionary, so 東京大学 analyzes to four single-character terms rather than to words. Search over such text is therefore effectively per-character. If a field is predominantly CJK and you need whole-value matching, keyword_v1 is the honest choice until a dictionary-backed analyzer is declared.
  • A value can analyze to no terms at all. A value made only of punctuation, whitespace, or emoji produces an empty term list, so that row cannot be found through this index by any query. It is still authoritative and still readable by key; it is simply absent from the tokenized index.

The declaration selects boolean_v1; public named tokenized queries compile bounded matching(...) predicates (conjunction, disjunction, phrase, and ordered proximity) and optional order by relevance desc using the fixed riff_bm25_v1 ranking model (caller-supplied boosts and raw floating-point scores are not exposed). The stale-entity and replay ceilings are durable contract identity for the tokenized provider stage. Changing an analyzer or field weight changes the contract bundle identity. See Tokenized Text Search.

Explicit covering indexes

A new index identity may declare bounded authoritative fields that are stored atomically with each index entry:

index by_board_project_status
    (organization_id, project_id, status, ticket_id)
    cover (title, reporter_id, assignee_id)

The compiler may use this index for a named query only when its key plus cover contains every predicate, ordering, and selected field and the complete result has one statically bounded positional layout. The application still names only the query; it cannot select the index, request a partial cover, provide field ordinals, or disable validation. Covered bytes are derived from the same authoritative post-image as the index key and commit atomically with it.

cover (...) is intentionally explicit and ordered. Adding it to an existing index is not an in-place optimization: add a new index identity and use the normal migration/rebuild path. Missing, stale, malformed, duplicate, or non-derivable covered values fail closed; the executor never falls back to entity reads after selecting a covered plan. Unbounded, secret, unsupported, or policy-incompatible layouts remain on the ordinary checked result path.

Vector fields (alpha)

An entity may declare one or more fixed-dimension embedding fields:

entity Document {
    key (org_id: uuid, doc_id: uuid)
    field title: string<256>
    field body: string<65536>
    vector_field embedding(1536, cosine, (title, body), staleness_slo 60,
        model "text-embedding-3-small", current_version "2026-08-21",
        replay_age_seconds 86400, replay_bytes 1073741824,
        replay_backlog 100000,
        ann_threshold 256, recall_target_bps 9500)
}

The declaration carries the search configuration into the compiled contract bundle, where it is part of the contract’s durable identity:

  • The dimension is a positive integer of at most 4,096.
  • The metric is exactly one of cosine, euclidean, or dot_product.
  • The source fields name 1 to 1,024 distinct existing fields on the same entity — the fields whose edits make a stored embedding stale. A repeated name or the vector field itself is a compile error at its span.
  • staleness_slo is the positive v1 stale-entity count threshold. A staleness observer breaches the SLO only when stale_count > staleness_slo; equality does not breach it. Duration-based or clock-based staleness is a named future amendment, not a v1 interpretation of this declaration.
  • The production clause is atomic: model, current_version, replay_age_seconds, replay_bytes, and replay_backlog must all appear in that order or all be absent. Model strings are nonempty and at most 256 bytes. Replay limits are positive and compiler-bounded (one year, one TiB, and 100,000,000 sequences respectively). They are projection retention ceilings, not request parameters.
  • Omitting the production clause preserves legacy exact-vector bundle bytes, but that field cannot back a production nearest query. A production clause selects contract IR V15; earlier vector and ANN records remain byte-frozen.
  • The optional ANN clause is atomic: ann_threshold and recall_target_bps must appear together in that order. The threshold is 1 to 65,536 rows per organization. Search remains exact at or below the threshold. Above it, a matching warm graph may engage; a cold or unavailable graph uses the exact path.
  • recall_target_bps is 1 to 10,000 basis points. For example, 9500 declares recall@K of at least 0.95 against exact scan at the same projection frontier. Application query inputs cannot lower or omit this projection-owned target.

The approximate graph is derived, bounded by the query’s organization partition and scan budget, and rebuilt deterministically from the visible snapshot in a bounded background build. Primary and follower providers may reuse an exact population match at the same generation and frontier. Graph entry points and links are computed only after scalar filters and principal-policy admission, so another organization or a denied row cannot shape traversal or reported graph statistics. Every cache hit repeats current admission. Exact KNN remains the reference and cold fallback path.

Production vector fields are written only through the compiler-sealed embed effect; generic set is rejected at the target field:

command SetDocumentEmbedding {
    input request_id: string<128>
    input org_id: uuid
    input doc_id: uuid
    input embedding: vector<1536>
    input submitted_model: string<256>
    input submitted_version: string<256>

    idempotency_key request_id
    mutate Document(org_id, doc_id) as document else Missing {}

    embed document.embedding = embedding
        from (submitted_model, submitted_version)

    return Embedded { document: document }
}

The vector dimension is part of the input type. Model identity and version are distinct direct command inputs and must exactly match the field’s compiled production declaration; a mismatch is rejected before admission with the symbolic input path. The entity post-image and its authoritative embedding evidence (model identity, model version, and commit sequence) are derived from the same checked command and persist atomically with its outcome, provenance, and idempotency record. An application cannot submit evidence separately or use generic field mutation to bypass it.

Typed vector values cross the native, Protobuf, gRPC, hosted MCP, CLI, and generated Rust, Go, TypeScript, and Python conversion boundaries with finite-component and exact-dimension checks. Each generated embedding command also has a field-specific constructor that fills the contract-sealed model identity and version; generated constants and accessors keep that evidence inspectable without asking application code to duplicate it. Direct input construction remains checked by the same server authority. The production application surface also exposes bounded symbolic stale/outdated inspection for the exact vector field when its role declares that authority. The authoritative observer maintains the distinct vector_staleness health component without scanning entities during a probe. Production exact-nearest queries use an explicit projected source and freshness declaration. The running server synchronizes compiler-owned vector sources after checked contract activation, including the first deployment into an empty database; no daemon restart is required. A successor that would reuse incompatible derived state allocates a distinct durable generation and fails closed until its rebuild publishes a complete replacement. Rebuild captures one bounded authoritative entity snapshot, applies the retained commit tail, checkpoints the candidate, and only then publishes Ready; a partial snapshot is never queryable. The compiler-owned replay age, byte, and backlog ceilings detach an over-budget generation from retention and trigger the same snapshot-plus-tail path. Building, RebuildRequired, and Rebuilding are typed temporary states, not permission to serve the previous generation. Only Ready controls hold commit-log retention. A crash or restart resumes from the durable control record without claiming a frontier beyond the exact checkpoint. See the RiffQL language reference.

Revision-checked workflow transitions

A workflow declaration closes the legal state graph for one aggregate-owned entity. A transition effect must name a mutable binding and an exact revision supplied by the caller:

workflow WorkLifecycle {
    entity WorkItem
    state state
    transition Start from (Queued) to Running
}

command StartWork {
    input request_key: string<128>
    input organization_id: uuid
    input work_id: uuid
    input expected_revision: u64
    idempotency_key request_key

    mutate WorkItem(organization_id, work_id) as work else Missing {}
    transition Start on work revision expected_revision
        stale StaleRevision {}
        illegal IllegalState {}
    return Started { work: work }
}

RiffDB compares expected_revision with the observed entity revision before it checks the source state. A mismatch returns the declared StaleRevision outcome; a matching revision in any state outside Start’s source set returns IllegalState. Both are ordinary idempotent zero-mutation command outcomes. The legal path assigns the declared destination and retains the exact entity version as a transaction-current read dependency, so a concurrent replacement cannot turn the checked result into a lost update at commit.

There is no unconditional workflow update, automatic substitution of a newer revision, generic compare-and-swap, or caller-supplied transition name. The current executable surface covers revision-checked declared transitions plus sealed service-owned values:

command StartWork {
    input organization_id: uuid
    input work_id: uuid
    input expected_revision: u64
    service execution_id: uuid_v7
    service started_at: transaction_time
    idempotency_key request_key

    mutate WorkItem(organization_id, work_id) as work else Missing {}
    transition Start on work revision expected_revision
        stale StaleRevision {}
        illegal IllegalState {}
    set work.execution_id = execution_id
    set work.started_at = started_at
    return Started { work: work }
}

transaction_time is the logical time sampled for durable admission; uuid_v7 is generated by the service before deterministic evaluation. Neither value is a caller input, and the runtime receives the values without clock, randomness, filesystem, or network authority. RiffDB stores the canonical service-value record with pending and terminal command evidence and retains it inside the authoritative command segment. A retry, uncertain-outcome recovery, or restart therefore returns and reconstructs the originally sealed values rather than sampling replacements. Service values consequently require a durably idempotent mutation command; unjournaled read-only commands cannot declare them.

For fenced workflow leases (lease claim, renew, release, expire, and fence), see Workflow Transitions and Fenced Leases.

CLI application JSON

The application CLI accepts inline JSON, @path, a legacy bare path, or - for stdin:

riffdb command run CreateItem --input \
  '{"item_id":"01900000-0000-7000-8000-000000000001","priority":2,"mode":"Linear"}'

riffdb command run CreateItem --input @fixtures/item.json
riffdb command run CreateItem --input fixtures/item.json
riffdb command run CreateItem --input - < fixtures/item.json

Natural schema-directed values are:

Contract typeJSON
booltrue
i64, u642
uuid"01900000-0000-7000-8000-000000000001"
enum"Linear"
bounded string"text"
optional absencenull
list[...]

The explicit $i64, $u64, $uuid, $enum, $decimal, $money, $bytes, $date, and $timestamp objects remain compatibility forms for lossless transport or fixture work. Public type failures name the operation and symbolic path when that context is available. They never release numeric field IDs through the application error envelope; numeric IDs remain confined to the explicitly low-level kernel interface.

Successor contract evolution

Every changed deployment must declare a contract version greater than the active version and must compare against the exact expected active version. Version numbers alone do not establish compatibility; the compiler compares the checked successor with the active bundle.

For application source trees, application lock --write obtains that exact parent through the authorized read-only candidate preview. The resulting lock pins both the parent identity and generated/riffdb.contract.bundle. Never manufacture a successor lock with an offline genesis compile or copy a hash from an error.

The pre-alpha compatibility classes are:

Successor changeClassification
Add an enum with its complete initial variantsCompatible
Add an entity with its complete initial key, fields, indexes, and local invariantsCompatible
Add an aggregate whose root and children are all new in this successorCompatible
Add a relationship or unique constraint confined to new entitiesCompatible
Append a fresh variant to an existing enumRequires explicit version
Add an optional field to an existing entityCompatible
Add a required field, index, invariant, relationship, or unique constraint to an existing entityRequires migration
Add a projection or columnar_projection over existing authoritative stateRequires migration
Structural rename, checked field conversion, enum mapping, retirement (Gate B), or rekey/repartition (Gate C) paired with a .riffm migration artifactRequires migration
Unmigrated incompatible change to an existing declaration, type, key, variant, constraint, aggregate membership, partition rule, or conflict ruleIncompatible

The complete initial definition of a newly added entity or aggregate is treated as one addition. A new aggregate cannot take ownership of an existing entity. These restrictions ensure that a compatible successor does not scan, rewrite, or reinterpret an existing durable row.

Use the application workflow when deploying a locked source tree:

riffdb application check
riffdb application lock --write
riffdb application generate --locked
riffdb application deploy

For a direct contract deployment, validate before deploying and supply the active version expected by that CLI version:

riffdb --config "$HOME/.config/riffdb/client-ea.toml" \
  contract validate riffdb/contract.riff
riffdb --config "$HOME/.config/riffdb/client-ea.toml" \
  contract deploy --expected-version 3 riffdb/contract.riff

An invalid source returns bounded syntax or semantic diagnostics. A checked but incompatible successor returns incompatible_candidate with its parent, overall compatibility class, and stable compatibility-code counts. Neither result activates the candidate. Do not work around incompatibility by reusing stable IDs or editing the active database file.

A valid successor that needs existing state to be checked or transformed returns migration_required. Use Application Source V3, a parent-specific .riffm proof, Application Lock V4, and the read-only plan described in Contract Migrations. Offline check, apply, and status operations activate that successor through the migration CLI and operator service.

contract deploy is a mutating operator command, not a compatibility probe. There is no contract rollback RPC. Use contract validate for source-only validation or application lock --write for an exact read-only successor preview, then review the lock before application deploy. If a direct deploy activates an unwanted successor, stop the service and follow the documented offline backup/restore or disposable pre-alpha reset procedure; do not deploy another probe hoping to undo it.