"""Zero-write proof for a Firebird database (READ-ONLY tool). python scripts/prove_zero_write.py snapshot # before the run python scripts/prove_zero_write.py compare A snapshot records: (1) database-level DML counters (MON$RECORD_STATS inserts/updates/deletes), (2) a SHA-256 over every row/column of C000025 (and C000004), (3) row counts of C000025/C000004/C000030/NFCE/NFCE_ITENS, (4) the file SHA-256 and size. `compare` reports whether the DML counters, table digests and counts are identical. Only SELECT statements are executed. The file SHA-256 is informational only: any attach (even an idle read-only one) touches header/page metadata, so it is not part of the verdict. """ import hashlib import json import os import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] def load_env(): f = ROOT / ".env" if f.exists(): for line in f.read_text(encoding="utf-8").splitlines(): if "=" in line and not line.startswith("#"): k, _, v = line.partition("=") os.environ.setdefault(k.strip(), v.strip()) def table_digest(cur, table): cur.execute(f"SELECT * FROM {table} ORDER BY 1") h = hashlib.sha256() n = 0 for row in cur.fetchall(): h.update(repr([v.read() if hasattr(v, "read") else v for v in row]).encode("utf-8", "replace")) n += 1 return {"rows": n, "sha256": h.hexdigest()} def snapshot(path): import fdb conn = fdb.connect(database=path, user=os.environ["FB_USER"], password=os.environ["FB_PASSWORD"], fb_library_name=os.environ.get("FIREBIRD_CLIENT_LIBRARY"), isolation_level=fdb.ISOLATION_LEVEL_READ_COMMITED_RO) try: cur = conn.cursor() stats = None try: cur.execute("SELECT r.MON$RECORD_INSERTS, r.MON$RECORD_UPDATES, r.MON$RECORD_DELETES FROM MON$RECORD_STATS r JOIN MON$DATABASE d ON d.MON$STAT_ID = r.MON$STAT_ID") row = cur.fetchone() stats = {"inserts": row[0], "updates": row[1], "deletes": row[2]} except Exception as e: # noqa: BLE001 stats = {"unavailable": type(e).__name__} digests = {t: table_digest(cur, t) for t in ("C000025", "C000004")} counts = {} for t in ("C000030", "NFCE", "NFCE_ITENS", "C000061", "C000032"): try: cur.execute(f"SELECT COUNT(*) FROM {t}") counts[t] = cur.fetchone()[0] except Exception: # noqa: BLE001 counts[t] = None finally: conn.close() h = hashlib.sha256() with open(path, "rb") as f: for block in iter(lambda: f.read(8 * 1024 * 1024), b""): h.update(block) return {"database": Path(path).name, "dml_counters": stats, "table_digests": digests, "row_counts": counts, "file_sha256": h.hexdigest(), "file_size": Path(path).stat().st_size} def compare(before, after): res = { "dml_counters_identical": before["dml_counters"] == after["dml_counters"], "table_digests_identical": before["table_digests"] == after["table_digests"], "row_counts_identical": before["row_counts"] == after["row_counts"], "file_sha256_identical": before["file_sha256"] == after["file_sha256"], "dml_counters": {"before": before["dml_counters"], "after": after["dml_counters"]}, } res["ZERO_WRITE_PROVEN"] = res["table_digests_identical"] and res["row_counts_identical"] and res["dml_counters_identical"] return res def main(argv): load_env() if argv[0] == "snapshot": Path(argv[2]).write_text(json.dumps(snapshot(argv[1]), indent=1), encoding="utf-8") print("snapshot written") elif argv[0] == "compare": r = compare(json.loads(Path(argv[1]).read_text(encoding="utf-8")), json.loads(Path(argv[2]).read_text(encoding="utf-8"))) print(json.dumps(r, indent=1)) raise SystemExit(0 if r["ZERO_WRITE_PROVEN"] else 1) else: raise SystemExit(__doc__) if __name__ == "__main__": main(sys.argv[1:])