Current section
Files
Jump to
Current section
Files
statifier_persistence
CHANGELOG.md
CHANGELOG.md
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Entries for unreleased work are not written here directly. Each issue drops a
fragment in [`changelog.d/`](changelog.d/README.md); the fragments are assembled
into a version section at release. See that README for the format and for when a
change warrants an entry at all.
## [0.12.0] 2026-09-13
Breaking release: `run` is retired as the noun for the durable record, and
the package speaks `execution` throughout - modules, functions, callbacks,
types, error atoms, telemetry events and metadata, the durable table and its
identity column (ADR-0011). No behaviour, arity or return shape changed; only
names did, and no compatibility shim, deprecated delegate or alias module
ships.
**Breaking for storage adapters**: all seven
`StatifierPersistence.Storage.Adapter` callbacks rename, and the table under
Changed below lists every one of them - the compiler names each in turn.
Hosts also rename the `run_id:` context key, this package's `:run_*` error
atoms, the `:tables` key (`:runs` is now rejected), and any raw query naming
`statifier_runs`; telemetry handlers must move to the `:execution` prefix,
which does not dual-emit. Existing databases are migrated **in place** by the
new **V06**, which copies no data; drain in-flight durable subchart children
before upgrading.
### Changed
- **Breaking for storage adapters.** `run` is retired as the noun for the
durable record; it is `execution` everywhere (ADR-0011). Nothing about the
behaviour, the arities, the return shapes or the identity guard changed -
only names. This entry is the complete list of what moved. Rename the seven
`StatifierPersistence.Storage.Adapter` callbacks below in your adapter; the
compiler names every one of them, and no compatibility shim, deprecated
delegate or alias module ships.
| Callback before | After |
|---|---|
| `insert_run/2` | `insert_execution/2` |
| `fetch_run/2` | `fetch_execution/2` |
| `update_run/2` | `update_execution/2` |
| `lock_run/3` | `lock_execution/3` |
| `list_runs_by_metadata/2` | `list_executions_by_metadata/2` |
| `supports_run_outcome?/1` | `supports_execution_outcome?/1` |
| `list_run_states_by_metadata/2` | `list_execution_states_by_metadata/2` |
`append_input/3` and `list_inputs/2` keep their names; their `run_id()`
parameter is now `execution_id()`.
- **Breaking for a host-supplied serialization strategy.** The second
behaviour a host may implement renames its one callback:
`StatifierPersistence.Serialization.with_run/3` is now `with_execution/3`,
and the shipped `Serialization.AdapterLock.with_run/3` is now
`AdapterLock.with_execution/3`. A host that left the strategy at its default
implements nothing and is unaffected. The two module names are unchanged -
they name a strategy, not the durable record.
- **Breaking, silently, for a host effect executor and a `:dispatch`
function.** The `run_id:` key of `StatifierPersistence.Executor.context/0`
and of `StatifierPersistence.Driver.dispatch_context/0` is now
`execution_id:`. An implementation that pattern-matches `%{run_id: id}`
raises at the first effect; one that reads the key by name gets `nil`.
Rename the key in both.
- **Breaking for a host that pattern-matches this package's error atoms.**
`:run_exists`, `:run_not_found`, `:run_outcome_unsupported`,
`:run_states_unsupported`, `:run_position_missing` are now
`:execution_exists`, `:execution_not_found`,
`:execution_outcome_unsupported`, `:execution_states_unsupported`,
`:execution_position_missing`. A `case` with no catch-all raises; one with a
catch-all quietly reclassifies a known refusal.
- **Breaking for anything subscribed to this package's telemetry.** The event
family is now `[:statifier_persistence, :execution, :step, :start | :stop]`,
`[:statifier_persistence, :execution, :lock]` and
`[:statifier_persistence, :execution, :created | :terminated | :discarded]`.
There is no dual emit: a handler attached to the old `:run` names goes
silent with no error, so grep your handlers for the old prefix as part of
the upgrade. `opentelemetry_statifier` 0.6.0 moves in lockstep.
- Telemetry metadata keys `run_id`, `parent_run_id` and `child_run_id` are now
`execution_id`, `parent_execution_id` and `child_execution_id` on every
event that carries them, including the events whose own names did not
change (`[:statifier_persistence, :adapter, :call]`, `:identity, :refused`,
`:effect, :failed`, `:drive, :turns_exhausted` and the six
`[:statifier_persistence, :child, ...]` events).
- Two documented telemetry metadata *values* rename with them: `stage: :run`
becomes `stage: :execution` on `[:statifier_persistence, :identity,
:refused]`, and `reason: :terminal_run` becomes `reason:
:terminal_execution` on `[:statifier_persistence, :execution, :discarded]`.
- The six documented `StatifierPersistence.Telemetry` emitters rename with
their events: `run_step_start/3`, `run_step_stop/2`, `run_lock/2`,
`run_created/1`, `run_terminated/1` and `run_discarded/1` are now
`execution_step_start/3`, `execution_step_stop/2`, `execution_lock/2`,
`execution_created/1`, `execution_terminated/1` and
`execution_discarded/1`. The other ten emitters keep their names.
- Modules: `StatifierPersistence.Run` is now `StatifierPersistence.Execution`
(struct field `:run_id` is now `:execution_id`), `StatifierPersistence.Runs`
is now `StatifierPersistence.Executions`, and
`StatifierPersistence.Run.Linkage` is now
`StatifierPersistence.Execution.Linkage`, whose `child_run_id/3` is now
`child_execution_id/3`. The lifecycle doors keep their own names and
arities: `Executions.create/4`, `step/5`, `fail/4`, `cancel/3`,
`cascade_cancel/3` and `inputs/2`.
- `StatifierPersistence.Storage` renames nine functions, arities unchanged:
`insert_run/5`, `update_run/5`, `update_run_status/4`, `fetch_run/2`,
`run_outcome_supported?/1`, `run_states_supported?/1`,
`list_run_states_by_metadata/2`, `list_runs_by_metadata/2` and
`load_run_position/3` become `insert_execution/5`, `update_execution/5`,
`update_execution_status/4`, `fetch_execution/2`,
`execution_outcome_supported?/1`, `execution_states_supported?/1`,
`list_execution_states_by_metadata/2`, `list_executions_by_metadata/2` and
`load_execution_position/3`.
- Types: `Storage.Adapter.run_id/0`, `run_status/0`, `run_record/0` and
`run_state/0` are now `execution_id/0`, `execution_status/0`,
`execution_record/0` and `execution_state/0`, and the `run_id:` key inside
`execution_record/0`, `execution_state/0`, `input_record/0` and
`Storage.input/0` is now `execution_id:`. `Runs.run_id/0` is now
`Executions.execution_id/0`, and `Storage.run_write_opt/0` is now
`Storage.execution_write_opt/0`.
- The reserved child-linkage metadata key written into a child's `metadata`
map is now `"parent_execution_id"`, and `Execution.Linkage`'s struct field
is `:parent_execution_id`. A child written by 0.11.x carries the old key;
re-key it or let those executions finish under 0.11.x.
- `use StatifierPersistence.Ecto` now generates `MyApp.Persistence.Execution`
instead of `MyApp.Persistence.Run`, and the executions and inputs schemas
expose the field as `execution_id` instead of `run_id`. A host naming the
module or the field in its own queries renames both.
- New surrogate ids for that table carry the `exec_` prefix instead of
`run_`. Existing rows keep the ids they have - nothing rewrites them - so
both spellings coexist permanently on an upgraded install. Nothing in this
package parses a prefix and no host should; one that does must accept both.
- The reserved `<donedata>` key a chart writes to fail itself is now
`statifier_persistence:execution_status`.
- **Breaking for hosts.** The durable table is now `statifier_executions`,
and its identity column - in that table and in `statifier_inputs` - is now
`execution_id` (ADR-0011 decision 3). A new migration, **V06**, makes the
move on an existing database: it renames the table, both columns, both
unique indexes and the `metadata` GIN index **in place**, copying no data,
and it is a no-op on a database created at 0.12.0 or later. Run
`StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence, from: 6)`
from an ordinary host migration; a fresh install gets the new names from
V01 and needs nothing extra. That one-line upgrade is for an install
already at V05: one capped below V04, which still owes V02, V03 or V04,
runs `up(for: MyApp.Persistence, from: 6, version: 6)` **first** and
then `up(for: MyApp.Persistence, from: <its cap + 1>, version: 5)` -
`from: 2` for a host capped at V01, `from: 3` at V02, `from: 4` at V03 -
because V02-V04 alter the executions table, which on such a database
carries that name only once V06 has renamed it; the migration's `down`
mirrors the two calls in reverse,
`down(for: MyApp.Persistence, from: 5, version: <its cap + 1>)` and then
`down(for: MyApp.Persistence, from: 6, version: 6)`. Rename the table in
any raw query, view,
materialized view, hand-written Ecto schema or dashboard of your own that
names it - it is `statifier_executions` on both paths. "In place" is exact
for the table and the columns on every backend; off Postgres, which has no
`ALTER INDEX ... RENAME TO`, the two unique indexes are dropped and
declared again under their new names instead, which still copies no data.
- **Breaking for a host that overrides table names.** The `:tables` key for
this table is now `:executions`; `:runs` is rejected with
`ArgumentError`, and no alias ships for a release. Rename the key in your
`use StatifierPersistence.Ecto` options. A `:tables` override's *value* is
untouched: V06 renames the columns and indexes under whatever name you
gave, and the table keeps that name.
- **Rolling back drops the tables under their new names.** V01-V05 are
rewritten to the new noun and drop the tables under those names, so
`StatifierPersistence.Ecto.Migrations.down(for: MyApp.Persistence)`
still removes everything this package owns, on a fresh install and on an
upgraded one alike. What it is not is a downgrade: to run 0.11.x again,
restore a backup or migrate up with 0.11.x's own migrations.
- **An in-flight durable subchart child does not survive the upgrade.** A
child created under 0.11.x carries its parent link in `metadata` under the
pre-0.12.0 key, and V06 renames no stored value - it is a catalog
operation and copies no data. `Execution.Linkage.from_metadata/1`
therefore answers `:no_linkage` for such a child, and its completion no
longer settles its parent's fan-out. **Drain your in-flight children
before upgrading**: let every durable subchart child reach a terminal
status under 0.11.x, then upgrade. Children created at 0.12.0 or later are
unaffected.
- The migration helper now knows six versions:
`StatifierPersistence.Ecto.Migrations.expected_version/0` answers `6`.
- The surrogate-key table map renames with the table key:
`t:StatifierPersistence.Ecto.KeyGenerator.table/0` is now
`:charts | :positions | :executions | :inputs`, and the shipped UXID
generator's prefix for that table is `"exec"` (it was already `"exec"` in
0.11.x under the old key). A host with its own `Ecto.KeyGenerator`
implementation renames the atom it matches on.
- V06's `down/1` is a no-op, so a rollback never renames the durable table
back to its pre-0.12.0 name. V01-V05 drop the tables under the execution
names on every install this package can reach at 0.12.0, and returning to
the retired names would only be meaningful under a downgrade to
pre-0.12.0 code, which is unsupported - restore from a backup instead.
This is what makes `mix ecto.rollback --all` work for a host that writes
one migration per package version: a conditional rename would run in its
own rollback step and the steps behind it would then name objects that
are no longer there.
### Deprecated
- The pre-0.12.0 `<donedata>` key,
`statifier_persistence:run_status`, is still **read** in this release and is
**dropped in 0.13.0**: where both are present the new key wins, and reading
the old one logs one deprecation line at `:debug` naming the new key. Update
your charts' `<param name="...">` before 0.13.0. It is the only
transitional reader this rename ships.
## [0.11.0] 2026-09-08
Feature release: a durably stepped run can now report itself as it goes.
`StatifierPersistence.Driver.new/3` takes an `after_step:` callback, fired
after every step the driver takes on a caller's behalf - a durable subchart
child's own steps and the parent's step on the answer path alike - so a host
can trace, project or checkpoint from one seam instead of wrapping each entry
point. Alongside it, `StatifierPersistence.Testing.StorageConformance` no
longer writes from a `setup`, so an adapter's own `setup` runs first.
### Added
- `StatifierPersistence.Driver.new/3` takes `after_step:`, a
`(run_id, machine_state, effects -> any)` callback fired after every step
the driver takes on a caller's behalf - a durable subchart child's own
steps and the parent's step on the answer path included, each under the id
of the run that was stepped, with the whole effect list that step produced.
It defaults to `nil` and may be overridden per call on `create/3`,
`send_event/4`, `done_invocation/5` and `failed_invocation/5`.
### Fixed
- `StatifierPersistence.Testing.StorageConformance` no longer registers a
`setup` that writes: the input-log cases build their fixture run inside
the case body, so a host's own `setup` - even one written below the
`use` - is no longer preceded by a write. The moduledoc states the
ordering contract a host binds against.
## [0.10.0] 2026-09-06
Feature release: a durable subchart child failed from outside the
interpreter now settles its parent's pending `<invoke>` instead of leaving
it hanging forever. `StatifierPersistence.Runs.fail/4` takes a `driver:`
option and answers the parent itself, and
`StatifierPersistence.Driver.resolve_and_answer_parent/3` is the public
form of that answer for a caller with no drive of the child to hang it
off. Alongside it, `StatifierPersistence.Ecto.Migrations.expected_version/0`
names the newest migration version this package knows, for a host whose
schema is hand-written DDL and which therefore has to check for itself
that its tables are current.
### Added
- `StatifierPersistence.Runs.fail/4` takes a `driver:` option: a durable
subchart child failed from outside the interpreter now answers its
parent's `<invoke>` with the failure instead of leaving it pending
forever (ADR-0008's note on the outside-fail seam).
- Adds `StatifierPersistence.Driver.resolve_and_answer_parent/3`, the
public form of the automatic answer - resolve the parent's chart through
`chart_resolver:`, then answer through `answer_parent/3` - for a caller
that has no drive of the child to hang it off.
- `StatifierPersistence.Ecto.Migrations.expected_version/0` returns the
newest migration version this package knows, for a host whose schema is
hand-written DDL rather than a delegated migration and which therefore has
to check for itself that its tables are current. There is no
`assert_version!/1` to go with it: the package records no version marker in
a repo's schema, so the comparison stays the host's - the function's docs
say why.
### Documentation
- ADR-0010 takes a note answering whether a host needs the V05 input log
table at all. An adapter that does not export the optional input-log
callbacks never touches it - a host on one caps its migration at V04 in
both directions rather than carrying an empty table - while a host storing
through `StatifierPersistence.Storage.Ecto` needs it unconditionally,
because that adapter declares input-log support without probing.
## [0.9.0] 2026-09-06
Feature release: a durably stepped run can now keep a verbatim log of every
input its interpreter saw, and a fan-out's settlement is visible in
telemetry. The storage-adapter behaviour gains three optional input-log
callbacks (`supports_input_log?/1`, `append_input/3`, `list_inputs/2`) and
`StatifierPersistence.Runs.inputs/2` reads the log back; `Storage.Ecto`
implements them on Postgres and SQLite alike through migration V05, with an
`input_log_cap:` bound and no log at all for an adapter that does not export
the callbacks (ADR-0010, accepted). Two new events,
`[:statifier_persistence, :child, :recorded]` and `:settled`, surface the
per-child answers and the settlement decision that reach no door, and
`child.answered`'s `outcome` is now the invocation's rather than the door's,
so a fan-out that failed no longer reports `:done`. And
`Storage.Ecto.list_runs_by_metadata/2` and `list_run_states_by_metadata/2`
refuse cleanly with `{:error, :metadata_unsupported}` off Postgres instead
of raising from the driver.
### Added
- Adds an optional per-run input log to the storage-adapter behaviour
(`supports_input_log?/1`, `append_input/3`, `list_inputs/2`): an adapter
that exports them records every input a run's interpreter saw - the
verbatim `%Statifier.Event{}`, the public door it entered by, and a
dense zero-based ordinal - which is what an offline replay of a durably
stepped run needs (ADR-0010).
- Adds `StatifierPersistence.Runs.inputs/2` and
`StatifierPersistence.Storage.input_log_supported?/1`,
`append_input/4` and `list_inputs/2` for reading and writing that log.
- Adds migration V05, the input log table, on Postgres and SQLite alike;
`StatifierPersistence.Storage.Ecto` implements all three callbacks and
takes an `input_log_cap:` option that bounds a run's log and closes it
with a marker entry rather than truncating it silently. The default is
`:infinity`.
- `:blob_type` now reaches the new `input_blob` column. An event's `data`
is host payload, so turning the log on is a data-retention decision:
an adapter that does not export `supports_input_log?/1` keeps no log
and behaves exactly as it did before.
- Adds `[:statifier_persistence, :child, :recorded]`, once per fan-out
child answer written under the parent's settlement exclusion. Every
index but the settling one records an answer that reaches no door, so
this is the only surface those answers appear on (ADR-0009's sp-8wv
amendment).
- Adds `[:statifier_persistence, :child, :settled]`, once per settlement
decision, carrying the invocation's `policy`, the `:answer` /
`:not_yet` decision, and the completed / failed / cancelled / unstarted
tallies it was decided from.
- `[:statifier_persistence, :run, :step, :stop]` now carries `invoke_id`
and `child_count`, `nil` on an ordinary drive and set on the
`entry: :answer_parent` step, so the step span that delivers a whole
fan-out's assembled answer is recognisable as that one.
### Changed
- `[:statifier_persistence, :child, :answered]`'s `outcome` is now the
**invocation's** for a fan-out, not the door's: `:failed` when any index
failed. A fan-out always answers its parent through `done_invocation/5`
- the failure shape is inside each entry - so the event previously said
`outcome: :done` for a settlement that had failed. It also gains
`child_count` and `failed_count`, both `nil` for a single-child
subchart, which is not an invocation with a width. A consumer counting
`outcome` across fan-outs will see failures it did not see before.
- `StatifierPersistence.Storage.Ecto.list_runs_by_metadata/2` and
`list_run_states_by_metadata/2` now return
`{:error, :metadata_unsupported}` on a backend that is not Postgres,
where they previously raised from the driver on `jsonb` containment SQL
it cannot parse. Both consult `supports_metadata?/1` before issuing
anything, so a host calling the raw adapter callback gets the same clean
refusal `StatifierPersistence.Storage` already gave through the facade.
A behaviour change on two adapter callbacks: code rescuing the raise
sees a tagged tuple instead. Nothing changes on Postgres, and the facade
is untouched.
## [0.8.0] 2026-09-06
Feature release: a chart can now report that its own run failed, and the
Ecto adapter gains room to move on large and on non-Postgres hosts. A
top-level `<final>` whose `<donedata>` sets `run_status` to `"failed"`
persists the run as `:failed` so `:first_error` settlement fires (the
ADR-0008 amendment, accepted). V04 of the DDL rebuilds V03's `metadata`
GIN index with `CREATE INDEX CONCURRENTLY` - `@current_version` is now 4,
and a host runs V04 as a migration of its own carrying
`@disable_ddl_transaction true` and `@disable_migration_lock true`. A host
that is not on Postgres has a documented opt-out: decline `lock_run/3`
with its own `serialization:` strategy and run the conformance suite with
`--exclude postgres`. And `Migrations.down/1` takes `from:`, the ceiling a
capped migration needs to roll all the way back.
### Added
- A chart can now fail its own run: settling in a top-level `<final>` whose
`<donedata>` carries `statifier_persistence:run_status` set to `"failed"`
persists the run as `:failed` with the `failure` string `"failed_final"`,
so a `:first_error` fan-out cancels the failed child's siblings with no
host-side translation.
- `docs/non-postgres-backends.md`: the supported way to run the Ecto adapter
on a backend that is not Postgres - decline `lock_run/3` with your own
`serialization:` strategy, what declining costs, and how to verify it.
- `StatifierPersistence.Ecto.Migrations.down/1` takes `from:`, the version it
starts rolling back from (default: the newest this package knows), so a
migration capped with `up(version: 2)` caps its rollback with
`down(from: 2)`.
- V04 of the Ecto DDL rebuilds V03's `metadata` GIN index with `CREATE INDEX
CONCURRENTLY`, so a host with a large runs table gets the index without the
`SHARE` lock a plain build holds. Give it a migration of its own carrying
`@disable_ddl_transaction true` and `@disable_migration_lock true` - Ecto
reads those from your module, not from the helper - and call
`StatifierPersistence.Ecto.Migrations.up(for: MyApp.Persistence, from: 4)`.
Called from an ordinary transactional migration it leaves V03's index in
place instead of raising, warning when the runs table already holds rows.
It is a no-op off `Ecto.Adapters.Postgres`, where V03 creates no index.
### Changed
- The conformance suite tags its four Postgres-only cases `@tag :postgres` -
the two `lock_run/3` cases and the two metadata-listing cases - so a host
running `Storage.Ecto` on another Ecto backend runs it green with
`mix test --exclude postgres` instead of forking the suite.
### Fixed
- `mix ecto.rollback --all` no longer fails for a host that caps one
migration and takes a later version in another: without a ceiling every
`down/1` started at the newest version, so the capped migration rolled the
later one's versions back a second time and failed on DDL that was already
gone. Cap the rollback with `from:` as above.
## [0.7.2] 2026-09-06
Patch release: a fan-out whose children settle at the same time assembles
with every child's donedata present. A settlement used to read a sibling's
status as terminal while that sibling's answer was still in flight, and
assembled a completed child with a `nil` donedata; a settlement now records
its own answer under the parent's exclusion and waits for every child's
recorded answer, not only for every child's terminal status.
### Fixed
- A fan-out invocation whose children settle concurrently no longer answers with
a `nil` donedata for a child that completed: a settlement waits for every
child's answer to be recorded, not only for every child's status to be
terminal, and records its own answer under the parent's exclusion.
## [0.7.1] 2026-09-05
Patch release: hosts that are not on Postgres can apply the package DDL.
V03 creates its `metadata` GIN index only on Postgres, and the Ecto
adapter now answers `supports_metadata?/1` by adapter, so a store that
could not settle a durable subchart or a fan-out is refused at open
rather than crashing partway through.
### Fixed
- V03 of the Ecto migration helper is adapter-aware: the `metadata` GIN
`jsonb_path_ops` index is created (and dropped) only when the
migration's repo runs on `Ecto.Adapters.Postgres`. On any other adapter
it is skipped and the migration runs to completion, so a host on
another backend can apply the package DDL at all - in 0.7.0 the index
raised, the whole migration rolled back, and the `outcome_blob` column
went with it. The column itself is still created on every adapter, and
`StatifierPersistence.Storage.Ecto.supports_run_outcome?/1` is still
true everywhere.
- `StatifierPersistence.Storage.Ecto.supports_metadata?/1` now answers
`false` off Postgres, because both metadata queries the adapter issues
are `jsonb` containment SQL. `StatifierPersistence.Storage`'s
`child_listing_supported?/1` and `run_states_supported?/1` consult it,
so a durable subchart or a fan-out over such a store is refused at open
(`:child_listing_unsupported`) rather than started and left with
children nothing could settle. Behavior on Postgres is unchanged.
## [0.7.0] 2026-09-05
Feature release: a scheduler can start a Tier A fan-out through a public
door, and the N children settle once as a dense, index-ordered list instead
of each answering the parent. Hosts on the Ecto adapter run a new V03
migration.
### Added
- `StatifierPersistence.Driver.start_child_at/6`: the public
start-with-index door a scheduler drives a Tier A fan-out through.
Starts child `i` of `N` for a parent's `<invoke>`, records the count and
the aggregation policy (`:all` or `:first_error`) on the child's
linkage, and is idempotent on the derived child run id, so a
re-delivered start adopts rather than duplicating. Refuses at open on a
store that could not settle the invocation afterwards.
- `StatifierPersistence.Run.Linkage.new/6`
and `fan_out?/1`: a child's
linkage can now carry its invocation's `child_count` and aggregation
policy (`:all` or `:first_error`), which is what marks it as one of a
fan-out's N rather than an ordinary durable subchart.
### Changed
- The storage-adapter `run_record` gains a nullable `outcome_blob`: a
run's own answer, written once when it reaches a terminal status
through `StatifierPersistence.Storage.update_run_status/4`'s new
`outcome_blob:` option. `update_run/2` carries a stored payload forward
when the record it is given carries none, so an ordinary step never
erases one. Adapters gain two optional callbacks alongside it -
`supports_run_outcome?/1` and `list_run_states_by_metadata/2`, the
indexed status projection - and an adapter that exports neither is
conformant unchanged.
- The Ecto adapter's V03 migration adds the `outcome_blob` column and a
GIN `jsonb_path_ops` index on `metadata`. A host already on V02 picks
it up with `StatifierPersistence.Ecto.Migrations.up(for: MyApp, from: 3)`.
Run it before deploying 0.7.0, and read the README's "Upgrading to V03
before deploying 0.7.0" first: the index build is a plain
`CREATE INDEX` that blocks writes to the runs table for the length of
the build.
- `StatifierPersistence.Driver` takes a `child_canceller:` option: how a
`:first_error` settlement asks the scheduler to cancel the start jobs of
a fan-out's not-yet-started children, which have no run record for the
cascade to reach.
- A fan-out child's completion now settles instead of answering its
parent's door: its answer is stored on its own run record, and only the
settlement that finds all N indices terminal assembles the dense,
index-ordered list and answers the invocation once.
`StatifierPersistence.Driver.answer_parent/3` routes a fan-out child the
same way and returns `:ok` for it. A child with no `child_count` on its
linkage - every child created before this release - is unaffected.
## [0.6.0] 2026-09-02
Feature release: a durably-stepped run is observable through statifier's own
session telemetry, so the OpenTelemetry bridge produces the same spans and
effect events for a durable run as for a session-hosted one.
### Added
- Durably-stepped runs now emit statifier's own `[:statifier, :session, ...]`
telemetry with `driver: :persistence`, so `opentelemetry_statifier` produces
the same macrostep spans and effect events for a durable run as for a
session-hosted one, with no bridge change.
## [0.5.0] 2026-09-01
Feature release: the durable step is observable, and a `:dispatch` fun can
see the whole `<invoke>` it is being handed.
### Added
- `StatifierPersistence.Driver.dispatch_context/0` carries `:invoke`, the whole
`Statifier.Effect.Invoke` being dispatched, so a `:dispatch` fun can read the
element's `src` - the document id a subchart handler resolves its child chart
by - along with `content`, `autoforward`, and the step counters.
- `StatifierPersistence.Telemetry` emits the fourteen `[:statifier_persistence,
...]` events ADR-0009 specifies - the durable step as a `:start`/`:stop` pair,
the per-run lock wait, every storage-adapter call, identity refusals, the run
lifecycle, executor failures, and the durable-subchart seam - and `events/0`
returns every name for a bridge to attach to.
- Adds a direct `:telemetry` dependency (already present transitively through
`statifier`, so no lock file grows).
## [0.4.0] 2026-09-01
Feature release: durable subcharts (ADR-0008) - an `<invoke>` may start a
child chart as an ordinary durable run, the parent rests holding no process
while the child runs, and leaving the invoking state cancels the child
subtree.
**Breaking for storage adapters**: `run_status/0` gains a fourth terminal
value, `:cancelled`. An adapter that encodes run statuses by an exhaustive
match must add a clause for it before upgrading, or cancelled runs will fail
to persist. The two adapters in this package already handle it.
### Added
- Durable subcharts (ADR-0008): a `<invoke>` whose `:dispatch` fun answers
`{:start_child, invoke, {:invoke, invoke}}` now starts the subchart as an
ordinary durable run instead of being refused, and the parent rests holding
no process for as long as the child takes.
- `StatifierPersistence.Run.Linkage` records a child's parent run id,
invocation id, and a mandatory pin of the child's chart identity under a
reserved key in the child's run `metadata`.
- `StatifierPersistence.Runs.create/4` takes `linkage:`, and raises
`ArgumentError` when a host's own `metadata:` writes into the reserved key.
- `StatifierPersistence.Driver.new/3` takes `chart_resolver:`, which lets a
finished child answer its parent through the existing `done_invocation/5`
and `failed_invocation/5` doors.
- `StatifierPersistence.Runs.cancel/3` and `cascade_cancel/3` cancel a
parent's child subtree when it leaves the invoking state, retaining every
record and position.
- Storage adapters may export the optional `list_runs_by_metadata/2`, reached
through `StatifierPersistence.Storage.list_runs_by_metadata/2` and
`child_listing_supported?/1`; a store whose adapter does not export it
refuses a durable subchart before any write.
- `StatifierPersistence.Storage.InMemory` implements `list_runs_by_metadata/2`,
which `StatifierPersistence.Storage.Ecto` already supported.
### Changed
- `StatifierPersistence.Storage.Adapter.run_status/0` gains a fourth terminal
value, `:cancelled`. An adapter that encodes statuses by an exhaustive match
must add a clause for it, or cancelled runs will fail to persist.
- `StatifierPersistence.Run` gains `donedata`, set only on the step that
completes a run and `nil` everywhere else.
## [0.3.0] 2026-09-01
Feature release: an asynchronous invocation seam on the durable driver - a
dispatch may answer `:pending` and the run rests holding no process, with
public doors that answer the invocation later from any process or node.
### Added
- `StatifierPersistence.Driver`'s `:dispatch` fun may answer `:pending`: the
call was started asynchronously and the run rests durably with the
invocation live, holding no process (ADR-0007).
- `StatifierPersistence.Driver.done_invocation/5` and
`StatifierPersistence.Driver.failed_invocation/5` answer a pending
invocation later, from any process or node, building the same
`done.invoke` / `error.communication.invoke` events a live
`Statifier.Session` builds. An answer for an invocation the chart has
cancelled is `{:discarded, run}`, decided from the persisted position
inside the run's serialization strategy.
- `StatifierPersistence.Runs.step/5` accepts an event builder - a fun over
the loaded position returning `{:ok, event}` or `:discard` - anywhere it
accepts a `Statifier.Event`.
### Changed
- The context handed to a `:dispatch` fun carries `invoke_id`, the
invocation id an asynchronous host keys its work by and hands back to the
re-entry doors.
## [0.2.0] 2026-08-31
Feature release: a durable run-to-quiescence driver, opaque run metadata,
and a custom blob type for encryption at rest.
### Added
- `StatifierPersistence.Driver` drives a durable run to quiescence over
`StatifierPersistence.Runs`: it performs each `<invoke>` through a
host-supplied dispatch fun inside the step that emitted it, then steps
every answer back in until the chart rests. Hosts that hand-rolled this
loop can delete it.
- `StatifierPersistence.Driver` builds an invocation's answer events -
`done.invoke.<id>` and `error.communication.invoke.<id>`, `origin` and
`origintype` included - field for field from `Statifier.Session`'s own
`done_invocation/3` and `failed_invocation/3`, so a chart sees the same
event in a session and out of storage. A conformance test answers one
document both ways and compares what each chart saw.
- `StatifierPersistence.Runs.create/4` and
`StatifierPersistence.Storage.insert_run/5` accept an optional
`metadata:` map of string keys, stored opaquely beside the run record
and returned by `fetch_run/2` unchanged (ADR-0006). Host identities
only, never personal data: blob encryption does not reach this column.
- `StatifierPersistence.Storage.Adapter` gains the optional
`supports_metadata?/1` callback and a `metadata` field on `run_record`.
An adapter that does not export it refuses a non-empty map at create
with `{:error, :metadata_unsupported}`; an empty or absent map is never
refused, so every existing adapter stays conformant unchanged.
- `StatifierPersistence.Storage.metadata_supported?/1` and
`check_metadata/2` report whether a store's adapter can hold metadata,
without writing anything.
- `StatifierPersistence.Storage.Ecto.list_runs_by_metadata/2` lists the
runs whose metadata contains every given key/value pair.
- Migration V02 adds a nullable `jsonb` `metadata` column to the runs
table, and `StatifierPersistence.Ecto.Migrations.up/1` accepts `from:`
so a host already on V01 applies later versions in its own second
migration.
- `StatifierPersistence.Testing.StorageConformance` gains metadata cases:
a conformant adapter either round-trips the map or refuses it at open,
and never silently drops it.
- `use StatifierPersistence.Ecto` accepts a `:blob_type` option to put a custom Ecto type on the three blob columns (`identity_blob`, `chart_blob`, `position_blob`), enabling encryption at rest with no wrapping adapter.
### Changed
- Requires `statifier` `~> 2.2 and >= 2.2.1` rather than `~> 2.0`: 2.2.1 is
the first release carrying the queue-discard-on-exit fix the completion
conformance cases need. (An interim git-ref pin served between 2.2.0 and
that release.)
### Fixed
- A run whose top-level `<final>` is reached while sibling `done.state.*` events
are still queued now persists as `completed`, instead of raising
"loop bug: non-quiescent MachineState reached the persist tail". The same
holds for a top-level `<final>` whose `<donedata>` expression fails.
- `StatifierPersistence.Runs.create/4` passes only its `metadata:` pair to
`StatifierPersistence.Storage.check_metadata/2`, whose contract is the
narrower `[Storage.run_write_opt()]`. Handing the whole option list over
made dialyzer derive a success typing for `create/4` that accepted no
`executor:` at all, so an embedder had to suppress "will never return" on
every correct call; that suppression can now be deleted.
## [0.1.3] 2026-08-27
Docs release: README and guide refresh onto the family's canonical example
domains. No library code changes.
### Changed
- The README now walks a full worked run in the card-processing domain -
load, step, execute effects, persist - and continues it across a restart,
with a new module map; the examples are executed by a test so they cannot
drift from the real API.
- Example domains follow the family rule: card processing and the signup
wizard with A/B testing only.
- Agent tooling: gate attestation points at `mix quality.verify` (shipped
by ex_quality 0.14) instead of a retired local task.
## [0.1.2] 2026-08-24
Docs release: the hexdocs/README overhaul from PR #20. No library code changes.
### Changed
- Hexdocs no longer publishes the ADRs: the ADR extras and their
`groups_for_extras` entry are removed, so the published docs are the README,
this changelog, and the restart-demo guide.
- `ex_doc` is pinned to `~> 0.40`, and `CHANGELOG.md` is listed in
`skip_undefined_reference_warnings_on`; `mix docs` now completes with zero
warnings.
- The README gains the standard badge row (CI, hex.pm version/downloads,
hexdocs, license) and a documentation index line linking the published
restart-demo guide.
## [0.1.1] 2026-08-24
Patch release: the key-generator compile-race fix from PR #18.
### Fixed
- Custom key-generator validation in `use StatifierPersistence.Ecto` no longer
fails spuriously when the generator module is still being compiled by the
host's parallel compiler; validation now waits for in-flight compilation
(`Code.ensure_compiled/1`) instead of checking `Code.ensure_loaded?/1`.
## [0.1.0] 2026-08-22
First release: the persistence-first execution loop for the
[statifier](https://hex.pm/packages/statifier) statechart engine - load a
persisted position, step it, execute the effects, persist - packaged as a
storage-adapter behaviour with an identity guard, an in-memory reference
adapter, a run lifecycle, and an Ecto/Postgres layer, all covered by one
conformance suite downstream adapters inherit.
### Added
- `StatifierPersistence.Storage.Adapter`, the storage contract, including
run records: `insert_run/2`, `fetch_run/2`, and `update_run/2` callbacks
with `run_record`/`run_status` types and the `:run_exists` /
`:run_not_found` error arms; `StatifierPersistence.Storage.InMemory` is
the reference implementation.
- Guarded run access on the facade: `StatifierPersistence.Storage.insert_run/5`,
`update_run/5`, `fetch_run/2`, and `load_run_position/3` (identity-guarded,
with the `:run_position_missing` arm for a run persisted without a
position).
- Run-record conformance tests in
`StatifierPersistence.Testing.StorageConformance`, so downstream adapters
inherit the same contract checks.
- The run lifecycle as a library: `StatifierPersistence.Runs.create/4` and
`step/5` drive the load -> re-stamp -> step -> execute -> persist loop
over durable run records, handing effects to a host-supplied
`StatifierPersistence.Executor` (behaviour or arity-2 fun) and returning
the host-facing `StatifierPersistence.Run` struct; events to a terminal
run come back as `{:discarded, run}`.
- Failure semantics on the loop: executor failures on actionable effects
re-enter the chart as `error.communication` events (single wave per step,
observational failures discarded); effect execution is at-least-once, with
a failed persist re-driving the same event and re-emitting the same
effects under identical deterministic keys; budget exhaustion persists a
`:failed` run (position untouched) and returns
`{:error, {:budget_exhausted, payload}}`.
- `StatifierPersistence.Runs.fail/4`, the host-driven abandonment: marks an
active run `:failed` with a reason, leaves the stored position untouched,
and discards on a terminal run - backed by the status-only writer
`StatifierPersistence.Storage.update_run_status/4`.
- Pluggable per-run serialization: the `StatifierPersistence.Serialization`
behaviour (`with_run/3`), selected per lifecycle call with
`serialization: {module, config}` on `Runs.create/4`, `step/5`, and
`fail/4`. The default strategy,
`StatifierPersistence.Serialization.AdapterLock`, delegates to the
optional adapter callback
`StatifierPersistence.Storage.Adapter.lock_run/3` (implemented by
`InMemory`, conformance-tested when exported) and refuses with
`{:error, {:serialization, :not_supported}}` when the adapter does not
export it.
- `use StatifierPersistence.Ecto`: compile-time configuration on the host's
module (`repo:`, `key:`, `table_prefix:`, `tables:`, `prefix:`) that
defines `Chart`, `Position`, and `Run` schema modules and exposes the
resolved config via `__statifier_persistence__/1`. Requires the optional
`ecto_sql` dependency.
- `StatifierPersistence.Ecto.KeyGenerator`: the behaviour a surrogate-key
scheme implements, with `:uxid` (default), `:uuid` (UUIDv7), `:bigserial`,
and `{module, opts}` resolved through `resolve/1`.
- `StatifierPersistence.Ecto.Migrations`: the versioned migrations helper
(`up/1`, `down/1`, taking `for: HostModule` or the same literal options
`use` takes) that creates the `charts`/`positions`/`runs` tables from the
same resolved config the schemas use.
- `StatifierPersistence.Storage.Ecto`: the Postgres storage adapter over
the schemas a host generates with `use StatifierPersistence.Ecto`
(`Storage.new(Storage.Ecto, persistence: MyApp.Persistence)`). Passes
the same conformance suite as the in-memory reference adapter; engine
identities stored verbatim; `:run_exists` enforced atomically by the
unique index.
- `Storage.Ecto.isolate/1`: with `sandbox: true`, wraps each test in its
own `Ecto.Adapters.SQL.Sandbox` checkout - the hook host test suites
(and this package's conformance suite) isolate through.
- `Storage.Ecto.lock_run/3`: per-run mutual exclusion as a
transaction-scoped `pg_advisory_xact_lock` plus a `SELECT ... FOR
UPDATE` row lock (ADR-0004 as amended), consumed by
`Serialization.AdapterLock`.
- `uxid` is a required dependency (the default key scheme works out of
the box); `ecto_sql` is optional and the package compiles without it.