Source code for spacr.cli_database

"""Command-line SQLite health and concurrency audit."""
from __future__ import annotations

import argparse
import json
import sys

from .database_concurrency import inspect_database, run_concurrency_probe


[docs] def build_parser() -> argparse.ArgumentParser: """Build the ``spacr-db-audit`` argument parser. :returns: parser for read-only database checks and disposable concurrency probes. """ parser = argparse.ArgumentParser( prog="spacr-db-audit", description=( "Inspect a spaCR SQLite database and optionally run a destructive " "reader/writer stress test on a separate new scratch database."), ) parser.add_argument( "database", nargs="?", help="existing database to inspect (never modified)") parser.add_argument( "--quick-check", action="store_true", help="run SQLite PRAGMA quick_check on the inspected database") parser.add_argument( "--probe", action="store_true", help="run a concurrent reader/writer probe in a disposable database") parser.add_argument( "--scratch", help="new, non-existing probe database path; default is temporary") parser.add_argument("--writers", type=int, default=4) parser.add_argument("--readers", type=int, default=3) parser.add_argument("--writes", type=int, default=50, help="commits per writer") parser.add_argument( "--journal-mode", choices=("WAL", "DELETE"), default="WAL") parser.add_argument("--json", action="store_true", help="emit one machine-readable JSON document") return parser
[docs] def main(argv=None) -> int: """Inspect or probe SQLite and report one combined audit result. :param argv: command-line arguments without the program name; ``None`` reads the process arguments. :returns: ``0`` when every requested integrity check and probe passes, or ``1`` when any requested operation fails or raises an exception. :raises SystemExit: with status ``2`` for invalid arguments or when neither a database nor ``--probe`` is supplied. """ args = build_parser().parse_args(argv) if not args.database and not args.probe: build_parser().error("provide DATABASE, --probe, or both") payload = {} ok = True try: if args.database: health = inspect_database( args.database, quick_check=args.quick_check) payload["database"] = health.to_dict() if health.quick_check not in (None, "ok"): ok = False if args.probe: probe = run_concurrency_probe( args.scratch, writers=args.writers, readers=args.readers, writes_per_writer=args.writes, journal_mode=args.journal_mode, ) payload["probe"] = probe.to_dict() ok = ok and probe.ok except Exception as exc: payload["error"] = f"{type(exc).__name__}: {exc}" ok = False if args.json: print(json.dumps(payload, indent=2, sort_keys=True)) else: if "database" in payload: item = payload["database"] print( f"Database: {item['path']}\n" f" journal={item['journal_mode']} " f"filesystem={item['filesystem'] or 'unknown'} " f"busy_timeout={item['busy_timeout_ms']} ms") if item["quick_check"] is not None: print(f" quick_check={item['quick_check']}") for warning in item["warnings"]: print(f" WARNING: {warning}") if "probe" in payload: item = payload["probe"] print( f"Probe: {'PASS' if item['ok'] else 'FAIL'} " f"{item['actual_rows']}/{item['expected_rows']} rows, " f"{item['reader_queries']} concurrent reads, " f"{item['duration_seconds']:.3f} s, " f"journal={item['journal_mode']}") for error in item["errors"]: print(f" ERROR: {error}") if "error" in payload: print(f"ERROR: {payload['error']}", file=sys.stderr) return 0 if ok else 1
if __name__ == "__main__": raise SystemExit(main())