40 lines
3.0 KiB
Python
40 lines
3.0 KiB
Python
"""Guarded rollback of one field to the BEFORE values recorded in the local-completion write audit.
|
|
usage: python scripts/rollback_field.py FIELD [--db NAME ...] Writes a rollback audit next to the original audit and verifies 100% against the BEFORE snapshot."""
|
|
import argparse, gzip, json, pickle, sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
from dotenv import load_dotenv
|
|
import local_completion as lc
|
|
|
|
ap = argparse.ArgumentParser(); ap.add_argument("field"); ap.add_argument("--db", nargs="*"); ap.add_argument("--plan", action="store_true", help="only the codes listed in rollback_plan.json"); a = ap.parse_args()
|
|
plan = json.loads((Path(__file__).resolve().parents[1] / "docs" / "runs" / "local_completion" / "rollback_plan.json").read_text()) if a.plan else None
|
|
load_dotenv(lc.ROOT / ".env")
|
|
ex = lc.load_executor(); al = lc.allowlist()
|
|
out = {}
|
|
for name in a.db or list(al):
|
|
path = al[name]; todo = []
|
|
for line in gzip.open(lc.STATE / f"{name}_write_audit.ndjson.gz", "rt", encoding="utf-8"):
|
|
r = json.loads(line)
|
|
if r.get("outcome") == "SUCCEEDED" and a.field in r["fields"] and (plan is None or r["product_code"] in plan.get(name, {}).get(a.field, [])):
|
|
todo.append((r["product_code"], r["fields"][a.field]["before"], r["fields"][a.field]["after"]))
|
|
conn = lc.connect(path, rw=True); ok = fail = 0; audit = []
|
|
for code, before, after in todo:
|
|
try:
|
|
res = ex.execute_guarded_product_update(conn, database_path=path, product_code=code, changes={a.field: (after, before)}, now=datetime.now(timezone.utc).isoformat(), restore_before=True)
|
|
audit.append({**res, "outcome": "ROLLED_BACK"}); ok += 1
|
|
except Exception as e: # noqa: BLE001
|
|
audit.append({"product_code": code, "outcome": "FAILED", "error": str(e)}); fail += 1
|
|
conn.close()
|
|
with gzip.open(lc.STATE / f"{name}_rollback_{a.field}{"_plan" if plan else ""}.ndjson.gz", "wt", encoding="utf-8") as f:
|
|
for r in audit: f.write(json.dumps(r, default=str) + "\n")
|
|
# independent verification against the BEFORE snapshot
|
|
snap = pickle.load(gzip.open(lc.STATE / f"{name}_before.pkl.gz", "rb")); cols = snap["columns"]; i = cols.index(a.field)
|
|
c2 = lc.connect(path); cur = c2.cursor(); cur.execute(f"select CODIGO, {a.field} from C000025"); live = {}
|
|
for k, v in cur.fetchall(): live.setdefault(k, []).append(v)
|
|
n = lambda v: None if v is None else str(v).strip() # noqa: E731
|
|
mism = sum(1 for k, r in snap["rows"].items() if (plan is None or k in plan.get(name, {}).get(a.field, [])) and k in live and any(n(v) != n(r[i]) for v in live[k]))
|
|
out[name] = {"rollback_planned": len(todo), "rolled_back": ok, "failed": fail, "column_mismatch_vs_before_snapshot": mism, "rows_snapshot": len(snap["rows"])}
|
|
print(name, out[name], flush=True)
|
|
json.dump(out, open(lc.STATE / f"rollback_{a.field}{"_plan" if plan else ""}_summary.json", "w"), indent=1)
|