trading-system-docs/notes/trading/system-docs/d1d2-schema-contract/tools/capture_schema.py

95 lines
3.2 KiB
Python

#!/usr/bin/env python3
"""
D2 — capture_schema.py
Liest Schema-Metadaten (read-only) von einer DB und erzeugt das kanonische Modell-JSON.
Quellenmodus: Erwartet eine Datei mit den 7 JSON-Aggregaten aus dem Capture-SQL
(der read-only psql-Ausgabe), ODER ein bereits geparstes Modell-JSON.
Usage:
capture_schema.py --from-capture <capture.json> --out <model.json>
capture_schema.py --from-model <model.json> --out <model.json> # Normalisierung
Der DB-Zugriff selbst erfolgt ausschließlich über das read-only Capture-SQL
(BEGIN TRANSACTION READ ONLY). Dieses Tool verarbeitet nur die Ergebnisse.
"""
import json
import sys
import argparse
AGG_NAMES = ["columns", "primary_keys", "unique_constraints",
"foreign_keys", "check_constraints", "indexes", "tables",
"sequences"]
def strip_psql_noise(text):
"""Entferne psql-Kontrollzeilen (BEGIN/on/COMMIT/leer/--)."""
out = []
for line in text.splitlines():
s = line.strip()
if s and s not in ("BEGIN", "on", "COMMIT") and not s.startswith("--"):
out.append(s)
return out
def parse_raw_capture(text):
"""Extrahiere die N JSON-Aggregate aus der psql-Ausgabe.
psql splittet große JSON-Arrays über mehrere Zeilen; daher wird nicht
zeilenzählend, sondern per JSON-Streaming (raw_decode) geparst.
"""
data = "\n".join(strip_psql_noise(text))
decoder = json.JSONDecoder()
aggs = []
i = 0
n = len(data)
while i < n and len(aggs) < len(AGG_NAMES):
while i < n and data[i] in " \t\r\n":
i += 1
if i >= n:
break
if data[i] not in "[{":
# non-JSON noise (z.B. Fehlermeldung) -> nächste Position
i += 1
continue
try:
val, end = decoder.raw_decode(data, i)
except json.JSONDecodeError as e:
raise SystemExit(f"FEHLER: JSON-Parse bei {i}: {e}")
aggs.append(val if val is not None else [])
i = end
if len(aggs) != len(AGG_NAMES):
raise SystemExit(
f"FEHLER: erwartete {len(AGG_NAMES)} JSON-Aggregate, fand {len(aggs)}")
return dict(zip(AGG_NAMES, aggs))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--from-capture", help="Rohe read-only psql-Capture-Datei (7 Aggregate)")
ap.add_argument("--from-model", help="Bereits geparstes Modell-JSON")
ap.add_argument("--out", required=True, help="Ziel-Modell-JSON")
args = ap.parse_args()
if args.from_capture:
with open(args.from_capture) as f:
model = parse_raw_capture(f.read())
elif args.from_model:
with open(args.from_model) as f:
model = json.load(f)
else:
raise SystemExit("FEHLER: --from-capture oder --from-model erforderlich")
# Konsistenz: columns/tables müssen übereinstimmen
col_tables = sorted({c["table"] for c in model["columns"]})
model["tables"] = sorted(model.get("tables") or col_tables)
with open(args.out, "w") as f:
json.dump(model, f, indent=2, sort_keys=True)
print(f"Modell geschrieben: {args.out}")
print(f"Tabellen: {len(model['tables'])} | Spalten: {len(model['columns'])}")
return 0
if __name__ == "__main__":
sys.exit(main())