Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user