velesdb_core/velesql/explain/
step.rs1use super::types::{
10 FilterPlan, JoinPlanNode, LimitPlan, PlanNode, PlanStep, PlanStepKind, QueryPlan,
11};
12
13use PlanStepKind as Kind;
14
15const VECTOR_DESC: &str = "ANN search using HNSW index with NEAR clause";
16const FILTER_DESC: &str = "Apply WHERE clause predicates";
17const GROUP_DESC: &str = "Group rows by specified columns";
18const AGGREGATE_DESC: &str = "Compute aggregate functions (COUNT, SUM, etc.)";
19const SORT_DESC: &str = "Sort results by ORDER BY clause";
20
21impl QueryPlan {
22 #[must_use]
29 pub fn to_plan_steps(&self) -> Vec<PlanStep> {
30 let mut flat = Vec::new();
31 Self::flatten_nodes(&self.root, &mut flat);
32 let offset = pagination_offset(&flat);
33 let has_limit = flat.iter().any(|n| matches!(n, PlanNode::Limit(_)));
34
35 let mut steps = Vec::new();
36 for node in flat {
37 if let Some(step) = step_for_node(node, steps.len() + 1, offset, has_limit) {
38 steps.push(step);
39 }
40 }
41 steps
42 }
43
44 fn flatten_nodes<'a>(node: &'a PlanNode, out: &mut Vec<&'a PlanNode>) {
46 match node {
47 PlanNode::Sequence(children) => {
48 for child in children {
49 Self::flatten_nodes(child, out);
50 }
51 }
52 other => out.push(other),
53 }
54 }
55}
56
57impl PlanStep {
58 #[must_use]
63 pub fn rest_operation(&self) -> String {
64 match self.operation {
65 Kind::VectorSearch => "VectorSearch".to_string(),
66 Kind::TableScan => "FullScan".to_string(),
67 Kind::IndexLookup => "IndexLookup".to_string(),
68 Kind::Filter => "Filter".to_string(),
69 Kind::Join => format!("{}Join", self.join_type.as_deref().unwrap_or_default()),
70 Kind::GroupBy => "GroupBy".to_string(),
71 Kind::Aggregate => "Aggregate".to_string(),
72 Kind::Sort => "Sort".to_string(),
73 Kind::Limit => "Limit".to_string(),
74 Kind::Offset => "Offset".to_string(),
75 Kind::MatchTraversal => "MatchTraversal".to_string(),
76 }
77 }
78}
79
80fn pagination_offset(flat: &[&PlanNode]) -> u64 {
82 flat.iter()
83 .find_map(|n| match n {
84 PlanNode::Offset(o) => Some(o.count),
85 _ => None,
86 })
87 .unwrap_or(0)
88}
89
90fn step_for_node(node: &PlanNode, step: usize, offset: u64, has_limit: bool) -> Option<PlanStep> {
96 let built = match node {
97 PlanNode::Sequence(_) => return None,
98 PlanNode::Offset(_) if has_limit => return None, PlanNode::Offset(o) => plain(
100 step,
101 Kind::Offset,
102 format!("Skip {} rows (OFFSET)", o.count),
103 ),
104 PlanNode::VectorSearch(_) => plain(step, Kind::VectorSearch, VECTOR_DESC.to_string()),
105 PlanNode::TableScan(ts) => plain(
106 step,
107 Kind::TableScan,
108 format!("Scan collection '{}'", ts.collection),
109 ),
110 PlanNode::IndexLookup(il) => plain(
111 step,
112 Kind::IndexLookup,
113 format!(
114 "Property index lookup {}.{} = {}",
115 il.label, il.property, il.value
116 ),
117 ),
118 PlanNode::GroupBy(_) => plain(step, Kind::GroupBy, GROUP_DESC.to_string()),
119 PlanNode::Aggregate(_) => plain(step, Kind::Aggregate, AGGREGATE_DESC.to_string()),
120 PlanNode::Sort(_) => plain(step, Kind::Sort, SORT_DESC.to_string()),
121 PlanNode::MatchTraversal(mt) => plain(
122 step,
123 Kind::MatchTraversal,
124 format!("Graph traversal: {}", mt.strategy),
125 ),
126 PlanNode::Filter(f) => filter_step(step, f),
127 PlanNode::Join(j) => join_step(step, j),
128 PlanNode::Limit(l) => limit_step(step, l, offset),
129 };
130 Some(built)
131}
132
133fn plain(step: usize, operation: PlanStepKind, description: String) -> PlanStep {
135 PlanStep {
136 step,
137 operation,
138 join_type: None,
139 description,
140 estimated_rows: None,
141 estimation_method: None,
142 }
143}
144
145fn filter_step(step: usize, filter: &FilterPlan) -> PlanStep {
147 PlanStep {
148 step,
149 operation: Kind::Filter,
150 join_type: None,
151 description: FILTER_DESC.to_string(),
152 estimated_rows: filter.estimated_rows,
153 estimation_method: filter.estimation_method.clone(),
154 }
155}
156
157fn join_step(step: usize, join: &JoinPlanNode) -> PlanStep {
159 PlanStep {
160 step,
161 operation: Kind::Join,
162 join_type: Some(join.join_type.clone()),
163 description: format!("Join with '{}'", join.table),
164 estimated_rows: None,
165 estimation_method: None,
166 }
167}
168
169fn limit_step(step: usize, limit: &LimitPlan, offset: u64) -> PlanStep {
171 PlanStep {
172 step,
173 operation: Kind::Limit,
174 join_type: None,
175 description: limit_description(limit, offset),
176 estimated_rows: Some(limit.count),
177 estimation_method: None,
178 }
179}
180
181fn limit_description(limit: &LimitPlan, offset: u64) -> String {
184 let marker = if limit.is_default { " (default)" } else { "" };
185 format!("Apply LIMIT {}{marker} OFFSET {offset}", limit.count)
186}