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