Skip to main content

linkrs_query/optimizer/analysis/
expression.rs

1//! Expression Analysis Module
2//!
3//! Provides a feature for analyzing the properties of expressions, including:
4//! Deterministic check (whether non-deterministic functions are contained)
5//! Complexity score
6//! Attribute/variable/function extraction
7
8use 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/// Expression analysis results
16#[derive(Debug, Clone, Default)]
17pub struct ExpressionAnalysis {
18    /// Is it deterministic? (excluding non-deterministic functions such as rand() and now())
19    pub is_deterministic: bool,
20    /// Complexity score (0-100)
21    pub complexity_score: u32,
22    /// List of attributes for the citation
23    pub referenced_properties: Vec<String>,
24    /// List of referenced variables
25    pub referenced_variables: Vec<String>,
26    /// List of called functions
27    pub called_functions: Vec<String>,
28    /// Does it contain aggregate functions?
29    pub contains_aggregate: bool,
30    /// Does it contain subqueries?
31    pub contains_subquery: bool,
32    /// Number of nodes
33    pub node_count: u32,
34}
35
36impl ExpressionAnalysis {
37    /// Create an empty analysis result.
38    pub fn new() -> Self {
39        Self {
40            is_deterministic: true, // The default assumption is one of certainty.
41            ..Default::default()
42        }
43    }
44}
45
46/// Expression Analysis Pattern
47///
48/// Predefined analysis modes simplify the configuration process.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum AnalysisMode {
51    /// Complete analysis (default)
52    Full,
53    /// Check only for certainty.
54    DeterministicOnly,
55    /// Extract only the attribute references:
56    PropertyExtractor,
57    /// Extract only the variable references.
58    VariableExtractor,
59}
60
61/// Expression Analysis Options
62#[derive(Debug, Clone)]
63pub struct AnalysisOptions {
64    /// Analyzing certainty
65    pub check_deterministic: bool,
66    /// Analyzing complexity
67    pub check_complexity: bool,
68    /// Extract attribute references.
69    pub extract_properties: bool,
70    /// Extract variable references
71    pub extract_variables: bool,
72    /// Statistical function calls
73    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    /// Create options from the analysis mode.
90    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
124/// Non-deterministic function checking
125///
126/// Improve performance by using compile-time static matching instead of runtime HashMaps.
127/// Non-deterministic functions may return different results on each call (e.g., rand(), now()).
128pub struct NondeterministicChecker;
129
130impl NondeterministicChecker {
131    /// Check whether the function is non-deterministic.
132    ///
133    /// Using the `match` method for compilation optimization is more efficient than using the `HashMap` for lookups.
134    pub fn is_nondeterministic(func_name: &str) -> bool {
135        match func_name {
136            // Time-related functions
137            "now" | "current_time" | "current_date" | "current_timestamp" | "localtime"
138            | "localtimestamp" => true,
139
140            // Random number function
141            "rand" | "random" | "uuid" | "gen_random_uuid" => true,
142
143            // Window functions (the result depends on the row position)
144            "row_number" | "rank" | "dense_rank" | "percent_rank" | "cume_dist" => true,
145
146            // Other non-deterministic functions
147            "last_insert_id" | "connection_id" | "current_user" | "session_user" => true,
148
149            // Deterministic function
150            _ => false,
151        }
152    }
153
154    /// Check whether an expression tree contains any non-deterministic
155    /// function call (e.g. `rand()`, `gen_random_uuid()`, `now()`).
156    ///
157    /// Non-deterministic functions must be evaluated tuple-at-a-time: if a
158    /// factorized group stays unflat, the value would be shared across rows
159    /// instead of generated per row. Callers (e.g. the factorization
160    /// rewriter) use this to fall back to flatten-all.
161    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    /// Check whether the function only generates random values.
178    ///
179    /// Factorization flatten-all fallback is reserved for random functions
180    /// (`rand()`, `random()`, `randint()`): they must be evaluated
181    /// tuple-at-a-time. Other non-deterministic functions (`uuid()`,
182    /// `now()`, ...) keep the usual flatten-all-but-one path.
183    pub fn is_random(func_name: &str) -> bool {
184        matches!(func_name, "rand" | "random" | "randint")
185    }
186
187    /// Check whether an expression tree contains any random function call.
188    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/// Expression Analyzer
202///
203/// Analyze various characteristics of the expression, with the option to perform analysis on demand (by configuring using predefined modes).
204#[derive(Debug, Clone)]
205pub struct ExpressionAnalyzer {
206    /// Analyze the options
207    options: AnalysisOptions,
208}
209
210impl ExpressionAnalyzer {
211    /// Create a default expression analyzer (full analysis mode).
212    pub fn new() -> Self {
213        Self {
214            options: AnalysisOptions::default(),
215        }
216    }
217
218    /// Create an expression analyzer with options
219    pub fn with_options(options: AnalysisOptions) -> Self {
220        Self { options }
221    }
222
223    /// Create an analyzer that only checks for certainty.
224    pub fn deterministic_only() -> Self {
225        Self {
226            options: AnalysisOptions::from_mode(AnalysisMode::DeterministicOnly),
227        }
228    }
229
230    /// Create an analyzer that only extracts references to attributes.
231    pub fn property_extractor() -> Self {
232        Self {
233            options: AnalysisOptions::from_mode(AnalysisMode::PropertyExtractor),
234        }
235    }
236
237    /// Create an analyzer that only extracts variable references.
238    pub fn variable_extractor() -> Self {
239        Self {
240            options: AnalysisOptions::from_mode(AnalysisMode::VariableExtractor),
241        }
242    }
243
244    /// Analyze the expression (accepts a ContextualExpression)
245    ///
246    /// # Parameters
247    /// `ctx_expr`: The context expression that needs to be analyzed.
248    ///
249    /// # Return
250    /// Analysis results of the expression
251    pub fn analyze(&self, ctx_expr: &ContextualExpression) -> ExpressionAnalysis {
252        let mut analysis = ExpressionAnalysis::new();
253
254        // Obtain the Expression using ContextualExpression.
255        if let Some(expr_meta) = ctx_expr.expression() {
256            let expr = expr_meta.inner();
257
258            // Use the existing Collector to collect information.
259            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            // Using a custom Visitor for complexity and certainty analysis
278            let mut visitor = AnalysisVisitor::new(&mut analysis, self.options.clone());
279            visitor.visit(expr);
280        }
281
282        analysis
283    }
284
285    /// Quickly check whether the expression is deterministic.
286    pub fn is_deterministic(&self, ctx_expr: &ContextualExpression) -> bool {
287        let analysis = self.analyze(ctx_expr);
288        analysis.is_deterministic
289    }
290
291    /// Quickly extract the attributes referenced by the expression.
292    pub fn extract_properties(&self, ctx_expr: &ContextualExpression) -> Vec<String> {
293        let analysis = self.analyze(ctx_expr);
294        analysis.referenced_properties
295    }
296
297    /// Quickly extract the variables referenced by the expressions.
298    pub fn extract_variables(&self, ctx_expr: &ContextualExpression) -> Vec<String> {
299        let analysis = self.analyze(ctx_expr);
300        analysis.referenced_variables
301    }
302}
303
304/// Expression analysis: “Visitor”
305///
306/// Using the Visitor pattern for complexity and certainty analysis
307struct 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        // Verification of successful creation.
601    }
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        // Direct non-deterministic call, including the runtime spelling.
646        assert!(NondeterministicChecker::contains_nondeterministic(
647            &Expression::Function {
648                name: "gen_random_uuid".to_string(),
649                args: vec![],
650            }
651        ));
652        // Nested inside a deterministic outer function.
653        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        // Deterministic trees stay negative.
666        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        // Other non-deterministic functions stay on the flatten-all-but-one
689        // path: they must not trigger the random fallback.
690        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        // Nested random call inside a deterministic outer function.
700        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        // Simple expressions
748        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        // Complex expressions
758        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}