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 Id128(crate::ingest::DocId),
15 I64(i64),
17 F64(f64),
19 Bool(bool),
21 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#[derive(Clone, Debug, PartialEq)]
40pub struct RangeBound {
41 pub value: PredicateValue,
43 pub inclusive: bool,
45}
46
47impl RangeBound {
48 #[must_use]
50 pub fn inclusive(value: PredicateValue) -> Self {
51 Self {
52 value,
53 inclusive: true,
54 }
55 }
56
57 #[must_use]
59 pub fn exclusive(value: PredicateValue) -> Self {
60 Self {
61 value,
62 inclusive: false,
63 }
64 }
65}
66
67#[derive(Clone, Debug, PartialEq)]
69pub struct RangePredicate {
70 pub column: ColumnId,
72 pub lower: Option<RangeBound>,
74 pub upper: Option<RangeBound>,
76}
77
78#[derive(Clone, Debug, PartialEq)]
84pub enum Predicate {
85 Eq {
87 column: ColumnId,
89 value: PredicateValue,
91 },
92 In {
94 column: ColumnId,
96 values: Vec<PredicateValue>,
98 },
99 Range(RangePredicate),
101 Exists(ColumnId),
103 IsNull(ColumnId),
105 And(Vec<Predicate>),
107 Or(Vec<Predicate>),
109 Not(Box<Predicate>),
111}
112
113#[derive(Clone, Debug, Eq, PartialEq)]
115pub enum EvalError {
116 RowCountMismatch {
118 columns: u32,
120 alive: u32,
122 },
123 UnknownColumn(ColumnId),
125 TypeMismatch {
127 column: ColumnId,
129 expected: ColumnType,
131 actual: ColumnType,
133 },
134 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
164pub 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}