1use super::alive::AliveSet;
4use super::bitmap::DocBitmap;
5use super::columns::{Column, ColumnStore, DictionaryColumn, RawStringColumn};
6use super::{ColumnId, ColumnType};
7
8#[derive(Clone, Debug, PartialEq)]
10pub enum PredicateValue {
11 U64(u64),
13 I64(i64),
15 F64(f64),
17 Bool(bool),
19 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#[derive(Clone, Debug, PartialEq)]
37pub struct RangeBound {
38 pub value: PredicateValue,
40 pub inclusive: bool,
42}
43
44impl RangeBound {
45 #[must_use]
47 pub fn inclusive(value: PredicateValue) -> Self {
48 Self {
49 value,
50 inclusive: true,
51 }
52 }
53
54 #[must_use]
56 pub fn exclusive(value: PredicateValue) -> Self {
57 Self {
58 value,
59 inclusive: false,
60 }
61 }
62}
63
64#[derive(Clone, Debug, PartialEq)]
66pub struct RangePredicate {
67 pub column: ColumnId,
69 pub lower: Option<RangeBound>,
71 pub upper: Option<RangeBound>,
73}
74
75#[derive(Clone, Debug, PartialEq)]
81pub enum Predicate {
82 Eq {
84 column: ColumnId,
86 value: PredicateValue,
88 },
89 In {
91 column: ColumnId,
93 values: Vec<PredicateValue>,
95 },
96 Range(RangePredicate),
98 Exists(ColumnId),
100 IsNull(ColumnId),
102 And(Vec<Predicate>),
104 Or(Vec<Predicate>),
106 Not(Box<Predicate>),
108}
109
110#[derive(Clone, Debug, Eq, PartialEq)]
112pub enum EvalError {
113 RowCountMismatch {
115 columns: u32,
117 alive: u32,
119 },
120 UnknownColumn(ColumnId),
122 TypeMismatch {
124 column: ColumnId,
126 expected: ColumnType,
128 actual: ColumnType,
130 },
131 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
161pub 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}