Skip to content

Adds the Ecto storage adapter - #14

Merged
johnnyt merged 3 commits into
mainfrom
sp-4an.3.1-ecto-storage-adapter
Aug 22, 2026
Merged

Adds the Ecto storage adapter#14
johnnyt merged 3 commits into
mainfrom
sp-4an.3.1-ecto-storage-adapter

Conversation

@johnnyt

@johnnyt johnnyt commented Aug 22, 2026

Copy link
Copy Markdown
Member

Why

The sp-4an.3 epic's schemas and migrations landed in PR #13; this is the
adapter over them. Hosts get a Postgres storage backend that satisfies the
exact contract the behaviour and the lib-shipped conformance suite pin
(ADR-0003), with the identity guard staying in the facade above it.

What

  • StatifierPersistence.Storage.Ecto: the Storage.Adapter behaviour over
    the schemas a host generates with use StatifierPersistence.Ecto
    (Storage.new(Storage.Ecto, persistence: MyApp.Persistence)). Charts are
    idempotent on content_hash, positions upsert per session_id, and
    insert_run/2's :run_exists rides the V01 unique index through a
    changeset unique_constraint - one atomic insert, never check-then-insert.
    update_run/2 is a single update_all whose match count doubles as the
    :run_not_found check. Engine identities and blobs round-trip verbatim.
  • isolate/1 (sandbox: true): per-test Ecto.Adapters.SQL.Sandbox
    checkout, which is how the conformance suite runs its full 24 tests against
    this adapter - the identical suite InMemory passes - on real Postgres
    (ADR-0005 harness).
  • lock_run/3: one transaction spanning fun that takes
    pg_advisory_xact_lock(hashtextextended(run_id, 0)) first, then
    SELECT ... FOR UPDATE on the run row when it exists. The advisory half is
    load-bearing: a row lock alone excludes nothing for a run_id whose run
    has not been inserted yet, which the adapter contract (and the conformance
    lock tests) require. Recorded as a dated amendment to ADR-0004 decision 5.
  • Live lock tests run outside the SQL sandbox on two real pooled
    connections - the sandbox's single shared connection serializes callers by
    ownership and would mask a broken lock. The rowless test goes red with the
    advisory lock removed; the inserted-run test pins the row-lock path.
  • README walkthrough, changelog fragment, and the critic-reviewed plan.

Notes

  • Full mix quality green and attested via mix gate.verify before every
    commit and again before this push: 135/135 tests, 95.8% coverage,
    dialyzer and deps audit included. Doctor/Gettext/Sobelow are the standing
    not-applicable skips.
  • The ADR-0004 amendment (advisory-plus-row rather than row-only) is a
    locking decision this repo owns; the behaviour doc and InMemory's
    pointer were aligned in the same commit.
  • Test bootstrap now migrates the Default and Overridden fixture hosts'
    tables once per suite; adapter unit tests exercise the schema-prefixed
    workflows.workflow_runs path, not only the zero-config host.

Closes sp-4an.3.1

Implements StatifierPersistence.Storage.Ecto over the schemas a host
generates with use StatifierPersistence.Ecto: charts idempotent on
content_hash (insert ... on conflict do nothing), positions upserted
per session_id, and runs with the :run_exists refusal ridden on the
V01 unique index through a changeset unique_constraint - one atomic
insert, never a check-then-insert. update_run/2 overwrites through a
single update_all whose match count doubles as the :run_not_found
check. The status vocabulary maps explicitly in both directions;
engine identities and blobs round-trip verbatim.

isolate/1 checks out an SQL sandbox connection when the handle was
built with sandbox: true, which is how the lib-shipped conformance
suite now runs its 22 non-lock tests against this adapter on real
Postgres (ADR-0005) - the identical suite InMemory passes. Adapter
unit tests cover the host refusal, the full status vocabulary down
to the stored strings, row-count idempotence, and the same CRUD
against the schema-prefixed Overridden host. The test bootstrap
migrates the Default and Overridden tables once per suite.

lock_run/3 (the transaction-scoped per-run lock) lands separately;
its conformance tests generate only once the callback exists.
Includes the critic-reviewed implementation plan.

Refs: sp-4an.3.1
Implements the optional per-run lock as one transaction spanning fun:
pg_advisory_xact_lock(hashtextextended(run_id, 0)) first, then
SELECT ... FOR UPDATE on the run row when it exists. The advisory
half is load-bearing, not decoration: a row lock alone excludes
nothing for a run_id whose run has not been inserted yet, and the
adapter contract requires unconditional mutual exclusion. Recorded
as a dated amendment to ADR-0004 decision 5, with the lock_run/3 doc
in the behaviour and InMemory's pointer aligned to it.

Exporting the callback makes the conformance suite generate its two
lock tests against this adapter (verified present via --trace; the
suite is now the full 24). New live tests outside the SQL sandbox
prove the semantics on two real pooled connections - the sandbox's
single shared connection serializes callers by ownership and would
mask a broken lock: no overlap on an inserted run's lock, no overlap
on a rowless run_id (red with the advisory lock removed), and
release after a raising fun.

Refs: sp-4an.3.1
README gains the Ecto adapter walkthrough - configure the persistence
module, migrate through the versioned helper, build the guarded store,
run host test suites with sandbox: true - and the Status section stops
claiming nothing is implemented. The sp-4an.3.1 changelog fragment
records the adapter, isolate/1, and lock_run/3 for the public surface.

Refs: sp-4an.3.1
@johnnyt
johnnyt merged commit 862310f into main Aug 22, 2026
1 check passed
@johnnyt
johnnyt deleted the sp-4an.3.1-ecto-storage-adapter branch August 22, 2026 17:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant