ADR-0002: Storage keys and table names are host-configurable; engine identities are not
Copy Markdown View SourceStatus: accepted (2026-08-20) - amended 2026-08-22 (sp-02x Phase 1: collapses
decision 4's table sketch to charts/positions/runs and records that
decision 3's tenancy columns remain unimplemented option surface)
Context
The Ecto adapter (sp-4an.3) ships schemas and migrations that land in a host's database, and hosts arrive with conventions of their own: UUID or bigserial primary keys, table-naming standards, sometimes a dedicated Postgres schema. A package that hard-codes one answer forces either a fork or a wrapper; a package that defers every answer ships nothing usable out of the box.
Two kinds of identifier meet in these tables, and they have different owners:
- Engine identities, generated by statifier and carried in its
contracts: the session id (
sess_UXID, st-ADR-0008), and chart identity as a content hash of the SCXML source (st-ADR-0052), which the identity guard checks before any position is loaded. These arrive from outside this package and correctness hangs on them. - Storage surrogate keys, generated by the adapter for its own rows - what host tables will foreign-key to. Nothing in the engine contract constrains them; they exist for the host's convenience.
Two facts about the layering bound the design. The storage-adapter
behaviour (sp-4an.1) speaks engine identities and opaque blobs, so key
schemes and table names never reach it - they are Ecto-adapter concerns
only. And Ecto's @primary_key and schema source are compile-time,
which rules out runtime app-env configuration without contortions; the
ecosystem's answer (Oban's per-instance options, its versioned
Oban.Migration helper) is compile-time configuration carried on the
host's own module.
statifier-ex records the UXID format decisions but dropped the uxid
dependency (st-ADR-0008, amended); depending on it here is therefore a new
decision, not an inheritance.
Decision
1. Engine identities are stored verbatim and are not configurable. The chart content hash and the engine session id are opaque strings, uniquely indexed where the guard and the run-serialization rule need them. The identity guard (st-ADR-0052) keys on the content hash and never on a surrogate key, whatever the host configures. No knob in this record can weaken that.
2. Surrogate primary keys are configurable per host, defaulting to UXID. The options:
:uxid(default) - k-sortable strings via theuxidpackage, which becomes a real dependency (the default must work out of the box). Rows carry per-table prefixes in the engine's own style:chart_,chver_,run_.:uuid- generated as UUIDv7, not v4: random keys fragment b-tree indexes, and v7 keeps insertion locality.:bigserial- database-assigned auto-increment, for hosts with that convention. Supported, not defaulted.{module, opts}- any module implementing the key-generator behaviour (the Ecto type to use, plus generation).:uxidand:uuidare themselves implementations of it, so custom is not a separate code path.
3. Configuration is compile-time, on the host's module - no global app env. The shape (final spelling is implementation work):
use StatifierPersistence.Ecto,
repo: MyApp.Repo,
key: :uxid, # | :uuid | :bigserial | {Mod, opts}
table_prefix: "statifier_", # the default
tables: %{runs: "wf_runs"}, # optional per-table override
prefix: "workflow" # optional Postgres schema (Ecto prefix)A versioned migrations helper in the Oban.Migration mold takes the same
options, so DDL and schemas cannot disagree; it is the only supported way
to create or upgrade the tables. Tenancy columns (host-supplied, per the
charter) ride the same use so host configuration has one home.
(Amended 2026-08-22, sp-02x Phase 1: tenancy columns remain unimplemented
option surface. No host has specified any yet, and inventing placeholder
columns ahead of a real one would be the unexercised contract ADR-0003's
Consequences warn against. This decision's promise stands - tenancy
columns ride the same use when a host needs them - but nothing ships
until then.)
4. Table names default to the full statifier_ prefix:
statifier_charts, statifier_chart_versions, statifier_runs.
Discoverability wins over brevity - someone meeting statifier_runs in a
psql console knows where it came from, the way oban_jobs reads. One
prefix knob keeps the set consistent; the per-table override map is the
escape hatch for hosts with naming standards the prefix cannot satisfy;
the Postgres-schema option covers hosts that isolate by schema instead of
by name.
(Amended 2026-08-22, sp-02x Phase 1: this sketch's table set collapses to
charts, positions, and runs. The storage-adapter behaviour (ADR-0003
decision 3) keys a chart by content hash only and a position by the engine
session id; nothing in that contract exercises a logical-chart /
chart-versions split, so the statifier_chart_versions table above never
gets a callback that would use it - the same unexercised-contract
reasoning ADR-0003's Consequences apply to delete_position. V01
therefore ships the hash-keyed statifier_charts table and joins
statifier_positions to the set under the same prefix knob, and the
default UXID row prefixes become chart_, pos_, and run_ accordingly.
A logical-chart table returns, under whatever name fits then, when a real
embedder needs one - the charter's own design rule.)
5. The vocabulary is runs, not sessions. The charter's lifecycle
(create/step/complete/fail) operates on runs; "session" keeps the
meaning statifier-ex gives it - the live GenServer runtime this package
exists to make optional. A statifier_runs row carries the engine
session_id as a nullable column, so a run can reference the live
session driving it when one exists, and both vocabularies stay honest.
Consequences
- Host foreign keys get whatever type the host configured - the point of decision 2. Internal references between this package's tables use the same configured type, so one choice governs the whole set.
- The key-generator behaviour, the migrations helper, and the
usemacro are now sp-4an.3 deliverables in front of the first migration; a key-type change after migrations ship is the retrofit this record exists to avoid. uxidjoins the dependency list.:bigserialhosts should know a sequential id leaks row counts if exposed; that is the host's trade to make, which is why it is supported and not defaulted.- What would reopen this record: the behaviour (sp-4an.1) ever needing to see a surrogate key or table name (the layering claim failing), a second adapter needing key configuration the generator behaviour cannot express, or the engine's identity contracts moving upstream (st-ADR-0052 amendments re-open decision 1's storage columns, not its rule).