feat(api): persist cases and matches - #21
Open
Madeuss wants to merge 1 commit into
Open
Conversation
A match now survives a restart, which is what the HTTP endpoint and NPC memory both wait on — and memory sharing a transaction with game state is the argument ADR-0002 was making. The two things being stored are not alike, so they are not stored alike. A case is generated, immutable and always read whole; nothing queries across cases, and its shape moves with the generator. It is one JSONB document with the three columns that identify it — seed, generator version, setting — carrying a unique constraint, because seed alone identifies nothing. A match is the opposite: turns append, statements are read per character to find contradictions, and the budget changes every turn. Ordinary tables. The solution gets its own table, and that is the point of the change rather than a detail of it. `Case` and `Solution` are separate entities so isolation is a type signature instead of a discipline (RN-011); as a column on `cases`, the culprit would ride along in every SELECT *, and the guarantee would last exactly as long as nobody wrote a convenient query. In its own table, `load_case` cannot return it — the read does not touch where it lives. A turn is one transaction: statement, stance and budget move together, so a crash cannot leave a match charged for an answer it never recorded (RN-030). The database also refuses two turn 1s, so a retry cannot double-record. Storage tests run against Postgres, in CI as a service container. On SQLite they would prove the tests pass, not that a schema using JSONB and a composite unique constraint works. CI also applies the migrations from scratch on every run, which is the only way to find out that they still do. ADR-0009 records the shape of it, including what a JSONB case costs: it is opaque to SQL, so "which cases put someone in the cellar at 22:00" is a scan. Nothing asks yet.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of E2 (#6). Matches survive a restart, which the HTTP endpoint and NPC
memory both wait on. ADR-0009.
Two shapes, stored differently
A case is generated, immutable, and always read whole. Nothing queries
across cases, and its shape moves with the generator —
settingarrived daysafter
interval_count. One JSONB document, with seed / generator version /setting as real columns under a unique constraint, because seed alone
identifies nothing.
A match is the opposite: turns append, statements are read per character for
contradictions (RN-021), the budget moves every turn. Ordinary tables.
The part that matters
The solution has its own table.
CaseandSolutionare separate entitiesso isolation is a type signature rather than a discipline (RN-011). One layer
down, the same argument holds: as a column on
cases, the culprit rides alongin every
SELECT *, and the guarantee lasts exactly as long as nobody writes aconvenient query.
The first does not touch the table the solution lives in. That is the guarantee
— not the caller's restraint.
A turn is one transaction
Statement, stance and budget move together or not at all. A crash cannot leave a
match charged for an answer it never recorded (RN-030). And
uq_statements_turnmeans the database refuses two turn 1s, so a retry cannot double-record.
Tests run against Postgres
Not SQLite. A schema built on JSONB and a composite unique constraint, tested on
a database the application will never run on, proves the tests pass.
CI gets a
pgvector/pgvector:pg16service and applies the migrations fromscratch on every run — the only way to learn that they still apply.
Verified locally end to end:
alembic upgrade headagainst the composedatabase, four tables plus
alembic_version, and a full turn recorded and readback.
Choices worth arguing with
already owns the domain objects; an ORM would add a second object model to
keep in agreement with the first.
endpoints in a threadpool, and async would add a second execution model to
reason about for a saving that does not appear at one player per match.
migrations/versionsexcluded from lint. Alembic writes those;reformatting generated files on every autogenerate hides the one line a human
changed.
Costs are in the ADR, including the one that will bite first: a JSONB case is
opaque to SQL, so any future "find cases where…" is a scan.