#!/usr/bin/env python3 """Build a SQL read projection from enriched structured 1C metadata.""" from __future__ import annotations import argparse import json from pathlib import Path from typing import Any METADATA_FIELDS = ( "attributes", "dimensions", "resources", "addressing_attributes", "accounting_flags", ) STANDARD_COLUMNS = { "Document": ["_IDRRef", "_Marked", "_Date_Time", "_Number", "_Posted"], "Catalog": ["_IDRRef", "_Marked", "_PredefinedID", "_Description"], "AccumulationRegister": ["_Period", "_RecorderTRef", "_RecorderRRef", "_LineNo", "_Active", "_RecordKind"], "AccountingRegister": ["_Period", "_RecorderTRef", "_RecorderRRef", "_LineNo", "_Active"], # Information registers differ by periodicity/recorder settings. Standard # columns must be added from live table schema, not assumed globally. "InformationRegister": [], "BusinessProcess": ["_IDRRef", "_Marked", "_Date_Time", "_Number"], "Task": ["_IDRRef", "_Marked", "_Date_Time", "_Number"], "ChartOfAccounts": ["_IDRRef", "_Marked", "_PredefinedID", "_Code", "_Description"], "ChartOfCalculationTypes": ["_IDRRef", "_Marked", "_PredefinedID", "_Code", "_Description"], } def load_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8-sig")) def quote_ident(value: str) -> str: return "[" + value.replace("]", "]]") + "]" def alias(value: str) -> str: return value.replace("]", "").replace("[", "").replace(".", "__") def primary_tables(report: dict[str, Any]) -> list[str]: tables = [] for route in report.get("object_storage_routes") or []: role = route.get("storage_role") or "" table = route.get("physical_name_candidate") if not table or route.get("route_kind") != "table": continue if role.endswith("ChngR") or role.endswith("SInf"): continue if role in {"BPrPoints", "AccumRgT", "AccumRgOpt", "AccRgAT0", "AccRgCT", "AccRgOpt"}: continue tables.append(table) return tables def validation_column_map(validation: dict[str, Any]) -> dict[tuple[str, str], dict[str, Any]]: result = {} for row in validation.get("results") or []: if not row.get("found"): continue for match in row.get("matches") or []: result[(match["table"], match["column"])] = match return result def item_columns(item: dict[str, Any], candidate_tables: list[str], schema: dict[tuple[str, str], dict[str, Any]]) -> list[dict[str, Any]]: result = [] for column in item.get("physical_columns") or []: name = column.get("column") if not name: continue for table in candidate_tables: match = schema.get((table, name)) result.append( { "metadata_name": item.get("name"), "metadata_uuid": item.get("uuid"), "column": name, "sql_type": match, "value_type": item.get("value_type"), "source": item.get("source"), "extension_name": item.get("extension_name"), } ) return result def projected_items(items: list[dict[str, Any]], *, view: str, extension: str | None) -> list[dict[str, Any]]: if view == "effective": return items if view == "base": return [item for item in items if (item.get("source") or "base") != "extension"] if view == "extension": if not extension: raise SystemExit("Use --extension with --view extension.") result = [] for item in items: if item.get("source") == "extension" and item.get("extension_name") == extension: result.append(item) continue for override in item.get("extension_overrides") or []: if override.get("extension_name") == extension: copy = dict(item) copy["source"] = "extension" copy["extension_name"] = extension if override.get("value_type"): copy["value_type"] = override["value_type"] result.append(copy) break return result raise SystemExit(f"Unsupported view: {view}") def select_sql(table: str, columns: list[dict[str, Any]], *, top: int) -> str: parts = [] for index, col in enumerate(columns, start=1): name = col["column"] out_alias = col.get("alias") or f"c{index:03d}" col["select_alias"] = out_alias parts.append(f" {quote_ident(name)} AS {quote_ident(out_alias)}") select_list = ",\n".join(parts) if parts else " *" return f"SELECT TOP ({top})\n{select_list}\nFROM {quote_ident(table)};" def build_projection(report: dict[str, Any], validation: dict[str, Any], *, top: int, view: str, extension: str | None) -> dict[str, Any]: schema = validation_column_map(validation) kind = report.get("kind") tables = primary_tables(report) main_table = tables[0] if tables else None diagnostics: dict[str, Any] = {"standard_columns_without_validation": []} main_columns = [] if main_table: for column in STANDARD_COLUMNS.get(kind, []): match = schema.get((main_table, column)) if not match: diagnostics["standard_columns_without_validation"].append({"scope": "main", "table": main_table, "column": column}) main_columns.append( { "metadata_name": f"standard.{column}", "metadata_path": f"standard.{column}", "column": column, "sql_type": match, } ) for field in METADATA_FIELDS: for item in projected_items(report.get(field) or [], view=view, extension=extension): for column in item_columns(item, [main_table], schema): column["metadata_field"] = field column["metadata_path"] = f"{field}.{item.get('name')}" main_columns.append(column) table_parts = [] by_parent: dict[str, list[dict[str, Any]]] = {} for item in projected_items(report.get("tabular_section_attributes") or [], view=view, extension=extension): parent_uuid = item.get("tabular_section_uuid") or item.get("parent_uuid") by_parent.setdefault(parent_uuid, []).append(item) for section in report.get("tabular_sections") or []: physical_tables = [row["table"] for row in section.get("physical_tables") or [] if row.get("table")] if not physical_tables: continue table = physical_tables[0] columns = [] parent_id = f"{main_table}_IDRRef" if main_table else "" if parent_id: match = schema.get((table, parent_id)) if not match: diagnostics["standard_columns_without_validation"].append({"scope": f"table_part:{section.get('name')}", "table": table, "column": parent_id}) columns.append({"metadata_name": "standard.owner", "metadata_path": "standard.owner", "column": parent_id, "sql_type": match}) for route in section.get("storage_routes") or []: if route.get("storage_role") == "LineNo": line_column = f"_LineNo{route['sql_number']}" match = schema.get((table, line_column)) if not match: diagnostics["standard_columns_without_validation"].append({"scope": f"table_part:{section.get('name')}", "table": table, "column": line_column}) columns.append({"metadata_name": "standard.line_no", "metadata_path": "standard.line_no", "column": line_column, "sql_type": match}) for item in by_parent.get(section.get("uuid"), []): for column in item_columns(item, [table], schema): column["metadata_field"] = "tabular_section_attributes" column["metadata_path"] = f"{section.get('name')}.{item.get('name')}" columns.append(column) table_parts.append( { "name": section.get("name"), "uuid": section.get("uuid"), "table": table, "columns": columns, "select_sql": select_sql(table, columns, top=top), } ) return { "schema": "onec_sql_read_projection.v1", "kind": kind, "identity": report.get("identity"), "view": view, "extension": extension, "top": top, "main_table": main_table, "main_columns": main_columns, "main_select_sql": select_sql(main_table, main_columns, top=top) if main_table else None, "table_parts": table_parts, "diagnostics": diagnostics, } def main() -> int: parser = argparse.ArgumentParser(description="Build SQL read projection from enriched 1C metadata.") parser.add_argument("--metadata", type=Path, required=True) parser.add_argument("--validation", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--top", type=int, default=10) parser.add_argument("--view", choices=["effective", "base", "extension"], default="effective") parser.add_argument("--extension") args = parser.parse_args() projection = build_projection(load_json(args.metadata), load_json(args.validation), top=args.top, view=args.view, extension=args.extension) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(projection, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps({"output": str(args.output), "main_table": projection.get("main_table"), "table_parts": len(projection.get("table_parts") or [])}, ensure_ascii=False)) return 0 if __name__ == "__main__": raise SystemExit(main())