1use linkrs_core::types::expr::visitor::ExpressionVisitor;
9use linkrs_core::types::expr::visitor_collectors::{
10 FunctionCollector, PropertyCollector, VariableCollector,
11};
12use linkrs_core::types::ContextualExpression;
13use linkrs_core::Expression;
14
15#[derive(Debug, Clone, Default)]
17pub struct ExpressionAnalysis {
18 pub is_deterministic: bool,
20 pub complexity_score: u32,
22 pub referenced_properties: Vec<String>,
24 pub referenced_variables: Vec<String>,
26 pub called_functions: Vec<String>,
28 pub contains_aggregate: bool,
30 pub contains_subquery: bool,
32 pub node_count: u32,
34}
35
36impl ExpressionAnalysis {
37 pub fn new() -> Self {
39 Self {
40 is_deterministic: true, ..Default::default()
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum AnalysisMode {
51 Full,
53 DeterministicOnly,
55 PropertyExtractor,
57 VariableExtractor,
59}
60
61#[derive(Debug, Clone)]
63pub struct AnalysisOptions {
64 pub check_deterministic: bool,
66 pub check_complexity: bool,
68 pub extract_properties: bool,
70 pub extract_variables: bool,
72 pub count_functions: bool,
74}
75
76impl Default for AnalysisOptions {
77 fn default() -> Self {
78 Self {
79 check_deterministic: true,
80 check_complexity: true,
81 extract_properties: true,
82 extract_variables: true,
83 count_functions: true,
84 }
85 }
86}
87
88impl AnalysisOptions {
89 fn from_mode(mode: AnalysisMode) -> Self {
91 match mode {
92 AnalysisMode::Full => AnalysisOptions {
93 check_deterministic: true,
94 check_complexity: true,
95 extract_properties: true,
96 extract_variables: true,
97 count_functions: true,
98 },
99 AnalysisMode::DeterministicOnly => AnalysisOptions {
100 check_deterministic: true,
101 check_complexity: false,
102 extract_properties: false,
103 extract_variables: false,
104 count_functions: false,
105 },
106 AnalysisMode::PropertyExtractor => AnalysisOptions {
107 check_deterministic: false,
108 check_complexity: false,
109 extract_properties: true,
110 extract_variables: false,
111 count_functions: false,
112 },
113 AnalysisMode::VariableExtractor => AnalysisOptions {
114 check_deterministic: false,
115 check_complexity: false,
116 extract_properties: false,
117 extract_variables: true,
118 count_functions: false,
119 },
120 }
121 }
122}
123
124pub struct NondeterministicChecker;
129
130impl NondeterministicChecker {
131 pub fn is_nondeterministic(func_name: &str) -> bool {
135 match func_name {
136 "now" | "current_time" | "current_date" | "current_timestamp" | "localtime"
138 | "localtimestamp" => true,
139
140 "rand" | "random" | "uuid" | "gen_random_uuid" => true,
142
143 "row_number" | "rank" | "dense_rank" | "percent_rank" | "cume_dist" => true,
145
146 "last_insert_id" | "connection_id" | "current_user" | "session_user" => true,
148
149 _ => false,
151 }
152 }
153
154 pub fn contains_nondeterministic(expr: &Expression) -> bool {
162 match expr {
163 Expression::Function { name, args } => {
164 if Self::is_nondeterministic(name) {
165 return true;
166 }
167 args.iter()
168 .any(|a| Self::contains_nondeterministic(a.as_expr()))
169 }
170 _ => expr
171 .children()
172 .iter()
173 .any(|c| Self::contains_nondeterministic(c)),
174 }
175 }
176
177 pub fn is_random(func_name: &str) -> bool {
184 matches!(func_name, "rand" | "random" | "randint")
185 }
186
187 pub fn contains_random(expr: &Expression) -> bool {
189 match expr {
190 Expression::Function { name, args } => {
191 if Self::is_random(name) {
192 return true;
193 }
194 args.iter().any(|a| Self::contains_random(a.as_expr()))
195 }
196 _ => expr.children().iter().any(|c| Self::contains_random(c)),
197 }
198 }
199}
200
201#[derive(Debug, Clone)]
205pub struct ExpressionAnalyzer {
206 options: AnalysisOptions,
208}
209
210impl ExpressionAnalyzer {
211 pub fn new() -> Self {
213 Self {
214 options: AnalysisOptions::default(),
215 }
216 }
217
218 pub fn with_options(options: AnalysisOptions) -> Self {
220 Self { options }
221 }
222
223 pub fn deterministic_only() -> Self {
225 Self {
226 options: AnalysisOptions::from_mode(AnalysisMode::DeterministicOnly),
227 }
228 }
229
230 pub fn property_extractor() -> Self {
232 Self {
233 options: AnalysisOptions::from_mode(AnalysisMode::PropertyExtractor),
234 }
235 }
236
237 pub fn variable_extractor() -> Self {
239 Self {
240 options: AnalysisOptions::from_mode(AnalysisMode::VariableExtractor),
241 }
242 }
243
244 pub fn analyze(&self, ctx_expr: &ContextualExpression) -> ExpressionAnalysis {
252 let mut analysis = ExpressionAnalysis::new();
253
254 if let Some(expr_meta) = ctx_expr.expression() {
256 let expr = expr_meta.inner();
257
258 if self.options.extract_properties {
260 let mut collector = PropertyCollector::new();
261 collector.visit(expr);
262 analysis.referenced_properties = collector.properties;
263 }
264
265 if self.options.extract_variables {
266 let mut collector = VariableCollector::new();
267 collector.visit(expr);
268 analysis.referenced_variables = collector.variables;
269 }
270
271 if self.options.count_functions {
272 let mut collector = FunctionCollector::new();
273 collector.visit(expr);
274 analysis.called_functions = collector.functions;
275 }
276
277 let mut visitor = AnalysisVisitor::new(&mut analysis, self.options.clone());
279 visitor.visit(expr);
280 }
281
282 analysis
283 }
284
285 pub fn is_deterministic(&self, ctx_expr: &ContextualExpression) -> bool {
287 let analysis = self.analyze(ctx_expr);
288 analysis.is_deterministic
289 }
290
291 pub fn extract_properties(&self, ctx_expr: &ContextualExpression) -> Vec<String> {
293 let analysis = self.analyze(ctx_expr);
294 analysis.referenced_properties
295 }
296
297 pub fn extract_variables(&self, ctx_expr: &ContextualExpression) -> Vec<String> {
299 let analysis = self.analyze(ctx_expr);
300 analysis.referenced_variables
301 }
302}
303
304struct AnalysisVisitor<'a> {
308 analysis: &'a mut ExpressionAnalysis,
309 options: AnalysisOptions,
310}
311
312impl<'a> AnalysisVisitor<'a> {
313 fn new(analysis: &'a mut ExpressionAnalysis, options: AnalysisOptions) -> Self {
314 Self { analysis, options }
315 }
316}
317
318impl ExpressionVisitor for AnalysisVisitor<'_> {
319 fn visit_literal(&mut self, _value: &linkrs_core::Value) {
320 if self.options.check_complexity {
321 self.analysis.complexity_score += 1;
322 }
323 self.analysis.node_count += 1;
324 }
325
326 fn visit_variable(&mut self, _name: &str) {
327 if self.options.check_complexity {
328 self.analysis.complexity_score += 2;
329 }
330 self.analysis.node_count += 1;
331 }
332
333 fn visit_property(&mut self, object: &Expression, _property: &str) {
334 if self.options.check_complexity {
335 self.analysis.complexity_score += 5;
336 }
337 self.analysis.node_count += 1;
338 self.visit(object);
339 }
340
341 fn visit_binary(
342 &mut self,
343 op: linkrs_core::types::BinaryOperator,
344 left: &Expression,
345 right: &Expression,
346 ) {
347 if self.options.check_complexity {
348 self.analysis.complexity_score += 2;
349 if op == linkrs_core::types::BinaryOperator::Like {
350 self.analysis.complexity_score += 5;
351 }
352 }
353 self.analysis.node_count += 1;
354 self.visit(left);
355 self.visit(right);
356 }
357
358 fn visit_unary(&mut self, _op: linkrs_core::types::UnaryOperator, operand: &Expression) {
359 if self.options.check_complexity {
360 self.analysis.complexity_score += 1;
361 }
362 self.analysis.node_count += 1;
363 self.visit(operand);
364 }
365
366 fn visit_function(&mut self, name: &str, args: &[linkrs_core::types::expr::FunctionArg]) {
367 if self.options.check_deterministic && NondeterministicChecker::is_nondeterministic(name) {
368 self.analysis.is_deterministic = false;
369 }
370 if self.options.check_complexity {
371 self.analysis.complexity_score += 10 + args.len() as u32 * 2;
372 }
373 self.analysis.node_count += 1;
374 for arg in args {
375 self.visit(arg.as_expr());
376 }
377 }
378
379 fn visit_aggregate(
380 &mut self,
381 _func: &linkrs_core::types::operators::AggregateFunction,
382 args: &[Expression],
383 _distinct: bool,
384 ) {
385 self.analysis.contains_aggregate = true;
386 if self.options.check_complexity {
387 self.analysis.complexity_score += 20;
388 }
389 self.analysis.node_count += 1;
390 for arg in args {
391 self.visit(arg);
392 }
393 }
394
395 fn visit_case(
396 &mut self,
397 test_expr: Option<&Expression>,
398 conditions: &[(Expression, Expression)],
399 default: Option<&Expression>,
400 ) {
401 if self.options.check_complexity {
402 self.analysis.complexity_score += 5 + conditions.len() as u32 * 5;
403 }
404 self.analysis.node_count += 1;
405 if let Some(test) = test_expr {
406 self.visit(test);
407 }
408 for (when, then) in conditions {
409 self.visit(when);
410 self.visit(then);
411 }
412 if let Some(default_expr) = default {
413 self.visit(default_expr);
414 }
415 }
416
417 fn visit_type_cast(
418 &mut self,
419 expression: &Expression,
420 _target_type: &linkrs_core::types::DataType,
421 ) {
422 if self.options.check_complexity {
423 self.analysis.complexity_score += 3;
424 }
425 self.analysis.node_count += 1;
426 self.visit(expression);
427 }
428
429 fn visit_subscript(&mut self, collection: &Expression, index: &Expression) {
430 if self.options.check_complexity {
431 self.analysis.complexity_score += 4;
432 }
433 self.analysis.node_count += 1;
434 self.visit(collection);
435 self.visit(index);
436 }
437
438 fn visit_list(&mut self, items: &[Expression]) {
439 if self.options.check_complexity {
440 self.analysis.complexity_score += items.len() as u32;
441 }
442 self.analysis.node_count += 1;
443 for item in items {
444 self.visit(item);
445 }
446 }
447
448 fn visit_map(&mut self, entries: &[(String, Expression)]) {
449 if self.options.check_complexity {
450 self.analysis.complexity_score += entries.len() as u32 * 2;
451 }
452 self.analysis.node_count += 1;
453 for (_, value) in entries {
454 self.visit(value);
455 }
456 }
457
458 fn visit_list_comprehension(
459 &mut self,
460 _variable: &str,
461 source: &Expression,
462 filter: Option<&Expression>,
463 map: Option<&Expression>,
464 ) {
465 self.analysis.contains_subquery = true;
466 if self.options.check_complexity {
467 self.analysis.complexity_score += 30;
468 }
469 self.analysis.node_count += 1;
470 self.visit(source);
471 if let Some(f) = filter {
472 self.visit(f);
473 }
474 if let Some(m) = map {
475 self.visit(m);
476 }
477 }
478
479 fn visit_predicate(&mut self, _func: &str, args: &[Expression]) {
480 if self.options.check_complexity {
481 self.analysis.complexity_score += 15;
482 }
483 self.analysis.node_count += 1;
484 for arg in args {
485 self.visit(arg);
486 }
487 }
488
489 fn visit_reduce(
490 &mut self,
491 _accumulator: &str,
492 initial: &Expression,
493 _variable: &str,
494 source: &Expression,
495 mapping: &Expression,
496 ) {
497 self.analysis.contains_subquery = true;
498 if self.options.check_complexity {
499 self.analysis.complexity_score += 25;
500 }
501 self.analysis.node_count += 1;
502 self.visit(initial);
503 self.visit(source);
504 self.visit(mapping);
505 }
506
507 fn visit_path(&mut self, items: &[Expression]) {
508 if self.options.check_complexity {
509 self.analysis.complexity_score += items.len() as u32 * 3;
510 }
511 self.analysis.node_count += 1;
512 for item in items {
513 self.visit(item);
514 }
515 }
516
517 fn visit_path_build(&mut self, items: &[Expression]) {
518 if self.options.check_complexity {
519 self.analysis.complexity_score += items.len() as u32 * 2;
520 }
521 self.analysis.node_count += 1;
522 for item in items {
523 self.visit(item);
524 }
525 }
526
527 fn visit_range(
528 &mut self,
529 collection: &Expression,
530 start: Option<&Expression>,
531 end: Option<&Expression>,
532 ) {
533 if self.options.check_complexity {
534 self.analysis.complexity_score += 5;
535 }
536 self.analysis.node_count += 1;
537 self.visit(collection);
538 if let Some(s) = start {
539 self.visit(s);
540 }
541 if let Some(e) = end {
542 self.visit(e);
543 }
544 }
545
546 fn visit_label(&mut self, _label: &str) {
547 if self.options.check_complexity {
548 self.analysis.complexity_score += 3;
549 }
550 self.analysis.node_count += 1;
551 }
552
553 fn visit_label_tag_property(&mut self, tag: &Expression, _property: &str) {
554 if self.options.check_complexity {
555 self.analysis.complexity_score += 5;
556 }
557 self.analysis.node_count += 1;
558 self.visit(tag);
559 }
560
561 fn visit_tag_property(&mut self, _tag_name: &str, _property: &str) {
562 if self.options.check_complexity {
563 self.analysis.complexity_score += 3;
564 }
565 self.analysis.node_count += 1;
566 }
567
568 fn visit_edge_property(&mut self, _edge_name: &str, _property: &str) {
569 if self.options.check_complexity {
570 self.analysis.complexity_score += 3;
571 }
572 self.analysis.node_count += 1;
573 }
574
575 fn visit_parameter(&mut self, _name: &str) {
576 if self.options.check_complexity {
577 self.analysis.complexity_score += 1;
578 }
579 self.analysis.node_count += 1;
580 }
581}
582
583impl Default for ExpressionAnalyzer {
584 fn default() -> Self {
585 Self::new()
586 }
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592 use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
593 use linkrs_core::types::expr::FunctionArg;
594 use linkrs_core::Value;
595 use std::sync::Arc;
596
597 #[test]
598 fn test_expression_analyzer_new() {
599 let _analyzer = ExpressionAnalyzer::new();
600 }
602
603 #[test]
604 fn test_literal_is_deterministic() {
605 let analyzer = ExpressionAnalyzer::new();
606 let expr = Expression::Literal(Value::Int(42));
607 let expr_ctx = Arc::new(ExpressionAnalysisContext::new());
608 let expr_meta = linkrs_core::types::expr::ExpressionMeta::new(expr);
609 let expr_id = expr_ctx.register_expression(expr_meta);
610 let ctx_expr = linkrs_core::types::ContextualExpression::new(expr_id, expr_ctx);
611 let analysis = analyzer.analyze(&ctx_expr);
612 assert!(analysis.is_deterministic);
613 assert_eq!(analysis.node_count, 1);
614 }
615
616 #[test]
617 fn test_variable_extraction() {
618 let analyzer = ExpressionAnalyzer::new();
619 let expr = Expression::Variable("x".to_string());
620 let expr_ctx = Arc::new(ExpressionAnalysisContext::new());
621 let expr_meta = linkrs_core::types::expr::ExpressionMeta::new(expr);
622 let expr_id = expr_ctx.register_expression(expr_meta);
623 let ctx_expr = linkrs_core::types::ContextualExpression::new(expr_id, expr_ctx);
624 let analysis = analyzer.analyze(&ctx_expr);
625 assert!(analysis.referenced_variables.contains(&"x".to_string()));
626 }
627
628 #[test]
629 fn test_nondeterministic_function_detection() {
630 let analyzer = ExpressionAnalyzer::new();
631 let expr = Expression::Function {
632 name: "rand".to_string(),
633 args: vec![],
634 };
635 let expr_ctx = Arc::new(ExpressionAnalysisContext::new());
636 let expr_meta = linkrs_core::types::expr::ExpressionMeta::new(expr);
637 let expr_id = expr_ctx.register_expression(expr_meta);
638 let ctx_expr = linkrs_core::types::ContextualExpression::new(expr_id, expr_ctx);
639 let analysis = analyzer.analyze(&ctx_expr);
640 assert!(!analysis.is_deterministic);
641 }
642
643 #[test]
644 fn test_contains_nondeterministic_tree_walk() {
645 assert!(NondeterministicChecker::contains_nondeterministic(
647 &Expression::Function {
648 name: "gen_random_uuid".to_string(),
649 args: vec![],
650 }
651 ));
652 assert!(NondeterministicChecker::contains_nondeterministic(
654 &Expression::Function {
655 name: "concat".to_string(),
656 args: vec![
657 FunctionArg::Positional(Expression::Literal(Value::Int(1))),
658 FunctionArg::Positional(Expression::Function {
659 name: "rand".to_string(),
660 args: vec![],
661 }),
662 ],
663 }
664 ));
665 assert!(!NondeterministicChecker::contains_nondeterministic(
667 &Expression::Function {
668 name: "abs".to_string(),
669 args: vec![FunctionArg::Positional(Expression::Literal(Value::Int(-5)))],
670 }
671 ));
672 assert!(!NondeterministicChecker::contains_nondeterministic(
673 &Expression::Literal(Value::Int(1))
674 ));
675 }
676
677 #[test]
678 fn test_contains_random_only_matches_random_functions() {
679 for name in ["rand", "random", "randint"] {
680 assert!(
681 NondeterministicChecker::contains_random(&Expression::Function {
682 name: name.to_string(),
683 args: vec![],
684 }),
685 "{name} must count as random"
686 );
687 }
688 for name in ["uuid", "gen_random_uuid", "now", "row_number"] {
691 assert!(
692 !NondeterministicChecker::contains_random(&Expression::Function {
693 name: name.to_string(),
694 args: vec![],
695 }),
696 "{name} must not count as random"
697 );
698 }
699 assert!(NondeterministicChecker::contains_random(
701 &Expression::Function {
702 name: "abs".to_string(),
703 args: vec![FunctionArg::Positional(Expression::Function {
704 name: "rand".to_string(),
705 args: vec![],
706 })],
707 }
708 ));
709 assert!(!NondeterministicChecker::contains_random(
710 &Expression::Literal(Value::Int(1))
711 ));
712 }
713
714 #[test]
715 fn test_deterministic_function() {
716 let analyzer = ExpressionAnalyzer::new();
717 let expr = Expression::Function {
718 name: "abs".to_string(),
719 args: vec![FunctionArg::Positional(Expression::Literal(Value::Int(-5)))],
720 };
721 let expr_ctx = Arc::new(ExpressionAnalysisContext::new());
722 let expr_meta = linkrs_core::types::expr::ExpressionMeta::new(expr);
723 let expr_id = expr_ctx.register_expression(expr_meta);
724 let ctx_expr = linkrs_core::types::ContextualExpression::new(expr_id, expr_ctx);
725 let analysis = analyzer.analyze(&ctx_expr);
726 assert!(analysis.is_deterministic);
727 }
728
729 #[test]
730 fn test_property_extraction() {
731 let analyzer = ExpressionAnalyzer::property_extractor();
732 let expr = Expression::Property {
733 object: Box::new(Expression::Variable("n".to_string())),
734 property: "name".to_string(),
735 };
736 let expr_ctx = Arc::new(ExpressionAnalysisContext::new());
737 let expr_meta = linkrs_core::types::expr::ExpressionMeta::new(expr);
738 let expr_id = expr_ctx.register_expression(expr_meta);
739 let ctx_expr = linkrs_core::types::ContextualExpression::new(expr_id, expr_ctx);
740 let analysis = analyzer.analyze(&ctx_expr);
741 assert!(analysis.referenced_properties.contains(&"name".to_string()));
742 }
743
744 #[test]
745 fn test_complexity_score() {
746 let analyzer = ExpressionAnalyzer::new();
747 let simple = Expression::Literal(Value::Int(1));
749 let expr_ctx = Arc::new(ExpressionAnalysisContext::new());
750 let simple_meta = linkrs_core::types::expr::ExpressionMeta::new(simple);
751 let simple_id = expr_ctx.register_expression(simple_meta);
752 let simple_ctx_expr =
753 linkrs_core::types::ContextualExpression::new(simple_id, expr_ctx.clone());
754 let simple_analysis = analyzer.analyze(&simple_ctx_expr);
755 assert!(simple_analysis.complexity_score < 10);
756
757 let complex = Expression::Function {
759 name: "coalesce".to_string(),
760 args: vec![
761 FunctionArg::Positional(Expression::Property {
762 object: Box::new(Expression::Variable("a".to_string())),
763 property: "x".to_string(),
764 }),
765 FunctionArg::Positional(Expression::Property {
766 object: Box::new(Expression::Variable("b".to_string())),
767 property: "y".to_string(),
768 }),
769 FunctionArg::Positional(Expression::Literal(Value::Null(
770 linkrs_core::value::NullType::Null,
771 ))),
772 ],
773 };
774 let complex_meta = linkrs_core::types::expr::ExpressionMeta::new(complex);
775 let complex_id = expr_ctx.register_expression(complex_meta);
776 let complex_ctx_expr = linkrs_core::types::ContextualExpression::new(complex_id, expr_ctx);
777 let complex_analysis = analyzer.analyze(&complex_ctx_expr);
778 assert!(complex_analysis.complexity_score > simple_analysis.complexity_score);
779 }
780}