ADR-0003 — the database is real, and it is SQLite
Date: 2026-08-15 Status: accepted
Context
Most of what this service decides, it decides in SQL: which preference row applies, how many deliveries went out today, whether a dedup key already exists. Mocking the session would have tested the mock.
PostgreSQL is what runs in production, and requiring it would break ADR-0002.
Decision
Give each test an in-memory SQLite database built from Base.metadata, with StaticPool
so that every connection sees the same database, and let every query run for real.
The schema comes from the models, not from the migrations.
One PostgreSQL behaviour is simulated, deliberately and narrowly. _run_single_rule
recognises a duplicate by finding the literal uq_nudges_dedup_key in the driver's error
text. PostgreSQL puts the constraint name there; SQLite says UNIQUE constraint failed:
nudges_log.dedup_key and never names the constraint. Without help, the dedup branch would
be unreachable on SQLite and every duplicate would escape as a 500 — so the whole of
REQ-0035, the suppression this repository most needs verified, would be untested.
A handle_error listener in conftest.py rewrites SQLite's message into PostgreSQL's
wording for that one constraint, and tests/unit/test_engine_dedup.py pins the
literal against the model and against the migration that creates it.
Consequences
The migrations are not exercised. A model that has drifted from alembic/versions/
passes here and fails on deploy. This is the largest gap in the suite and it is stated in
docs/specifications/index.md rather than left to be discovered.
PostgreSQL-only behaviour is not exercised either: FOR UPDATE SKIP LOCKED in the
scheduler is a no-op on SQLite, so REQ-0065's claim that two replicas do not take the same
row is verified as code that asks for it, not as behaviour under contention. Types
differ too — JSON rather than JSONB, timestamps stored as strings.
The simulated error is the thing to watch. It is scoped to one message and one constraint, and the constraint name is pinned in three places, so a rename fails the suite rather than being papered over. Widening it — a generic "make SQLite talk like PostgreSQL" shim — would remove that guarantee, and is the change to refuse.
Superseded by running the suite against a real PostgreSQL, which would remove both gaps and the simulation with them.