""" High-level plan evaluator for comparing sets of execution plans. Provides core data structures and basic evaluation functionality. """ import hashlib from collections import defaultdict from dataclasses import dataclass, field from typing import Any, Dict, List, Optional from .plan_comparator import PlanComparator from .plan_parser import parse_text from .plan_types import Plan @dataclass class PlanEntry: """Represents a single plan entry.""" sql: str plan_text: str version: str = "v4" schema: str = "" metadata: Dict[str, Any] = field(default_factory=dict) parsed_plan: Optional[Plan] = None def get_sql_digest(self) -> str: """Get normalized SQL digest for grouping.""" normalized = " ".join(self.sql.lower().split()) return hashlib.md5(normalized.encode()).hexdigest() @dataclass class PlanAnalysisResult: """Result of single plan analysis.""" plan_entry: PlanEntry complexity_score: float node_count: int depth: int operator_counts: Dict[str, int] has_parse_error: bool = False error_message: str = "" @dataclass class EvaluationMetrics: """Standardized evaluation metrics.""" total_comparisons: int = 0 identical_plans: int = 0 different_plans: int = 0 comparison_errors: int = 0 avg_similarity: float = 0.0 min_similarity: float = 0.0 max_similarity: float = 0.0 similarity_distribution: Dict[str, int] = field(default_factory=dict) avg_complexity_ratio: float = 0.0 max_complexity_ratio: float = 0.0 performance_correlations: Dict[str, float] = field(default_factory=dict) @dataclass class EvaluationResult: """Result of plan evaluation.""" total_plans: int comparable_groups: int comparison_results: List[Dict[str, Any]] metrics: EvaluationMetrics analysis_results: List[PlanAnalysisResult] = field(default_factory=list) class PlanEvaluator: """High-level evaluator for comparing plan sets.""" def __init__(self): self.comparator = PlanComparator() def analyze_single_plan( self, plan_entry: PlanEntry, parse_if_needed: bool = True ) -> PlanAnalysisResult: """Analyze a single execution plan.""" result = PlanAnalysisResult( plan_entry=plan_entry, complexity_score=0.0, node_count=0, depth=0, operator_counts={}, ) try: if parse_if_needed and not plan_entry.parsed_plan and plan_entry.plan_text: plan_entry.parsed_plan = parse_text( plan_entry.sql, plan_entry.plan_text, plan_entry.version ) if plan_entry.parsed_plan and plan_entry.parsed_plan.root: result.complexity_score = self._calculate_complexity_score( plan_entry.parsed_plan ) result.node_count = self._count_nodes(plan_entry.parsed_plan.root) result.depth = self._calculate_depth(plan_entry.parsed_plan.root) result.operator_counts = self._count_operators( plan_entry.parsed_plan.root ) except Exception as e: result.has_parse_error = True result.error_message = str(e) return result def analyze_plan_batch( self, plan_entries: List[PlanEntry] ) -> List[PlanAnalysisResult]: """Analyze a batch of plans.""" results = [] for entry in plan_entries: result = self.analyze_single_plan(entry) results.append(result) return results def _calculate_complexity_score(self, plan: Plan) -> float: """Calculate plan complexity score.""" if not plan or not plan.root: return 0.0 node_count = self._count_nodes(plan.root) depth = self._calculate_depth(plan.root) return node_count * (1 + depth * 0.3) def _count_nodes(self, node) -> int: """Count total nodes in plan tree.""" if node is None: return 0 count = 1 for child in node.get_children(): if child is not None: count += self._count_nodes(child) return count def _calculate_depth(self, node) -> int: """Calculate depth of plan tree.""" if node is None: return 0 children = node.get_children() if not children: return 1 # Filter out None children valid_children = [child for child in children if child is not None] if not valid_children: return 1 return 1 + max(self._calculate_depth(child) for child in valid_children) def _count_operators(self, node) -> Dict[str, int]: """Count operators by type in plan tree.""" counts = defaultdict(int) if node is None: return counts counts[str(node.get_type())] += 1 for child in node.get_children(): if child is not None: child_counts = self._count_operators(child) for op_type, count in child_counts.items(): counts[op_type] += count return dict(counts) def analyze_single_plan( sql: str, plan_text: str, version: str = "v4" ) -> PlanAnalysisResult: """Analyze a single plan.""" evaluator = PlanEvaluator() plan_entry = PlanEntry(sql=sql, plan_text=plan_text, version=version) return evaluator.analyze_single_plan(plan_entry)