In development

Documents, models and CRUD

Ordinary records and optional schemas are the starting point for every RURAL workload.

P05 provides ordinary durable document envelopes on data branches only. The command adapter validates a bounded JSON object against a versioned schema before lowering create, replace, upsert or merge-patch to the existing engine's atomic batch path. Delete retains the existing body and does not require a replacement body or revalidate it against a newly supplied schema. Document keys use document/<escaped-collection>/<escaped-id>: encode each slash within either identifier as %2F; percent is forbidden in input IDs. Slash-free keys retain their bytes. Service-control and other reserved collections are rejected. Reads and mutation lowering reject an envelope whose collection/ID differs from the requested address. Legacy unescaped slash keys are unsupported: no ambiguous lookup fallback or automatic migration; migrate their documents and relation references in a separately qualified procedure before use.

Each stored envelope records identity, schema version, owner/scope, revision, body, semantics, provenance, and deletion state. A merge patch accepts explicit set values and unset field names; null remains a stored value. Validation and derived scalar-unique checks occur before the engine transaction, so a rejected request does not publish a record or receipt.

The 64 KiB byte limit applies to the complete encoded envelope, including its header and JSON delimiters. Exact-limit encodings are accepted; oversized encodings return DocumentTooLarge and release the temporary allocation. Successful encoding transfers the returned buffer to the caller, which must free it. A body that fits alone can still exceed the envelope limit.

Run the narrow document tests:

zsh scripts/zig.sh build test-prototype -Dcase=documents

Single writes and atomic batches use the same lowering function. Batch items are validated in order against a retained snapshot overlay that includes prior items, so duplicate creates and unique collisions reject the whole batch. Overlay roots and lowering allocations are released on failure. A batch patch supplies BatchItem.patch; missing patch input returns InvalidPatch.

Document revision starts at one and increments the previous document revision with overflow checking, independently of the branch head. expected_revision, when supplied, must match the existing record, including a tombstone; the engine separately checks expected_head. Replace requires an explicit expected_revision; omission returns MissingExpectedRevision. Replace, patch and delete require a live record. Ordinary delete rejects incident graph relations with IncidentEdges before creating a tombstone, including when called in an atomic document batch. Removing an endpoint together with its edges requires the separate explicit graph lowering operation. get and list hide tombstones. Create treats a tombstone as absent; create/upsert revive it using its prior revision plus one. Existing owner/scope, unknown envelope metadata and untouched fields survive edits and revival. Body-changing operations update schema identity/version to the schema that validated the new body. Delete retains the prior schema and body.

Service callers can set Write.logical_request_hash or use batchWithOptions(..., .{ .logical_request_hash = hash }). The caller must first authorize the current request and derive this hash from its canonical logical operation; supplying a hash is not authorization. An existing matching receipt returns before state-dependent lowering, even after an intervening edit or restart. Reusing the identity with a different logical hash returns IdempotencyConflict. The logical hash travels separately from the engine's lowered-command hash. Existing callers without a logical hash retain the command-derived path; they do not gain this stronger retry behavior. Direct domain callers must serialize mutations across receipt lookup, lowering and commit when guaranteeing same-ID retries. The service wrapper owns this admission gate for callers sharing one service instance. Separate wrappers or raw Database mutations are outside that gate.

get returns an owned copy. list appends buffers owned by its supplied allocator to the caller's list; on failure it frees newly appended buffers and restores the original length, preserving the caller's existing entries. Malformed stored rows keep the prior skip behavior, but allocation failure propagates as OutOfMemory rather than silently omitting a row.

The service wire spelling for an explicit set/unset edit is merge_patch. The obsolete patch alias returns unsupported_operation without publishing a record or receipt. It is never remapped to a canonical request or a new identity. The domain API's BatchItem.patch remains the same typed lowering input.