1use crate::ast::RelationshipDirection;
9use crate::error::Result;
10use crate::logical_plan::*;
11use std::collections::{HashMap, HashSet};
12
13#[derive(Debug, Clone, Default)]
15pub struct QueryAnalysis {
16 pub var_to_label: HashMap<String, String>,
18
19 pub relationship_instances: Vec<RelationshipInstance>,
21
22 pub required_datasets: HashSet<String>,
24}
25
26#[derive(Debug, Clone)]
28pub struct RelationshipInstance {
29 pub id: usize, pub rel_type: String,
31 pub source_var: String,
32 pub target_var: String,
33 pub direction: RelationshipDirection,
34 pub alias: String, }
36
37pub struct PlanningContext<'a> {
39 pub analysis: &'a QueryAnalysis,
40 pub(crate) path_sources: crate::path_inventory::SourceBatches,
41 pub(crate) relationship_instance_idx: HashMap<String, usize>,
42 pub(crate) call_input: Option<datafusion::logical_expr::LogicalPlan>,
44 pub(crate) comprehension_elements: Vec<datafusion::logical_expr::LogicalPlan>,
45}
46
47impl<'a> PlanningContext<'a> {
48 pub fn new(analysis: &'a QueryAnalysis) -> Self {
49 Self {
50 analysis,
51 path_sources: Default::default(),
52 relationship_instance_idx: HashMap::new(),
53 comprehension_elements: Vec::new(),
54 call_input: None,
55 }
56 }
57
58 pub fn next_relationship_instance(&mut self, rel_type: &str) -> Result<RelationshipInstance> {
60 let idx = self
61 .relationship_instance_idx
62 .entry(rel_type.to_string())
63 .and_modify(|i| *i += 1)
64 .or_insert(0);
65
66 self.analysis
67 .relationship_instances
68 .iter()
69 .filter(|r| r.rel_type == rel_type)
70 .nth(*idx)
71 .cloned()
72 .ok_or_else(|| crate::error::GraphError::PlanError {
73 message: format!("No relationship instance found for: {}", rel_type),
74 location: snafu::Location::new(file!(), line!(), column!()),
75 })
76 }
77}
78
79pub(crate) fn variable_length_target_label(
85 target_label: &str,
86 source_label: Option<&String>,
87) -> String {
88 match source_label {
89 Some(source) if target_label == "Node" => source.clone(),
90 _ => target_label.to_string(),
91 }
92}
93
94pub fn analyze(logical_plan: &LogicalOperator) -> Result<QueryAnalysis> {
95 let mut analysis = QueryAnalysis::default();
96 let mut rel_counter: HashMap<String, usize> = HashMap::new();
97
98 analyze_operator(logical_plan, &mut analysis, &mut rel_counter)?;
99 Ok(analysis)
100}
101
102fn analyze_operator(
104 op: &LogicalOperator,
105 analysis: &mut QueryAnalysis,
106 rel_counter: &mut HashMap<String, usize>,
107) -> Result<()> {
108 match op {
109 LogicalOperator::ScanByLabel {
110 variable, label, ..
111 } => {
112 analysis
113 .var_to_label
114 .insert(variable.clone(), label.clone());
115 analysis.required_datasets.insert(label.clone());
116 }
117 LogicalOperator::Expand {
118 input,
119 source_variable,
120 target_variable,
121 target_label,
122 relationship_types,
123 direction,
124 relationship_variable,
125 ..
126 } => {
127 analyze_operator(input, analysis, rel_counter)?;
129
130 analysis
132 .var_to_label
133 .insert(target_variable.clone(), target_label.clone());
134
135 if let Some(rel_type) = relationship_types.first() {
137 let instance_id = rel_counter
138 .entry(rel_type.clone())
139 .and_modify(|c| *c += 1)
140 .or_insert(1);
141
142 let alias = if let Some(rel_var) = relationship_variable {
144 rel_var.clone()
145 } else {
146 format!("{}_{}", rel_type.to_lowercase(), instance_id)
147 };
148
149 analysis.relationship_instances.push(RelationshipInstance {
150 id: *instance_id,
151 rel_type: rel_type.clone(),
152 source_var: source_variable.clone(),
153 target_var: target_variable.clone(),
154 direction: direction.clone(),
155 alias,
156 });
157
158 analysis.required_datasets.insert(rel_type.clone());
159 }
160 }
161 LogicalOperator::VariableLengthExpand {
162 input,
163 source_variable,
164 target_variable,
165 target_label,
166 relationship_types,
167 direction,
168 relationship_variable,
169 min_length,
170 max_length,
171 ..
172 } => {
173 analyze_operator(input, analysis, rel_counter)?;
175
176 let label = variable_length_target_label(
177 target_label,
178 analysis.var_to_label.get(source_variable),
179 );
180 analysis.var_to_label.insert(target_variable.clone(), label);
181
182 if let Some(rel_type) = relationship_types.first() {
185 let max_hops = max_length.unwrap_or(crate::MAX_VARIABLE_LENGTH_HOPS);
186 let min_hops = min_length.unwrap_or(1).max(1);
187
188 for hop_count in min_hops..=max_hops {
190 for _ in 0..hop_count {
191 let instance_id = rel_counter
192 .entry(rel_type.clone())
193 .and_modify(|c| *c += 1)
194 .or_insert(1);
195
196 let alias = if let Some(rel_var) = relationship_variable {
198 format!("{}_{}", rel_var, instance_id)
199 } else {
200 format!("{}_{}", rel_type.to_lowercase(), instance_id)
201 };
202
203 analysis.relationship_instances.push(RelationshipInstance {
204 id: *instance_id,
205 rel_type: rel_type.clone(),
206 source_var: source_variable.clone(),
207 target_var: target_variable.clone(),
208 direction: direction.clone(),
209 alias,
210 });
211 }
212 }
213
214 analysis.required_datasets.insert(rel_type.clone());
215 }
216 }
217 LogicalOperator::Filter { input, .. }
218 | LogicalOperator::Project { input, .. }
219 | LogicalOperator::Sort { input, .. }
220 | LogicalOperator::Limit { input, .. }
221 | LogicalOperator::Offset { input, .. }
222 | LogicalOperator::Distinct { input } => {
223 analyze_operator(input, analysis, rel_counter)?;
224 }
225 LogicalOperator::Join { left, right, .. }
226 | LogicalOperator::QueryUnion { left, right, .. } => {
227 analyze_operator(left, analysis, rel_counter)?;
228 analyze_operator(right, analysis, rel_counter)?;
229 }
230 LogicalOperator::Unwind {
231 input,
232 alias,
233 label,
234 ..
235 } => {
236 if let Some(op) = input {
237 analyze_operator(op, analysis, rel_counter)?;
238 }
239 if let Some(label) = label {
240 analysis.var_to_label.insert(alias.clone(), label.clone());
241 analysis.required_datasets.insert(label.clone());
242 }
243 }
244 LogicalOperator::PathMatch { input, path } => {
245 analyze_operator(input, analysis, rel_counter)?;
246 for node in
247 std::iter::once(&path.start_node).chain(path.segments.iter().map(|s| &s.end_node))
248 {
249 if let (Some(variable), Some(label)) = (&node.variable, node.labels.first()) {
250 analysis
251 .var_to_label
252 .insert(variable.clone(), label.clone());
253 }
254 }
255 }
256 LogicalOperator::ShortestPath { input, .. } => {
257 analyze_operator(input, analysis, rel_counter)?;
258 }
259 LogicalOperator::OptionalMatch {
261 input, optional, ..
262 } => {
263 analyze_operator(input, analysis, rel_counter)?;
264 analyze_operator(optional, analysis, rel_counter)?;
265 }
266 LogicalOperator::Union { inputs } => {
268 for input in inputs {
269 analyze_operator(input, analysis, rel_counter)?;
270 }
271 }
272 LogicalOperator::ListComprehension {
274 input,
275 variable,
276 label,
277 element,
278 ..
279 } => {
280 analyze_operator(input, analysis, rel_counter)?;
281 if let Some(label) = label {
282 analysis
283 .var_to_label
284 .insert(variable.clone(), label.clone());
285 analysis.required_datasets.insert(label.clone());
286 }
287 analyze_operator(element, analysis, rel_counter)?;
288 }
289 LogicalOperator::Apply {
290 input,
291 inner,
292 labels,
293 ..
294 } => {
295 analyze_operator(input, analysis, rel_counter)?;
296 analysis
297 .required_datasets
298 .extend(analyze(inner)?.required_datasets);
299 analysis.var_to_label.extend(labels.clone());
300 }
301 LogicalOperator::CallInput { labels } => {
302 analysis.var_to_label.extend(labels.clone());
303 }
304 LogicalOperator::ComprehensionElement | LogicalOperator::SingleRow => {}
305 }
306 Ok(())
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use crate::ast::RelationshipDirection;
313 use crate::logical_plan::LogicalOperator;
314 use std::collections::HashMap;
315
316 #[test]
317 fn test_query_analysis_single_hop() {
318 let scan_a = LogicalOperator::ScanByLabel {
320 variable: "a".to_string(),
321 label: "Person".to_string(),
322 properties: Default::default(),
323 };
324 let expand = LogicalOperator::Expand {
325 input: Box::new(scan_a),
326 source_variable: "a".to_string(),
327 target_variable: "b".to_string(),
328 target_label: "Person".to_string(),
329 relationship_types: vec!["KNOWS".to_string()],
330 direction: RelationshipDirection::Outgoing,
331 relationship_variable: None,
332 properties: Default::default(),
333 target_properties: Default::default(),
334 };
335
336 let cfg = crate::config::GraphConfig::builder()
337 .node_table("Person", "id")
338 .rel_table("KNOWS", "Person", "Person", "src_id", "dst_id")
339 .build()
340 .unwrap();
341 let _planner = crate::datafusion_planner::DataFusionPlanner::new(cfg);
342 let analysis = analyze(&expand).unwrap();
343
344 assert_eq!(analysis.var_to_label.len(), 2);
346 assert_eq!(analysis.var_to_label.get("a"), Some(&"Person".to_string()));
347 assert_eq!(analysis.var_to_label.get("b"), Some(&"Person".to_string()));
348
349 assert_eq!(analysis.relationship_instances.len(), 1);
351 assert_eq!(analysis.relationship_instances[0].rel_type, "KNOWS");
352 assert_eq!(analysis.relationship_instances[0].alias, "knows_1");
353 assert_eq!(analysis.relationship_instances[0].id, 1);
354 }
355
356 #[test]
357 fn test_query_analysis_two_hop() {
358 let scan_a = LogicalOperator::ScanByLabel {
360 variable: "a".to_string(),
361 label: "Person".to_string(),
362 properties: Default::default(),
363 };
364 let expand1 = LogicalOperator::Expand {
365 input: Box::new(scan_a),
366 source_variable: "a".to_string(),
367 target_variable: "b".to_string(),
368 target_label: "Person".to_string(),
369 relationship_types: vec!["KNOWS".to_string()],
370 direction: RelationshipDirection::Outgoing,
371 relationship_variable: None,
372 properties: Default::default(),
373 target_properties: Default::default(),
374 };
375 let expand2 = LogicalOperator::Expand {
376 input: Box::new(expand1),
377 source_variable: "b".to_string(),
378 target_variable: "c".to_string(),
379 target_label: "Person".to_string(),
380 relationship_types: vec!["KNOWS".to_string()],
381 direction: RelationshipDirection::Outgoing,
382 relationship_variable: None,
383 properties: Default::default(),
384 target_properties: Default::default(),
385 };
386
387 let cfg = crate::config::GraphConfig::builder()
388 .node_table("Person", "id")
389 .rel_table("KNOWS", "Person", "Person", "src_id", "dst_id")
390 .build()
391 .unwrap();
392 let _planner = crate::datafusion_planner::DataFusionPlanner::new(cfg);
393 let analysis = analyze(&expand2).unwrap();
394
395 assert_eq!(analysis.relationship_instances.len(), 2);
397 assert_eq!(analysis.relationship_instances[0].alias, "knows_1");
398 assert_eq!(analysis.relationship_instances[1].alias, "knows_2");
399
400 assert_eq!(analysis.relationship_instances[0].rel_type, "KNOWS");
402 assert_eq!(analysis.relationship_instances[1].rel_type, "KNOWS");
403 assert_eq!(analysis.relationship_instances[0].id, 1);
404 assert_eq!(analysis.relationship_instances[1].id, 2);
405 }
406
407 #[test]
408 fn test_varlength_expand_analysis_registers_instances() {
409 let scan_a = LogicalOperator::ScanByLabel {
411 variable: "a".to_string(),
412 label: "Person".to_string(),
413 properties: Default::default(),
414 };
415 let vlexpand = LogicalOperator::VariableLengthExpand {
416 input: Box::new(scan_a),
417 source_variable: "a".to_string(),
418 target_variable: "b".to_string(),
419 target_label: "Person".to_string(),
420 relationship_types: vec!["KNOWS".to_string()],
421 direction: RelationshipDirection::Outgoing,
422 relationship_variable: None,
423 min_length: Some(1),
424 max_length: Some(2),
425 target_properties: HashMap::new(),
426 };
427
428 let cfg = crate::config::GraphConfig::builder()
429 .node_table("Person", "id")
430 .rel_table(
431 "KNOWS",
432 "Person",
433 "Person",
434 "src_person_id",
435 "dst_person_id",
436 )
437 .build()
438 .unwrap();
439 let _planner = crate::datafusion_planner::DataFusionPlanner::new(cfg);
440 let analysis = analyze(&vlexpand).unwrap();
441
442 let knows_instances: Vec<_> = analysis
444 .relationship_instances
445 .iter()
446 .filter(|r| r.rel_type == "KNOWS")
447 .collect();
448
449 assert_eq!(
450 knows_instances.len(),
451 3,
452 "Expected 3 KNOWS instances (1 for 1-hop + 2 for 2-hop)"
453 );
454 }
455
456 #[test]
457 fn test_planning_context_tracks_instances() {
458 let instances = vec![
460 RelationshipInstance {
461 id: 1,
462 rel_type: "KNOWS".to_string(),
463 source_var: "a".to_string(),
464 target_var: "b".to_string(),
465 direction: RelationshipDirection::Outgoing,
466 alias: "knows_1".to_string(),
467 },
468 RelationshipInstance {
469 id: 2,
470 rel_type: "KNOWS".to_string(),
471 source_var: "b".to_string(),
472 target_var: "c".to_string(),
473 direction: RelationshipDirection::Outgoing,
474 alias: "knows_2".to_string(),
475 },
476 ];
477
478 let analysis = QueryAnalysis {
479 var_to_label: HashMap::new(),
480 relationship_instances: instances,
481 required_datasets: HashSet::new(),
482 };
483
484 let mut ctx = PlanningContext::new(&analysis);
485
486 let inst1 = ctx.next_relationship_instance("KNOWS").unwrap();
488 assert_eq!(inst1.alias, "knows_1");
489
490 let inst2 = ctx.next_relationship_instance("KNOWS").unwrap();
492 assert_eq!(inst2.alias, "knows_2");
493
494 assert!(ctx.next_relationship_instance("KNOWS").is_err());
496 }
497}