From a894b001b1c6bdcf62dcde813b64dc529be3221e Mon Sep 17 00:00:00 2001 From: serversdown Date: Sat, 22 Aug 2026 05:50:45 +0000 Subject: [PATCH] feat(db): shape_* columns on events (+ auto-migrate) --- sfm/database.py | 8 ++++++++ tests/test_shape_columns.py | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 tests/test_shape_columns.py diff --git a/sfm/database.py b/sfm/database.py index ff042f9..8ce35d7 100644 --- a/sfm/database.py +++ b/sfm/database.py @@ -94,6 +94,10 @@ CREATE TABLE IF NOT EXISTS events ( vert_zc_above_range INTEGER, long_zc_above_range INTEGER, mic_zc_above_range INTEGER, + shape_crest_factor REAL, -- peak / rms of the triggering channel + shape_near_peak_count INTEGER, -- samples >= 0.5 * peak (FT: few; real: many) + shape_sample_count INTEGER, -- total samples (to normalize near_peak_count) + shape_axis TEXT, -- geophone channel measured ("Tran"/"Vert"/"Long") created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), UNIQUE(serial, timestamp) ); @@ -216,6 +220,10 @@ class SeismoDb: ("vert_zc_above_range", "INTEGER"), ("long_zc_above_range", "INTEGER"), ("mic_zc_above_range", "INTEGER"), + ("shape_crest_factor", "REAL"), + ("shape_near_peak_count", "INTEGER"), + ("shape_sample_count", "INTEGER"), + ("shape_axis", "TEXT"), ): if col not in existing_cols: log.info("_migrate: events ADD COLUMN %s %s", col, ddl) diff --git a/tests/test_shape_columns.py b/tests/test_shape_columns.py new file mode 100644 index 0000000..9bb92ac --- /dev/null +++ b/tests/test_shape_columns.py @@ -0,0 +1,27 @@ +import sqlite3 +from sfm.database import SeismoDb + +_SHAPE_COLS = {"shape_crest_factor", "shape_near_peak_count", + "shape_sample_count", "shape_axis"} + +def _cols(db): + with sqlite3.connect(db.db_path) as c: + return {r[1] for r in c.execute("PRAGMA table_info(events)")} + +def test_fresh_db_has_shape_columns(tmp_path): + db = SeismoDb(tmp_path / "s.db") + assert _SHAPE_COLS <= _cols(db) + +def test_existing_db_migrates_shape_columns(tmp_path): + p = tmp_path / "s.db" + db = SeismoDb(p) + with sqlite3.connect(p) as c: # simulate an older DB missing the columns + for col in _SHAPE_COLS: + c.execute(f"ALTER TABLE events DROP COLUMN {col}") + # sanity: dropped. Read via the already-constructed `db` (a raw PRAGMA + # read against db.db_path) rather than a fresh SeismoDb(p) — the latter + # would immediately re-trigger _migrate's self-healing ADD COLUMN loop + # and mask the gap we're trying to confirm. + assert not (_SHAPE_COLS <= _cols(db)) + SeismoDb(p) # re-open triggers _migrate + assert _SHAPE_COLS <= _cols(SeismoDb(p))