"""Transactional SQLite restore, tested with a second connection before touching the live cache."""
import json
import pathlib
import shutil
import sqlite3
import sys
import time
sys.stdout.reconfigure(encoding="utf-8")
root = pathlib.Path(sys.argv[1]).resolve()
source_path = root / "validated-markets-03.db"
source = sqlite3.connect(source_path.as_uri() + "?mode=ro", uri=True)
if source.execute("PRAGMA integrity_check").fetchall() != [("ok",)]:
raise RuntimeError("Recovery candidate failed integrity checking")
expected = tuple(source.execute("SELECT (SELECT count(*) FROM stations),(SELECT count(*) FROM quotes),(SELECT count(*) FROM snapshots)").fetchone())
if min(expected) < 1000:
raise RuntimeError("Unexpectedly small recovery candidate")
apply_live = len(sys.argv) > 2
if apply_live:
live = pathlib.Path(sys.argv[2]).resolve()
if live.name != "markets.db" or live == source_path:
raise RuntimeError("Explicit market database target required")
# A coherent SQLite backup retains the exact current state, including its WAL.
snapshot_path = root / "before-restore-online.db"
if snapshot_path.exists():
raise RuntimeError("Refusing to overwrite the pre-restore backup")
with sqlite3.connect(live.as_uri() + "?mode=ro", uri=True) as current, sqlite3.connect(snapshot_path) as snapshot:
current.backup(snapshot)
else:
live = root / "restore-simulation.db"
if live.exists():
raise RuntimeError("Use a fresh simulation target")
for suffix in ("", "-wal", "-shm"):
original = root / ("markets.db" + suffix)
if original.exists():
shutil.copy2(original, pathlib.Path(str(live) + suffix))
reader = sqlite3.connect(live.as_uri() + "?mode=ro", uri=True, timeout=20)
before_error = None
try:
reader.execute("SELECT count(*) FROM quotes").fetchone()
except sqlite3.DatabaseError as error:
before_error = str(error)
started = time.monotonic()
def progress(status, remaining, total):
if time.monotonic() - started > 45:
raise TimeoutError("Restore deadline exceeded; SQLite rolls back the destination transaction")
with sqlite3.connect(live, timeout=20) as destination:
source.backup(destination, pages=512, progress=progress, sleep=.05)
after = tuple(reader.execute("SELECT (SELECT count(*) FROM stations),(SELECT count(*) FROM quotes),(SELECT count(*) FROM snapshots)").fetchone())
integrity = reader.execute("PRAGMA integrity_check").fetchall()
report = {"live": apply_live, "target": str(live), "before_error": before_error, "expected": expected, "actual": after,
"existing_connection_sees_restored_database": after == expected, "integrity": integrity,
"elapsed_seconds": time.monotonic() - started, "api": "sqlite3_backup", "raw_files_replaced": False}
(root / ("live-restore-report.json" if apply_live else "restore-simulation-report.json")).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(report, ensure_ascii=False), flush=True)
if after != expected or integrity != [("ok",)]:
raise RuntimeError("Restored cache validation failed")
"""Transactional SQLite restore, tested with a second connection before touching the live cache."""
import json
import pathlib
import shutil
import sqlite3
import sys
import time
sys.stdout.reconfigure(encoding="utf-8")
root = pathlib.Path(sys.argv[1]).resolve()
source_path = root / "validated-markets-03.db"
source = sqlite3.connect(source_path.as_uri() + "?mode=ro", uri=True)
if source.execute("PRAGMA integrity_check").fetchall() != [("ok",)]:
raise RuntimeError("Recovery candidate failed integrity checking")
expected = tuple(source.execute("SELECT (SELECT count(*) FROM stations),(SELECT count(*) FROM quotes),(SELECT count(*) FROM snapshots)").fetchone())
if min(expected) < 1000:
raise RuntimeError("Unexpectedly small recovery candidate")
apply_live = len(sys.argv) > 2
if apply_live:
live = pathlib.Path(sys.argv[2]).resolve()
if live.name != "markets.db" or live == source_path:
raise RuntimeError("Explicit market database target required")
# A coherent SQLite backup retains the exact current state, including its WAL.
snapshot_path = root / "before-restore-online.db"
if snapshot_path.exists():
raise RuntimeError("Refusing to overwrite the pre-restore backup")
with sqlite3.connect(live.as_uri() + "?mode=ro", uri=True) as current, sqlite3.connect(snapshot_path) as snapshot:
current.backup(snapshot)
else:
live = root / "restore-simulation.db"
if live.exists():
raise RuntimeError("Use a fresh simulation target")
for suffix in ("", "-wal", "-shm"):
original = root / ("markets.db" + suffix)
if original.exists():
shutil.copy2(original, pathlib.Path(str(live) + suffix))
reader = sqlite3.connect(live.as_uri() + "?mode=ro", uri=True, timeout=20)
before_error = None
try:
reader.execute("SELECT count(*) FROM quotes").fetchone()
except sqlite3.DatabaseError as error:
before_error = str(error)
started = time.monotonic()
def progress(status, remaining, total):
if time.monotonic() - started > 45:
raise TimeoutError("Restore deadline exceeded; SQLite rolls back the destination transaction")
with sqlite3.connect(live, timeout=20) as destination:
source.backup(destination, pages=512, progress=progress, sleep=.05)
after = tuple(reader.execute("SELECT (SELECT count(*) FROM stations),(SELECT count(*) FROM quotes),(SELECT count(*) FROM snapshots)").fetchone())
integrity = reader.execute("PRAGMA integrity_check").fetchall()
report = {"live": apply_live, "target": str(live), "before_error": before_error, "expected": expected, "actual": after,
"existing_connection_sees_restored_database": after == expected, "integrity": integrity,
"elapsed_seconds": time.monotonic() - started, "api": "sqlite3_backup", "raw_files_replaced": False}
(root / ("live-restore-report.json" if apply_live else "restore-simulation-report.json")).write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(report, ensure_ascii=False), flush=True)
if after != expected or integrity != [("ok",)]:
raise RuntimeError("Restored cache validation failed")