Three pieces, all read-only or additive — no shard mutation, no
schema-version bump:
1. shard_for_document(document_root, M) in arborist/document.py.
Pure function: int(document_root[:8], 16) % M. 22 tests cover
determinism, range-bounds, near-uniform distribution (±5pp at
N=20k), and seven lock-in fixtures so peers will disagree
loudly if anyone changes the formula.
2. corpus_shard_count meta field + get/set helpers in store.py.
Lives in the existing key/value meta table; SCHEMA_VERSION
stays at v9.8.0 (the DDL doesn't change and source_root is
layout-independent, so cache records survive a reshard).
Legacy shards (without the field) return None; reshard tool
populates it on every target shard at migration time.
3. Pre-migration snapshot captured to
bench/results/pre-migration-snapshot.json:
docs 3,468,392 (3,468,226 globally unique)
chunks 6,235,764
edges 90,593,537
audit 3,468,403
This is the reference set post-reshard row counts must match.
4. Audit-event extraction script writes all 3.47M events from
all 4 shards to /tmp/audit-events.ndjson (2.0 GB) for the
Option-A canonical-chain consolidation step. Verifies chain
integrity on extract — all 4 source chains report 0 breaks.
5. Fixed a wrong chunk count in docs/corpus-history.md
(had ~3.54M/shard; actual is ~1.56M/shard) and added the
edge-count column (~22.6M/shard, 90.6M total). 6.24M chunks
total, not 14.12M.
Tests: 29 new pass (22 routing + 7 meta). No existing tests
touched.
118 lines
4 KiB
Python
118 lines
4 KiB
Python
"""Pre-migration row-count snapshot.
|
|
|
|
Captures per-shard (docs, chunks, edges, audit_events) + global
|
|
document_root count for ~/.arborist/shards/00[0-3].db. Writes to
|
|
bench/results/pre-migration-snapshot.json so post-#000065 hydration
|
|
can verify row counts match.
|
|
|
|
Read-only; no shard mutation.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
SHARDS_DIR = Path(os.path.expanduser("~/.arborist/shards"))
|
|
OUT_PATH = Path(__file__).resolve().parent / "results" / "pre-migration-snapshot.json"
|
|
SHARD_FILES = sorted(SHARDS_DIR.glob("00[0-9].db"))
|
|
|
|
|
|
def _count(conn: sqlite3.Connection, sql: str) -> int:
|
|
row = conn.execute(sql).fetchone()
|
|
return int(row[0]) if row else 0
|
|
|
|
|
|
def _snapshot_shard(path: Path) -> dict:
|
|
conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
|
|
try:
|
|
info = {
|
|
"path": str(path),
|
|
"size_bytes": path.stat().st_size,
|
|
"docs": _count(conn, "SELECT COUNT(*) FROM documents"),
|
|
"chunks": _count(conn, "SELECT COUNT(*) FROM chunks"),
|
|
"edges": _count(conn, "SELECT COUNT(*) FROM edges"),
|
|
"audit_events": _count(conn, "SELECT COUNT(*) FROM audit_events"),
|
|
"providence_cache": _count(
|
|
conn, "SELECT COUNT(*) FROM providence_cache"
|
|
) if _table_exists(conn, "providence_cache") else 0,
|
|
"schema_version": _scalar(
|
|
conn, "SELECT value FROM meta WHERE key='schema_version'"
|
|
),
|
|
}
|
|
cur = conn.execute(
|
|
"SELECT source_type, COUNT(*) FROM documents GROUP BY source_type"
|
|
)
|
|
info["docs_by_source_type"] = {row[0] or "": int(row[1]) for row in cur}
|
|
info["audit_first_ts"] = _scalar(
|
|
conn, "SELECT MIN(ts) FROM audit_events"
|
|
)
|
|
info["audit_last_ts"] = _scalar(
|
|
conn, "SELECT MAX(ts) FROM audit_events"
|
|
)
|
|
cur = conn.execute(
|
|
"SELECT event_type, COUNT(*) FROM audit_events GROUP BY event_type"
|
|
)
|
|
info["audit_by_type"] = {row[0]: int(row[1]) for row in cur}
|
|
return info
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _scalar(conn: sqlite3.Connection, sql: str):
|
|
row = conn.execute(sql).fetchone()
|
|
return row[0] if row else None
|
|
|
|
|
|
def _table_exists(conn: sqlite3.Connection, name: str) -> bool:
|
|
row = conn.execute(
|
|
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (name,)
|
|
).fetchone()
|
|
return row is not None
|
|
|
|
|
|
def _global_unique_docs(shards: list[Path]) -> int:
|
|
seen: set[bytes] = set()
|
|
for shard in shards:
|
|
conn = sqlite3.connect(f"file:{shard}?mode=ro", uri=True)
|
|
try:
|
|
cur = conn.execute("SELECT document_root FROM documents")
|
|
for (root,) in cur:
|
|
seen.add(root)
|
|
finally:
|
|
conn.close()
|
|
return len(seen)
|
|
|
|
|
|
def main() -> int:
|
|
if not SHARD_FILES:
|
|
print(f"no shards under {SHARDS_DIR}", file=sys.stderr)
|
|
return 1
|
|
t0 = time.time()
|
|
out = {
|
|
"captured_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
"shards_dir": str(SHARDS_DIR),
|
|
"shards": [_snapshot_shard(p) for p in SHARD_FILES],
|
|
}
|
|
out["totals"] = {
|
|
"size_bytes": sum(s["size_bytes"] for s in out["shards"]),
|
|
"docs": sum(s["docs"] for s in out["shards"]),
|
|
"chunks": sum(s["chunks"] for s in out["shards"]),
|
|
"edges": sum(s["edges"] for s in out["shards"]),
|
|
"audit_events": sum(s["audit_events"] for s in out["shards"]),
|
|
}
|
|
out["totals"]["docs_globally_unique"] = _global_unique_docs(SHARD_FILES)
|
|
out["elapsed_seconds"] = round(time.time() - t0, 2)
|
|
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
OUT_PATH.write_text(json.dumps(out, indent=2, default=str))
|
|
print(json.dumps(out["totals"], indent=2, default=str))
|
|
print(f"\nwrote {OUT_PATH}")
|
|
print(f"elapsed {out['elapsed_seconds']}s")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|