Files

160 lines
5.8 KiB
Python

from __future__ import annotations
from typing import List, Optional
from uuid import uuid4
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.logging import get_logger
from app.db.models.base_entity import LineageNode, LineageEdge
log = get_logger(__name__)
class LineageService:
"""血缘服务(Python 版)"""
def __init__(self, db: AsyncSession):
self.db = db
async def generate_graph(
self,
from_node: Optional[LineageNode],
edge: Optional[LineageEdge],
to_node: Optional[LineageNode]
) -> None:
"""
生成血缘图
Args:
from_node: 源节点
edge: 边
to_node: 目的节点
"""
# 1. 如果 from_node 为空,直接返回不做任何处理
if from_node is None:
return
# 2. 如果 from_node 有值,检查 from_node 是否存在
existing_from = await self.get_node_by_id(from_node.id) if from_node.id else None
if existing_from is None:
# from_node 不存在,创建 from_node,记录 from_graph_id
from_graph_id = str(uuid4())
if not from_node.id:
from_node.id = str(uuid4())
from_node.graph_id = from_graph_id
self.db.add(from_node)
await self.db.flush()
else:
# from_node 存在,记录 from_graph_id
from_graph_id = existing_from.graph_id
# 处理 edge 和 to_node
await self._generate_graph(edge, to_node, from_graph_id)
async def _generate_graph(
self,
edge: Optional[LineageEdge],
to_node: Optional[LineageNode],
graph_id: Optional[str]
) -> None:
# 无 edge,直接返回
if edge is None:
return
# 有 edge,处理 edge,检查是否有 to_node
await self._handle_lineage_edge(graph_id, edge)
if to_node is None:
return
# 有 to_node,检查 to_node 是否存在
existing_to = await self.get_node_by_id(to_node.id) if to_node.id else None
if existing_to is None:
# to_node 不存在,创建 to_node 后结束
if not to_node.id:
to_node.id = str(uuid4())
to_node.graph_id = graph_id
self.db.add(to_node)
await self.db.flush()
else:
# to_node 存在,将 from_node 所在的图并入 to_node 所在的图后结束
await self._merge_graph(graph_id, existing_to.graph_id)
async def _handle_lineage_edge(self, graph_id: Optional[str], edge: LineageEdge) -> None:
stmt = select(LineageEdge).where(
LineageEdge.graph_id == graph_id,
LineageEdge.from_node_id == edge.from_node_id,
LineageEdge.to_node_id == edge.to_node_id
)
result = await self.db.execute(stmt)
existing = result.scalar_one_or_none()
if existing is None:
if not edge.id:
edge.id = str(uuid4())
edge.graph_id = graph_id
self.db.add(edge)
await self.db.flush()
else:
edge.id = existing.id
if edge.graph_id is None:
edge.graph_id = existing.graph_id
# 覆盖更新现有记录
existing.graph_id = edge.graph_id
existing.process_id = edge.process_id
existing.edge_type = edge.edge_type
existing.name = edge.name
existing.description = edge.description
existing.edge_metadata = edge.edge_metadata
existing.from_node_id = edge.from_node_id
existing.to_node_id = edge.to_node_id
await self.db.flush()
async def _merge_graph(self, from_graph_id: Optional[str], to_graph_id: Optional[str]) -> None:
if not from_graph_id or not to_graph_id or from_graph_id == to_graph_id:
return
from_nodes = await self.get_nodes_by_graph_id(from_graph_id)
to_nodes = await self.get_nodes_by_graph_id(to_graph_id)
# choose smaller graph as source, larger as target
source_graph_id = from_graph_id if len(from_nodes) <= len(to_nodes) else to_graph_id
target_graph_id = to_graph_id if source_graph_id == from_graph_id else from_graph_id
source_nodes = from_nodes if source_graph_id == from_graph_id else to_nodes
# update nodes' graph_id
for node in source_nodes:
if node is None:
continue
node.graph_id = target_graph_id
self.db.add(node)
# update edges' graph_id (edges belonging to the source graph)
edges = await self.get_edges_by_graph_id(source_graph_id)
for edge in edges:
if edge is None:
continue
edge.graph_id = target_graph_id
self.db.add(edge)
await self.db.flush()
async def get_nodes_by_graph_id(self, graph_id: str) -> List[LineageNode]:
"""从图ID获取图的节点列表"""
stmt = select(LineageNode).where(LineageNode.graph_id == graph_id)
result = await self.db.execute(stmt)
return list(result.scalars().all())
async def get_edges_by_graph_id(self, graph_id: str) -> List[LineageEdge]:
"""从图ID获取图的边列表"""
stmt = select(LineageEdge).where(LineageEdge.graph_id == graph_id)
result = await self.db.execute(stmt)
return list(result.scalars().all())
async def get_node_by_id(self, node_id: str) -> Optional[LineageNode]:
"""从节点ID获取节点"""
return await self.db.get(LineageNode, node_id)