Initial SQL-only 1C adapter baseline

This commit is contained in:
2026-07-22 03:03:47 +03:00
commit e2503b77e7
545 changed files with 184711 additions and 0 deletions
@@ -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())