Skip to main content

qubit_metadata/filter/
filter_expression.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Immutable filter expressions and their read-only views.
9
10use std::fmt;
11
12use qubit_redact::Redact;
13use qubit_redact::RedactionWriter;
14use qubit_redact::Redactor;
15
16use crate::Condition;
17use crate::FilterExpressionBuilder;
18use crate::FilterExpressionView;
19use crate::FilterLimitKind;
20use crate::FilterLimits;
21use crate::FilterMatchOptions;
22use crate::Metadata;
23use crate::MetadataError;
24use crate::MetadataResult;
25use crate::filter::internal::ExpressionChildren;
26use crate::filter::internal::FilterExpressionNode;
27use crate::filter::internal::MatchOutcome;
28
29/// An immutable Boolean expression in a [`crate::MetadataFilter`].
30///
31/// Expressions are constructed by [`FilterExpressionBuilder`] and can be
32/// inspected without allocation through [`FilterExpression::view`]. Their
33/// private representation prevents callers from constructing structurally
34/// invalid expression trees. The structure is Boolean, while evaluation uses
35/// a private three-valued outcome so missing data stays unknown through NOT,
36/// AND, and OR.
37///
38/// # Examples
39///
40/// ```
41/// use qubit_metadata::FilterExpression;
42///
43/// # fn main() -> qubit_metadata::MetadataResult<()> {
44/// let expression = FilterExpression::builder()
45///     .eq("tenant", "acme")
46///     .build()?;
47/// assert!(matches!(
48///     expression.view(),
49///     qubit_metadata::FilterExpressionView::Condition(_)
50/// ));
51/// # Ok(())
52/// # }
53/// ```
54#[derive(Clone, PartialEq)]
55#[must_use]
56pub struct FilterExpression {
57    /// Private expression node.
58    node: FilterExpressionNode,
59    /// Total number of nodes after logical-group flattening.
60    node_count: usize,
61    /// Maximum node depth after logical-group flattening.
62    max_depth: usize,
63}
64
65impl fmt::Debug for FilterExpression {
66    /// Formats the expression without exposing its cached implementation
67    /// metrics.
68    /// # Parameters
69    ///
70    /// * `formatter` - Formatter that receives the output.
71    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72        let output = Redactor::strict().redact_text(self);
73        let text = output.text_or_marker("<redaction incomplete>");
74        formatter.write_str(text.as_ref())
75    }
76}
77
78impl Redact for FilterExpression {
79    /// Writes the expression tree through the caller's cumulative budget.
80    /// # Parameters
81    ///
82    /// * `writer` - Destination that receives the encoded or redacted output.
83    fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
84        writer.record("FilterExpression", |fields| match &self.node {
85            FilterExpressionNode::Condition(condition) => {
86                fields.unmarked("operator", || "Condition");
87                fields.nested("condition", condition);
88            }
89            FilterExpressionNode::And(children) => {
90                fields.unmarked("operator", || "And");
91                fields.nested("children", &ExpressionChildren(children));
92            }
93            FilterExpressionNode::Or(children) => {
94                fields.unmarked("operator", || "Or");
95                fields.nested("children", &ExpressionChildren(children));
96            }
97            FilterExpressionNode::Not(inner) => {
98                fields.unmarked("operator", || "Not");
99                fields.nested("inner", inner.as_ref());
100            }
101            FilterExpressionNode::True => {
102                fields.unmarked("operator", || "True");
103            }
104            FilterExpressionNode::False => {
105                fields.unmarked("operator", || "False");
106            }
107        });
108    }
109}
110
111impl FilterExpression {
112    /// Creates a builder for a non-empty filter expression.
113    #[inline(always)]
114    #[must_use]
115    pub const fn builder() -> FilterExpressionBuilder {
116        FilterExpressionBuilder::new()
117    }
118
119    /// Creates an expression that matches every metadata object.
120    #[inline(always)]
121    pub const fn match_all() -> Self {
122        Self::true_expression()
123    }
124
125    /// Creates an expression that matches no metadata object.
126    #[inline(always)]
127    pub const fn match_none() -> Self {
128        Self::false_expression()
129    }
130
131    /// Creates a condition expression.
132    ///
133    /// # Parameters
134    ///
135    /// * `condition` - Leaf condition to store.
136    ///
137    /// # Returns
138    ///
139    /// A new condition expression.
140    ///
141    /// # Errors
142    ///
143    /// Returns [`MetadataError::InvalidFilterOperand`] when a comparison
144    /// operand is unset or cannot be represented by the V1 wire format.
145    #[inline]
146    pub(crate) fn condition(condition: Condition) -> MetadataResult<Self> {
147        condition.validate_operands()?;
148        Ok(Self {
149            node: FilterExpressionNode::Condition(condition),
150            node_count: 1,
151            max_depth: 1,
152        })
153    }
154
155    /// Creates a constant true expression.
156    ///
157    /// # Returns
158    ///
159    /// A new constant true expression.
160    #[inline]
161    pub(crate) const fn true_expression() -> Self {
162        Self {
163            node: FilterExpressionNode::True,
164            node_count: 1,
165            max_depth: 1,
166        }
167    }
168
169    /// Creates a constant false expression.
170    ///
171    /// # Returns
172    ///
173    /// A new constant false expression.
174    #[inline]
175    pub(crate) const fn false_expression() -> Self {
176        Self {
177            node: FilterExpressionNode::False,
178            node_count: 1,
179            max_depth: 1,
180        }
181    }
182
183    /// Creates a NOT expression without simplifying its child.
184    ///
185    /// # Parameters
186    ///
187    /// * `expression` - Child expression to negate.
188    ///
189    /// # Returns
190    ///
191    /// A NOT expression containing the supplied child.
192    #[inline]
193    pub(crate) fn not_expression(expression: Self) -> Self {
194        let node_count = expression.node_count + 1;
195        let max_depth = expression.max_depth + 1;
196        Self {
197            node: FilterExpressionNode::Not(Box::new(expression)),
198            node_count,
199            max_depth,
200        }
201    }
202
203    /// Combines this expression with `other` using logical AND.
204    ///
205    /// # Parameters
206    ///
207    /// * `other` - Expression whose conditions must also match.
208    ///
209    /// # Errors
210    ///
211    /// Returns [`MetadataError::FilterLimitExceeded`] when the resulting
212    /// expression exceeds library hard limits.
213    #[inline]
214    pub fn try_and(self, other: Self) -> MetadataResult<Self> {
215        let expression = Self::and_unchecked(self, other);
216        if expression.validate_structure_limits(FilterLimits::MAX).is_err() {
217            expression.validate_limits(FilterLimits::MAX)?;
218        }
219        Ok(expression)
220    }
221
222    /// Combines this expression with `other` using logical OR.
223    ///
224    /// # Parameters
225    ///
226    /// * `other` - Expression whose conditions may also match.
227    ///
228    /// # Errors
229    ///
230    /// Returns [`MetadataError::FilterLimitExceeded`] when the resulting
231    /// expression exceeds library hard limits.
232    #[inline]
233    pub fn try_or(self, other: Self) -> MetadataResult<Self> {
234        let expression = Self::or_unchecked(self, other);
235        if expression.validate_structure_limits(FilterLimits::MAX).is_err() {
236            expression.validate_limits(FilterLimits::MAX)?;
237        }
238        Ok(expression)
239    }
240
241    /// Negates this expression.
242    ///
243    /// # Errors
244    ///
245    /// Returns [`MetadataError::FilterLimitExceeded`] when the resulting
246    /// expression exceeds library hard limits.
247    #[inline]
248    pub fn try_not(self) -> MetadataResult<Self> {
249        let expression = self.negated_unchecked();
250        if expression.validate_structure_limits(FilterLimits::MAX).is_err() {
251            expression.validate_limits(FilterLimits::MAX)?;
252        }
253        Ok(expression)
254    }
255
256    /// Returns a borrowed view of this expression node.
257    ///
258    /// # Returns
259    ///
260    /// A zero-copy view preserving the node's Boolean structure.
261    #[inline(always)]
262    #[must_use = "the expression view should be inspected"]
263    pub fn view(&self) -> FilterExpressionView<'_> {
264        match &self.node {
265            FilterExpressionNode::Condition(condition) => FilterExpressionView::Condition(condition),
266            FilterExpressionNode::And(children) => FilterExpressionView::And(children),
267            FilterExpressionNode::Or(children) => FilterExpressionView::Or(children),
268            FilterExpressionNode::Not(inner) => FilterExpressionView::Not(inner),
269            FilterExpressionNode::True => FilterExpressionView::True,
270            FilterExpressionNode::False => FilterExpressionView::False,
271        }
272    }
273
274    /// Combines two expressions with logical AND.
275    ///
276    /// # Parameters
277    ///
278    /// * `left` - Left child expression.
279    /// * `right` - Right child expression.
280    ///
281    /// # Returns
282    ///
283    /// A simplified AND expression.
284    pub(crate) fn and_unchecked(left: Self, right: Self) -> Self {
285        if left.is_false() || right.is_false() {
286            return Self::false_expression();
287        }
288        if left.is_true() {
289            return right;
290        }
291        if right.is_true() {
292            return left;
293        }
294        Self::combine_and(left, right)
295    }
296
297    /// Combines two expressions with logical OR.
298    ///
299    /// # Parameters
300    ///
301    /// * `left` - Left child expression.
302    /// * `right` - Right child expression.
303    ///
304    /// # Returns
305    ///
306    /// A simplified OR expression.
307    pub(crate) fn or_unchecked(left: Self, right: Self) -> Self {
308        if left.is_true() || right.is_true() {
309            return Self::true_expression();
310        }
311        if left.is_false() {
312            return right;
313        }
314        if right.is_false() {
315            return left;
316        }
317        Self::combine_or(left, right)
318    }
319
320    /// Returns the three-valued logical negation of this expression.
321    ///
322    /// # Returns
323    ///
324    /// A simplified negated expression.
325    pub(crate) fn negated_unchecked(self) -> Self {
326        match self {
327            Self {
328                node: FilterExpressionNode::True,
329                ..
330            } => Self::false_expression(),
331            Self {
332                node: FilterExpressionNode::False,
333                ..
334            } => Self::true_expression(),
335            Self {
336                node: FilterExpressionNode::Not(inner),
337                ..
338            } => *inner,
339            expression => Self::not_expression(expression),
340        }
341    }
342
343    /// Reports whether this is a constant true expression.
344    ///
345    /// # Returns
346    ///
347    /// `true` only for the constant true node.
348    #[inline]
349    pub(crate) const fn is_true(&self) -> bool {
350        matches!(&self.node, FilterExpressionNode::True)
351    }
352
353    /// Reports whether this is a constant false expression.
354    ///
355    /// # Returns
356    ///
357    /// `true` only for the constant false node.
358    #[inline]
359    pub(crate) const fn is_false(&self) -> bool {
360        matches!(&self.node, FilterExpressionNode::False)
361    }
362
363    /// Evaluates this expression against one metadata object.
364    ///
365    /// # Parameters
366    ///
367    /// * `metadata` - Metadata object being matched.
368    /// * `options` - Match options to apply.
369    ///
370    /// # Returns
371    ///
372    /// The three-valued expression outcome.
373    pub(crate) fn evaluate(&self, metadata: &Metadata, options: FilterMatchOptions) -> MatchOutcome {
374        match &self.node {
375            FilterExpressionNode::Condition(condition) => {
376                condition.evaluate(metadata, options.numeric_comparison_policy())
377            }
378            FilterExpressionNode::And(children) => {
379                MatchOutcome::and(children.iter().map(|child| child.evaluate(metadata, options)))
380            }
381            FilterExpressionNode::Or(children) => {
382                MatchOutcome::or(children.iter().map(|child| child.evaluate(metadata, options)))
383            }
384            FilterExpressionNode::Not(inner) => inner.evaluate(metadata, options).not(),
385            FilterExpressionNode::True => MatchOutcome::True,
386            FilterExpressionNode::False => MatchOutcome::False,
387        }
388    }
389
390    /// Visits every leaf condition in this expression.
391    ///
392    /// # Type Parameters
393    ///
394    /// * `F` - Callback type used to observe each leaf condition.
395    ///
396    /// # Parameters
397    ///
398    /// * `visitor` - Callback invoked for each condition.
399    ///
400    /// # Returns
401    ///
402    /// `Ok(())` after all conditions have been visited.
403    ///
404    /// # Errors
405    ///
406    /// Returns the first error produced by `visitor`.
407    #[cfg(feature = "schema")]
408    pub(crate) fn visit_conditions<F>(&self, visitor: &mut F) -> MetadataResult<()>
409    where
410        F: FnMut(&Condition) -> MetadataResult<()>,
411    {
412        match &self.node {
413            FilterExpressionNode::Condition(condition) => visitor(condition),
414            FilterExpressionNode::And(children) | FilterExpressionNode::Or(children) => {
415                for child in children {
416                    child.visit_conditions(visitor)?;
417                }
418                Ok(())
419            }
420            FilterExpressionNode::Not(inner) => inner.visit_conditions(visitor),
421            FilterExpressionNode::True | FilterExpressionNode::False => Ok(()),
422        }
423    }
424
425    /// Validates this expression against resource limits.
426    ///
427    /// # Parameters
428    ///
429    /// * `limits` - Bounds to enforce.
430    ///
431    /// # Returns
432    ///
433    /// `Ok(())` when every node and condition fits within `limits`.
434    ///
435    /// # Errors
436    ///
437    /// Returns [`MetadataError::FilterLimitExceeded`] when depth, node count,
438    /// key length, or membership values exceed a configured bound.
439    pub(crate) fn validate_limits(&self, limits: FilterLimits) -> MetadataResult<()> {
440        let mut node_count = 0;
441        self.validate_limits_at(limits, 1, &mut node_count)
442    }
443
444    /// Validates cached structural metrics against resource limits in O(1).
445    ///
446    /// # Parameters
447    ///
448    /// * `limits` - Structural bounds to enforce.
449    ///
450    /// # Errors
451    ///
452    /// Returns [`MetadataError::FilterLimitExceeded`] when the cached maximum
453    /// depth or total node count exceeds `limits`. The reported value is the
454    /// first value beyond the configured maximum, matching recursive
455    /// validation semantics.
456    pub(crate) fn validate_structure_limits(&self, limits: FilterLimits) -> MetadataResult<()> {
457        if self.max_depth > limits.max_depth() {
458            return Err(MetadataError::FilterLimitExceeded {
459                kind: FilterLimitKind::Depth,
460                value: limits.max_depth() + 1,
461                maximum: limits.max_depth(),
462            });
463        }
464        if self.node_count > limits.max_nodes() {
465            return Err(MetadataError::FilterLimitExceeded {
466                kind: FilterLimitKind::Nodes,
467                value: limits.max_nodes() + 1,
468                maximum: limits.max_nodes(),
469            });
470        }
471        Ok(())
472    }
473
474    /// Recursively validates one expression node in depth-first order.
475    ///
476    /// # Parameters
477    ///
478    /// * `limits` - Bounds to enforce.
479    /// * `depth` - Root-inclusive depth of this node.
480    /// * `node_count` - Number of nodes visited before this node.
481    ///
482    /// # Errors
483    ///
484    /// Returns the first depth, node-count, or condition-limit error reached
485    /// by the depth-first traversal.
486    fn validate_limits_at(&self, limits: FilterLimits, depth: usize, node_count: &mut usize) -> MetadataResult<()> {
487        if depth > limits.max_depth() {
488            return Err(MetadataError::FilterLimitExceeded {
489                kind: FilterLimitKind::Depth,
490                value: depth,
491                maximum: limits.max_depth(),
492            });
493        }
494        *node_count += 1;
495        if *node_count > limits.max_nodes() {
496            return Err(MetadataError::FilterLimitExceeded {
497                kind: FilterLimitKind::Nodes,
498                value: *node_count,
499                maximum: limits.max_nodes(),
500            });
501        }
502        match &self.node {
503            FilterExpressionNode::Condition(condition) => condition.validate_limits(limits),
504            FilterExpressionNode::And(children) | FilterExpressionNode::Or(children) => {
505                for child in children {
506                    child.validate_limits_at(limits, depth + 1, node_count)?;
507                }
508                Ok(())
509            }
510            FilterExpressionNode::Not(inner) => inner.validate_limits_at(limits, depth + 1, node_count),
511            FilterExpressionNode::True | FilterExpressionNode::False => Ok(()),
512        }
513    }
514
515    /// Asserts that cached structural metrics equal recursively computed
516    /// metrics.
517    #[cfg(test)]
518    fn assert_cached_metrics_consistent(&self) {
519        match &self.node {
520            FilterExpressionNode::And(children) | FilterExpressionNode::Or(children) => {
521                for child in children {
522                    child.assert_cached_metrics_consistent();
523                }
524            }
525            FilterExpressionNode::Not(inner) => inner.assert_cached_metrics_consistent(),
526            FilterExpressionNode::Condition(_) | FilterExpressionNode::True | FilterExpressionNode::False => {}
527        }
528        let (node_count, max_depth) = self.recursive_metrics();
529        assert_eq!(
530            self.node_count, node_count,
531            "cached node count differs from the expression tree"
532        );
533        assert_eq!(
534            self.max_depth, max_depth,
535            "cached maximum depth differs from the expression tree"
536        );
537    }
538
539    /// Recursively computes the node count and maximum depth for tests.
540    #[cfg(test)]
541    fn recursive_metrics(&self) -> (usize, usize) {
542        match &self.node {
543            FilterExpressionNode::Condition(_) | FilterExpressionNode::True | FilterExpressionNode::False => (1, 1),
544            FilterExpressionNode::And(children) | FilterExpressionNode::Or(children) => {
545                let mut node_count = 1;
546                let mut max_child_depth = 0;
547                for child in children {
548                    let (child_node_count, child_max_depth) = child.recursive_metrics();
549                    node_count += child_node_count;
550                    max_child_depth = max_child_depth.max(child_max_depth);
551                }
552                (node_count, max_child_depth + 1)
553            }
554            FilterExpressionNode::Not(inner) => {
555                let (node_count, max_depth) = inner.recursive_metrics();
556                (node_count + 1, max_depth + 1)
557            }
558        }
559    }
560
561    /// Combines two non-constant expressions with logical AND while reusing a
562    /// left AND group.
563    /// # Parameters
564    ///
565    /// * `left` - Left expression operand.
566    /// * `right` - Right expression operand.
567    fn combine_and(left: Self, right: Self) -> Self {
568        let left_same_kind = matches!(&left.node, FilterExpressionNode::And(_));
569        let right_same_kind = matches!(&right.node, FilterExpressionNode::And(_));
570        let (node_count, max_depth) = Self::combined_metrics(&left, &right, left_same_kind, right_same_kind);
571        let mut children = match left {
572            Self {
573                node: FilterExpressionNode::And(children),
574                ..
575            } => children,
576            expression => vec![expression],
577        };
578        match right {
579            Self {
580                node: FilterExpressionNode::And(mut nested),
581                ..
582            } => children.append(&mut nested),
583            expression => children.push(expression),
584        }
585        Self {
586            node: FilterExpressionNode::And(children),
587            node_count,
588            max_depth,
589        }
590    }
591
592    /// Combines two non-constant expressions with logical OR while reusing a
593    /// left OR group.
594    /// # Parameters
595    ///
596    /// * `left` - Left expression operand.
597    /// * `right` - Right expression operand.
598    fn combine_or(left: Self, right: Self) -> Self {
599        let left_same_kind = matches!(&left.node, FilterExpressionNode::Or(_));
600        let right_same_kind = matches!(&right.node, FilterExpressionNode::Or(_));
601        let (node_count, max_depth) = Self::combined_metrics(&left, &right, left_same_kind, right_same_kind);
602        let mut children = match left {
603            Self {
604                node: FilterExpressionNode::Or(children),
605                ..
606            } => children,
607            expression => vec![expression],
608        };
609        match right {
610            Self {
611                node: FilterExpressionNode::Or(mut nested),
612                ..
613            } => children.append(&mut nested),
614            expression => children.push(expression),
615        }
616        Self {
617            node: FilterExpressionNode::Or(children),
618            node_count,
619            max_depth,
620        }
621    }
622
623    /// Calculates metrics for two expressions combined under one logical
624    /// operator, accounting for same-kind root flattening.
625    ///
626    /// # Parameters
627    ///
628    /// * `left` - Left expression before flattening.
629    /// * `right` - Right expression before flattening.
630    /// * `left_same_kind` - Whether the left root is flattened into the result.
631    /// * `right_same_kind` - Whether the right root is flattened into the
632    ///   result.
633    ///
634    /// # Returns
635    ///
636    /// The result's total node count and maximum depth.
637    fn combined_metrics(left: &Self, right: &Self, left_same_kind: bool, right_same_kind: bool) -> (usize, usize) {
638        let node_count = match (left_same_kind, right_same_kind) {
639            (true, true) => left.node_count + right.node_count - 1,
640            (true, false) | (false, true) => left.node_count + right.node_count,
641            (false, false) => left.node_count + right.node_count + 1,
642        };
643        let max_depth = match (left_same_kind, right_same_kind) {
644            (true, true) => left.max_depth.max(right.max_depth),
645            (true, false) => left.max_depth.max(right.max_depth + 1),
646            (false, true) => (left.max_depth + 1).max(right.max_depth),
647            (false, false) => left.max_depth.max(right.max_depth) + 1,
648        };
649        (node_count, max_depth)
650    }
651}
652
653#[cfg(test)]
654mod tests {
655    use super::FilterExpression;
656
657    /// Verifies that cached metrics agree with a recursive traversal for
658    /// representative simplified and nested expression shapes.
659    #[test]
660    fn test_cached_metrics_match_recursive_metrics() {
661        let leaf = FilterExpression::builder()
662            .exists("leaf")
663            .build()
664            .expect("leaf expression should build");
665        leaf.assert_cached_metrics_consistent();
666
667        let left = FilterExpression::builder()
668            .exists("left_1")
669            .exists("left_2")
670            .build()
671            .expect("left expression should build");
672        let right = FilterExpression::builder()
673            .exists("right_1")
674            .exists("right_2")
675            .build()
676            .expect("right expression should build");
677        let flattened = left.try_and(right).expect("flattened AND should build");
678        flattened.assert_cached_metrics_consistent();
679
680        let nested = flattened
681            .try_or(
682                FilterExpression::builder()
683                    .exists("alternative")
684                    .build()
685                    .expect("alternative expression should build"),
686            )
687            .expect("nested OR should build")
688            .try_not()
689            .expect("negated expression should build");
690        nested.assert_cached_metrics_consistent();
691
692        FilterExpression::match_all()
693            .try_and(nested.clone())
694            .expect("true AND expression should simplify")
695            .assert_cached_metrics_consistent();
696        FilterExpression::match_none()
697            .try_or(nested)
698            .expect("false OR expression should simplify")
699            .assert_cached_metrics_consistent();
700        FilterExpression::match_all()
701            .try_not()
702            .expect("constant negation should simplify")
703            .assert_cached_metrics_consistent();
704    }
705
706    #[test]
707    fn test_cached_metrics_combination_matrix() {
708        use crate::FilterLimitKind;
709        use crate::FilterLimits;
710        use crate::MetadataError;
711
712        let leaf = FilterExpression::builder()
713            .exists("chunk_index")
714            .build()
715            .expect("leaf expression should build");
716        let and = leaf.clone().try_and(leaf.clone()).expect("AND expression should build");
717        let or = leaf.clone().try_or(leaf.clone()).expect("OR expression should build");
718        let deep = and
719            .clone()
720            .try_or(leaf.clone())
721            .expect("nested OR expression should build")
722            .try_not()
723            .expect("NOT expression should build");
724        let tall_and = and
725            .clone()
726            .try_and(or.clone().try_not().expect("NOT expression should build"))
727            .expect("taller AND expression should build");
728        let tall_or = or
729            .clone()
730            .try_or(and.clone().try_not().expect("NOT expression should build"))
731            .expect("taller OR expression should build");
732        let roots = [leaf, and, or, deep, tall_and, tall_or];
733
734        for left in &roots {
735            for right in &roots {
736                for expression in [
737                    left.clone()
738                        .try_and(right.clone())
739                        .expect("AND combination should build"),
740                    left.clone().try_or(right.clone()).expect("OR combination should build"),
741                ] {
742                    expression.assert_cached_metrics_consistent();
743                    let (nodes, depth) = expression.recursive_metrics();
744                    let exact = FilterLimits::builder()
745                        .max_nodes(nodes)
746                        .max_depth(depth)
747                        .build()
748                        .expect("exact limits should build");
749                    assert!(expression.validate_limits(exact).is_ok());
750
751                    if nodes > 1 {
752                        let low_nodes = FilterLimits::builder()
753                            .max_nodes(nodes - 1)
754                            .build()
755                            .expect("lower node limit should build");
756                        assert!(matches!(
757                            expression.validate_limits(low_nodes),
758                            Err(MetadataError::FilterLimitExceeded {
759                                kind: FilterLimitKind::Nodes,
760                                ..
761                            })
762                        ));
763                    }
764
765                    if depth > 1 {
766                        let low_depth = FilterLimits::builder()
767                            .max_depth(depth - 1)
768                            .build()
769                            .expect("lower depth limit should build");
770                        assert!(matches!(
771                            expression.validate_limits(low_depth),
772                            Err(MetadataError::FilterLimitExceeded {
773                                kind: FilterLimitKind::Depth,
774                                ..
775                            })
776                        ));
777                    }
778                }
779            }
780        }
781    }
782
783    #[test]
784    fn test_cached_metrics_after_combination_and_simplification_sequence() {
785        let leaf = FilterExpression::builder()
786            .exists("chunk_index")
787            .build()
788            .expect("leaf expression should build");
789        leaf.assert_cached_metrics_consistent();
790
791        let and = leaf.clone().try_and(leaf.clone()).expect("AND should build");
792        and.assert_cached_metrics_consistent();
793        let or = and.try_or(leaf.clone()).expect("OR should build");
794        or.assert_cached_metrics_consistent();
795        let negated = or.try_not().expect("NOT should build");
796        negated.assert_cached_metrics_consistent();
797        let and_true = negated
798            .clone()
799            .try_and(FilterExpression::match_all())
800            .expect("true AND should simplify");
801        and_true.assert_cached_metrics_consistent();
802        let or_false = and_true
803            .try_or(FilterExpression::match_none())
804            .expect("false OR should simplify");
805        or_false.assert_cached_metrics_consistent();
806        let once_negated = or_false.try_not().expect("first NOT should build");
807        once_negated.assert_cached_metrics_consistent();
808        let twice_negated = once_negated.try_not().expect("second NOT should build");
809        twice_negated.assert_cached_metrics_consistent();
810    }
811}