Mutations

Queries turn facts into answers without changing them. Many projects also need to apply one change across entities that only current knowledge can identify. Stardust defines a Mutation as a query selection plus a Patch template. The definition stays declarative while its execution crosses the write boundary.

One write model

A Mutation builds on the same Merge Patch model as a direct Patch. A direct Patch supplies concrete intended values for known entities. A Mutation first selects entities with a query and then applies the template to each selected row. Null retraction, omission, minimization, and committed effects keep the same meaning in both operations.

This DUST Mutation marks each pending entity as active:

dust96B
query {find  [?entity]
       where [[?entity status pending]]}
patch {?entity {status active}}

The query uses the same Datalog-style clauses as a read. A Patch target can be a variable bound by the selection, such as ?entity. A generated target, such as _new, creates one fresh entity. A generated target requires a selection with exactly one row.

Field values accept every Stardust value, including numbers, lists, maps, and references. A field-value variable substitutes its complete bound value. Null retracts a field.

Mutation definition

Query selection

Current facts

Resolve template per row

Patch template

Minimal logical patch

Schema validation

Atomic commit with head check

Change notification

Resolve before write

Execution runs the complete selection, resolves the template against every selected row, and compares the result with current facts. Values that already match stored facts disappear from the change. Every target, field, and value is concrete before the store writes anything. Macros in the selection expand from one callable snapshot before the query compiles.

When a selection clause names a Schema, commit validates each complete post-patch entity against that schema. An entity that would become invalid rejects the complete Mutation with a Schema_Validation error. The stored facts never pass through the invalid state.

One atomic change

Different selected rows can identify the same entity and field, for example through a join fan-out. Equal values collapse into one effect, while different values for the same entity and field reject the complete Mutation. Row order never chooses durable state.

The selected rows form one atomic logical change. One transaction applies all minimized effects or none of them. Commit uses an optimistic check against the transaction head. When a concurrent commit moves the head, Stardust rejects the Mutation with a Conflict error. It does not retry the Mutation.

Preview and confirm

Execution has two steps. A preview runs the selection only and gives the selected rows, the intended Patch, and the execution cost. Nothing commits. A confirmed execution runs the selection again against current facts, resolves the Patch, and commits.

A confirmed result reports one of three statuses. Committed records a transaction with its fact count. No_Match means the selection chose no rows. No_Change means the Patch already matched current state. Neither of the last two adds a transaction, so suitable repeated Mutations remain idempotent.

A committed Mutation announces its touched entities like any other write, so live query subscribers see the change.

Temporary and durable changes

An inline Mutation supports a one-time change without a durable definition. A repeated operation can become a stored Mutation under definitions/mutations. Stardust validates the definition when it commits: the selection and template must compile, and every Patch variable must be bound by the selection. The stored tree therefore holds only runnable Mutations.

Stored Mutation definitions are reified data, like stored Queries. The generated Go SDK gives each stored Mutation a preview method and an execution method. A direct Patch remains clearer when the targets and intended values are already known. Mutation adds value when the selection depends on current facts.

Live mutations

A Mutation with live true is a standing rule rather than a one-shot call:

dust130B
live     true
priority 1
query    {find  [?entity]
          where [[?entity status pending]]}
patch    {?entity {status active}}

Live Mutations belong to the reactors plugin. A database runs them only when it enables that plugin at open. Without the plugin, the definitions are stored but nothing reacts. No outside worker or subscriber drains a live Mutation: Stardust runs it on the thread that commits every write. A client execution of the same definition still runs once, regardless of live.

A live Mutation rejects keyset page state and historical selections, because each run reads the facts of one source transaction.

Rounds

After a commit, the plugin runs a round for that source transaction. Every live Mutation evaluates its query as of the source transaction. Their Patches merge into one Patch, which commits as one output transaction. When no live Mutation produces a change, the round commits no output transaction.

The merge settles overlapping writes path by path. Identical values are one write, and two objects merge member by member. When two live Mutations write different values to one path, the one with the higher priority wins. An absent priority is 0. Equal priorities fail the round with Reactor_Conflict, which names both live Mutations and the path. The result does not depend on the order in which the definitions were stored.

The combined Patch must satisfy the scoped schemas of every contributing live Mutation. Two Patches that are valid separately can produce an invalid combined document. The plugin validates that document before committing. A schema rejection records Schema_Validation and writes no output transaction.

A round always uses the current definitions. A live Mutation reacts only to transactions from the one that last committed its definition. A new or edited live Mutation therefore does not replay earlier history.

Durable progress

The plugin keeps one durable cursor that names the last source transaction it processed. When a round produces output, the output facts, the cursor advance and causation metadata commit in one transaction. A crash exposes both or neither. When a round produces no output, only the cursor advances.

Commit notifications only wake the runtime. Each round reads the transactions after the cursor, so correctness does not depend on every notification arriving. One round processes at most 64 source transactions, then yields the write path to other commits and continues. After a restart, or after a period with the plugin disabled, the next commit or sync resumes from the cursor and does not process a source transaction twice.

Chains and loop protection

An output transaction is a source transaction in turn, so live Mutations can chain. Each round of a chain commits one output. Each output records the source transaction that caused it, the live Mutations that contributed to it, its depth, and a correlation root: the first transaction of the chain that is not an output.

  • A live Mutation does not react to an output that it contributed to, so a Patch that writes a field its own selection reads settles after one output.
  • The first output of a chain has depth 0. An output deeper than 64 fails with Chain_Depth before it writes.
  • One correlation root can produce at most 4096 outputs. Further work fails with Correlation_Cap.

Failure

Most errors recur on every retry, such as a selection error, a Schema_Validation rejection, a Reactor_Conflict or a cap breach. The plugin records them as a failure at once. A commit conflict or a store error is transient. The plugin retries it after 1, 2, 4, 8 and 16 seconds. When the attempt after the last delay also fails, the error is recorded as a failure.

A failure is durable. It records the source transaction, the error code, the live Mutations involved and, for a conflict, the path. The cursor stays before the source transaction, and no later transaction is processed. The failure stands until a live Mutation definition is added, edited or removed. The next round then retries the same source transaction with the new definitions.

Hosts observe the plugin through two operations. reactors_status returns the cursor, the head, whether transactions are pending, each live Mutation, and the standing failure. reactors_sync waits until the plugin processed every transaction committed before the call. It reports failed while a failure stands. Both report unavailable on a database that does not enable the plugin.

A Mutation defines one deterministic change against current facts. Some changes identify large media or other payloads that should not become ordinary fact Components. Binaries keeps those payloads separate from facts.