Skip to content

fix(store): add a schema migration runner - #46

Open
trionnemesis wants to merge 2 commits into
mainfrom
fix/store-schema-migration-runner
Open

fix(store): add a schema migration runner#46
trionnemesis wants to merge 2 commits into
mainfrom
fix/store-schema-migration-runner

Conversation

@trionnemesis

Copy link
Copy Markdown
Owner

What changed

SCHEMA_VERSION moved from 1 to 2 when run_counter was added (#12), but _init_schema only wrote the schema_version row when it was absent. A database created back under version 1 kept reporting version 1 forever, regardless of what its actual tables looked like — a correctness gap, since any code branching on the stored version would silently misbehave against a pre-existing database. _init_schema now applies an ordered, idempotent list of migrations for any version between what's stored and SCHEMA_VERSION, then corrects the stored row; a version newer than this build supports raises SchemaVersionError rather than being silently opened.

Closes #44.

Type

  • New scenario (Attack–Detection Contract)
  • Evidence collector
  • Executor
  • Evaluator / verdict logic
  • MCP gateway surface
  • Docs / ADR
  • Fix

Purple coverage

Question Answer
Which scenario(s) cover this change? n/a — store/persistence layer only, no scenario touches it
Does it change how a verdict is produced? No
Does it widen what the MCP gateway can reach? No

Checks

  • make check passes (ruff, mypy, pytest — 412 tests, 1 pre-existing skip)
  • agentsec validate --strict passes
  • agentsec run --target demo-agent-fixture --profile nightly still exits 1 with exactly AGT-TENANT-001 and AGT-MEMPOIS-001 blocking

Notes for the reviewer

  • _SCHEMA now holds only the version-1 shape (schema_version, runs, findings, audit_log); the run_counter table that shipped in Close all twelve findings from the local deployment review #12 moved into _MIGRATIONS as the entry for version 2, so it's applied once, explicitly, rather than folded permanently into the base bootstrap script. Both the base schema and every migration must stay idempotent (IF NOT EXISTS / equivalent), since _init_schema runs the base script unconditionally on every open and a fresh database walks every migration in sequence rather than starting pre-upgraded.
  • New tests/test_store_migrations.py builds a byte-for-byte version-1 database (no run_counter, version row = 1) to exercise the real upgrade path, plus: fresh-database bootstrap lands directly on SCHEMA_VERSION; reopening an already-migrated database is a no-op (would raise if a migration re-ran without IF NOT EXISTS); a database claiming a version newer than this build supports raises SchemaVersionError and — importantly — the stored version is not rewritten by the failed attempt, so the problem doesn't silently disappear on retry.
  • docs/roadmap.md and CHANGELOG.md updated; the "Migration runner — overdue" line moves from Core — open to the Core table as ✅.

Generated by Claude Code

SCHEMA_VERSION moved from 1 to 2 when run_counter was added (#12), but
_init_schema only wrote the version row when it was absent -- a database
created under version 1 kept reporting version 1 forever, regardless of
what its tables actually looked like.

_init_schema now applies an ordered, idempotent list of migrations for
any version between the database's recorded version and SCHEMA_VERSION,
and writes the corrected version afterward. A recorded version newer than
this build supports raises SchemaVersionError rather than being silently
opened.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2jDXUwrbM4eupm7CyLivS
@trionnemesis
trionnemesis marked this pull request as ready for review August 11, 2026 09:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e2c3181a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +95 to +98
CREATE TABLE IF NOT EXISTS run_counter (
day TEXT PRIMARY KEY,
next_n INTEGER NOT NULL
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Seed migrated counters from existing run IDs

When a genuine v1 database already contains runs for the current day, this migration creates an empty run_counter, so the first subsequent next_run_id(day) returns RUN-<day>-001; because save_run upserts on run_id, that can silently overwrite the existing run with the same ID. The migration should initialize each day's counter from the maximum matching suffix already present in runs before the upgraded store is used.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, and confirmed real: a v1 database can already hold today's runs under the old MAX(run_id)-in-Python scheme, and an empty counter would collide with them since save_run upserts on run_id. Fixed in 6ff293a — the migration now seeds each day's counter from the highest suffix already in use, with a test that pins the exact scenario (existing RUN-20260811-001/002, migration lands the counter at 2 so the next claim is 003).


Generated by Claude Code

Comment on lines +138 to +143
if current > SCHEMA_VERSION:
raise SchemaVersionError(
f"{self.path} reports schema version {current}, newer than the "
f"{SCHEMA_VERSION} this build supports; refusing to open it",
details={"found_version": current, "supported_version": SCHEMA_VERSION},
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check future versions before applying the legacy schema

When a newer schema has renamed or removed any v1 table or indexed column, _SCHEMA has already executed before this version guard, so opening it can mutate the database with missing legacy objects or raise sqlite3.OperationalError from an incompatible index statement instead of the promised SchemaVersionError. Read or minimally bootstrap the version table and reject unsupported future versions before executing the v1 schema.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — fixed in 6ff293a. _init_schema now bootstraps and checks only the schema_version table first; _SCHEMA and the migrations only run once that guard has passed, so a future-version database gets the clean SchemaVersionError refusal instead of _SCHEMA mutating it or raising a raw sqlite3.OperationalError.


Generated by Claude Code

… applying legacy schema

Addresses two review findings from Codex on #46:

- P1: a v1 database can already hold runs for "today" under the old
  MAX(run_id)-in-Python scheme. Migrating to run_counter with an empty
  counter would hand out RUN-<day>-001 again, and because save_run
  upserts on run_id, that would silently overwrite the pre-existing run.
  The migration now seeds each day's counter from the highest suffix
  already in use.
- P2: _SCHEMA was applied before the version guard ran, so a database
  from a future build that had renamed or dropped a v1 table could be
  mutated, or raise a raw sqlite3.OperationalError, instead of getting
  the clean SchemaVersionError refusal. The version table is now
  bootstrapped and checked first, before _SCHEMA or any migration runs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H2jDXUwrbM4eupm7CyLivS
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.

fix(store): add a migration runner — SCHEMA_VERSION is 2 but a DB created under 1 reports 1 forever

2 participants