Initial SFERA platform baseline
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# sfera-query-intelligence
|
||||
|
||||
Query/table usage summaries over SIR snapshots.
|
||||
@@ -0,0 +1,11 @@
|
||||
[project]
|
||||
name = "sfera-query-intelligence"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"pydantic>=2.0",
|
||||
"sfera-sir",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
package = true
|
||||
@@ -0,0 +1,62 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from sir import EdgeKind, SemanticNode, SirSnapshot
|
||||
|
||||
|
||||
class TableUsage(BaseModel):
|
||||
table: SemanticNode
|
||||
queries: list[SemanticNode] = Field(default_factory=list)
|
||||
readers: list[SemanticNode] = Field(default_factory=list)
|
||||
writers: list[SemanticNode] = Field(default_factory=list)
|
||||
|
||||
@property
|
||||
def has_read_write_conflict(self) -> bool:
|
||||
return bool(self.readers and self.writers)
|
||||
|
||||
|
||||
def table_usage(snapshot: SirSnapshot, table_name: str | None = None) -> list[TableUsage]:
|
||||
nodes = {node.lineage_id: node for node in snapshot.nodes}
|
||||
query_owner: dict[str, SemanticNode] = {}
|
||||
for edge in snapshot.edges:
|
||||
if edge.kind == EdgeKind.OWNS_QUERY and edge.target_lineage in nodes and edge.source_lineage in nodes:
|
||||
query_owner[edge.target_lineage] = nodes[edge.source_lineage]
|
||||
|
||||
usage_by_table: dict[str, TableUsage] = {}
|
||||
for edge in snapshot.edges:
|
||||
if edge.kind != EdgeKind.READS_TABLE:
|
||||
continue
|
||||
query = nodes.get(edge.source_lineage)
|
||||
table = nodes.get(edge.target_lineage)
|
||||
if query is None or table is None:
|
||||
continue
|
||||
if table_name is not None and table.name.casefold() != table_name.casefold() and table.qualified_name.casefold() != table_name.casefold():
|
||||
continue
|
||||
usage = usage_by_table.setdefault(table.lineage_id, TableUsage(table=table))
|
||||
usage.queries.append(query)
|
||||
if owner := query_owner.get(query.lineage_id):
|
||||
usage.readers.append(owner)
|
||||
|
||||
for edge in snapshot.edges:
|
||||
if edge.kind != EdgeKind.WRITES:
|
||||
continue
|
||||
writer = nodes.get(edge.source_lineage)
|
||||
table = nodes.get(edge.target_lineage)
|
||||
if writer is None or table is None:
|
||||
continue
|
||||
if table_name is not None and table.name.casefold() != table_name.casefold() and table.qualified_name.casefold() != table_name.casefold():
|
||||
continue
|
||||
usage = usage_by_table.setdefault(table.lineage_id, TableUsage(table=table))
|
||||
usage.writers.append(writer)
|
||||
|
||||
result = list(usage_by_table.values())
|
||||
result.sort(key=lambda item: item.table.qualified_name)
|
||||
return result
|
||||
|
||||
|
||||
def tables_with_read_write_conflicts(snapshot: SirSnapshot) -> list[TableUsage]:
|
||||
return [usage for usage in table_usage(snapshot) if usage.has_read_write_conflict]
|
||||
|
||||
|
||||
__all__ = ["TableUsage", "table_usage", "tables_with_read_write_conflicts"]
|
||||
@@ -0,0 +1,72 @@
|
||||
from pathlib import Path
|
||||
|
||||
from query_intelligence import table_usage, tables_with_read_write_conflicts
|
||||
from semantic_kernel import index_project
|
||||
|
||||
|
||||
def test_table_usage_finds_query_readers(tmp_path: Path):
|
||||
module = tmp_path / "demo_module.bsl"
|
||||
module.write_text(
|
||||
"""
|
||||
Процедура ПроверитьОстатки()
|
||||
Запрос = Новый Запрос;
|
||||
Запрос.Текст =
|
||||
"ВЫБРАТЬ
|
||||
Остатки.Номенклатура
|
||||
ИЗ
|
||||
РегистрНакопления.ОстаткиТоваров КАК Остатки";
|
||||
КонецПроцедуры
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
snapshot = index_project(tmp_path, project_id="demo")
|
||||
|
||||
usage = table_usage(snapshot, "РегистрНакопления.ОстаткиТоваров")
|
||||
|
||||
assert usage[0].table.name == "ОстаткиТоваров"
|
||||
assert usage[0].readers[0].name == "ПроверитьОстатки"
|
||||
|
||||
|
||||
def test_table_usage_finds_writers(tmp_path: Path):
|
||||
module = tmp_path / "demo_module.bsl"
|
||||
module.write_text(
|
||||
"""
|
||||
Процедура Проведение()
|
||||
Движения.ОстаткиТоваров.Записать();
|
||||
КонецПроцедуры
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
snapshot = index_project(tmp_path, project_id="demo")
|
||||
|
||||
usage = table_usage(snapshot, "ОстаткиТоваров")
|
||||
|
||||
assert usage[0].writers[0].name == "Проведение"
|
||||
|
||||
|
||||
def test_tables_with_read_write_conflicts_find_register_dependencies(tmp_path: Path):
|
||||
module = tmp_path / "demo_module.bsl"
|
||||
module.write_text(
|
||||
"""
|
||||
Процедура ПроверитьОстатки()
|
||||
Запрос = Новый Запрос;
|
||||
Запрос.Текст =
|
||||
"ВЫБРАТЬ
|
||||
Остатки.Номенклатура
|
||||
ИЗ
|
||||
РегистрНакопления.ОстаткиТоваров КАК Остатки";
|
||||
КонецПроцедуры
|
||||
|
||||
Процедура Проведение()
|
||||
Движения.ОстаткиТоваров.Записать();
|
||||
КонецПроцедуры
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
snapshot = index_project(tmp_path, project_id="query-conflicts")
|
||||
|
||||
conflicts = tables_with_read_write_conflicts(snapshot)
|
||||
|
||||
assert [usage.table.qualified_name for usage in conflicts] == ["РегистрНакопления.ОстаткиТоваров"]
|
||||
assert [node.name for node in conflicts[0].readers] == ["ПроверитьОстатки"]
|
||||
assert [node.name for node in conflicts[0].writers] == ["Проведение"]
|
||||
Reference in New Issue
Block a user