Skip to main content

zeppelin_embed/meta/
predicate.rs

1//! Closed structured-predicate AST and bitmap evaluation over typed columns.
2
3use super::alive::AliveSet;
4use super::bitmap::DocBitmap;
5use super::columns::{Column, ColumnStore, DictionaryColumn, RawStringColumn};
6use super::{ColumnId, ColumnType};
7
8/// A typed scalar carried by an equality, membership, or range predicate.
9#[derive(Clone, Debug, PartialEq)]
10pub enum PredicateValue {
11    /// An unsigned 64-bit integer.
12    U64(u64),
13    /// A full document identifier, supporting equality and membership.
14    Id128(crate::ingest::DocId),
15    /// A signed 64-bit integer.
16    I64(i64),
17    /// An IEEE-754 64-bit floating-point value.
18    F64(f64),
19    /// A Boolean value.
20    Bool(bool),
21    /// A UTF-8 string value.
22    String(String),
23}
24
25impl PredicateValue {
26    pub(crate) fn value_type(&self) -> ColumnType {
27        match self {
28            Self::Id128(_) => ColumnType::Id128,
29            Self::U64(_) => ColumnType::U64,
30            Self::I64(_) => ColumnType::I64,
31            Self::F64(_) => ColumnType::F64,
32            Self::Bool(_) => ColumnType::Bool,
33            Self::String(_) => ColumnType::RawString,
34        }
35    }
36}
37
38/// One inclusive or exclusive endpoint of a numeric range.
39#[derive(Clone, Debug, PartialEq)]
40pub struct RangeBound {
41    /// The typed numeric endpoint.
42    pub value: PredicateValue,
43    /// Whether a row exactly equal to the endpoint matches.
44    pub inclusive: bool,
45}
46
47impl RangeBound {
48    /// Creates an inclusive endpoint.
49    #[must_use]
50    pub fn inclusive(value: PredicateValue) -> Self {
51        Self {
52            value,
53            inclusive: true,
54        }
55    }
56
57    /// Creates an exclusive endpoint.
58    #[must_use]
59    pub fn exclusive(value: PredicateValue) -> Self {
60        Self {
61            value,
62            inclusive: false,
63        }
64    }
65}
66
67/// A two-sided or open numeric range over one typed column.
68#[derive(Clone, Debug, PartialEq)]
69pub struct RangePredicate {
70    /// The numeric schema column.
71    pub column: ColumnId,
72    /// The optional lower endpoint.
73    pub lower: Option<RangeBound>,
74    /// The optional upper endpoint.
75    pub upper: Option<RangeBound>,
76}
77
78/// The complete v1 metadata predicate language.
79///
80/// This enum intentionally has no free-form or unsupported marker. Bindings
81/// must reject LIKE, regex, GLOB, and every other operation not represented by
82/// one of these variants before calling the engine.
83#[derive(Clone, Debug, PartialEq)]
84pub enum Predicate {
85    /// Exact typed equality.
86    Eq {
87        /// The schema column.
88        column: ColumnId,
89        /// The typed query value.
90        value: PredicateValue,
91    },
92    /// Membership in a typed set. An empty set matches no rows.
93    In {
94        /// The schema column.
95        column: ColumnId,
96        /// Typed query values.
97        values: Vec<PredicateValue>,
98    },
99    /// A numeric range.
100    Range(RangePredicate),
101    /// Rows in which a column is non-null.
102    Exists(ColumnId),
103    /// Rows in which a column is null.
104    IsNull(ColumnId),
105    /// Logical conjunction. The empty conjunction is true within the alive set.
106    And(Vec<Predicate>),
107    /// Logical disjunction. The empty disjunction is false.
108    Or(Vec<Predicate>),
109    /// Logical negation bounded by the current alive set.
110    Not(Box<Predicate>),
111}
112
113/// A typed predicate-evaluation failure.
114#[derive(Clone, Debug, Eq, PartialEq)]
115pub enum EvalError {
116    /// The alive set and column store describe different document spaces.
117    RowCountMismatch {
118        /// Number of metadata rows.
119        columns: u32,
120        /// Number of rows tracked by the alive set.
121        alive: u32,
122    },
123    /// The predicate referred to a column absent from the schema.
124    UnknownColumn(ColumnId),
125    /// A query value did not match the declared column type.
126    TypeMismatch {
127        /// The mismatched column.
128        column: ColumnId,
129        /// The declared physical type.
130        expected: ColumnType,
131        /// The supplied query value type.
132        actual: ColumnType,
133    },
134    /// Range predicates are limited to numeric columns.
135    RangeRequiresNumericColumn(ColumnId),
136}
137
138impl std::fmt::Display for EvalError {
139    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        match self {
141            Self::RowCountMismatch { columns, alive } => write!(
142                formatter,
143                "column row count {columns} differs from alive row count {alive}"
144            ),
145            Self::UnknownColumn(column) => write!(formatter, "unknown column {}", column.get()),
146            Self::TypeMismatch {
147                column,
148                expected,
149                actual,
150            } => write!(
151                formatter,
152                "column {} expects {expected:?}, received {actual:?}",
153                column.get()
154            ),
155            Self::RangeRequiresNumericColumn(column) => {
156                write!(formatter, "column {} is not numeric", column.get())
157            }
158        }
159    }
160}
161
162impl std::error::Error for EvalError {}
163
164/// Evaluates a structured predicate into an exact alive-bounded bitmap.
165pub fn evaluate(
166    predicate: &Predicate,
167    columns: &ColumnStore,
168    alive: &AliveSet,
169) -> Result<DocBitmap, EvalError> {
170    #[cfg(any(test, feature = "test-seams"))]
171    crate::fts::preparation_observer::record_filter_work(1, 0, 0);
172    if columns.row_count() != alive.row_count() {
173        return Err(EvalError::RowCountMismatch {
174            columns: columns.row_count(),
175            alive: alive.row_count(),
176        });
177    }
178    evaluate_in_scope(predicate, columns, alive.alive_bitmap())
179}
180
181fn evaluate_in_scope(
182    predicate: &Predicate,
183    columns: &ColumnStore,
184    scope: &DocBitmap,
185) -> Result<DocBitmap, EvalError> {
186    match predicate {
187        Predicate::Eq { column, value } => eval_eq(*column, value, columns, scope),
188        Predicate::In { column, values } => eval_in(*column, values, columns, scope),
189        Predicate::Range(range) => eval_range(range, columns, scope),
190        Predicate::Exists(column) => {
191            let column = get_column(columns, *column)?;
192            let mut result = column.present().clone();
193            result.intersect_with(scope);
194            Ok(result)
195        }
196        Predicate::IsNull(column) => {
197            let column = get_column(columns, *column)?;
198            let mut result = scope.clone();
199            result.subtract(column.present());
200            Ok(result)
201        }
202        Predicate::And(children) => eval_and(children, columns, scope),
203        Predicate::Or(children) => {
204            let mut result = DocBitmap::new();
205            for child in children {
206                let mut remaining = scope.clone();
207                remaining.subtract(&result);
208                if remaining.is_empty() {
209                    break;
210                }
211                let child_result = evaluate_in_scope(child, columns, &remaining)?;
212                result.union_with(&child_result);
213            }
214            Ok(result)
215        }
216        Predicate::Not(child) => {
217            let child_result = evaluate_in_scope(child, columns, scope)?;
218            let mut result = scope.clone();
219            result.subtract(&child_result);
220            Ok(result)
221        }
222    }
223}
224
225fn eval_and(
226    children: &[Predicate],
227    columns: &ColumnStore,
228    scope: &DocBitmap,
229) -> Result<DocBitmap, EvalError> {
230    let mut ordered = children.iter().collect::<Vec<_>>();
231    ordered.sort_by_key(|child| estimate_cardinality(child, columns, scope));
232
233    let mut result = scope.clone();
234    for child in ordered {
235        if result.is_empty() {
236            break;
237        }
238        result = evaluate_in_scope(child, columns, &result)?;
239    }
240    Ok(result)
241}
242
243fn estimate_cardinality(predicate: &Predicate, columns: &ColumnStore, scope: &DocBitmap) -> u64 {
244    let scope_count = scope.cardinality();
245    match predicate {
246        Predicate::Eq { .. } => scope_count.div_ceil(16),
247        Predicate::In { values, .. } => scope_count
248            .saturating_mul(u64::try_from(values.len()).unwrap_or(u64::MAX))
249            .div_ceil(16)
250            .min(scope_count),
251        Predicate::Range(_) => scope_count / 2,
252        Predicate::Exists(column) => columns.column(*column).map_or(scope_count, |column| {
253            let mut present = column.present().clone();
254            present.intersect_with(scope);
255            present.cardinality()
256        }),
257        Predicate::IsNull(column) => columns.column(*column).map_or(scope_count, |column| {
258            let mut missing = scope.clone();
259            missing.subtract(column.present());
260            missing.cardinality()
261        }),
262        Predicate::And(children) => children
263            .iter()
264            .map(|child| estimate_cardinality(child, columns, scope))
265            .min()
266            .unwrap_or(scope_count),
267        Predicate::Or(children) => children
268            .iter()
269            .map(|child| estimate_cardinality(child, columns, scope))
270            .fold(0_u64, u64::saturating_add)
271            .min(scope_count),
272        Predicate::Not(_) => scope_count / 2,
273    }
274}
275
276fn get_column(columns: &ColumnStore, id: ColumnId) -> Result<&Column, EvalError> {
277    columns.column(id).ok_or(EvalError::UnknownColumn(id))
278}
279
280fn eval_eq(
281    id: ColumnId,
282    query: &PredicateValue,
283    columns: &ColumnStore,
284    scope: &DocBitmap,
285) -> Result<DocBitmap, EvalError> {
286    let column = get_column(columns, id)?;
287    ensure_type(id, column, query)?;
288    let result = match (column, query) {
289        (Column::Id128(column), PredicateValue::Id128(query)) => {
290            eval_values_eq(column.values(), column.present(), scope, query)
291        }
292        (Column::U64(column), PredicateValue::U64(query)) => {
293            eval_values_eq(column.values(), column.present(), scope, query)
294        }
295        (Column::I64(column), PredicateValue::I64(query)) => {
296            eval_values_eq(column.values(), column.present(), scope, query)
297        }
298        (Column::F64(column), PredicateValue::F64(query)) => {
299            eval_values_eq(column.values(), column.present(), scope, query)
300        }
301        (Column::Bool(column), PredicateValue::Bool(query)) => {
302            let byte = u8::from(*query);
303            eval_values_eq(column.values(), column.present(), scope, &byte)
304        }
305        (Column::DictionaryString(column), PredicateValue::String(query)) => {
306            eval_dictionary_eq(column, query, scope)
307        }
308        (Column::RawString(column), PredicateValue::String(query)) => {
309            eval_raw_string_eq(column, query, scope)
310        }
311        _ => DocBitmap::new(),
312    };
313    Ok(result)
314}
315
316fn eval_values_eq<T: PartialEq>(
317    values: &[T],
318    present: &DocBitmap,
319    scope: &DocBitmap,
320    query: &T,
321) -> DocBitmap {
322    let mut result = DocBitmap::new();
323    for (position, value) in values.iter().enumerate() {
324        let Some(row) = u32::try_from(position).ok() else {
325            break;
326        };
327        if scope.contains(row) && present.contains(row) && value == query {
328            result.insert(row);
329        }
330    }
331    result
332}
333
334fn eval_dictionary_eq(column: &DictionaryColumn, query: &str, scope: &DocBitmap) -> DocBitmap {
335    let Some(query_code) = column.dictionary().find(query) else {
336        return DocBitmap::new();
337    };
338    let mut result = DocBitmap::new();
339    for (position, code) in column.codes().iter().enumerate() {
340        let Some(row) = u32::try_from(position).ok() else {
341            break;
342        };
343        if scope.contains(row) && column.present().contains(row) && code == query_code {
344            result.insert(row);
345        }
346    }
347    result
348}
349
350fn eval_raw_string_eq(column: &RawStringColumn, query: &str, scope: &DocBitmap) -> DocBitmap {
351    let mut result = DocBitmap::new();
352    #[cfg(any(test, feature = "test-seams"))]
353    crate::fts::preparation_observer::record_filter_work(0, column.len(), 0);
354    for position in 0..column.len() {
355        let Some(row) = u32::try_from(position).ok() else {
356            break;
357        };
358        if scope.contains(row)
359            && column.present().contains(row)
360            && column.physical_get(position) == Some(query)
361        {
362            result.insert(row);
363        }
364    }
365    result
366}
367
368fn eval_in(
369    id: ColumnId,
370    queries: &[PredicateValue],
371    columns: &ColumnStore,
372    scope: &DocBitmap,
373) -> Result<DocBitmap, EvalError> {
374    let column = get_column(columns, id)?;
375    for query in queries {
376        ensure_type(id, column, query)?;
377    }
378    if let Column::RawString(column) = column {
379        if queries.is_empty() {
380            return Ok(DocBitmap::new());
381        }
382        let members: std::collections::HashSet<&str> = queries
383            .iter()
384            .filter_map(|value| {
385                if let PredicateValue::String(value) = value {
386                    Some(value.as_str())
387                } else {
388                    None
389                }
390            })
391            .collect();
392        let mut result = DocBitmap::new();
393        #[cfg(any(test, feature = "test-seams"))]
394        crate::fts::preparation_observer::record_filter_work(0, column.len(), 0);
395        for position in 0..column.len() {
396            let Some(row) = u32::try_from(position).ok() else {
397                break;
398            };
399            if scope.contains(row)
400                && column.present().contains(row)
401                && column
402                    .physical_get(position)
403                    .is_some_and(|value| members.contains(value))
404            {
405                result.insert(row);
406            }
407        }
408        return Ok(result);
409    }
410    let mut result = DocBitmap::new();
411    for query in queries {
412        result.union_with(&eval_eq(id, query, columns, scope)?);
413    }
414    Ok(result)
415}
416
417fn ensure_type(id: ColumnId, column: &Column, query: &PredicateValue) -> Result<(), EvalError> {
418    let expected = column.column_type();
419    let compatible = matches!(
420        (expected, query),
421        (ColumnType::Id128, PredicateValue::Id128(_))
422            | (ColumnType::U64, PredicateValue::U64(_))
423            | (ColumnType::I64, PredicateValue::I64(_))
424            | (ColumnType::F64, PredicateValue::F64(_))
425            | (ColumnType::Bool, PredicateValue::Bool(_))
426            | (
427                ColumnType::DictionaryString | ColumnType::RawString,
428                PredicateValue::String(_)
429            )
430    );
431    if compatible {
432        Ok(())
433    } else {
434        Err(EvalError::TypeMismatch {
435            column: id,
436            expected,
437            actual: query.value_type(),
438        })
439    }
440}
441
442fn eval_range(
443    range: &RangePredicate,
444    columns: &ColumnStore,
445    scope: &DocBitmap,
446) -> Result<DocBitmap, EvalError> {
447    let column = get_column(columns, range.column)?;
448    match column {
449        Column::U64(column) => {
450            let lower = u64_bound(range.column, range.lower.as_ref())?;
451            let upper = u64_bound(range.column, range.upper.as_ref())?;
452            Ok(eval_ordered_range(
453                column.values(),
454                column.present(),
455                scope,
456                lower,
457                upper,
458            ))
459        }
460        Column::I64(column) => {
461            let lower = i64_bound(range.column, range.lower.as_ref())?;
462            let upper = i64_bound(range.column, range.upper.as_ref())?;
463            Ok(eval_ordered_range(
464                column.values(),
465                column.present(),
466                scope,
467                lower,
468                upper,
469            ))
470        }
471        Column::F64(column) => {
472            let lower = f64_bound(range.column, range.lower.as_ref())?;
473            let upper = f64_bound(range.column, range.upper.as_ref())?;
474            Ok(eval_f64_range(
475                column.values(),
476                column.present(),
477                scope,
478                lower,
479                upper,
480            ))
481        }
482        Column::Id128(_) | Column::Bool(_) | Column::DictionaryString(_) | Column::RawString(_) => {
483            Err(EvalError::RangeRequiresNumericColumn(range.column))
484        }
485    }
486}
487
488fn u64_bound(
489    column: ColumnId,
490    bound: Option<&RangeBound>,
491) -> Result<Option<(u64, bool)>, EvalError> {
492    bound
493        .map(|bound| match bound.value {
494            PredicateValue::U64(value) => Ok((value, bound.inclusive)),
495            _ => Err(type_error(column, ColumnType::U64, &bound.value)),
496        })
497        .transpose()
498}
499
500fn i64_bound(
501    column: ColumnId,
502    bound: Option<&RangeBound>,
503) -> Result<Option<(i64, bool)>, EvalError> {
504    bound
505        .map(|bound| match bound.value {
506            PredicateValue::I64(value) => Ok((value, bound.inclusive)),
507            _ => Err(type_error(column, ColumnType::I64, &bound.value)),
508        })
509        .transpose()
510}
511
512fn f64_bound(
513    column: ColumnId,
514    bound: Option<&RangeBound>,
515) -> Result<Option<(f64, bool)>, EvalError> {
516    bound
517        .map(|bound| match bound.value {
518            PredicateValue::F64(value) => Ok((value, bound.inclusive)),
519            _ => Err(type_error(column, ColumnType::F64, &bound.value)),
520        })
521        .transpose()
522}
523
524fn type_error(column: ColumnId, expected: ColumnType, value: &PredicateValue) -> EvalError {
525    EvalError::TypeMismatch {
526        column,
527        expected,
528        actual: value.value_type(),
529    }
530}
531
532fn eval_ordered_range<T: Copy + PartialOrd>(
533    values: &[T],
534    present: &DocBitmap,
535    scope: &DocBitmap,
536    lower: Option<(T, bool)>,
537    upper: Option<(T, bool)>,
538) -> DocBitmap {
539    let mut result = DocBitmap::new();
540    for (position, value) in values.iter().copied().enumerate() {
541        let Some(row) = u32::try_from(position).ok() else {
542            break;
543        };
544        if scope.contains(row)
545            && present.contains(row)
546            && lower_matches(value, lower)
547            && upper_matches(value, upper)
548        {
549            result.insert(row);
550        }
551    }
552    result
553}
554
555fn lower_matches<T: Copy + PartialOrd>(value: T, bound: Option<(T, bool)>) -> bool {
556    bound.is_none_or(|(endpoint, inclusive)| {
557        if inclusive {
558            value >= endpoint
559        } else {
560            value > endpoint
561        }
562    })
563}
564
565fn upper_matches<T: Copy + PartialOrd>(value: T, bound: Option<(T, bool)>) -> bool {
566    bound.is_none_or(|(endpoint, inclusive)| {
567        if inclusive {
568            value <= endpoint
569        } else {
570            value < endpoint
571        }
572    })
573}
574
575fn eval_f64_range(
576    values: &[f64],
577    present: &DocBitmap,
578    scope: &DocBitmap,
579    lower: Option<(f64, bool)>,
580    upper: Option<(f64, bool)>,
581) -> DocBitmap {
582    if lower.is_some_and(|(value, _)| value.is_nan())
583        || upper.is_some_and(|(value, _)| value.is_nan())
584    {
585        return DocBitmap::new();
586    }
587    let mut result = DocBitmap::new();
588    for (position, value) in values.iter().copied().enumerate() {
589        let Some(row) = u32::try_from(position).ok() else {
590            break;
591        };
592        if !value.is_nan()
593            && scope.contains(row)
594            && present.contains(row)
595            && lower_matches(value, lower)
596            && upper_matches(value, upper)
597        {
598            result.insert(row);
599        }
600    }
601    result
602}
603
604#[allow(
605    clippy::expect_used,
606    clippy::indexing_slicing,
607    clippy::panic,
608    clippy::unwrap_used
609)]
610#[cfg(test)]
611mod tests {
612    use super::*;
613    use crate::meta::{
614        ColumnDefinition, ColumnInput, ColumnStoreBuilder, ColumnValue, Schema, TIMESTAMP_COLUMN,
615    };
616    use proptest::prelude::*;
617    use proptest::test_runner::{Config, RngSeed, TestRunner};
618    use rand::RngCore;
619
620    #[test]
621    fn ze_262_raw_string_in_visits_rows_once() {
622        let id = ColumnId::new(1);
623        let schema = Schema::new(vec![ColumnDefinition::new(
624            id,
625            "note",
626            ColumnType::RawString,
627            true,
628        )])
629        .expect("schema");
630        let mut builder = ColumnStoreBuilder::new(schema);
631        for value in [
632            Some("note-0"),
633            Some("note-499"),
634            None,
635            Some("note-0"),
636            Some("missing"),
637        ] {
638            let inputs = value.map(|v| ColumnInput {
639                column: id,
640                value: ColumnValue::String(v),
641            });
642            builder.push_row(0, inputs.as_slice()).expect("row");
643        }
644        let columns = builder.finish().expect("columns");
645        let mut alive = AliveSet::new(5);
646        alive.tombstone(3).expect("tombstone");
647        let mut values: Vec<_> = (0..500)
648            .map(|n| PredicateValue::String(format!("note-{n}")))
649            .collect();
650        values.push(PredicateValue::String("note-0".into()));
651        crate::fts::preparation_observer::begin();
652        let hits = evaluate(
653            &Predicate::In {
654                column: id,
655                values: values.clone(),
656            },
657            &columns,
658            &alive,
659        )
660        .expect("eval");
661        assert_eq!(hits.iter().collect::<Vec<_>>(), vec![0, 1]);
662        assert!(
663            crate::fts::preparation_observer::filter_work().1 <= 5,
664            "membership must visit each row at most once"
665        );
666        crate::fts::preparation_observer::begin();
667        values.push(PredicateValue::Bool(false));
668        assert!(matches!(
669            evaluate(&Predicate::In { column: id, values }, &columns, &alive),
670            Err(EvalError::TypeMismatch { .. })
671        ));
672        assert_eq!(crate::fts::preparation_observer::filter_work().1, 0);
673        assert_eq!(
674            evaluate(
675                &Predicate::In {
676                    column: id,
677                    values: vec![]
678                },
679                &columns,
680                &alive
681            )
682            .expect("empty")
683            .cardinality(),
684            0
685        );
686    }
687
688    #[derive(Clone, Copy, Debug)]
689    enum ModelType {
690        U64,
691        I64,
692        F64,
693        Bool,
694        DictionaryString,
695        RawString,
696    }
697
698    impl ModelType {
699        fn column_type(self) -> ColumnType {
700            match self {
701                Self::U64 => ColumnType::U64,
702                Self::I64 => ColumnType::I64,
703                Self::F64 => ColumnType::F64,
704                Self::Bool => ColumnType::Bool,
705                Self::DictionaryString => ColumnType::DictionaryString,
706                Self::RawString => ColumnType::RawString,
707            }
708        }
709
710        fn is_numeric(self) -> bool {
711            matches!(self, Self::U64 | Self::I64 | Self::F64)
712        }
713    }
714
715    #[derive(Clone, Debug)]
716    struct CellSeed {
717        present: bool,
718        unsigned: u64,
719        signed: i64,
720        float: f64,
721        boolean: bool,
722        string: String,
723    }
724
725    impl Arbitrary for CellSeed {
726        type Parameters = ();
727        type Strategy = BoxedStrategy<Self>;
728
729        fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
730            (
731                any::<bool>(),
732                any::<u64>(),
733                any::<i64>(),
734                prop_oneof![16 => any::<f64>(), 1 => Just(f64::NAN)],
735                any::<bool>(),
736                "[a-z]{0,8}",
737            )
738                .prop_map(|(present, unsigned, signed, float, boolean, string)| Self {
739                    present,
740                    unsigned,
741                    signed,
742                    float,
743                    boolean,
744                    string,
745                })
746                .boxed()
747        }
748    }
749
750    #[derive(Clone, Debug)]
751    enum PredicateSeed {
752        Eq(u8, CellSeed),
753        In(u8, Vec<CellSeed>),
754        Range {
755            column: u8,
756            lower: Option<(CellSeed, bool)>,
757            upper: Option<(CellSeed, bool)>,
758        },
759        Exists(u8),
760        IsNull(u8),
761        And(Vec<Self>),
762        Or(Vec<Self>),
763        Not(Box<Self>),
764    }
765
766    impl Arbitrary for PredicateSeed {
767        type Parameters = ();
768        type Strategy = BoxedStrategy<Self>;
769
770        fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
771            let leaf = prop_oneof![
772                (any::<u8>(), any::<CellSeed>())
773                    .prop_map(|(column, value)| Self::Eq(column, value)),
774                (any::<u8>(), prop::collection::vec(any::<CellSeed>(), 0..=4))
775                    .prop_map(|(column, values)| Self::In(column, values)),
776                (
777                    any::<u8>(),
778                    prop::option::of((any::<CellSeed>(), any::<bool>())),
779                    prop::option::of((any::<CellSeed>(), any::<bool>()))
780                )
781                    .prop_map(|(column, lower, upper)| Self::Range {
782                        column,
783                        lower,
784                        upper,
785                    }),
786                any::<u8>().prop_map(Self::Exists),
787                any::<u8>().prop_map(Self::IsNull),
788            ];
789
790            leaf.prop_recursive(4, 64, 4, |inner| {
791                prop_oneof![
792                    prop::collection::vec(inner.clone(), 0..=3).prop_map(Self::And),
793                    prop::collection::vec(inner.clone(), 0..=3).prop_map(Self::Or),
794                    inner.prop_map(|child| Self::Not(Box::new(child))),
795                ]
796            })
797            .boxed()
798        }
799    }
800
801    #[derive(Clone, Debug, PartialEq)]
802    enum ModelValue {
803        U64(u64),
804        I64(i64),
805        F64(f64),
806        Bool(bool),
807        String(String),
808    }
809
810    impl ModelValue {
811        fn from_seed(column_type: ModelType, seed: &CellSeed) -> Option<Self> {
812            if !seed.present {
813                return None;
814            }
815            Some(match column_type {
816                ModelType::U64 => Self::U64(seed.unsigned),
817                ModelType::I64 => Self::I64(seed.signed),
818                ModelType::F64 => Self::F64(seed.float),
819                ModelType::Bool => Self::Bool(seed.boolean),
820                ModelType::DictionaryString | ModelType::RawString => {
821                    Self::String(seed.string.clone())
822                }
823            })
824        }
825
826        fn as_column_value(&self) -> ColumnValue<'_> {
827            match self {
828                Self::U64(value) => ColumnValue::U64(*value),
829                Self::I64(value) => ColumnValue::I64(*value),
830                Self::F64(value) => ColumnValue::F64(*value),
831                Self::Bool(value) => ColumnValue::Bool(*value),
832                Self::String(value) => ColumnValue::String(value),
833            }
834        }
835    }
836
837    #[derive(Clone, Debug)]
838    struct ModelRow {
839        timestamp: i64,
840        alive: bool,
841        values: Vec<Option<ModelValue>>,
842    }
843
844    #[derive(Clone, Debug)]
845    struct GeneratedCase {
846        types: Vec<ModelType>,
847        rows: Vec<ModelRow>,
848        predicate: Predicate,
849    }
850
851    impl Arbitrary for GeneratedCase {
852        type Parameters = ();
853        type Strategy = BoxedStrategy<Self>;
854
855        fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
856            prop::collection::vec(
857                prop_oneof![
858                    Just(ModelType::U64),
859                    Just(ModelType::I64),
860                    Just(ModelType::F64),
861                    Just(ModelType::Bool),
862                    Just(ModelType::DictionaryString),
863                    Just(ModelType::RawString),
864                ],
865                0..=6,
866            )
867            .prop_flat_map(|types| {
868                let cells = prop::collection::vec(any::<CellSeed>(), types.len());
869                let row = (any::<i64>(), any::<bool>(), cells);
870                let rows = prop_oneof![
871                    1023 => prop::collection::vec(row.clone(), 0..=128),
872                    1 => prop::collection::vec(row, 9_990..=10_000),
873                ];
874                (Just(types), rows, any::<PredicateSeed>())
875            })
876            .prop_map(|(types, rows, predicate_seed)| {
877                let rows = rows
878                    .into_iter()
879                    .map(|(timestamp, alive, cells)| ModelRow {
880                        timestamp,
881                        alive,
882                        values: types
883                            .iter()
884                            .copied()
885                            .zip(&cells)
886                            .map(|(column_type, seed)| ModelValue::from_seed(column_type, seed))
887                            .collect(),
888                    })
889                    .collect();
890                let predicate = materialize_predicate(&predicate_seed, &types);
891                GeneratedCase {
892                    types,
893                    rows,
894                    predicate,
895                }
896            })
897            .boxed()
898        }
899    }
900
901    fn materialize_predicate(seed: &PredicateSeed, types: &[ModelType]) -> Predicate {
902        match seed {
903            PredicateSeed::Eq(column, value) => {
904                let (id, column_type) = select_column(*column, types);
905                Predicate::Eq {
906                    column: id,
907                    value: predicate_value_from_seed(column_type, value),
908                }
909            }
910            PredicateSeed::In(column, values) => {
911                let (id, column_type) = select_column(*column, types);
912                Predicate::In {
913                    column: id,
914                    values: values
915                        .iter()
916                        .map(|value| predicate_value_from_seed(column_type, value))
917                        .collect(),
918                }
919            }
920            PredicateSeed::Range {
921                column,
922                lower,
923                upper,
924            } => {
925                let (id, column_type) = select_column(*column, types);
926                if !column_type.is_numeric() {
927                    return Predicate::Exists(id);
928                }
929                Predicate::Range(RangePredicate {
930                    column: id,
931                    lower: lower.as_ref().map(|(value, inclusive)| RangeBound {
932                        value: predicate_value_from_seed(column_type, value),
933                        inclusive: *inclusive,
934                    }),
935                    upper: upper.as_ref().map(|(value, inclusive)| RangeBound {
936                        value: predicate_value_from_seed(column_type, value),
937                        inclusive: *inclusive,
938                    }),
939                })
940            }
941            PredicateSeed::Exists(column) => {
942                let (id, _) = select_column(*column, types);
943                Predicate::Exists(id)
944            }
945            PredicateSeed::IsNull(column) => {
946                let (id, _) = select_column(*column, types);
947                Predicate::IsNull(id)
948            }
949            PredicateSeed::And(children) => Predicate::And(
950                children
951                    .iter()
952                    .map(|child| materialize_predicate(child, types))
953                    .collect(),
954            ),
955            PredicateSeed::Or(children) => Predicate::Or(
956                children
957                    .iter()
958                    .map(|child| materialize_predicate(child, types))
959                    .collect(),
960            ),
961            PredicateSeed::Not(child) => {
962                Predicate::Not(Box::new(materialize_predicate(child, types)))
963            }
964        }
965    }
966
967    fn select_column(selector: u8, types: &[ModelType]) -> (ColumnId, ModelType) {
968        let column_count = types.len().saturating_add(1);
969        let position = usize::from(selector) % column_count;
970        if position == 0 {
971            return (TIMESTAMP_COLUMN, ModelType::I64);
972        }
973        let model_type = types
974            .get(position.saturating_sub(1))
975            .copied()
976            .unwrap_or(ModelType::I64);
977        let id = u32::try_from(position).unwrap_or_default();
978        (ColumnId::new(id), model_type)
979    }
980
981    fn predicate_value_from_seed(column_type: ModelType, seed: &CellSeed) -> PredicateValue {
982        match column_type {
983            ModelType::U64 => PredicateValue::U64(seed.unsigned),
984            ModelType::I64 => PredicateValue::I64(seed.signed),
985            ModelType::F64 => PredicateValue::F64(seed.float),
986            ModelType::Bool => PredicateValue::Bool(seed.boolean),
987            ModelType::DictionaryString | ModelType::RawString => {
988                PredicateValue::String(seed.string.clone())
989            }
990        }
991    }
992
993    fn build_case(case: &GeneratedCase) -> (ColumnStore, AliveSet) {
994        let definitions = case
995            .types
996            .iter()
997            .copied()
998            .enumerate()
999            .map(|(position, column_type)| {
1000                let id = u32::try_from(position.saturating_add(1)).unwrap_or_default();
1001                ColumnDefinition::new(
1002                    ColumnId::new(id),
1003                    format!("column_{id}"),
1004                    column_type.column_type(),
1005                    true,
1006                )
1007            })
1008            .collect();
1009        let schema = Schema::new(definitions).expect("generated schema is valid");
1010        let mut builder = ColumnStoreBuilder::new(schema);
1011        for row in &case.rows {
1012            let inputs = row
1013                .values
1014                .iter()
1015                .enumerate()
1016                .filter_map(|(position, value)| {
1017                    let value = value.as_ref()?;
1018                    let id = u32::try_from(position.saturating_add(1)).ok()?;
1019                    Some(ColumnInput {
1020                        column: ColumnId::new(id),
1021                        value: value.as_column_value(),
1022                    })
1023                })
1024                .collect::<Vec<_>>();
1025            builder
1026                .push_row(row.timestamp, &inputs)
1027                .expect("generated row matches schema");
1028        }
1029        let store = builder.finish().expect("generated store seals");
1030        let row_count = u32::try_from(case.rows.len()).expect("at most 10k rows");
1031        let mut alive = AliveSet::new(row_count);
1032        for (position, row) in case.rows.iter().enumerate() {
1033            if !row.alive {
1034                let id = u32::try_from(position).expect("at most 10k rows");
1035                alive
1036                    .tombstone(id)
1037                    .expect("generated tombstone is in range");
1038            }
1039        }
1040        (store, alive)
1041    }
1042
1043    fn naive_matches(predicate: &Predicate, row: &ModelRow) -> bool {
1044        match predicate {
1045            Predicate::Eq { column, value } => {
1046                row_value(row, *column).is_some_and(|stored| model_eq(&stored, value))
1047            }
1048            Predicate::In { column, values } => row_value(row, *column)
1049                .is_some_and(|stored| values.iter().any(|query| model_eq(&stored, query))),
1050            Predicate::Range(range) => row_value(row, range.column).is_some_and(|stored| {
1051                model_range(&stored, range.lower.as_ref(), range.upper.as_ref())
1052            }),
1053            Predicate::Exists(column) => row_value(row, *column).is_some(),
1054            Predicate::IsNull(column) => row_value(row, *column).is_none(),
1055            Predicate::And(children) => children.iter().all(|child| naive_matches(child, row)),
1056            Predicate::Or(children) => children.iter().any(|child| naive_matches(child, row)),
1057            Predicate::Not(child) => !naive_matches(child, row),
1058        }
1059    }
1060
1061    fn row_value(row: &ModelRow, column: ColumnId) -> Option<ModelValue> {
1062        if column == TIMESTAMP_COLUMN {
1063            return Some(ModelValue::I64(row.timestamp));
1064        }
1065        let position = usize::try_from(column.get()).ok()?.checked_sub(1)?;
1066        row.values.get(position).cloned().flatten()
1067    }
1068
1069    fn model_eq(stored: &ModelValue, query: &PredicateValue) -> bool {
1070        match (stored, query) {
1071            (ModelValue::U64(stored), PredicateValue::U64(query)) => stored == query,
1072            (ModelValue::I64(stored), PredicateValue::I64(query)) => stored == query,
1073            (ModelValue::F64(stored), PredicateValue::F64(query)) => stored == query,
1074            (ModelValue::Bool(stored), PredicateValue::Bool(query)) => stored == query,
1075            (ModelValue::String(stored), PredicateValue::String(query)) => stored == query,
1076            _ => false,
1077        }
1078    }
1079
1080    fn model_range(
1081        stored: &ModelValue,
1082        lower: Option<&RangeBound>,
1083        upper: Option<&RangeBound>,
1084    ) -> bool {
1085        match stored {
1086            ModelValue::U64(value) => {
1087                model_bound_u64(*value, lower, true) && model_bound_u64(*value, upper, false)
1088            }
1089            ModelValue::I64(value) => {
1090                model_bound_i64(*value, lower, true) && model_bound_i64(*value, upper, false)
1091            }
1092            ModelValue::F64(value) => {
1093                !value.is_nan()
1094                    && model_bound_f64(*value, lower, true)
1095                    && model_bound_f64(*value, upper, false)
1096            }
1097            ModelValue::Bool(_) | ModelValue::String(_) => false,
1098        }
1099    }
1100
1101    fn model_bound_u64(value: u64, bound: Option<&RangeBound>, lower: bool) -> bool {
1102        bound.is_none_or(|bound| match bound.value {
1103            PredicateValue::U64(endpoint) => compare_bound(value, endpoint, bound.inclusive, lower),
1104            _ => false,
1105        })
1106    }
1107
1108    fn model_bound_i64(value: i64, bound: Option<&RangeBound>, lower: bool) -> bool {
1109        bound.is_none_or(|bound| match bound.value {
1110            PredicateValue::I64(endpoint) => compare_bound(value, endpoint, bound.inclusive, lower),
1111            _ => false,
1112        })
1113    }
1114
1115    fn model_bound_f64(value: f64, bound: Option<&RangeBound>, lower: bool) -> bool {
1116        bound.is_none_or(|bound| match bound.value {
1117            PredicateValue::F64(endpoint) if !endpoint.is_nan() => {
1118                compare_bound(value, endpoint, bound.inclusive, lower)
1119            }
1120            _ => false,
1121        })
1122    }
1123
1124    fn compare_bound<T: PartialOrd>(value: T, endpoint: T, inclusive: bool, lower: bool) -> bool {
1125        match (lower, inclusive) {
1126            (true, true) => value >= endpoint,
1127            (true, false) => value > endpoint,
1128            (false, true) => value <= endpoint,
1129            (false, false) => value < endpoint,
1130        }
1131    }
1132
1133    fn one_i64_column(values: &[i64]) -> (ColumnStore, AliveSet) {
1134        let schema = Schema::new(vec![ColumnDefinition::new(
1135            ColumnId::new(1),
1136            "number",
1137            ColumnType::I64,
1138            true,
1139        )])
1140        .expect("valid schema");
1141        let mut builder = ColumnStoreBuilder::new(schema);
1142        for value in values {
1143            builder
1144                .push_row(
1145                    0,
1146                    &[ColumnInput {
1147                        column: ColumnId::new(1),
1148                        value: ColumnValue::I64(*value),
1149                    }],
1150                )
1151                .expect("valid i64 row");
1152        }
1153        let store = builder.finish().expect("store seals");
1154        let alive = AliveSet::new(store.row_count());
1155        (store, alive)
1156    }
1157
1158    fn one_f64_column(values: &[f64]) -> (ColumnStore, AliveSet) {
1159        let schema = Schema::new(vec![ColumnDefinition::new(
1160            ColumnId::new(1),
1161            "number",
1162            ColumnType::F64,
1163            true,
1164        )])
1165        .expect("valid schema");
1166        let mut builder = ColumnStoreBuilder::new(schema);
1167        for value in values {
1168            builder
1169                .push_row(
1170                    0,
1171                    &[ColumnInput {
1172                        column: ColumnId::new(1),
1173                        value: ColumnValue::F64(*value),
1174                    }],
1175                )
1176                .expect("valid f64 row");
1177        }
1178        let store = builder.finish().expect("store seals");
1179        let alive = AliveSet::new(store.row_count());
1180        (store, alive)
1181    }
1182
1183    #[test]
1184    fn range_bounds_are_inclusive_or_exclusive() {
1185        let (columns, alive) = one_i64_column(&[1, 2, 3]);
1186        let inclusive = Predicate::Range(RangePredicate {
1187            column: ColumnId::new(1),
1188            lower: Some(RangeBound::inclusive(PredicateValue::I64(1))),
1189            upper: Some(RangeBound::inclusive(PredicateValue::I64(3))),
1190        });
1191        let exclusive = Predicate::Range(RangePredicate {
1192            column: ColumnId::new(1),
1193            lower: Some(RangeBound::exclusive(PredicateValue::I64(1))),
1194            upper: Some(RangeBound::exclusive(PredicateValue::I64(3))),
1195        });
1196
1197        assert_eq!(
1198            evaluate(&inclusive, &columns, &alive)
1199                .expect("valid range")
1200                .iter()
1201                .collect::<Vec<_>>(),
1202            vec![0, 1, 2]
1203        );
1204        assert_eq!(
1205            evaluate(&exclusive, &columns, &alive)
1206                .expect("valid range")
1207                .iter()
1208                .collect::<Vec<_>>(),
1209            vec![1]
1210        );
1211    }
1212
1213    #[test]
1214    fn range_handles_i64_extremes() {
1215        let (columns, alive) = one_i64_column(&[i64::MIN, 0, i64::MAX]);
1216        let predicate = Predicate::Range(RangePredicate {
1217            column: ColumnId::new(1),
1218            lower: Some(RangeBound::inclusive(PredicateValue::I64(i64::MIN))),
1219            upper: Some(RangeBound::inclusive(PredicateValue::I64(i64::MAX))),
1220        });
1221
1222        assert_eq!(
1223            evaluate(&predicate, &columns, &alive)
1224                .expect("valid extreme range")
1225                .iter()
1226                .collect::<Vec<_>>(),
1227            vec![0, 1, 2]
1228        );
1229    }
1230
1231    #[test]
1232    fn f64_nan_never_matches_ranges() {
1233        let (columns, alive) = one_f64_column(&[f64::NAN, 1.0]);
1234        let unbounded = Predicate::Range(RangePredicate {
1235            column: ColumnId::new(1),
1236            lower: None,
1237            upper: None,
1238        });
1239
1240        assert_eq!(
1241            evaluate(&unbounded, &columns, &alive)
1242                .expect("valid f64 range")
1243                .iter()
1244                .collect::<Vec<_>>(),
1245            vec![1]
1246        );
1247    }
1248
1249    #[test]
1250    fn empty_in_set_matches_no_rows() {
1251        let (columns, alive) = one_i64_column(&[1, 2]);
1252        let predicate = Predicate::In {
1253            column: ColumnId::new(1),
1254            values: Vec::new(),
1255        };
1256
1257        assert_eq!(
1258            evaluate(&predicate, &columns, &alive)
1259                .expect("empty set is valid")
1260                .iter()
1261                .collect::<Vec<_>>(),
1262            Vec::<u32>::new()
1263        );
1264    }
1265
1266    #[test]
1267    fn negation_is_bounded_by_alive_set() {
1268        let (columns, mut alive) = one_i64_column(&[1, 2, 3]);
1269        alive.tombstone(1).expect("row exists");
1270        let predicate = Predicate::Not(Box::new(Predicate::Eq {
1271            column: ColumnId::new(1),
1272            value: PredicateValue::I64(1),
1273        }));
1274
1275        assert_eq!(
1276            evaluate(&predicate, &columns, &alive)
1277                .expect("valid negation")
1278                .iter()
1279                .collect::<Vec<_>>(),
1280            vec![2]
1281        );
1282    }
1283
1284    fn is_closed_v1_variant(predicate: &Predicate) -> bool {
1285        match predicate {
1286            Predicate::Eq { .. }
1287            | Predicate::In { .. }
1288            | Predicate::Range(_)
1289            | Predicate::Exists(_)
1290            | Predicate::IsNull(_)
1291            | Predicate::And(_)
1292            | Predicate::Or(_)
1293            | Predicate::Not(_) => true,
1294        }
1295    }
1296
1297    #[test]
1298    fn predicate_ast_is_closed_without_unsupported_variant() {
1299        let column = ColumnId::new(1);
1300        let variants = [
1301            Predicate::Eq {
1302                column,
1303                value: PredicateValue::I64(1),
1304            },
1305            Predicate::In {
1306                column,
1307                values: vec![PredicateValue::I64(1)],
1308            },
1309            Predicate::Range(RangePredicate {
1310                column,
1311                lower: None,
1312                upper: None,
1313            }),
1314            Predicate::Exists(column),
1315            Predicate::IsNull(column),
1316            Predicate::And(Vec::new()),
1317            Predicate::Or(Vec::new()),
1318            Predicate::Not(Box::new(Predicate::Exists(column))),
1319        ];
1320        assert_eq!(
1321            variants
1322                .iter()
1323                .map(is_closed_v1_variant)
1324                .collect::<Vec<_>>(),
1325            vec![true; 8]
1326        );
1327    }
1328
1329    #[test]
1330    fn prop_predicate_eval_equals_naive_row_loop() {
1331        let name = "meta::predicate::prop_predicate_eval_equals_naive_row_loop";
1332        let mut seeded = crate::test_support::seeded_rng(name);
1333        let config = Config {
1334            rng_seed: RngSeed::Fixed(seeded.next_u64()),
1335            ..Config::default()
1336        };
1337        let mut runner = TestRunner::new(config);
1338        let result = runner.run(&any::<GeneratedCase>(), |case| {
1339            let (columns, alive) = build_case(&case);
1340            let actual = evaluate(&case.predicate, &columns, &alive)
1341                .expect("generated predicate is schema-valid")
1342                .iter()
1343                .collect::<Vec<_>>();
1344            let expected = case
1345                .rows
1346                .iter()
1347                .enumerate()
1348                .filter_map(|(position, row)| {
1349                    (row.alive && naive_matches(&case.predicate, row))
1350                        .then(|| u32::try_from(position).ok())
1351                        .flatten()
1352                })
1353                .collect::<Vec<_>>();
1354            prop_assert_eq!(actual, expected);
1355            Ok(())
1356        });
1357        assert!(result.is_ok(), "property result: {result:?}");
1358    }
1359}