From 31660d24e9758ac590ec74310341a07a2984eae2 Mon Sep 17 00:00:00 2001 From: serversdown Date: Thu, 2 Jul 2026 06:22:46 +0000 Subject: [PATCH] feat(db): WAL-safe seismo_relay.db snapshot helper --- sfm/db_snapshot.py | 24 ++++++++++++++++++++ tests/test_db_snapshot.py | 47 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 sfm/db_snapshot.py create mode 100644 tests/test_db_snapshot.py diff --git a/sfm/db_snapshot.py b/sfm/db_snapshot.py new file mode 100644 index 0000000..0bd2d68 --- /dev/null +++ b/sfm/db_snapshot.py @@ -0,0 +1,24 @@ +""" +db_snapshot.py — WAL-safe snapshot / restore helpers for seismo_relay.db and +the waveform store, backing the /db/snapshot, /db/waveforms/recent.zip and +/db/restore endpoints in server.py. Kept out of server.py so the logic is +unit-testable without HTTP. +""" +from __future__ import annotations +import sqlite3 +from pathlib import Path + + +def snapshot_db(src_path: Path, dest_path: Path) -> Path: + """WAL-safe copy of a live SQLite DB via the SQLite online backup API. + Consistent even while the source is being written under WAL, and never + blocks the source's writers.""" + src = sqlite3.connect(str(src_path)) + dest = sqlite3.connect(str(dest_path)) + try: + with dest: + src.backup(dest) + finally: + src.close() + dest.close() + return Path(dest_path) diff --git a/tests/test_db_snapshot.py b/tests/test_db_snapshot.py new file mode 100644 index 0000000..32cacaa --- /dev/null +++ b/tests/test_db_snapshot.py @@ -0,0 +1,47 @@ +""" +test_db_snapshot.py — unit tests for sfm/db_snapshot.py (snapshot/restore +helpers behind the /db/snapshot, /db/waveforms/recent.zip, /db/restore routes). + +Run: + python -m pytest tests/test_db_snapshot.py -v +""" +from __future__ import annotations +import os +import sys +import sqlite3 +from pathlib import Path + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + +def _make_db(path: Path, rows: int = 3) -> None: + conn = sqlite3.connect(str(path)) + conn.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, v TEXT)") + conn.executemany("INSERT INTO t (v) VALUES (?)", [(f"row{i}",) for i in range(rows)]) + conn.commit() + conn.close() + + +def _seed_event(db_path: Path, serial: str, fn: str, ts: str) -> None: + conn = sqlite3.connect(str(db_path)) + conn.execute( + "INSERT INTO events (id, serial, waveform_key, timestamp, " + "blastware_filename, a5_pickle_filename, sidecar_filename, false_trigger) " + "VALUES (?, ?, ?, ?, ?, ?, ?, 0)", + (f"id-{fn}", serial, fn[:8], ts, fn, f"{fn}.a5.pkl", f"{fn}.sfm.json"), + ) + conn.commit() + conn.close() + + +def test_snapshot_db_produces_queryable_copy(tmp_path): + from sfm.db_snapshot import snapshot_db + src = tmp_path / "src.db" + _make_db(src, rows=5) + dest = tmp_path / "snap.db" + out = snapshot_db(src, dest) + assert out == dest and dest.exists() + conn = sqlite3.connect(str(dest)) + n = conn.execute("SELECT COUNT(*) FROM t").fetchone()[0] + conn.close() + assert n == 5