#!/usr/bin/env python3 """Compare SQL DBNames GUIDs with a 1C XML GUID index. This report is a bridge between SQL storage-role records and XML metadata objects. It keeps DBNames storage roles unchanged and only attaches XML facts when the same GUID is present in the XML dump. """ from __future__ import annotations import argparse import json from collections import Counter, defaultdict from pathlib import Path from typing import Any def load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) def group_dbnames(dbnames_report: dict[str, Any]) -> dict[str, Any]: grouped: dict[str, Any] = defaultdict(lambda: {"records": [], "roles": Counter(), "sources": Counter()}) for source in dbnames_report.get("dbnames") or []: file_name = source.get("file_name") or "" for record in source.get("records") or []: if record.get("status") != "parsed": continue guid = str(record.get("guid") or "").lower() if not guid: continue item = grouped[guid] compact = { "source_file": file_name, "storage_role": record.get("storage_role"), "sql_number": record.get("sql_number"), "index": record.get("index"), } item["records"].append(compact) item["roles"][compact["storage_role"]] += 1 item["sources"][file_name] += 1 result = {} for guid, item in grouped.items(): result[guid] = { "records": sorted(item["records"], key=lambda row: (row["source_file"], str(row["storage_role"]), row["sql_number"])), "storage_roles": dict(sorted(item["roles"].items())), "source_files": dict(sorted(item["sources"].items())), } return dict(sorted(result.items())) def main() -> int: parser = argparse.ArgumentParser(description="Compare SQL DBNames GUIDs with XML GUID index.") parser.add_argument("--dbnames", type=Path, required=True) parser.add_argument("--xml-index", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--max-objects", type=int, default=1000) args = parser.parse_args() dbnames = load_json(args.dbnames) xml_index = load_json(args.xml_index) sql_by_guid = group_dbnames(dbnames) xml_map = xml_index.get("guid_map") or {} matches = [] top_object_matches = [] occurrence_only_matches = [] unmatched_sql = [] matched_role_counts: Counter[str] = Counter() top_object_matched_role_counts: Counter[str] = Counter() occurrence_only_matched_role_counts: Counter[str] = Counter() unmatched_role_counts: Counter[str] = Counter() xml_kind_counts: Counter[str] = Counter() for guid, sql_item in sql_by_guid.items(): xml_item = xml_map.get(guid) roles = sql_item["storage_roles"] if not xml_item: unmatched_sql.append({"guid": guid, "storage_roles": roles, "records": sql_item["records"][:20]}) unmatched_role_counts.update(roles) continue top_objects = xml_item.get("top_objects") or [] for role, count in roles.items(): matched_role_counts[role] += count if top_objects: top_object_matched_role_counts[role] += count else: occurrence_only_matched_role_counts[role] += count for top_object in top_objects: xml_kind_counts[top_object.get("xml_kind") or ""] += 1 match_item = { "guid": guid, "storage_roles": roles, "source_files": sql_item["source_files"], "xml_top_objects": top_objects, "xml_occurrences": xml_item.get("occurrences") or [], "records": sql_item["records"][:50], } matches.append(match_item) if top_objects: top_object_matches.append(match_item) else: occurrence_only_matches.append(match_item) report = { "schema": "onec_sql_xml_guid_compare.v1", "dbnames": str(args.dbnames), "xml_index": str(args.xml_index), "sql_guid_count": len(sql_by_guid), "xml_guid_count": len(xml_map), "matched_guid_count": len(matches), "top_object_matched_guid_count": len(top_object_matches), "occurrence_only_matched_guid_count": len(occurrence_only_matches), "unmatched_sql_guid_count": len(unmatched_sql), "matched_guids": [item["guid"] for item in matches], "top_object_matched_guids": [item["guid"] for item in top_object_matches], "occurrence_only_matched_guids": [item["guid"] for item in occurrence_only_matches], "unmatched_sql_guids": [item["guid"] for item in unmatched_sql], "matched_storage_role_counts": dict(sorted(matched_role_counts.items(), key=lambda item: (-item[1], item[0]))), "top_object_matched_storage_role_counts": dict( sorted(top_object_matched_role_counts.items(), key=lambda item: (-item[1], item[0])) ), "occurrence_only_matched_storage_role_counts": dict( sorted(occurrence_only_matched_role_counts.items(), key=lambda item: (-item[1], item[0])) ), "unmatched_storage_role_counts": dict(sorted(unmatched_role_counts.items(), key=lambda item: (-item[1], item[0]))), "matched_xml_kind_counts": dict(sorted(xml_kind_counts.items(), key=lambda item: (-item[1], item[0]))), "matches": matches[: args.max_objects], "top_object_matches": top_object_matches[: args.max_objects], "occurrence_only_matches": occurrence_only_matches[: args.max_objects], "unmatched_sql": unmatched_sql[: args.max_objects], } args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print( json.dumps( { "output": str(args.output), "sql_guids": len(sql_by_guid), "xml_guids": len(xml_map), "matched": len(matches), "top_object_matched": len(top_object_matches), "occurrence_only_matched": len(occurrence_only_matches), "unmatched_sql": len(unmatched_sql), }, ensure_ascii=False, ) ) return 0 if __name__ == "__main__": raise SystemExit(main())