Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,465 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a read-projection metadata card from resolved XML object evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from resolve_1c_object import load_json, physical_name, resolve_object # noqa: E402
|
||||
|
||||
|
||||
REFERENCE_MARKERS = ("Ref.",)
|
||||
|
||||
TYPE_PRESENTATION_RU = {
|
||||
"xs:string": "Строка",
|
||||
"xs:decimal": "Число",
|
||||
"xs:boolean": "Булево",
|
||||
"xs:dateTime": "Дата",
|
||||
"v8:UUID": "УникальныйИдентификатор",
|
||||
"cfg:AnyRef": "ЛюбаяСсылка",
|
||||
"cfg:AnyIBRef": "ЛюбаяСсылка",
|
||||
}
|
||||
|
||||
REFERENCE_PRESENTATION_RU = {
|
||||
"CatalogRef": "СправочникСсылка",
|
||||
"DocumentRef": "ДокументСсылка",
|
||||
"EnumRef": "ПеречислениеСсылка",
|
||||
"ChartOfAccountsRef": "ПланСчетовСсылка",
|
||||
"ChartOfCalculationTypesRef": "ПланВидовРасчетаСсылка",
|
||||
"ChartOfCharacteristicTypesRef": "ПланВидовХарактеристикСсылка",
|
||||
"BusinessProcessRef": "БизнесПроцессСсылка",
|
||||
"TaskRef": "ЗадачаСсылка",
|
||||
}
|
||||
|
||||
|
||||
def local_name(tag: str) -> str:
|
||||
return tag.rsplit("}", 1)[-1] if "}" in tag else tag
|
||||
|
||||
|
||||
def direct_child(node: ET.Element, name: str) -> ET.Element | None:
|
||||
return next((child for child in list(node) if local_name(child.tag) == name), None)
|
||||
|
||||
|
||||
def text_child(node: ET.Element | None, name: str) -> str | None:
|
||||
if node is None:
|
||||
return None
|
||||
child = direct_child(node, name)
|
||||
if child is None or child.text is None:
|
||||
return None
|
||||
return child.text.strip()
|
||||
|
||||
|
||||
def synonym(properties: ET.Element | None) -> str | None:
|
||||
if properties is None:
|
||||
return None
|
||||
syn = direct_child(properties, "Synonym")
|
||||
if syn is None:
|
||||
return None
|
||||
for node in syn.iter():
|
||||
if local_name(node.tag) == "content" and node.text:
|
||||
return node.text.strip()
|
||||
return None
|
||||
|
||||
|
||||
def value_types(properties: ET.Element | None) -> list[str]:
|
||||
if properties is None:
|
||||
return []
|
||||
type_node = direct_child(properties, "Type")
|
||||
if type_node is None:
|
||||
return []
|
||||
result = []
|
||||
for node in type_node.iter():
|
||||
if local_name(node.tag) == "Type" and node.text and ":" in node.text:
|
||||
result.append(node.text.strip())
|
||||
elif local_name(node.tag) == "TypeSet" and node.text and ":" in node.text:
|
||||
result.append(node.text.strip())
|
||||
return result
|
||||
|
||||
|
||||
def type_presentation_ru(value: str) -> str:
|
||||
if value in TYPE_PRESENTATION_RU:
|
||||
return TYPE_PRESENTATION_RU[value]
|
||||
if value.startswith("cfg:"):
|
||||
payload = value[4:]
|
||||
if "." in payload:
|
||||
family, name = payload.split(".", 1)
|
||||
prefix = REFERENCE_PRESENTATION_RU.get(family)
|
||||
if prefix:
|
||||
return f"{prefix}.{name}"
|
||||
return f"{family}.{name}"
|
||||
return value
|
||||
|
||||
|
||||
def route_kind(role: str) -> str:
|
||||
if role == "Fld":
|
||||
return "field"
|
||||
if role in {"VT", "LineNo"}:
|
||||
return "structural"
|
||||
return "table"
|
||||
|
||||
|
||||
def storage_routes(index: dict[str, Any], guid: str | None) -> list[dict[str, Any]]:
|
||||
if not guid:
|
||||
return []
|
||||
item = (index.get("objects") or {}).get(guid.lower()) or {}
|
||||
result = []
|
||||
for route in item.get("dbnames") or []:
|
||||
copy = {
|
||||
"guid": guid,
|
||||
"storage_role": route.get("storage_role"),
|
||||
"sql_number": route.get("sql_number"),
|
||||
"source": route.get("source_file") or "DBNames",
|
||||
"route_kind": route_kind(str(route.get("storage_role") or "")),
|
||||
"physical_name_candidate": physical_name(route),
|
||||
}
|
||||
result.append(copy)
|
||||
return result
|
||||
|
||||
|
||||
def value_type_payload(types: list[str]) -> dict[str, Any] | None:
|
||||
if not types:
|
||||
return None
|
||||
return {
|
||||
"types": types,
|
||||
"presentation": {
|
||||
"default_language": "ru",
|
||||
"ru": [type_presentation_ru(item) for item in types],
|
||||
},
|
||||
"qualifiers": {},
|
||||
"is_composite": len(types) > 1,
|
||||
}
|
||||
|
||||
|
||||
def field_columns(routes: list[dict[str, Any]], types: list[str]) -> list[dict[str, Any]]:
|
||||
fld = next((route for route in routes if route.get("storage_role") == "Fld" and route.get("sql_number") is not None), None)
|
||||
if not fld:
|
||||
return [{"status": "no_storage_route"}]
|
||||
base = f"_Fld{fld['sql_number']}"
|
||||
if len(types) > 1:
|
||||
return [
|
||||
{"column": f"{base}_TYPE", "reason": "composite value discriminator", "value_types": types},
|
||||
{"column": f"{base}_S", "reason": "composite string value", "value_types": types},
|
||||
{"column": f"{base}_N", "reason": "composite numeric value", "value_types": types},
|
||||
{"column": f"{base}_L", "reason": "composite boolean value", "value_types": types},
|
||||
{"column": f"{base}_T", "reason": "composite datetime value", "value_types": types},
|
||||
{"column": f"{base}_RTRef", "reason": "composite reference type id", "value_types": types},
|
||||
{"column": f"{base}_RRRef", "reason": "composite reference value", "value_types": types},
|
||||
]
|
||||
value_type = types[0] if types else None
|
||||
if value_type in {"cfg:AnyRef", "cfg:AnyIBRef"}:
|
||||
return [
|
||||
{"column": f"{base}_TYPE", "reason": "any reference discriminator", "value_types": types},
|
||||
{"column": f"{base}_RTRef", "reason": "any reference type id", "value_types": types},
|
||||
{"column": f"{base}_RRRef", "reason": "any reference value", "value_types": types},
|
||||
]
|
||||
if value_type and any(marker in value_type for marker in REFERENCE_MARKERS):
|
||||
return [{"column": f"{base}RRef", "reason": "single 1C reference type", "value_type": value_type}]
|
||||
return [{"column": base, "reason": "single primitive value", "value_type": value_type}]
|
||||
|
||||
|
||||
def extension_name_from_path(path: str | None) -> str | None:
|
||||
parts = str(path or "").replace("/", "\\").split("\\")
|
||||
lowered = [part.casefold() for part in parts]
|
||||
if "расширения" in lowered:
|
||||
index = lowered.index("расширения")
|
||||
if index + 1 < len(parts):
|
||||
return parts[index + 1]
|
||||
if "extensions" in lowered:
|
||||
index = lowered.index("extensions")
|
||||
if index + 1 < len(parts):
|
||||
return parts[index + 1]
|
||||
return None
|
||||
|
||||
|
||||
def metadata_item(
|
||||
node: ET.Element,
|
||||
*,
|
||||
index: dict[str, Any],
|
||||
category: str,
|
||||
parent_category: str,
|
||||
parent_name: str,
|
||||
parent_uuid: str,
|
||||
tabular_section_name: str | None = None,
|
||||
tabular_section_uuid: str | None = None,
|
||||
record_index: int,
|
||||
) -> dict[str, Any]:
|
||||
properties = direct_child(node, "Properties")
|
||||
guid = (node.get("uuid") or "").lower()
|
||||
name = text_child(properties, "Name")
|
||||
types = value_types(properties)
|
||||
routes = storage_routes(index, guid)
|
||||
item = {
|
||||
"category": category,
|
||||
"name": name,
|
||||
"synonym": synonym(properties),
|
||||
"uuid": guid,
|
||||
"value_type": value_type_payload(types),
|
||||
"parent_category": parent_category,
|
||||
"parent_name": parent_name,
|
||||
"parent_uuid": parent_uuid,
|
||||
"record_index": record_index,
|
||||
"evidence": {"name": bool(name), "synonym": bool(synonym(properties)), "uuid": bool(guid)},
|
||||
"storage_routes": routes,
|
||||
"storage_route_count": len(routes),
|
||||
"physical_columns": field_columns(routes, types) if category == "Attribute" else [{"status": "no_value_type"}],
|
||||
}
|
||||
object_belonging = text_child(properties, "ObjectBelonging")
|
||||
extended_object = text_child(properties, "ExtendedConfigurationObject")
|
||||
if object_belonging:
|
||||
item["object_belonging"] = object_belonging
|
||||
if extended_object:
|
||||
item["extended_configuration_object"] = extended_object.lower()
|
||||
if tabular_section_name:
|
||||
item["tabular_section_name"] = tabular_section_name
|
||||
item["tabular_section_uuid"] = tabular_section_uuid
|
||||
return item
|
||||
|
||||
|
||||
def object_node(root: ET.Element, kind: str) -> ET.Element:
|
||||
for node in root.iter():
|
||||
if local_name(node.tag) == kind:
|
||||
return node
|
||||
raise SystemExit(f"XML object node not found: {kind}")
|
||||
|
||||
|
||||
def merge_attribute_overlay(base: list[dict[str, Any]], overlay: dict[str, Any]) -> None:
|
||||
extended_uuid = overlay.get("extended_configuration_object")
|
||||
target = None
|
||||
if extended_uuid:
|
||||
target = next((item for item in base if item.get("uuid") == extended_uuid), None)
|
||||
if target is None and overlay.get("name"):
|
||||
target = next((item for item in base if item.get("name") == overlay.get("name")), None)
|
||||
if target is None:
|
||||
base.append(overlay)
|
||||
return
|
||||
|
||||
record = {
|
||||
"source": overlay.get("source"),
|
||||
"extension_name": overlay.get("extension_name"),
|
||||
"path": overlay.get("source_path"),
|
||||
"uuid": overlay.get("uuid"),
|
||||
"object_belonging": overlay.get("object_belonging"),
|
||||
"extended_configuration_object": overlay.get("extended_configuration_object"),
|
||||
"value_type": overlay.get("value_type"),
|
||||
"synonym": overlay.get("synonym"),
|
||||
}
|
||||
target.setdefault("extension_overrides", []).append(record)
|
||||
target["effective_source"] = "base+extension"
|
||||
if overlay.get("value_type"):
|
||||
target["base_value_type"] = target.get("base_value_type") or target.get("value_type")
|
||||
target["value_type"] = overlay["value_type"]
|
||||
target["physical_columns"] = field_columns(target.get("storage_routes") or [], overlay["value_type"].get("types") or [])
|
||||
if overlay.get("synonym"):
|
||||
target["synonym"] = overlay["synonym"]
|
||||
|
||||
|
||||
def apply_object_overlay(
|
||||
*,
|
||||
index: dict[str, Any],
|
||||
overlay: dict[str, Any],
|
||||
kind: str,
|
||||
attributes: list[dict[str, Any]],
|
||||
tabular_sections: list[dict[str, Any]],
|
||||
tabular_section_attributes: list[dict[str, Any]],
|
||||
) -> dict[str, int]:
|
||||
path = Path(str(overlay.get("path") or ""))
|
||||
if not path.is_file():
|
||||
return {"missing": 1, "attributes_added": 0, "attributes_changed": 0}
|
||||
root = ET.parse(path).getroot()
|
||||
node = object_node(root, kind)
|
||||
children = direct_child(node, "ChildObjects")
|
||||
if children is None:
|
||||
return {"missing": 0, "attributes_added": 0, "attributes_changed": 0}
|
||||
|
||||
added = 0
|
||||
changed = 0
|
||||
extension_name = extension_name_from_path(str(path))
|
||||
for child in list(children):
|
||||
if local_name(child.tag) != "Attribute":
|
||||
continue
|
||||
before = len(attributes)
|
||||
item = metadata_item(
|
||||
child,
|
||||
index=index,
|
||||
category="Attribute",
|
||||
parent_category=kind,
|
||||
parent_name=str(overlay.get("name") or ""),
|
||||
parent_uuid=str(overlay.get("guid") or ""),
|
||||
record_index=len(attributes),
|
||||
)
|
||||
item["source"] = "extension"
|
||||
item["extension_name"] = extension_name
|
||||
item["source_path"] = str(path)
|
||||
merge_attribute_overlay(attributes, item)
|
||||
if len(attributes) > before:
|
||||
added += 1
|
||||
else:
|
||||
changed += 1
|
||||
return {"missing": 0, "attributes_added": added, "attributes_changed": changed}
|
||||
|
||||
|
||||
def build_metadata(index: dict[str, Any], *, kind: str, name: str) -> dict[str, Any]:
|
||||
resolution = resolve_object(index, kind=kind, name=name, limit=50)
|
||||
canonical = resolution.get("canonical")
|
||||
if not canonical:
|
||||
raise SystemExit(f"Object not found: {kind}.{name}")
|
||||
xml_path = Path(str(canonical.get("path") or ""))
|
||||
if not xml_path.is_file():
|
||||
raise SystemExit(f"Object XML file not found: {xml_path}")
|
||||
|
||||
root = ET.parse(xml_path).getroot()
|
||||
node = object_node(root, str(canonical["kind"]))
|
||||
properties = direct_child(node, "Properties")
|
||||
object_name = text_child(properties, "Name") or str(canonical["name"])
|
||||
object_uuid = str(canonical["guid"]).lower()
|
||||
main_routes = storage_routes(index, object_uuid)
|
||||
main_table = next((physical_name(route) for route in main_routes if route.get("storage_role") == canonical["kind"]), None)
|
||||
if not main_table:
|
||||
main_table = next((route.get("physical_name_candidate") for route in main_routes if route.get("route_kind") == "table"), None)
|
||||
|
||||
attributes = []
|
||||
tabular_sections = []
|
||||
tabular_section_attributes = []
|
||||
children = direct_child(node, "ChildObjects")
|
||||
if children is not None:
|
||||
attr_index = 0
|
||||
ts_index = 0
|
||||
for child in list(children):
|
||||
child_kind = local_name(child.tag)
|
||||
if child_kind == "Attribute":
|
||||
attributes.append(
|
||||
{
|
||||
**metadata_item(
|
||||
child,
|
||||
index=index,
|
||||
category="Attribute",
|
||||
parent_category=str(canonical["kind"]),
|
||||
parent_name=object_name,
|
||||
parent_uuid=object_uuid,
|
||||
record_index=attr_index,
|
||||
),
|
||||
"source": "base",
|
||||
}
|
||||
)
|
||||
attr_index += 1
|
||||
elif child_kind == "TabularSection":
|
||||
section = metadata_item(
|
||||
child,
|
||||
index=index,
|
||||
category="TabularSection",
|
||||
parent_category=str(canonical["kind"]),
|
||||
parent_name=object_name,
|
||||
parent_uuid=object_uuid,
|
||||
record_index=ts_index,
|
||||
)
|
||||
vt_route = next((route for route in section["storage_routes"] if route.get("storage_role") == "VT"), None)
|
||||
line_numbers = [route.get("sql_number") for route in section["storage_routes"] if route.get("storage_role") == "LineNo"]
|
||||
if main_table and vt_route and vt_route.get("sql_number") is not None:
|
||||
section["physical_tables"] = [
|
||||
{
|
||||
"table": f"{main_table}_VT{vt_route['sql_number']}",
|
||||
"reason": "tabular section VT route under object table",
|
||||
"vt_sql_number": vt_route["sql_number"],
|
||||
"line_no_sql_numbers": line_numbers,
|
||||
}
|
||||
]
|
||||
tabular_sections.append(section)
|
||||
section_children = direct_child(child, "ChildObjects")
|
||||
if section_children is not None:
|
||||
for record_index, section_child in enumerate([item for item in list(section_children) if local_name(item.tag) == "Attribute"]):
|
||||
attr = metadata_item(
|
||||
section_child,
|
||||
index=index,
|
||||
category="Attribute",
|
||||
parent_category="TabularSection",
|
||||
parent_name=section["name"],
|
||||
parent_uuid=section["uuid"],
|
||||
tabular_section_name=section["name"],
|
||||
tabular_section_uuid=section["uuid"],
|
||||
record_index=record_index,
|
||||
)
|
||||
attr["parent_physical_tables"] = section.get("physical_tables") or []
|
||||
attr["source"] = "base"
|
||||
tabular_section_attributes.append(attr)
|
||||
ts_index += 1
|
||||
|
||||
overlay_stats = []
|
||||
for overlay in resolution.get("extension_overlays") or []:
|
||||
stats = apply_object_overlay(
|
||||
index=index,
|
||||
overlay=overlay,
|
||||
kind=str(canonical["kind"]),
|
||||
attributes=attributes,
|
||||
tabular_sections=tabular_sections,
|
||||
tabular_section_attributes=tabular_section_attributes,
|
||||
)
|
||||
overlay_stats.append(
|
||||
{
|
||||
"extension_name": extension_name_from_path(overlay.get("path")),
|
||||
"path": overlay.get("path"),
|
||||
**stats,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"schema": "onec_structured_metadata_from_resolved_xml.v1",
|
||||
"kind": canonical["kind"],
|
||||
"xml_file": str(xml_path),
|
||||
"identity": {
|
||||
"guid": object_uuid,
|
||||
"name": object_name,
|
||||
"synonyms": {"ru": synonym(properties)} if synonym(properties) else {},
|
||||
},
|
||||
"resolution": {"schema": resolution.get("schema"), "canonical": canonical, "summary": resolution.get("summary")},
|
||||
"effective_metadata": {
|
||||
"base_path": str(xml_path),
|
||||
"extension_overlays_applied": overlay_stats,
|
||||
},
|
||||
"attributes": attributes,
|
||||
"tabular_sections": tabular_sections,
|
||||
"tabular_section_attributes": tabular_section_attributes,
|
||||
"dimensions": [],
|
||||
"resources": [],
|
||||
"forms": [],
|
||||
"templates": [],
|
||||
"commands": [],
|
||||
"addressing_attributes": [],
|
||||
"accounting_flags": [],
|
||||
"columns": [],
|
||||
"enum_values": [],
|
||||
"object_storage_routes": main_routes,
|
||||
"storage_route_summary": {
|
||||
"metadata_items_with_routes": sum(1 for item in attributes + tabular_sections + tabular_section_attributes if item.get("storage_routes")),
|
||||
"object_routes": len(main_routes),
|
||||
},
|
||||
"counts": {
|
||||
"attributes": len(attributes),
|
||||
"tabular_sections": len(tabular_sections),
|
||||
"tabular_section_attributes": len(tabular_section_attributes),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Build metadata card from resolved 1C object XML.")
|
||||
parser.add_argument("--index", type=Path, required=True)
|
||||
parser.add_argument("--kind", required=True)
|
||||
parser.add_argument("--name", required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
result = build_metadata(load_json(args.index), kind=args.kind, name=args.name)
|
||||
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), "counts": result["counts"]}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user