154 lines
6.4 KiB
Python
154 lines
6.4 KiB
Python
#!/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 "<unknown>"] += 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())
|