#!/usr/bin/env python3 """Resolve composite primitive/reference value groups from a 1C SQL read result.""" from __future__ import annotations import argparse import json from collections import defaultdict from pathlib import Path from typing import Any SUFFIX_KIND = { "_TYPE": "type_marker", "_S": "string", "_N": "number", "_L": "boolean", "_T": "datetime", "_RRRef": "reference_id", "_RTRef": "reference_type", } def load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8-sig")) def value_hex(value: Any) -> str | None: if isinstance(value, dict) and value.get("kind") == "binary": return value.get("hex") return None def is_zero_binary(value: Any) -> bool: hex_value = value_hex(value) return bool(hex_value) and set(hex_value) <= {"0"} def is_present(value: Any) -> bool: if value is None: return False if isinstance(value, str): return value != "" if isinstance(value, dict) and value.get("kind") == "binary": return not is_zero_binary(value) return True def column_suffix(column: str) -> str | None: for suffix in sorted(SUFFIX_KIND, key=len, reverse=True): if column.endswith(suffix): return suffix return None def group_row(row: dict[str, Any]) -> dict[str, list[dict[str, Any]]]: groups: dict[str, list[dict[str, Any]]] = defaultdict(list) for cell_key, cell in row.items(): column = str(cell.get("column") or "") suffix = column_suffix(column) if not suffix: continue metadata_path = str(cell.get("metadata_path") or cell_key) groups[metadata_path].append({"cell_key": cell_key, "suffix": suffix, "cell": cell}) return groups def reference_types(types: list[str]) -> list[str]: return [item for item in types if item.startswith("cfg:") and "Ref." in item] def primitive_types(types: list[str]) -> list[str]: return [item for item in types if item.startswith("xs:")] def selected_branch(parts: dict[str, dict[str, Any]], types: list[str]) -> dict[str, Any]: primitive_candidates = [] for suffix in ("_S", "_N", "_L", "_T"): part = parts.get(suffix) if part and is_present(part["cell"].get("value")): primitive_candidates.append( { "suffix": suffix, "kind": SUFFIX_KIND[suffix], "value": part["cell"].get("value"), } ) rrref = parts.get("_RRRef") rtref = parts.get("_RTRef") if rrref and is_present(rrref["cell"].get("value")): refs = reference_types(types) return { "branch": "reference", "reference_id": rrref["cell"].get("value"), "reference_type_marker": rtref["cell"].get("value") if rtref else None, "candidate_reference_types": refs, "selection_evidence": "RRRef is non-zero; RTRef chooses target when present, otherwise value_type must contain a single reference type", } if primitive_candidates: return { "branch": "primitive", "primitive_values": primitive_candidates, "candidate_primitive_types": primitive_types(types), "selection_evidence": "primitive physical value column is present/non-empty", } return { "branch": "empty_or_unknown", "candidate_primitive_types": primitive_types(types), "candidate_reference_types": reference_types(types), "selection_evidence": "no non-empty primitive or reference value column observed", } def collect_scope(rows: list[dict[str, Any]], *, scope: str, table_part_name: str | None, table_part_table: str | None) -> list[dict[str, Any]]: result = [] for row_index, row in enumerate(rows): for metadata_path, members in group_row(row).items(): if len(members) < 2: continue parts = {member["suffix"]: member for member in members} if "_TYPE" not in parts: continue first = members[0]["cell"] value_type = first.get("value_type") or {} types = list(value_type.get("types") or []) if len(types) < 2: continue result.append( { "scope": scope, "table_part_name": table_part_name, "table_part_table": table_part_table, "row_index": row_index, "metadata_path": metadata_path, "metadata_name": first.get("metadata_name"), "metadata_uuid": first.get("metadata_uuid"), "value_types": types, "type_marker": parts["_TYPE"]["cell"].get("value"), "physical_parts": { suffix: { "kind": SUFFIX_KIND[suffix], "column": part["cell"].get("column"), "cell_key": part["cell_key"], "value": part["cell"].get("value"), } for suffix, part in sorted(parts.items()) }, "selected": selected_branch(parts, types), } ) return result def main() -> int: parser = argparse.ArgumentParser(description="Resolve composite primitive/reference SQL values.") parser.add_argument("--read-result", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() read_result = load_json(args.read_result) composites = [] composites.extend( collect_scope( read_result.get("main", {}).get("rows") or [], scope="main", table_part_name=None, table_part_table=read_result.get("main", {}).get("table"), ) ) for part in read_result.get("table_parts") or []: composites.extend( collect_scope( part.get("rows") or [], scope="table_part", table_part_name=part.get("name"), table_part_table=part.get("table"), ) ) summary = defaultdict(int) for item in composites: summary[item["selected"]["branch"]] += 1 result = { "schema": "onec_sql_composite_value_resolution.v1", "read_result_path": str(args.read_result), "source": {"kind": read_result.get("kind"), "identity": read_result.get("identity")}, "summary": { "composite_values": len(composites), "by_branch": dict(sorted(summary.items())), }, "composites": composites, } args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps({"output": str(args.output), "summary": result["summary"]}, ensure_ascii=False)) return 0 if __name__ == "__main__": raise SystemExit(main())