Skip to main content
A mutable dataspace is a table of current rows: many live records per device, last-write-wins on version, deletes as tombstones. It is the ClickHouse sibling of state — same idea, different scale and guarantees. Use it for edge relational mirrors (work orders, inventory, child rows) and cloud catalogs you upsert and scan at analytics volume. The API modality is mutable. All modalities

When to use it

Use mutable when
  • Each source holds many current rows, keyed by a business primary key.
  • You upsert and delete as a bulk replica, not as one document per device.
  • Columnar scans and filters matter more than GET-after-PUT.
  • Last-write-wins on a monotonic version is enough. A missed change event on crash is acceptable.
Use state instead when you need HTTP row CRUD, read-your-write, downsync of a small keyset, or a change event that commits with the row (shadow, e-stop, settings). Use telemetry instead when rows are samples you will never update. Do not use mutable for compare-and-set (UPDATE … WHERE status = 'running'). ReplacingMergeTree has no row locks. Job/claim state belongs in ordinary platform tables.

Backing store

Mutable lives in ClickHouse as ReplacingMergeTree(version, is_deleted) (replicated variant in clustered deployments).

How reads collapse versions

A raw SELECT on the hot table is not last-value-wins while extra versions still sit unmerged. The read view collapses for you:
  • Group by (source_id, natural_key)
  • Take argMax(column, version) for each projected field
  • Hide keys whose latest version is a tombstone (HAVING argMax(is_deleted, version) = 0)
Query through that view (QueryScript dataspace name, or catalog read_view). Do not scan the physical table and assume one row per key.
Read-your-write is not guaranteed the instant after an insert. The view is correct once it aggregates versions, but this is not Postgres. That is why mutable has no HTTP row CRUD. An API whose GET after PUT is usually right is worse than no API.

Identity and natural_key

Mutable requires a natural key at create. A keyed table with no identity is rejected.
  • source_id — which producer (device id on the edge path).
  • natural_key — the row’s business identity. You declare which source fields compose it (natural_key: ["sku"] or a composite, in order). The table stores a single natural_key column plus source_id; the catalog remembers the field list.
Edge mirrors typically hash the SQLite primary key into natural_key and set source_id to the device. Cloud producers should send a stable encoding of the same fields on every write and retry. version must be stable across redeliveries — the producer’s logical version (commit_seq, source_commit_seq), never wall clock. A clock default would turn last-write-wins into last-arrival-wins and emit duplicate events. You can rekey only if the dataspace currently has no key. Mutable always has a key, so identity is fixed at create.

Meta columns

In addition to the ClickHouse meta set (source_type, source_id, event_ts, ingest_ts): event_ts is present but not the sort or partition key. Mutable rows are not a time series.

Schema and layouts

Default hybrid so a mirrored SQLite table can ingest before every column is approved. Sync-tagged dataspaces force review before promotion.

Events and edge

Mutable does emit row-changed events (full row) so cloud-authoritative tables can downsync. The ClickHouse insert and the Postgres outbox row are not one transaction. A crash between them can miss an event or require the poll sweep. Treat delivery as at-least-once and best-effort relative to state. Consumers must be idempotent on identity + version. Edge:
  • Policy strategies rows, mutable, relational, row_batch provision a mutable dataspace.
  • Upsync always. Downsync only if the table’s authority is cloud.
  • Device-authoritative mirrors still upsync; the cloud does not push those rows down.
  • Tombstones replicate: a source DELETE is an is_deleted insert, so a stale replay stays a read-time no-op.

Mutable vs state

If you are unsure and the table has a primary key and UPDATE/DELETE in SQLite, it is almost always mutable. If it is “the device’s current JSON blob”, it is state.

Create

$API is the platform base URL.