fix(persistence): derive SCHEMA_VERSION from the migrations directory

db.py hardcoded SCHEMA_VERSION = 26 while migrations/ had already reached
v29 (v27 dispatcher floor-drop counter, v28 mesh_observations, v29 IPAWS).
The migration runner globs the directory and applies every vN.sql it finds
regardless of the constant, so a fresh DB actually landed at 29 while the
constant claimed 26 -- a three-version drift that three tests were
correctly catching.

Derive it from the highest vN.sql present instead of bumping the literal,
so it cannot drift again the next time someone adds a migration. Falls back
to 0 if the directory is missing so import never fails; the migrations dir
sits alongside db.py and ships with the package (Dockerfile COPYs meshai/).

Adds a regression guard asserting the constant matches the highest
migration file, and updates three tests that hardcoded 26 as a literal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Johnson 2026-07-16 18:26:07 +00:00
commit bc94cf237a
4 changed files with 48 additions and 8 deletions

View file

@ -30,9 +30,11 @@ logger = logging.getLogger(__name__)
DEFAULT_DB_PATH = "/data/meshai.sqlite"
MESHAI_DB_PATH_ENV = "MESHAI_DB_PATH"
SCHEMA_VERSION = 26
SCHEMA_META_TABLE = "schema_meta"
MIGRATIONS_DIR = Path(__file__).parent / "migrations"
# SCHEMA_VERSION is derived below (after _read_migration_files is defined)
# from the highest vN.sql present in MIGRATIONS_DIR, so it can never drift
# from what a fresh DB actually lands at -- see _derive_schema_version().
# Per-thread connection pool. Each thread that calls get_db() gets its
# own sqlite3.Connection cached on threading.local. Tests can clear
@ -163,6 +165,22 @@ def _read_migration_files() -> list[tuple[int, str, str]]:
return out
def _derive_schema_version() -> int:
"""Highest migration version present in MIGRATIONS_DIR, i.e. the
version a fresh DB lands at once _apply_migrations() runs (it applies
every vN.sql it finds, independent of any hardcoded constant). Falls
back to 0 if the directory is missing/empty so import never fails;
that would only happen in a broken packaging layout, since
MIGRATIONS_DIR sits alongside this file and is copied/installed with
the rest of the meshai package (see Dockerfile: `COPY meshai/ ./meshai/`
then `pip install -e .`)."""
versions = [n for n, _, _ in _read_migration_files()]
return max(versions) if versions else 0
SCHEMA_VERSION = _derive_schema_version()
def _current_version(conn: sqlite3.Connection) -> int:
"""Return the highest applied migration version, or 0 if none."""
# Does the schema_meta table exist?

View file

@ -14,8 +14,11 @@ from meshai.persistence.observer_locations import (
# -- schema / migration -------------------------------------------------------
def test_schema_version_is_25():
assert SCHEMA_VERSION == 26
def test_schema_version_is_current():
# SCHEMA_VERSION is derived from the highest vN.sql in migrations/, so
# this just guards against the derivation returning something bogus
# (e.g. 0, which would mean the migrations dir wasn't found).
assert SCHEMA_VERSION >= 23
def test_observer_locations_table_exists():
@ -30,7 +33,7 @@ def test_schema_meta_at_current():
conn = get_db()
row = conn.execute(
"SELECT value FROM schema_meta WHERE key='version'").fetchone()
assert int(row["value"]) == 26
assert int(row["value"]) == SCHEMA_VERSION
# -- accessors ----------------------------------------------------------------

View file

@ -90,6 +90,23 @@ def test_schema_version_recorded(tmp_db):
assert int(row["value"]) == SCHEMA_VERSION
def test_schema_version_matches_highest_migration_file():
"""SCHEMA_VERSION is derived from migrations/ at import time (see
db._derive_schema_version); this test independently re-derives the
expected value straight off the filenames so a future regression
(e.g. someone re-hardcoding the constant) is caught even if the
derivation logic itself is what breaks."""
import re
versions = []
for p in persistence_db.MIGRATIONS_DIR.iterdir():
m = re.match(r"^v(\d+)", p.stem)
if p.suffix.lower() == ".sql" and m:
versions.append(int(m.group(1)))
assert versions, "no vN.sql migration files found"
assert SCHEMA_VERSION == max(versions)
def test_migration_idempotent_rerun(tmp_db):
init_db()
# Force a "second startup" by closing the connection and clearing the

View file

@ -99,9 +99,11 @@ def _ingest_envelope(norad_id=25544, observer="Boise", max_el=72.5,
# ── schema / migration ───────────────────────────────────────────────
def test_schema_version_is_current():
# Bumped to 26 by the generic-source migration (v26 generic_events); v24
# avalanche_events, v25 ducting_events; was 23 at native-satpass (v23).
assert SCHEMA_VERSION == 26
# SCHEMA_VERSION is derived from the highest vN.sql in migrations/, so
# this just guards against the derivation returning something bogus
# (e.g. 0, which would mean the migrations dir wasn't found). The v22
# migration (this file's focus) must always be <= the current version.
assert SCHEMA_VERSION >= 22
def test_v22_migration_applies_and_adds_due_at_column(tmp_path, monkeypatch):
@ -114,7 +116,7 @@ def test_v22_migration_applies_and_adds_due_at_column(tmp_path, monkeypatch):
close_thread_connection()
conn = init_db()
row = conn.execute("SELECT value FROM schema_meta WHERE key='version'").fetchone()
assert int(row["value"]) == 26
assert int(row["value"]) == SCHEMA_VERSION
cols = {r["name"] for r in conn.execute("PRAGMA table_info(satpass_pending)")}
assert "due_at" in cols
close_thread_connection()