Skip to main content

sqry_core/query/
optimizer.rs

1//! Query optimizer for efficient execution
2//!
3//! This module provides query optimization through clause reordering and AST transformations
4//! to maximize index usage and minimize evaluation cost.
5//!
6//! # Optimization Strategies
7//!
8//! 1. **Flattening**: Collapse nested AND/OR operators
9//!    - `AND(AND(A, B), C)` → `AND(A, B, C)`
10//!    - `OR(OR(A, B), C)` → `OR(A, B, C)`
11//!
12//! 2. **Clause Reordering**: Order AND clauses for optimal execution
13//!    - Indexed fields first (can use index lookups)
14//!    - Within indexed fields, order by selectivity (most selective first)
15//!    - Non-indexed fields last (require full scans)
16//!
17//! 3. **Selectivity Estimation**: Heuristic estimates for field selectivity
18//!    - `kind`: 0.1 (10%) - most selective indexed field
19//!    - `name`: 0.01 (1%) - very selective
20//!    - `path`: 0.3 (30%)
21//!    - `lang`: 0.4 (40%)
22//!    - `text`: 0.5 (50%) - least selective, not indexed
23//!
24//! # Example
25//!
26//! ```ignore
27//! use sqry_core::query::optimizer::Optimizer;
28//! use sqry_core::query::registry::FieldRegistry;
29//!
30//! let registry = FieldRegistry::with_core_fields();
31//! let optimizer = Optimizer::new(registry);
32//!
33//! // Optimize: text:TODO AND kind:function
34//! // Result:   kind:function AND text:TODO (indexed first)
35//! let optimized = optimizer.optimize(ast);
36//! ```
37
38use super::registry::FieldRegistry;
39use super::types::{Expr, Query};
40
41/// Query optimizer that reorders clauses for efficient execution
42///
43/// The optimizer uses the field registry to determine which fields are indexed
44/// and estimates selectivity to reorder clauses optimally.
45#[derive(Debug)]
46pub struct Optimizer {
47    /// Field registry for index and type information
48    registry: FieldRegistry,
49}
50
51impl Optimizer {
52    /// Create a new optimizer with the given field registry
53    #[must_use]
54    pub fn new(registry: FieldRegistry) -> Self {
55        Self { registry }
56    }
57
58    /// Optimize a complete query
59    ///
60    /// Applies all optimization passes to the query's root expression.
61    #[must_use]
62    pub fn optimize_query(&self, query: Query) -> Query {
63        Query {
64            root: self.optimize(query.root),
65            span: query.span,
66        }
67    }
68
69    /// Optimize an expression tree
70    ///
71    /// Applies the following optimizations recursively:
72    /// 1. Flatten nested AND/OR operators
73    /// 2. Reorder AND clauses by index availability and selectivity
74    /// 3. Recursively optimize child expressions
75    #[must_use]
76    pub fn optimize(&self, expr: Expr) -> Expr {
77        match expr {
78            Expr::And(operands) => {
79                // First, recursively optimize each operand
80                let optimized: Vec<Expr> = operands.into_iter().map(|e| self.optimize(e)).collect();
81
82                // Flatten nested ANDs
83                let flattened = Self::flatten_and(optimized);
84
85                // Reorder for optimal execution
86                self.reorder_and(flattened)
87            }
88            Expr::Or(operands) => {
89                // First, recursively optimize each operand
90                let optimized: Vec<Expr> = operands.into_iter().map(|e| self.optimize(e)).collect();
91
92                // Flatten nested ORs
93                let flattened = Self::flatten_or(optimized);
94
95                // Don't reorder OR clauses (all branches must be evaluated)
96                Expr::Or(flattened)
97            }
98            Expr::Not(operand) => {
99                // Recursively optimize the negated expression
100                Expr::Not(Box::new(self.optimize(*operand)))
101            }
102            Expr::Condition(_) => {
103                // Conditions are leaf nodes, no optimization needed
104                expr
105            }
106            Expr::Join(join) => {
107                // Optimize both sides of the join independently
108                Expr::Join(crate::query::types::JoinExpr {
109                    left: Box::new(self.optimize(*join.left)),
110                    edge: join.edge,
111                    right: Box::new(self.optimize(*join.right)),
112                    span: join.span,
113                })
114            }
115        }
116    }
117
118    /// Flatten nested AND operators
119    ///
120    /// Transforms `AND(AND(A, B), C)` into `AND(A, B, C)`.
121    /// This enables more effective reordering across all AND clauses.
122    fn flatten_and(operands: Vec<Expr>) -> Vec<Expr> {
123        let mut result = Vec::new();
124
125        for operand in operands {
126            match operand {
127                Expr::And(nested) => {
128                    // Recursively flatten nested ANDs
129                    result.extend(Self::flatten_and(nested));
130                }
131                other => {
132                    result.push(other);
133                }
134            }
135        }
136
137        result
138    }
139
140    /// Flatten nested OR operators
141    ///
142    /// Transforms `OR(OR(A, B), C)` into `OR(A, B, C)`.
143    fn flatten_or(operands: Vec<Expr>) -> Vec<Expr> {
144        let mut result = Vec::new();
145
146        for operand in operands {
147            match operand {
148                Expr::Or(nested) => {
149                    // Recursively flatten nested ORs
150                    result.extend(Self::flatten_or(nested));
151                }
152                other => {
153                    result.push(other);
154                }
155            }
156        }
157
158        result
159    }
160
161    /// Reorder AND clauses for optimal execution
162    ///
163    /// Ordering strategy:
164    /// 1. Indexed fields first (can use index lookups)
165    /// 2. Within indexed fields, order by selectivity (most selective first)
166    /// 3. Non-indexed fields last (require full scans)
167    /// 4. Non-condition expressions (AND, OR, NOT) maintain relative order at the end
168    fn reorder_and(&self, operands: Vec<Expr>) -> Expr {
169        if operands.is_empty() {
170            // Edge case: empty AND (shouldn't happen in valid queries)
171            return Expr::And(vec![]);
172        }
173
174        if operands.len() == 1 {
175            // Single operand: return it directly (no need for AND wrapper)
176            return operands.into_iter().next().unwrap();
177        }
178
179        // Separate conditions from complex expressions
180        let mut conditions = Vec::new();
181        let mut complex_exprs = Vec::new();
182
183        for operand in operands {
184            match &operand {
185                Expr::Condition(_) => conditions.push(operand),
186                _ => complex_exprs.push(operand),
187            }
188        }
189
190        // Sort conditions by (indexed, selectivity)
191        conditions.sort_by(|a, b| {
192            let (a_indexed, a_selectivity) = self.analyze_operand(a);
193            let (b_indexed, b_selectivity) = self.analyze_operand(b);
194
195            // First, compare by index availability (indexed first)
196            match (a_indexed, b_indexed) {
197                (true, false) => std::cmp::Ordering::Less,
198                (false, true) => std::cmp::Ordering::Greater,
199                _ => {
200                    // If both indexed or both not indexed, compare by selectivity
201                    // Lower selectivity = more selective = should come first
202                    a_selectivity
203                        .partial_cmp(&b_selectivity)
204                        .unwrap_or(std::cmp::Ordering::Equal)
205                }
206            }
207        });
208
209        // Combine: conditions (optimally ordered) + complex expressions
210        let mut result = conditions;
211        result.extend(complex_exprs);
212
213        Expr::And(result)
214    }
215
216    /// Analyze an operand to determine index availability and selectivity
217    ///
218    /// Returns: (`is_indexed`, `selectivity_estimate`)
219    /// - `is_indexed`: true if the field can use an index
220    /// - selectivity: estimated fraction of results (lower is more selective)
221    fn analyze_operand(&self, operand: &Expr) -> (bool, f64) {
222        match operand {
223            Expr::Condition(condition) => {
224                let field_name = condition.field.as_str();
225
226                // Check if field is indexed
227                let is_indexed = self
228                    .registry
229                    .get(field_name)
230                    .is_some_and(|desc| desc.indexed);
231
232                // Estimate selectivity
233                let selectivity = Self::estimate_selectivity(field_name);
234
235                (is_indexed, selectivity)
236            }
237            _ => {
238                // Complex expressions (AND, OR, NOT) are not directly indexed
239                // and have unknown selectivity (assume high cost)
240                (false, 0.5)
241            }
242        }
243    }
244
245    /// Estimate selectivity for a field
246    ///
247    /// Returns the estimated fraction of symbols that will match (0.0 to 1.0).
248    /// Lower values are more selective (filter out more results).
249    ///
250    /// # Heuristic Estimates
251    ///
252    /// - `kind`: 0.1 (10%) - most selective indexed field
253    /// - `name`: 0.01 (1%) - very selective (unique names)
254    /// - `path`: 0.3 (30%) - moderately selective
255    /// - `lang`: 0.4 (40%) - less selective (few languages)
256    /// - `scope`: 0.35 (35%) - moderately selective
257    /// - `text`: 0.5 (50%) - least selective, expensive full scan
258    /// - Unknown fields: 0.45 (45%) - default moderate selectivity
259    fn estimate_selectivity(field: &str) -> f64 {
260        match field {
261            "kind" => 0.1,   // 10% - very selective
262            "name" => 0.01,  // 1% - most selective
263            "path" => 0.3,   // 30%
264            "lang" => 0.4,   // 40%
265            "scope" => 0.35, // 35%
266            "text" => 0.5,   // 50% - least selective, not indexed
267            _ => 0.45,       // Default for unknown/plugin fields
268        }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use crate::query::types::{Condition, Field, Operator, Span, Value};
276    use approx::assert_abs_diff_eq;
277
278    fn make_condition(field: &str, value: &str) -> Expr {
279        Expr::Condition(Condition {
280            field: Field::new(field),
281            operator: Operator::Equal,
282            value: Value::String(value.to_string()),
283            span: Span::default(),
284        })
285    }
286
287    #[test]
288    fn test_optimize_single_condition() {
289        let registry = FieldRegistry::with_core_fields();
290        let optimizer = Optimizer::new(registry);
291
292        let expr = make_condition("kind", "function");
293        let rewritten_expr = optimizer.optimize(expr.clone());
294
295        // Single condition should remain unchanged
296        assert_eq!(rewritten_expr, expr);
297    }
298
299    #[test]
300    fn test_flatten_nested_and() {
301        let registry = FieldRegistry::with_core_fields();
302        let optimizer = Optimizer::new(registry);
303
304        // AND(AND(A, B), C)
305        let a = make_condition("kind", "function");
306        let b = make_condition("name", "foo");
307        let c = make_condition("lang", "rust");
308
309        let nested_and = Expr::And(vec![Expr::And(vec![a.clone(), b.clone()]), c.clone()]);
310
311        let rewritten_expr = optimizer.optimize(nested_and);
312
313        // Should flatten to AND(A, B, C) and reorder
314        match rewritten_expr {
315            Expr::And(operands) => {
316                assert_eq!(operands.len(), 3);
317            }
318            _ => panic!("Expected And expression"),
319        }
320    }
321
322    #[test]
323    fn test_flatten_deeply_nested_and() {
324        let registry = FieldRegistry::with_core_fields();
325        let optimizer = Optimizer::new(registry);
326
327        // AND(AND(AND(A, B), C), D)
328        let a = make_condition("kind", "function");
329        let b = make_condition("name", "foo");
330        let c = make_condition("lang", "rust");
331        let d = make_condition("path", "src/main.rs");
332
333        let deeply_nested = Expr::And(vec![
334            Expr::And(vec![Expr::And(vec![a.clone(), b.clone()]), c.clone()]),
335            d.clone(),
336        ]);
337
338        let rewritten_expr = optimizer.optimize(deeply_nested);
339
340        // Should flatten to AND(A, B, C, D)
341        match rewritten_expr {
342            Expr::And(operands) => {
343                assert_eq!(operands.len(), 4);
344            }
345            _ => panic!("Expected And expression"),
346        }
347    }
348
349    #[test]
350    fn test_flatten_nested_or() {
351        let registry = FieldRegistry::with_core_fields();
352        let optimizer = Optimizer::new(registry);
353
354        // OR(OR(A, B), C)
355        let a = make_condition("kind", "function");
356        let b = make_condition("kind", "method");
357        let c = make_condition("kind", "class");
358
359        let nested_or = Expr::Or(vec![Expr::Or(vec![a.clone(), b.clone()]), c.clone()]);
360
361        let rewritten_expr = optimizer.optimize(nested_or);
362
363        // Should flatten to OR(A, B, C)
364        match rewritten_expr {
365            Expr::Or(operands) => {
366                assert_eq!(operands.len(), 3);
367            }
368            _ => panic!("Expected Or expression"),
369        }
370    }
371
372    #[test]
373    fn test_reorder_for_index() {
374        let registry = FieldRegistry::with_core_fields();
375        let optimizer = Optimizer::new(registry);
376
377        // text (NOT indexed) AND kind (indexed)
378        let text_cond = make_condition("text", "TODO");
379        let kind_cond = make_condition("kind", "function");
380
381        let expr = Expr::And(vec![text_cond.clone(), kind_cond.clone()]);
382        let rewritten_expr = optimizer.optimize(expr);
383
384        // kind should be first (indexed)
385        match rewritten_expr {
386            Expr::And(operands) => {
387                assert_eq!(operands.len(), 2);
388                // First operand should be kind (indexed)
389                match &operands[0] {
390                    Expr::Condition(cond) => {
391                        assert_eq!(cond.field.as_str(), "kind");
392                    }
393                    _ => panic!("Expected Condition"),
394                }
395                // Second operand should be text (not indexed)
396                match &operands[1] {
397                    Expr::Condition(cond) => {
398                        assert_eq!(cond.field.as_str(), "text");
399                    }
400                    _ => panic!("Expected Condition"),
401                }
402            }
403            _ => panic!("Expected And expression"),
404        }
405    }
406
407    #[test]
408    fn test_reorder_by_selectivity() {
409        let registry = FieldRegistry::with_core_fields();
410        let optimizer = Optimizer::new(registry);
411
412        // lang (40%) AND name (1%)
413        // Both indexed, but name is more selective
414        let lang_cond = make_condition("lang", "rust");
415        let name_cond = make_condition("name", "foo");
416
417        let expr = Expr::And(vec![lang_cond.clone(), name_cond.clone()]);
418        let rewritten_expr = optimizer.optimize(expr);
419
420        // name should be first (more selective)
421        match rewritten_expr {
422            Expr::And(operands) => {
423                assert_eq!(operands.len(), 2);
424                // First operand should be name (more selective)
425                match &operands[0] {
426                    Expr::Condition(cond) => {
427                        assert_eq!(cond.field.as_str(), "name");
428                    }
429                    _ => panic!("Expected Condition"),
430                }
431            }
432            _ => panic!("Expected And expression"),
433        }
434    }
435
436    #[test]
437    fn test_reorder_mixed_indexed_and_selectivity() {
438        let registry = FieldRegistry::with_core_fields();
439        let optimizer = Optimizer::new(registry);
440
441        // path (indexed, 30%) AND kind (indexed, 10%) AND text (not indexed, 50%)
442        let path_cond = make_condition("path", "src/**/*.rs");
443        let kind_cond = make_condition("kind", "function");
444        let text_cond = make_condition("text", "TODO");
445
446        let expr = Expr::And(vec![
447            path_cond.clone(),
448            kind_cond.clone(),
449            text_cond.clone(),
450        ]);
451        let rewritten_expr = optimizer.optimize(expr);
452
453        // Expected order: kind (indexed, 10%), path (indexed, 30%), text (not indexed, 50%)
454        match rewritten_expr {
455            Expr::And(operands) => {
456                assert_eq!(operands.len(), 3);
457
458                // First: kind (indexed, most selective)
459                match &operands[0] {
460                    Expr::Condition(cond) => {
461                        assert_eq!(cond.field.as_str(), "kind");
462                    }
463                    _ => panic!("Expected Condition"),
464                }
465
466                // Second: path (indexed, less selective)
467                match &operands[1] {
468                    Expr::Condition(cond) => {
469                        assert_eq!(cond.field.as_str(), "path");
470                    }
471                    _ => panic!("Expected Condition"),
472                }
473
474                // Third: text (not indexed)
475                match &operands[2] {
476                    Expr::Condition(cond) => {
477                        assert_eq!(cond.field.as_str(), "text");
478                    }
479                    _ => panic!("Expected Condition"),
480                }
481            }
482            _ => panic!("Expected And expression"),
483        }
484    }
485
486    #[test]
487    fn test_preserve_or_order() {
488        let registry = FieldRegistry::with_core_fields();
489        let optimizer = Optimizer::new(registry);
490
491        // OR clauses should not be reordered (all branches must be evaluated)
492        let text_cond = make_condition("text", "TODO");
493        let kind_cond = make_condition("kind", "function");
494
495        let expr = Expr::Or(vec![text_cond.clone(), kind_cond.clone()]);
496        let rewritten_expr = optimizer.optimize(expr);
497
498        // Order should be preserved
499        match rewritten_expr {
500            Expr::Or(operands) => {
501                assert_eq!(operands.len(), 2);
502                match &operands[0] {
503                    Expr::Condition(cond) => {
504                        assert_eq!(cond.field.as_str(), "text");
505                    }
506                    _ => panic!("Expected Condition"),
507                }
508            }
509            _ => panic!("Expected Or expression"),
510        }
511    }
512
513    #[test]
514    fn test_optimize_not() {
515        let registry = FieldRegistry::with_core_fields();
516        let optimizer = Optimizer::new(registry);
517
518        let kind_cond = make_condition("kind", "function");
519        let expr = Expr::Not(Box::new(kind_cond.clone()));
520
521        let rewritten_expr = optimizer.optimize(expr);
522
523        // NOT should be preserved and inner expression optimized
524        match rewritten_expr {
525            Expr::Not(inner) => {
526                assert_eq!(*inner, kind_cond);
527            }
528            _ => panic!("Expected Not expression"),
529        }
530    }
531
532    #[test]
533    fn test_optimize_complex_nested() {
534        let registry = FieldRegistry::with_core_fields();
535        let optimizer = Optimizer::new(registry);
536
537        // (text:TODO OR name:foo) AND kind:function
538        let text_cond = make_condition("text", "TODO");
539        let name_cond = make_condition("name", "foo");
540        let kind_cond = make_condition("kind", "function");
541
542        let or_expr = Expr::Or(vec![text_cond.clone(), name_cond.clone()]);
543        let expr = Expr::And(vec![or_expr.clone(), kind_cond.clone()]);
544
545        let rewritten_expr = optimizer.optimize(expr);
546
547        // kind should be first (indexed), OR should be second
548        match rewritten_expr {
549            Expr::And(operands) => {
550                assert_eq!(operands.len(), 2);
551
552                // First should be kind
553                match &operands[0] {
554                    Expr::Condition(cond) => {
555                        assert_eq!(cond.field.as_str(), "kind");
556                    }
557                    _ => panic!("Expected Condition"),
558                }
559
560                // Second should be OR
561                match &operands[1] {
562                    Expr::Or(_) => {
563                        // Correct
564                    }
565                    _ => panic!("Expected Or expression"),
566                }
567            }
568            _ => panic!("Expected And expression"),
569        }
570    }
571
572    #[test]
573    fn test_optimize_query() {
574        let registry = FieldRegistry::with_core_fields();
575        let optimizer = Optimizer::new(registry);
576
577        let text_cond = make_condition("text", "TODO");
578        let kind_cond = make_condition("kind", "function");
579
580        let query = Query {
581            root: Expr::And(vec![text_cond, kind_cond]),
582            span: Span::default(),
583        };
584
585        let rewritten_query = optimizer.optimize_query(query);
586
587        // Should reorder to kind first
588        match rewritten_query.root {
589            Expr::And(operands) => match &operands[0] {
590                Expr::Condition(cond) => {
591                    assert_eq!(cond.field.as_str(), "kind");
592                }
593                _ => panic!("Expected Condition"),
594            },
595            _ => panic!("Expected And expression"),
596        }
597    }
598
599    #[test]
600    fn test_analyze_operand_indexed_field() {
601        let registry = FieldRegistry::with_core_fields();
602        let optimizer = Optimizer::new(registry);
603
604        let kind_cond = make_condition("kind", "function");
605        let (indexed, selectivity) = optimizer.analyze_operand(&kind_cond);
606
607        assert!(indexed);
608        assert_abs_diff_eq!(selectivity, 0.1, epsilon = 1e-10); // kind selectivity
609    }
610
611    #[test]
612    fn test_analyze_operand_non_indexed_field() {
613        let registry = FieldRegistry::with_core_fields();
614        let optimizer = Optimizer::new(registry);
615
616        let text_cond = make_condition("text", "TODO");
617        let (indexed, selectivity) = optimizer.analyze_operand(&text_cond);
618
619        assert!(!indexed);
620        assert_abs_diff_eq!(selectivity, 0.5, epsilon = 1e-10); // text selectivity
621    }
622
623    #[test]
624    fn test_analyze_operand_complex_expression() {
625        let registry = FieldRegistry::with_core_fields();
626        let optimizer = Optimizer::new(registry);
627
628        let a = make_condition("kind", "function");
629        let b = make_condition("name", "foo");
630        let or_expr = Expr::Or(vec![a, b]);
631
632        let (indexed, selectivity) = optimizer.analyze_operand(&or_expr);
633
634        // Complex expressions are not directly indexed
635        assert!(!indexed);
636        assert_abs_diff_eq!(selectivity, 0.5, epsilon = 1e-10); // Default for complex expressions
637    }
638
639    #[test]
640    fn test_estimate_selectivity_known_fields() {
641        assert_abs_diff_eq!(
642            Optimizer::estimate_selectivity("kind"),
643            0.1,
644            epsilon = 1e-10
645        );
646        assert_abs_diff_eq!(
647            Optimizer::estimate_selectivity("name"),
648            0.01,
649            epsilon = 1e-10
650        );
651        assert_abs_diff_eq!(
652            Optimizer::estimate_selectivity("path"),
653            0.3,
654            epsilon = 1e-10
655        );
656        assert_abs_diff_eq!(
657            Optimizer::estimate_selectivity("lang"),
658            0.4,
659            epsilon = 1e-10
660        );
661        assert_abs_diff_eq!(
662            Optimizer::estimate_selectivity("scope"),
663            0.35,
664            epsilon = 1e-10
665        );
666        assert_abs_diff_eq!(
667            Optimizer::estimate_selectivity("text"),
668            0.5,
669            epsilon = 1e-10
670        );
671    }
672
673    #[test]
674    fn test_estimate_selectivity_unknown_field() {
675        // Unknown field should have default selectivity
676        assert_abs_diff_eq!(
677            Optimizer::estimate_selectivity("unknown_field"),
678            0.45,
679            epsilon = 1e-10
680        );
681    }
682
683    #[test]
684    fn test_single_and_operand_unwrapped() {
685        let registry = FieldRegistry::with_core_fields();
686        let optimizer = Optimizer::new(registry);
687
688        // AND with single operand should return the operand directly
689        let kind_cond = make_condition("kind", "function");
690        let expr = Expr::And(vec![kind_cond.clone()]);
691
692        let rewritten_expr = optimizer.optimize(expr);
693
694        // Should return the condition directly, not wrapped in AND
695        assert_eq!(rewritten_expr, kind_cond);
696    }
697
698    #[test]
699    fn test_empty_and() {
700        let registry = FieldRegistry::with_core_fields();
701        let optimizer = Optimizer::new(registry);
702
703        let expr = Expr::And(vec![]);
704        let rewritten_expr = optimizer.optimize(expr);
705
706        // Empty AND should remain empty AND
707        match rewritten_expr {
708            Expr::And(operands) => {
709                assert_eq!(operands.len(), 0);
710            }
711            _ => panic!("Expected And expression"),
712        }
713    }
714
715    #[test]
716    fn test_flatten_multiple_and_levels() {
717        let registry = FieldRegistry::with_core_fields();
718        let optimizer = Optimizer::new(registry);
719
720        let a = make_condition("kind", "function");
721        let b = make_condition("name", "foo");
722        let c = make_condition("lang", "rust");
723        let d = make_condition("path", "src/main.rs");
724
725        // (A AND B) AND (C AND D)
726        let expr = Expr::And(vec![
727            Expr::And(vec![a.clone(), b.clone()]),
728            Expr::And(vec![c.clone(), d.clone()]),
729        ]);
730
731        let rewritten_expr = optimizer.optimize(expr);
732
733        // Should flatten to AND(A, B, C, D) and reorder
734        match rewritten_expr {
735            Expr::And(operands) => {
736                assert_eq!(operands.len(), 4, "Should flatten to 4 operands");
737
738                // Verify all expected fields are present
739                let field_names: Vec<_> = operands
740                    .iter()
741                    .filter_map(|op| match op {
742                        Expr::Condition(cond) => Some(cond.field.as_str()),
743                        _ => None,
744                    })
745                    .collect();
746
747                assert_eq!(field_names.len(), 4);
748                assert!(field_names.contains(&"kind"));
749                assert!(field_names.contains(&"name"));
750                assert!(field_names.contains(&"lang"));
751                assert!(field_names.contains(&"path"));
752            }
753            _ => panic!("Expected And expression"),
754        }
755    }
756
757    #[test]
758    fn test_flatten_with_mixed_nesting() {
759        let registry = FieldRegistry::with_core_fields();
760        let optimizer = Optimizer::new(registry);
761
762        let a = make_condition("kind", "function");
763        let b = make_condition("name", "foo");
764        let c = make_condition("lang", "rust");
765        let d = make_condition("path", "src/main.rs");
766
767        // A AND (B AND (C AND D))
768        let expr = Expr::And(vec![
769            a.clone(),
770            Expr::And(vec![b.clone(), Expr::And(vec![c.clone(), d.clone()])]),
771        ]);
772
773        let rewritten_expr = optimizer.optimize(expr);
774
775        // Expected: AND(A, B, C, D) flattened and reordered
776        match rewritten_expr {
777            Expr::And(operands) => {
778                assert_eq!(operands.len(), 4, "Should flatten to 4 operands");
779
780                // Verify all expected fields are present
781                let field_names: Vec<_> = operands
782                    .iter()
783                    .filter_map(|op| match op {
784                        Expr::Condition(cond) => Some(cond.field.as_str()),
785                        _ => None,
786                    })
787                    .collect();
788
789                assert_eq!(field_names.len(), 4);
790                assert!(field_names.contains(&"kind"));
791                assert!(field_names.contains(&"name"));
792                assert!(field_names.contains(&"lang"));
793                assert!(field_names.contains(&"path"));
794            }
795            _ => panic!("Expected And expression"),
796        }
797    }
798}