Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,30 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 30

# ADR-0005: database-backed tests are ordinary tests in the ordinary
# suite, against a real Postgres server - no tag skips them here either.
# Credentials match docker-compose.yml's local `db` service so the PG*
# env below is an override mechanism, not a setup step.
services:
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10

env:
PGHOST: localhost
PGPORT: 5432
PGUSER: postgres
PGPASSWORD: postgres
PGDATABASE: statifier_persistence_test

steps:
- uses: actions/checkout@v4

Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ crash semantics. This package is that loop, packaged.

Nothing is implemented yet. This repository holds the scaffold only.

## Running the tests

The suite includes database-backed tests against a real Postgres server -
ADR-0005 rejects a skip tag for when one is absent, so `mix quality` and
`mix test` both need one reachable. Start it once with:

docker compose up -d db

which brings up `postgres:17` on `localhost:5432` with user/password
`postgres`. Override host, port, user, password, or database name with the
`PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, and `PGDATABASE` env vars (see
`config/test.exs` for the defaults) if a server is already running
elsewhere.

## The contract this package builds on

The persisted-position story is already specified upstream, and this package
Expand Down
22 changes: 22 additions & 0 deletions changelog.d/sp-02x.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# sp-02x

## Added

- `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.

## Changed

- `uxid` is now a required dependency (the default key scheme works out of
the box); `ecto_sql` is an optional dependency and the package compiles
without it.
9 changes: 9 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Config

# This package carries no runtime app-env configuration of its own (ADR-0002
# decision 3: Ecto configuration is compile-time, on the host's module, never
# app env). The only thing config/ configures is the test harness's own repo,
# so only :test has an env-specific file to import.
if config_env() == :test do
import_config "test.exs"
end
14 changes: 14 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Config

# ADR-0005: the test harness is a real Postgres server (docker compose
# locally, a service container in CI), reached through these PG* env vars so
# both environments configure the same repo without a mix.exs edit. Defaults
# match docker-compose.yml's `db` service.
config :statifier_persistence, StatifierPersistence.TestRepo,
hostname: System.get_env("PGHOST", "localhost"),
port: String.to_integer(System.get_env("PGPORT", "5432")),
username: System.get_env("PGUSER", "postgres"),
password: System.get_env("PGPASSWORD", "postgres"),
database: System.get_env("PGDATABASE", "statifier_persistence_test"),
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: System.schedulers_online() * 2
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: postgres
ports:
- "${PGPORT:-5432}:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 10
24 changes: 23 additions & 1 deletion docs/adr/0002-configurable-keys-and-table-names.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# ADR-0002: Storage keys and table names are host-configurable; engine identities are not

Status: accepted (2026-08-20)
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

Expand Down Expand Up @@ -76,6 +78,13 @@ 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
Expand All @@ -85,6 +94,19 @@ 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
Expand Down
89 changes: 89 additions & 0 deletions docs/adr/0005-ecto-in-package-and-postgres-test-harness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# ADR-0005: The Ecto layer ships in this package; the test harness is real Postgres

Status: accepted (2026-08-22)

## Context

The Ecto adapter epic (sp-4an.3) opens on two questions its charter left
deliberately unanswered until the work started. `mix.exs` recorded the
first verbatim: does the Ecto layer live in this package behind an
optional dependency, or in a separate `statifier_ecto` package? The second
is what the epic's database-backed tests run against: a real Postgres
server, an embedded stand-in (SQLite), or an in-memory fake.

The surrounding records constrain both answers. ADR-0002 decision 3 names
`use StatifierPersistence.Ecto` - this package's namespace - as where a
host configures keys and tables, and makes the versioned migrations helper
"the only supported way to create or upgrade the tables". ADR-0003
decision 5 ships the conformance suite in this package's `lib/` precisely
so a downstream adapter can `use` it; ADR-0004's Consequences hand the
Ecto adapter three run callbacks and an optional `lock_run/3` to implement
as a transaction-scoped row lock (`SELECT ... FOR UPDATE`). Ecto already
has the pattern for a library that is optional to compile against:
`Code.ensure_loaded?/1` guards around the modules that reference it, and
the host brings its own database driver.

On the harness side, the repository's own gate rules bind harder than
convenience: "never go green by weakening the check", explicitly including
`@tag :skip` on tests that cannot run. A database suite that silently
skips when no server is reachable is exactly that weakening - the gate
reports green while the adapter is untested.

## Decision

**1. The Ecto layer lives in this package, behind optional `ecto_sql`.**
No separate `statifier_ecto` package: it would split one contract
(behaviour + conformance suite here, the adapter that must pass them
there) across two repos, duplicate the test surface, and contradict
ADR-0002's already-accepted `use StatifierPersistence.Ecto` spelling, all
for no consumer benefit. Concretely:

- `{:ecto_sql, "~> 3.10", optional: true}` - a host that only wants the
behaviour, the in-memory adapter, or the stepper loop compiles this
package without Ecto anywhere in its tree.
- Every module that references Ecto is wrapped in
`if Code.ensure_loaded?(Ecto)` so the package compiles clean either way.
- `{:uxid, "~> 2.0"}` is a required dependency: ADR-0002 decision 2 makes
`:uxid` the default key scheme and the default must work out of the box.
- `{:postgrex, "~> 0.19", only: :test}` - a host brings its own database
driver; this package needs one only to test itself.

**2. The test harness is a real Postgres server with the SQL sandbox.**
Database-backed tests are ordinary tests in the ordinary suite, isolated
per test through `Ecto.Adapters.SQL.Sandbox` (the checkout the
conformance suite's optional `isolate/1` callback already anticipates).
The server comes from `docker compose up -d db` locally (postgres:17,
credentials and port overridable via `PG*` env vars) and a `postgres:17`
service container in CI. Rejected alternatives:

- SQLite or any embedded stand-in: `lock_run/3` is specified as a
transaction-scoped row lock, and `SELECT ... FOR UPDATE` semantics are
exactly what an embedded engine fakes differently or not at all. A
migration suite proven against a database no host will run proves
little.
- An in-memory fake: the conformance suite exists to test adapters
against real backends; running it against a fake of the backend is
circular.
- A skip tag for when the server is absent: an auto-skipped database
suite is the gate weakening this repository's rules forbid. Absent
server, red suite, loudly.

## Consequences

- From this record on, every `mix quality` run in this repository
requires a reachable Postgres server, by design. `docker compose up -d
db` is now part of standing up a working checkout, and the README says
so. A run without the server fails loudly in the connectivity smoke
test rather than skipping quietly.
- CI carries a Postgres service container from here forward; its
credentials and the local compose defaults are the same values, so the
`PG*` env vars are an override mechanism, not a setup step.
- Hosts compiling without `ecto_sql` get no Ecto modules and no
migrations helper - the `Code.ensure_loaded?` guard is the seam, and a
missing-guard compile failure in such a host is a bug in this package.
- `uxid` becomes a transitive dependency of every host, ecto or not.
- What would reopen this record: a host that cannot take `uxid`
transitively (decision 1's required-dependency clause); a second
database the adapter must support (decision 2's Postgres-only harness);
or the optional-dependency seam failing in practice - a host without
`ecto_sql` that this package will not compile for.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
| [0002](0002-configurable-keys-and-table-names.md) | Storage keys and table names are host-configurable at compile time (UXID default, `statifier_` prefix, runs vocabulary); engine identities are not | accepted |
| [0003](0003-storage-adapter-behaviour-and-the-identity-guard.md) | The storage adapter stores opaque blobs keyed by engine identities; the identity guard lives above every adapter and cannot be skipped | accepted |
| [0004](0004-run-lifecycle-executor-seam-and-serialization.md) | The run record owns its position, the loop's order is the contract, effects cross a host-executor seam (failures re-enter as `error.communication`), and per-run serialization is a pluggable strategy | accepted |
| [0005](0005-ecto-in-package-and-postgres-test-harness.md) | The Ecto layer ships in this package behind optional `ecto_sql`; the test harness is a real Postgres server with the SQL sandbox, no skip tag | accepted |

New ADRs: next number, same three-section format (Context, Decision,
Consequences). Pick the number against a freshly fetched remote. A bare
Expand Down
Loading
Loading