ADR-0002: Storage keys and table names are host-configurable; engine identities are not

Copy Markdown View Source

Status: 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 the uxid package, 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). :uxid and :uuid are 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 use macro 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.
  • uxid joins the dependency list. :bigserial hosts 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).