Contextfulv0.23.0

Docs / Guides

contract store · 10-store.md · 11-snapshot.md · 12-time.md · 13-replica.md · 14-drain.md · 15-endpoint.md · 16-collect.md · 17-merge.md · 18-pull.md · guide · card

The store and its sync

The problem the store solves

Writers append rows while readers query and a background pass compacts. A reader needs a consistent answer, and a crashed writer must leave nothing half visible. The store holds Parquet files, JSON manifests and one pointer object per table (components). Every commit is a conditional write, so a batch is either whole or absent (primitive).

Setting up a project

contextful init writes contextful.toml and creates the store root beside it (declaration-file). The store receives an identity that clones keep, so two different stores do not merge by accident (store-identity).

One table, from write to read

The example table documents declares primary_key = ["document_id", "page"] and order_by = "revised_at". A keyed table reads through a view that keeps one survivor per key (dedup-view); an unkeyed table reads as the union of its runs (unkeyed-union).

A run writes parts into its own directory and commits by creating that run’s manifest conditionally (run-manifest). Until the manifest exists, no read lists the run’s parts (uncommitted-run).

The engine injects provenance columns such as _ingested_at and _run_id, replacing any value the producer sent (injected). It also assigns _commit_seq above every value the table holds, so later commits sort later (commit-seq).

Schemas grow additively: an unseen column joins, and older files read it as null (additive). One batch sends page as a float. That batch fails before any Parquet, because a key column takes no float promotion (key-widening).

A snapshot is a compacted, immutable set of a table’s parts, named by a sortable id (snapshot-id). A query with no time bound reads the current snapshot plus the committed runs it omits (unbounded-latest). Every read passes an explicit sorted file list, not a glob (explicit-file-list).

Leases and a paused writer

A lease gives one node exclusive ownership of a pipeline or of compaction, and a fence is the number that rises with each new holder. A paused machine that lost its lease later tries to commit. The higher fence wins the condition, and the stale run stays unreadable (stale-fence).

Compaction

Runs accumulate until a fold compacts them. A pass stages Parquet and every declared sidecar, an index file beside the snapshot, then commits (pass). Each table declares its sidecars under indexes (declaration).

The commit replaces the table pointer, which names the current snapshot, on the ETag the pass read (table-pointer). A reader sees a snapshot and its sidecars together or neither (partial-snapshot).

A statement already running keeps the snapshot it started on, so a fold blocks no reader (non-blocking).

A fold job puts compaction on a schedule, which pipeline serve keeps (scheduled-maintenance); each fire folds the job’s target table (job-pass).

A sidecar is a vector graph, a full-text index or a key lookup (kinds). A sidecar returns candidate ids only, and retrieval joins them through the access-controlled relation (candidate-ids).

Three clocks

Transaction time records when a commit happened, valid time when a fact held, and availability time when the source released it. The diagrams follow one quarterly measurement through all three.

Flowchart diagram Flowchart, top to bottom. 6 nodes, 7 edges. Declared columns: valid time 2014Q1 → measurements [valid_as_of compares] Declared columns: availability time April 2014 → measurements [available_as_of compares] Engine stamp: transaction time 2026 → measurements [as_of compares] source publisher → valid time 2014Q1 [measures the quarter]; → availability time April 2014 [releases first figure] 2026 backfill run → transaction time 2026 [commit writes]; → measurements [lands source rows] Declared columns valid time 2014Q1 availability timeApril 2014 Engine stamp transaction time2026 measures thequarter releases firstfigure commit writes lands sourcerows valid_as_ofcompares available_as_ofcompares as_of compares source publisher 2026 backfill run measurements

An auditor reads documents as of last week and gets the snapshot current then, so a later fold changes no answer (as-of). An instant older than retained history fails and names the oldest answerable instant (as-of-unretained).

A table declaring available_time keeps every revision as a vintage beside the earlier value (vintage-rows).

Flowchart diagram Flowchart, top to bottom. 9 nodes, 8 edges. source publisher → content hash unchanged? [releases revised figure] content hash unchanged? → skipped re-pull [unchanged]; → measurements [changed] measurements → first vintage requested? [read resolves vintage] first vintage requested? → earliest vintage [first]; → availability bound given? [default] availability bound given? → latest vintage before bound [bounded]; → latest vintage [unbounded] releases revised figure unchanged changed read resolves vintage first default bounded unbounded source publisher content hash unchanged? skipped re-pull measurements first vintage requested? earliest vintage availability bound given? latest vintage before bound latest vintage

A document capture commits as one store snapshot that an evaluation case names by id (capture).

When a project binds an encryption key, Parquet files use modular encryption and sidecars use authenticated per-file keys (cipher).

Sync to a bucket

A bucket mirrors the store tree. The [sync] endpoint scheme selects a filesystem, S3 or R2 bucket (schemes). Before trusting conditional writes, a probe writes a sentinel object and tests its conditions (sentinel).

contextful sync manifest --emit prints the plan a push commits, without touching the bucket (plan). A push uploads files the bucket lacks and commits the bucket manifest under If-Match (manifest-commit).

When two writers race, the push merges manifests and retries within a bound (retries). Deletions travel as signed tombstones (tombstone-signed).

contextful sync collect removes tombstoned objects, superseded parts and folded runs but keeps every _manifest.json, so a returning copy of a run reads once (manifest-kept). Each key is recorded first, so no push lists it again (drop-first).

A pull checks each downloaded object against its manifest digest (digest-mismatch). Before downloading anything, a pull refuses a store holding a different identity (identity-conflict).

A laptop replica is a read-only copy that records each snapshot it holds (descriptor). It refreshes from the bucket and refuses writes (write-refused). A query needing a sidecar the replica lacks fails and names the refresh that supplies it (missing-index).

Under a database profile, each run carries an unpushed mark until a push clears it (mark). If the push at shutdown fails, the marks stay and name the runs still unpushed (unfinished).