linkrs_query/optimizer/factorization/
group_dependency_analyzer.rs1use std::collections::{HashMap, HashSet};
2
3use linkrs_core::function::{is_list_lambda, list_lambda_arg_index};
4use linkrs_core::types::expr::ExpressionId;
5
6use crate::planning::plan::factorization::FactorizedSchema;
7
8#[derive(Debug, Clone, Default)]
10pub struct GroupDependencyAnalysis {
11 pub dependent_groups: HashSet<u32>,
12 pub required_flat_groups: HashSet<u32>,
13 pub dependent_exprs: HashSet<ExpressionId>,
14}
15
16pub struct GroupDependencyAnalyzer<'a> {
22 schema: &'a FactorizedSchema,
23 collect_dependent_expr: bool,
24 dependent_groups: HashSet<u32>,
25 required_flat_groups: HashSet<u32>,
26 dependent_exprs: HashSet<ExpressionId>,
27 expr_store: Option<&'a HashMap<ExpressionId, linkrs_core::Expression>>,
31 unresolved: Vec<String>,
36}
37
38impl<'a> GroupDependencyAnalyzer<'a> {
39 pub fn new(schema: &'a FactorizedSchema, collect_dependent_expr: bool) -> Self {
40 Self {
41 schema,
42 collect_dependent_expr,
43 dependent_groups: HashSet::new(),
44 required_flat_groups: HashSet::new(),
45 dependent_exprs: HashSet::new(),
46 expr_store: None,
47 unresolved: Vec::new(),
48 }
49 }
50
51 pub fn with_expr_store(
52 schema: &'a FactorizedSchema,
53 collect_dependent_expr: bool,
54 store: &'a HashMap<ExpressionId, linkrs_core::Expression>,
55 ) -> Self {
56 Self {
57 schema,
58 collect_dependent_expr,
59 dependent_groups: HashSet::new(),
60 required_flat_groups: HashSet::new(),
61 dependent_exprs: HashSet::new(),
62 expr_store: Some(store),
63 unresolved: Vec::new(),
64 }
65 }
66
67 pub fn dependent_groups(&self) -> &HashSet<u32> {
68 &self.dependent_groups
69 }
70
71 pub fn required_flat_groups(&self) -> &HashSet<u32> {
72 &self.required_flat_groups
73 }
74
75 pub fn dependent_exprs(&self) -> &HashSet<ExpressionId> {
76 &self.dependent_exprs
77 }
78
79 pub fn has_unresolved(&self) -> bool {
81 !self.unresolved.is_empty()
82 }
83
84 pub fn unresolved(&self) -> &[String] {
86 &self.unresolved
87 }
88
89 fn mark_unresolved(&mut self, what: String) {
95 let context = format!(
96 "{what} (schema: {} groups, {} expressions in scope)",
97 self.schema.num_groups(),
98 self.schema.expressions_in_scope().len()
99 );
100 log::warn!("GroupDependencyAnalyzer: unresolved {context}");
101 self.unresolved.push(context);
102 for (pos, group) in self.schema.groups().iter().enumerate() {
103 if !group.is_flat() {
104 self.required_flat_groups.insert(pos as u32);
105 }
106 }
107 }
108
109 pub fn into_analysis(self) -> GroupDependencyAnalysis {
110 GroupDependencyAnalysis {
111 dependent_groups: self.dependent_groups,
112 required_flat_groups: self.required_flat_groups,
113 dependent_exprs: self.dependent_exprs,
114 }
115 }
116
117 pub fn visit(&mut self, expr_id: &ExpressionId) {
124 let owned_name = self.schema.expression_name(expr_id).map(str::to_string);
125 if let Some(pos) = self
126 .schema
127 .resolve_group_pos(Some(expr_id), owned_name.as_deref())
128 {
129 self.dependent_groups.insert(pos);
130 if self.collect_dependent_expr {
131 self.dependent_exprs.insert(expr_id.clone());
132 }
133 return;
134 }
135
136 if let Some(expr) = self.expr_store.and_then(|store| store.get(expr_id)) {
138 self.visit_expression(expr);
139 } else {
140 self.mark_unresolved(format!("expression {expr_id:?} not in scope"));
141 }
142 }
143
144 pub fn visit_expression(&mut self, expr: &linkrs_core::Expression) {
145 use linkrs_core::Expression;
146 match expr {
147 Expression::Variable(name) => {
148 if let Some(pos) = self.schema.resolve_group_pos(None, Some(name)) {
151 self.dependent_groups.insert(pos);
152 } else {
153 self.mark_unresolved(format!("variable `{name}` not bound in scope"));
154 }
155 }
156 Expression::Property { object, .. } => {
157 self.visit_expression(object);
158 }
159 Expression::Binary { left, right, .. } => {
160 self.visit_expression(left);
161 self.visit_expression(right);
162 }
163 Expression::Unary { operand, .. } => {
164 self.visit_expression(operand);
165 }
166 Expression::Function { args, name } => {
167 for arg in args {
168 self.visit_expression(arg.as_expr());
169 }
170 if is_list_lambda(name) {
172 if let Some(body) = list_lambda_arg_index(name).and_then(|i| args.get(i)) {
176 let mut lambda_analyzer =
177 GroupDependencyAnalyzer::new(self.schema, self.collect_dependent_expr);
178 lambda_analyzer.expr_store = self.expr_store;
179 lambda_analyzer.visit_expression(body.as_expr());
180 self.required_flat_groups
181 .extend(lambda_analyzer.dependent_groups.iter().copied());
182 self.required_flat_groups
183 .extend(lambda_analyzer.required_flat_groups.iter().copied());
184 self.unresolved.extend(lambda_analyzer.unresolved);
185 }
186 }
187 }
188 Expression::Aggregate { args, filter, .. } => {
189 for a in args {
190 self.visit_expression(a);
191 }
192 if let Some(f) = filter {
193 self.visit_expression(f);
194 }
195 }
196 Expression::List(items) => {
197 for item in items {
198 self.visit_expression(item);
199 }
200 }
201 Expression::Map(pairs) => {
202 for (_, v) in pairs {
203 self.visit_expression(v);
204 }
205 }
206 Expression::Case {
207 test_expr,
208 conditions,
209 default,
210 } => {
211 if let Some(t) = test_expr {
212 self.visit_expression(t);
213 }
214 for (when, then) in conditions {
215 self.visit_expression(when);
216 self.visit_expression(then);
217 }
218 if let Some(d) = default {
219 self.visit_expression(d);
220 }
221 }
222 Expression::Subscript { collection, index } => {
223 self.visit_expression(collection);
224 self.visit_expression(index);
225 }
226 Expression::Range {
227 collection,
228 start,
229 end,
230 } => {
231 self.visit_expression(collection);
232 if let Some(s) = start {
233 self.visit_expression(s);
234 }
235 if let Some(e) = end {
236 self.visit_expression(e);
237 }
238 }
239 Expression::Literal(_)
240 | Expression::Parameter(_)
241 | Expression::Label(_)
242 | Expression::SessionVariable(_) => {}
243 Expression::Path(items) | Expression::PathBuild(items) => {
244 for item in items {
245 self.visit_expression(item);
246 }
247 }
248 Expression::TypeCast {
249 expression: inner, ..
250 } => {
251 self.visit_expression(inner);
252 }
253 Expression::Reduce {
254 initial,
255 source,
256 mapping,
257 ..
258 } => {
259 self.visit_expression(initial);
260 self.visit_expression(source);
261 self.visit_expression(mapping);
262 }
263 Expression::ListComprehension {
264 variable: _,
265 source,
266 filter,
267 map,
268 } => {
269 self.visit_expression(source);
270 if let Some(f) = filter {
271 self.visit_expression(f);
272 }
273 if let Some(m) = map {
274 self.visit_expression(m);
275 }
276 }
277 _ => {
278 for child in expr.children() {
280 self.visit_expression(child);
281 }
282 }
283 }
284 }
285
286 pub fn analyze_ids(
288 schema: &'a FactorizedSchema,
289 expr_ids: &[ExpressionId],
290 store: &'a HashMap<ExpressionId, linkrs_core::Expression>,
291 ) -> GroupDependencyAnalysis {
292 let mut analyzer = GroupDependencyAnalyzer::with_expr_store(schema, false, store);
293 for id in expr_ids {
294 analyzer.visit(id);
295 }
296 analyzer.into_analysis()
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use crate::planning::plan::factorization::FactorizedSchema;
304 use linkrs_core::types::expr::ExpressionId;
305 use linkrs_core::types::expr::FunctionArg;
306
307 fn expr(id: u64) -> ExpressionId {
308 ExpressionId::new(id)
309 }
310
311 #[test]
312 fn single_expr_dependency() {
313 let mut schema = FactorizedSchema::new();
314 let g0 = schema.create_flat_group(false);
315 let g1 = schema.create_group();
316 schema.insert_to_group_and_scope(expr(1), g0).unwrap();
317 schema.insert_to_group_and_scope(expr(2), g1).unwrap();
318
319 let mut analyzer = GroupDependencyAnalyzer::new(&schema, false);
320 analyzer.visit(&expr(2));
321 assert!(analyzer.dependent_groups().contains(&g1));
322 assert!(!analyzer.dependent_groups().contains(&g0));
323 }
324
325 #[test]
326 fn expression_tree_walk() {
327 let mut schema = FactorizedSchema::new();
328 let g0 = schema.create_flat_group(false);
329 let g1 = schema.create_group();
330 let id_a = expr(10);
331 let id_b = expr(20);
332 schema.insert_to_group_and_scope(id_a.clone(), g0).unwrap();
333 schema.insert_to_group_and_scope(id_b.clone(), g1).unwrap();
334
335 let store = HashMap::new();
337 let _combined_expr = linkrs_core::Expression::Binary {
338 left: Box::new(linkrs_core::Expression::Variable("a".to_string())),
339 op: linkrs_core::types::operators::BinaryOperator::Add,
340 right: Box::new(linkrs_core::Expression::Variable("b".to_string())),
341 };
342 let mut analyzer = GroupDependencyAnalyzer::with_expr_store(&schema, false, &store);
346 analyzer.visit(&id_a);
349 analyzer.visit(&id_b);
350 assert_eq!(analyzer.dependent_groups().len(), 2);
351 }
352
353 #[test]
354 fn variable_id_primary() {
355 let mut schema = FactorizedSchema::new();
358 let g0 = schema.create_flat_group(false);
359 let g1 = schema.create_group();
360 schema.insert_to_group_and_scope(expr(10), g0).unwrap();
361 schema.insert_to_group_and_scope(expr(20), g1).unwrap();
362
363 let mut analyzer = GroupDependencyAnalyzer::new(&schema, false);
364 analyzer.visit(&expr(20));
365 assert!(analyzer.dependent_groups().contains(&g1));
366 assert!(!analyzer.has_unresolved());
367 }
368
369 #[test]
370 fn variable_name_fallback() {
371 let mut schema = FactorizedSchema::new();
372 let g0 = schema.create_flat_group(false);
373 let g1 = schema.create_group();
374 schema
375 .insert_to_group_and_scope_with_name(expr(10), Some("a".to_string()), g0)
376 .unwrap();
377 schema
378 .insert_to_group_and_scope_with_name(expr(20), Some("b".to_string()), g1)
379 .unwrap();
380
381 let the_expr = linkrs_core::Expression::Binary {
382 left: Box::new(linkrs_core::Expression::Variable("a".to_string())),
383 op: linkrs_core::types::operators::BinaryOperator::Add,
384 right: Box::new(linkrs_core::Expression::Variable("b".to_string())),
385 };
386 let mut store = HashMap::new();
387 let fake_id = expr(999);
388 store.insert(fake_id.clone(), the_expr);
389 let mut analyzer = GroupDependencyAnalyzer::with_expr_store(&schema, false, &store);
390 analyzer.visit(&fake_id);
391 assert!(analyzer.dependent_groups().contains(&g0));
392 assert!(analyzer.dependent_groups().contains(&g1));
393 assert_eq!(analyzer.dependent_groups().len(), 2);
394 }
395
396 #[test]
397 fn unscoped_expression_requires_flatten_all() {
398 let mut schema = FactorizedSchema::new();
399 let g0 = schema.create_flat_group(false);
400 let g1 = schema.create_group();
401 schema.insert_to_group_and_scope(expr(1), g0).unwrap();
402 schema.insert_to_group_and_scope(expr(2), g1).unwrap();
403
404 let empty_store = HashMap::new();
405 let mut analyzer = GroupDependencyAnalyzer::with_expr_store(&schema, false, &empty_store);
406 analyzer.visit(&expr(999));
407 assert!(analyzer.has_unresolved());
408 assert_eq!(analyzer.unresolved().len(), 1);
409 assert!(analyzer.required_flat_groups().contains(&g1));
410 assert!(!analyzer.required_flat_groups().contains(&g0));
411 }
412
413 #[test]
414 fn unbound_variable_requires_flatten_all() {
415 let mut schema = FactorizedSchema::new();
416 let g0 = schema.create_flat_group(false);
417 let g1 = schema.create_group();
418 schema.insert_to_group_and_scope(expr(1), g0).unwrap();
419 schema.insert_to_group_and_scope(expr(2), g1).unwrap();
420
421 let mut analyzer = GroupDependencyAnalyzer::new(&schema, false);
422 analyzer.visit_expression(&linkrs_core::Expression::Variable("ghost".to_string()));
423 assert!(analyzer.has_unresolved());
424 assert!(analyzer.required_flat_groups().contains(&g1));
425 }
426
427 #[test]
428 fn list_filter_lambda_body_requires_flat() {
429 let mut schema = FactorizedSchema::new();
430 let g0 = schema.create_flat_group(false);
431 let g1 = schema.create_group();
432 schema
433 .insert_to_group_and_scope_with_name(expr(10), Some("a".to_string()), g0)
434 .unwrap();
435 schema
436 .insert_to_group_and_scope_with_name(expr(20), Some("items".to_string()), g1)
437 .unwrap();
438
439 let body = linkrs_core::Expression::Binary {
442 left: Box::new(linkrs_core::Expression::Property {
443 object: Box::new(linkrs_core::Expression::Variable("items".to_string())),
444 property: "age".to_string(),
445 }),
446 op: linkrs_core::types::operators::BinaryOperator::GreaterThan,
447 right: Box::new(linkrs_core::Expression::Literal(
448 linkrs_core::value::Value::Int(30),
449 )),
450 };
451 let the_expr = linkrs_core::Expression::Function {
452 name: "list_filter".to_string(),
453 args: vec![
454 FunctionArg::Positional(linkrs_core::Expression::Variable("a".to_string())),
455 FunctionArg::Positional(body),
456 ],
457 };
458 let mut store = HashMap::new();
459 let fake_id = expr(999);
460 store.insert(fake_id.clone(), the_expr);
461 let mut analyzer = GroupDependencyAnalyzer::with_expr_store(&schema, false, &store);
462 analyzer.visit(&fake_id);
463 assert!(analyzer.required_flat_groups().contains(&g1));
464 assert!(!analyzer.has_unresolved());
465 }
466
467 #[test]
468 fn path_expression_visits_items() {
469 let mut schema = FactorizedSchema::new();
470 let g0 = schema.create_flat_group(false);
471 let g1 = schema.create_group();
472 schema
473 .insert_to_group_and_scope_with_name(expr(10), Some("a".to_string()), g0)
474 .unwrap();
475 schema
476 .insert_to_group_and_scope_with_name(expr(20), Some("b".to_string()), g1)
477 .unwrap();
478
479 let the_expr =
480 linkrs_core::Expression::Path(vec![linkrs_core::Expression::Variable("b".to_string())]);
481 let mut analyzer = GroupDependencyAnalyzer::new(&schema, false);
482 analyzer.visit_expression(&the_expr);
483 assert!(analyzer.dependent_groups().contains(&g1));
484 assert!(!analyzer.has_unresolved());
485 }
486
487 #[test]
488 fn list_any_all_single_lambda_body_requires_flat() {
489 for name in ["list_any", "list_all", "list_single"] {
490 let mut schema = FactorizedSchema::new();
491 let g0 = schema.create_flat_group(false);
492 let g1 = schema.create_group();
493 schema
494 .insert_to_group_and_scope_with_name(expr(10), Some("a".to_string()), g0)
495 .unwrap();
496 schema
497 .insert_to_group_and_scope_with_name(expr(20), Some("items".to_string()), g1)
498 .unwrap();
499
500 let body = linkrs_core::Expression::Binary {
501 left: Box::new(linkrs_core::Expression::Property {
502 object: Box::new(linkrs_core::Expression::Variable("items".to_string())),
503 property: "age".to_string(),
504 }),
505 op: linkrs_core::types::operators::BinaryOperator::GreaterThan,
506 right: Box::new(linkrs_core::Expression::Literal(
507 linkrs_core::value::Value::Int(30),
508 )),
509 };
510 let the_expr = linkrs_core::Expression::Function {
511 name: name.to_string(),
512 args: vec![
513 FunctionArg::Positional(linkrs_core::Expression::Variable("a".to_string())),
514 FunctionArg::Positional(body),
515 ],
516 };
517 let mut store = HashMap::new();
518 let fake_id = expr(999);
519 store.insert(fake_id.clone(), the_expr);
520 let mut analyzer = GroupDependencyAnalyzer::with_expr_store(&schema, false, &store);
521 analyzer.visit(&fake_id);
522 assert!(
523 analyzer.required_flat_groups().contains(&g1),
524 "{name} lambda body must require flat"
525 );
526 assert!(!analyzer.has_unresolved());
527 }
528 }
529}