1use std::borrow::Cow;
21use std::cmp::Ordering;
22
23use arrow_array::{Array, RecordBatch, cast::AsArray, types::*};
24use arrow_schema::DataType;
25
26use crate::backend::types::{CmpOp, FilterExpr, Scalar, StringMatchKind};
27
28#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum EvalError {
34 UnknownColumn(String),
36 Unsupported(String),
40}
41
42impl std::fmt::Display for EvalError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 EvalError::UnknownColumn(c) => write!(f, "no such column: {c}"),
46 EvalError::Unsupported(why) => write!(f, "cannot evaluate filter: {why}"),
47 }
48 }
49}
50
51impl std::error::Error for EvalError {}
52
53#[derive(Debug, Clone, PartialEq)]
55pub enum Cell<'a> {
56 Null,
57 Bool(bool),
58 Int(i64),
59 UInt(u64),
60 Float(f64),
61 Str(Cow<'a, str>),
65 List(Vec<Cell<'a>>),
67}
68
69pub trait RowAccessor {
74 fn column(&self, name: &str) -> Result<Cell<'_>, EvalError>;
81}
82
83pub struct ArrowRow<'a> {
85 batch: &'a RecordBatch,
86 row: usize,
87}
88
89impl<'a> ArrowRow<'a> {
90 pub fn new(batch: &'a RecordBatch, row: usize) -> Self {
92 Self { batch, row }
93 }
94}
95
96impl RowAccessor for ArrowRow<'_> {
97 fn column(&self, name: &str) -> Result<Cell<'_>, EvalError> {
98 let idx = self
99 .batch
100 .schema()
101 .index_of(name)
102 .map_err(|_| EvalError::UnknownColumn(name.to_string()))?;
103 cell_at(self.batch.column(idx).as_ref(), self.row)
104 }
105}
106
107fn cell_at(array: &dyn Array, row: usize) -> Result<Cell<'_>, EvalError> {
114 if array.is_null(row) {
115 return Ok(Cell::Null);
116 }
117 Ok(match array.data_type() {
118 DataType::Boolean => Cell::Bool(array.as_boolean().value(row)),
119 DataType::Int8 => Cell::Int(array.as_primitive::<Int8Type>().value(row) as i64),
120 DataType::Int16 => Cell::Int(array.as_primitive::<Int16Type>().value(row) as i64),
121 DataType::Int32 => Cell::Int(array.as_primitive::<Int32Type>().value(row) as i64),
122 DataType::Int64 => Cell::Int(array.as_primitive::<Int64Type>().value(row)),
123 DataType::UInt8 => Cell::UInt(array.as_primitive::<UInt8Type>().value(row) as u64),
124 DataType::UInt16 => Cell::UInt(array.as_primitive::<UInt16Type>().value(row) as u64),
125 DataType::UInt32 => Cell::UInt(array.as_primitive::<UInt32Type>().value(row) as u64),
126 DataType::UInt64 => Cell::UInt(array.as_primitive::<UInt64Type>().value(row)),
127 DataType::Float32 => Cell::Float(array.as_primitive::<Float32Type>().value(row) as f64),
128 DataType::Float64 => Cell::Float(array.as_primitive::<Float64Type>().value(row)),
129 DataType::Utf8 => Cell::Str(Cow::Borrowed(array.as_string::<i32>().value(row))),
130 DataType::LargeUtf8 => Cell::Str(Cow::Borrowed(array.as_string::<i64>().value(row))),
131 DataType::List(_) => {
132 let inner = array.as_list::<i32>().value(row);
133 Cell::List(collect_list(inner.as_ref())?)
134 }
135 DataType::LargeList(_) => {
136 let inner = array.as_list::<i64>().value(row);
137 Cell::List(collect_list(inner.as_ref())?)
138 }
139 other => {
140 return Err(EvalError::Unsupported(format!(
141 "no native evaluation for column type {other}"
142 )));
143 }
144 })
145}
146
147fn collect_list(inner: &dyn Array) -> Result<Vec<Cell<'static>>, EvalError> {
153 (0..inner.len())
154 .map(|i| {
155 Ok(match cell_at(inner, i)? {
156 Cell::Null => Cell::Null,
157 Cell::Bool(b) => Cell::Bool(b),
158 Cell::Int(v) => Cell::Int(v),
159 Cell::UInt(v) => Cell::UInt(v),
160 Cell::Float(v) => Cell::Float(v),
161 Cell::Str(s) => Cell::Str(Cow::Owned(s.into_owned())),
162 Cell::List(_) => {
163 return Err(EvalError::Unsupported("nested lists".to_string()));
164 }
165 })
166 })
167 .collect()
168}
169
170impl FilterExpr {
171 pub fn eval(&self, row: &dyn RowAccessor) -> Result<Option<bool>, EvalError> {
184 match self {
185 FilterExpr::Literal(b) => Ok(Some(*b)),
186 FilterExpr::And(parts) => {
187 let mut unknown = false;
190 for p in parts {
191 match p.eval(row)? {
192 Some(false) => return Ok(Some(false)),
193 Some(true) => {}
194 None => unknown = true,
195 }
196 }
197 Ok(if unknown { None } else { Some(true) })
198 }
199 FilterExpr::Or(parts) => {
200 let mut unknown = false;
202 for p in parts {
203 match p.eval(row)? {
204 Some(true) => return Ok(Some(true)),
205 Some(false) => {}
206 None => unknown = true,
207 }
208 }
209 Ok(if unknown { None } else { Some(false) })
210 }
211 FilterExpr::Not(inner) => Ok(inner.eval(row)?.map(|b| !b)),
215 FilterExpr::Compare { column, op, value } => compare(&row.column(column)?, *op, value),
216 FilterExpr::In { column, values } => {
217 let cell = row.column(column)?;
220 let mut unknown = false;
221 for v in values {
222 match compare(&cell, CmpOp::Eq, v)? {
223 Some(true) => return Ok(Some(true)),
224 Some(false) => {}
225 None => unknown = true,
226 }
227 }
228 Ok(if unknown { None } else { Some(false) })
229 }
230 FilterExpr::ArrayContains { column, value } => {
231 let cell = row.column(column)?;
232 list_contains(&cell, value)
233 }
234 FilterExpr::StringMatch {
235 column,
236 kind,
237 pattern,
238 } => match row.column(column)? {
239 Cell::Null => Ok(None),
240 Cell::Str(s) => Ok(Some(match kind {
243 StringMatchKind::Contains => s.contains(pattern.as_str()),
244 StringMatchKind::StartsWith => s.starts_with(pattern.as_str()),
245 StringMatchKind::EndsWith => s.ends_with(pattern.as_str()),
246 })),
247 other => Err(EvalError::Unsupported(format!(
248 "string match on a non-string cell {other:?}"
249 ))),
250 },
251 FilterExpr::IsNull(column) => Ok(Some(matches!(row.column(column)?, Cell::Null))),
253 FilterExpr::IsNotNull(column) => Ok(Some(!matches!(row.column(column)?, Cell::Null))),
254 FilterExpr::Raw(s) => Err(EvalError::Unsupported(format!(
255 "raw backend predicate {s:?} has no native evaluation"
256 ))),
257 }
258 }
259}
260
261fn compare(cell: &Cell<'_>, op: CmpOp, value: &Scalar) -> Result<Option<bool>, EvalError> {
263 if matches!(cell, Cell::Null) || matches!(value, Scalar::Null) {
264 return Ok(None);
265 }
266 Ok(order(cell, value)?.map(|o| match op {
267 CmpOp::Eq => o == Ordering::Equal,
268 CmpOp::NotEq => o != Ordering::Equal,
269 CmpOp::Lt => o == Ordering::Less,
270 CmpOp::LtEq => o != Ordering::Greater,
271 CmpOp::Gt => o == Ordering::Greater,
272 CmpOp::GtEq => o != Ordering::Less,
273 }))
274}
275
276fn list_contains(cell: &Cell<'_>, value: &Scalar) -> Result<Option<bool>, EvalError> {
282 let Cell::List(items) = cell else {
283 if matches!(cell, Cell::Null) {
284 return Ok(None);
285 }
286 return Err(EvalError::Unsupported(
287 "array_contains on a non-list column".to_string(),
288 ));
289 };
290 if matches!(value, Scalar::Null) {
291 return Ok(None);
292 }
293 let mut unknown = false;
294 for item in items {
295 match compare(item, CmpOp::Eq, value)? {
296 Some(true) => return Ok(Some(true)),
297 Some(false) => {}
298 None => unknown = true,
299 }
300 }
301 Ok(if unknown { None } else { Some(false) })
302}
303
304#[derive(Debug, Clone, Copy)]
306enum Num {
307 I(i64),
308 U(u64),
309 F(f64),
310}
311
312fn cell_num(c: &Cell<'_>) -> Option<Num> {
313 match c {
314 Cell::Int(v) => Some(Num::I(*v)),
315 Cell::UInt(v) => Some(Num::U(*v)),
316 Cell::Float(v) => Some(Num::F(*v)),
317 _ => None,
318 }
319}
320
321fn scalar_num(s: &Scalar) -> Option<Num> {
322 match s {
323 Scalar::Int(v) => Some(Num::I(*v)),
324 Scalar::UInt(v) => Some(Num::U(*v)),
325 Scalar::Float(v) => Some(Num::F(*v)),
326 _ => None,
327 }
328}
329
330fn order(cell: &Cell<'_>, value: &Scalar) -> Result<Option<Ordering>, EvalError> {
335 match (cell, value) {
336 (Cell::Bool(a), Scalar::Bool(b)) => Ok(Some(a.cmp(b))),
337 (Cell::Str(a), Scalar::Str(b)) => Ok(Some(a.as_ref().cmp(b.as_str()))),
338 _ => match (cell_num(cell), scalar_num(value)) {
339 (Some(a), Some(b)) => Ok(cmp_num(a, b)),
340 _ => Err(EvalError::Unsupported(format!(
341 "cannot compare cell {cell:?} with literal {value:?}"
342 ))),
343 },
344 }
345}
346
347fn cmp_num(a: Num, b: Num) -> Option<Ordering> {
360 match (a, b) {
361 (Num::I(x), Num::I(y)) => Some(x.cmp(&y)),
362 (Num::U(x), Num::U(y)) => Some(x.cmp(&y)),
363 (Num::I(x), Num::U(y)) => Some(if x < 0 {
364 Ordering::Less
365 } else {
366 (x as u64).cmp(&y)
367 }),
368 (Num::U(x), Num::I(y)) => Some(if y < 0 {
369 Ordering::Greater
370 } else {
371 x.cmp(&(y as u64))
372 }),
373 (Num::F(x), Num::F(y)) => x.partial_cmp(&y),
374 (Num::I(x), Num::F(y)) => cmp_i64_f64(x, y),
375 (Num::F(x), Num::I(y)) => cmp_i64_f64(y, x).map(Ordering::reverse),
376 (Num::U(x), Num::F(y)) => cmp_u64_f64(x, y),
377 (Num::F(x), Num::U(y)) => cmp_u64_f64(y, x).map(Ordering::reverse),
378 }
379}
380
381const TWO_POW_63: f64 = 9_223_372_036_854_775_808.0;
383const TWO_POW_64: f64 = 18_446_744_073_709_551_616.0;
385
386fn cmp_i64_f64(i: i64, f: f64) -> Option<Ordering> {
387 if f.is_nan() {
388 return None;
389 }
390 if f >= TWO_POW_63 {
391 return Some(Ordering::Less);
392 }
393 if f < -TWO_POW_63 {
394 return Some(Ordering::Greater);
395 }
396 let floor = f.floor();
399 Some(match i.cmp(&(floor as i64)) {
400 Ordering::Equal if f > floor => Ordering::Less,
401 o => o,
402 })
403}
404
405fn cmp_u64_f64(u: u64, f: f64) -> Option<Ordering> {
406 if f.is_nan() {
407 return None;
408 }
409 if f < 0.0 {
410 return Some(Ordering::Greater);
411 }
412 if f >= TWO_POW_64 {
413 return Some(Ordering::Less);
414 }
415 let floor = f.floor();
416 Some(match u.cmp(&(floor as u64)) {
417 Ordering::Equal if f > floor => Ordering::Less,
418 o => o,
419 })
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use crate::backend::types::{CmpOp, FilterExpr, Scalar, StringMatchKind, ToSqlError};
426 use arrow_array::builder::{ListBuilder, StringBuilder};
427 use arrow_array::{
428 BooleanArray, Float64Array, Int64Array, RecordBatchIterator, StringArray, UInt64Array,
429 };
430 use arrow_schema::{Field, Schema};
431 use futures::TryStreamExt;
432 use proptest::prelude::*;
433 use std::sync::Arc;
434
435 const INTS: [Option<i64>; 6] = [Some(-2), Some(-1), Some(0), Some(1), Some(2), None];
440 const UINTS: [Option<u64>; 5] = [Some(0), Some(1), Some(2), Some(3), None];
441 const FLOATS: [Option<f64>; 5] = [Some(-1.5), Some(0.0), Some(1.0), Some(2.5), None];
442 const STRS: [Option<&str>; 5] = [Some("a"), Some("b"), Some("it's"), Some("a%b_c"), None];
443 const BOOLS: [Option<bool>; 3] = [Some(true), Some(false), None];
444 const LABEL_SETS: [Option<&[&str]>; 5] = [
445 Some(&["Person"]),
446 Some(&["Person", "Admin"]),
447 Some(&[]),
448 Some(&["it's"]),
449 None,
450 ];
451
452 const ROWS: usize = 120;
453
454 fn schema() -> Arc<Schema> {
455 Arc::new(Schema::new(vec![
456 Field::new("rid", arrow_schema::DataType::UInt64, false),
457 Field::new("i", arrow_schema::DataType::Int64, true),
458 Field::new("u", arrow_schema::DataType::UInt64, true),
459 Field::new("f", arrow_schema::DataType::Float64, true),
460 Field::new("s", arrow_schema::DataType::Utf8, true),
461 Field::new("b", arrow_schema::DataType::Boolean, true),
462 Field::new(
463 "labels",
464 arrow_schema::DataType::List(Arc::new(Field::new(
465 "item",
466 arrow_schema::DataType::Utf8,
467 true,
468 ))),
469 true,
470 ),
471 ]))
472 }
473
474 fn batch() -> RecordBatch {
477 let mut labels = ListBuilder::new(StringBuilder::new());
478 for r in 0..ROWS {
479 match LABEL_SETS[r % LABEL_SETS.len()] {
480 Some(items) => {
481 for it in items {
482 labels.values().append_value(it);
483 }
484 labels.append(true);
485 }
486 None => labels.append(false),
487 }
488 }
489 let labels = labels.finish();
490 let schema = Arc::new(Schema::new(vec![
493 schema().field(0).clone(),
494 schema().field(1).clone(),
495 schema().field(2).clone(),
496 schema().field(3).clone(),
497 schema().field(4).clone(),
498 schema().field(5).clone(),
499 Field::new("labels", labels.data_type().clone(), true),
500 ]));
501 RecordBatch::try_new(
502 schema,
503 vec![
504 Arc::new(UInt64Array::from_iter_values(0..ROWS as u64)),
505 Arc::new(Int64Array::from_iter(
506 (0..ROWS).map(|r| INTS[r % INTS.len()]),
507 )),
508 Arc::new(UInt64Array::from_iter(
509 (0..ROWS).map(|r| UINTS[(r / 2) % UINTS.len()]),
510 )),
511 Arc::new(Float64Array::from_iter(
512 (0..ROWS).map(|r| FLOATS[(r / 3) % FLOATS.len()]),
513 )),
514 Arc::new(StringArray::from_iter(
515 (0..ROWS).map(|r| STRS[(r / 5) % STRS.len()]),
516 )),
517 Arc::new(BooleanArray::from_iter(
518 (0..ROWS).map(|r| BOOLS[(r / 7) % BOOLS.len()]),
519 )),
520 Arc::new(labels),
521 ],
522 )
523 .expect("batch")
524 }
525
526 fn eval_rids(expr: &FilterExpr, batch: &RecordBatch) -> Result<Vec<u64>, EvalError> {
528 let rid = batch.column(0).as_primitive::<UInt64Type>();
529 let mut out = Vec::new();
530 for r in 0..batch.num_rows() {
531 if expr.eval(&ArrowRow::new(batch, r))? == Some(true) {
532 out.push(rid.value(r));
533 }
534 }
535 Ok(out)
536 }
537
538 async fn sql_rids(ds: &lance::Dataset, sql: &str) -> anyhow::Result<Vec<u64>> {
540 let mut scanner = ds.scan();
541 scanner.project(&["rid"])?;
542 scanner.filter(sql)?;
543 let batches: Vec<RecordBatch> = scanner.try_into_stream().await?.try_collect().await?;
544 let mut out: Vec<u64> = batches
545 .iter()
546 .flat_map(|b| {
547 let a = b.column(0).as_primitive::<UInt64Type>();
548 (0..b.num_rows()).map(|i| a.value(i)).collect::<Vec<_>>()
549 })
550 .collect();
551 out.sort_unstable();
552 Ok(out)
553 }
554
555 fn cmp_op() -> impl Strategy<Value = CmpOp> {
556 prop_oneof![
557 Just(CmpOp::Eq),
558 Just(CmpOp::NotEq),
559 Just(CmpOp::Lt),
560 Just(CmpOp::LtEq),
561 Just(CmpOp::Gt),
562 Just(CmpOp::GtEq),
563 ]
564 }
565
566 fn typed_operand() -> impl Strategy<Value = (String, Scalar)> {
572 prop_oneof![
573 (-3i64..4).prop_map(|v| ("i".to_string(), Scalar::Int(v))),
574 (0u64..5).prop_map(|v| ("u".to_string(), Scalar::UInt(v))),
575 prop_oneof![
576 Just(-1.5f64),
577 Just(0.0),
578 Just(1.0),
579 Just(2.5),
580 Just(3.0),
581 Just(0.5)
582 ]
583 .prop_map(|v| ("f".to_string(), Scalar::Float(v))),
584 prop_oneof![Just("a"), Just("b"), Just("it's"), Just("z")]
585 .prop_map(|v| ("s".to_string(), Scalar::Str(v.to_string()))),
586 any::<bool>().prop_map(|v| ("b".to_string(), Scalar::Bool(v))),
587 Just(("i".to_string(), Scalar::Null)),
588 Just(("s".to_string(), Scalar::Null)),
589 ]
590 }
591
592 fn expr_strategy() -> impl Strategy<Value = FilterExpr> {
593 let leaf = prop_oneof![
594 8 => (typed_operand(), cmp_op())
595 .prop_map(|((column, value), op)| FilterExpr::Compare { column, op, value }),
596 3 => proptest::collection::vec(typed_operand(), 0..4).prop_map(|ops| {
597 let column = ops
600 .first()
601 .map(|(c, _)| c.clone())
602 .unwrap_or_else(|| "i".to_string());
603 let values = ops
604 .into_iter()
605 .filter(|(c, _)| *c == column)
606 .map(|(_, v)| v)
607 .collect();
608 FilterExpr::In { column, values }
609 }),
610 3 => prop_oneof![Just("Person"), Just("Admin"), Just("it's"), Just("Ghost")]
611 .prop_map(|v| FilterExpr::ArrayContains {
612 column: "labels".to_string(),
613 value: Scalar::Str(v.to_string()),
614 }),
615 3 => (
619 prop_oneof![
620 Just(StringMatchKind::Contains),
621 Just(StringMatchKind::StartsWith),
622 Just(StringMatchKind::EndsWith),
623 ],
624 prop_oneof![Just("a"), Just("b"), Just("it"), Just("'"), Just("z")],
625 )
626 .prop_map(|(kind, pattern)| FilterExpr::StringMatch {
627 column: "s".to_string(),
628 kind,
629 pattern: pattern.to_string(),
630 }),
631 2 => prop_oneof![Just("i"), Just("s"), Just("b"), Just("labels")].prop_map(|c| {
632 FilterExpr::IsNull(c.to_string())
633 }),
634 2 => prop_oneof![Just("u"), Just("f"), Just("s")].prop_map(|c| {
635 FilterExpr::IsNotNull(c.to_string())
636 }),
637 1 => any::<bool>().prop_map(FilterExpr::Literal),
638 ];
639 leaf.prop_recursive(3, 16, 4, |inner| {
640 prop_oneof![
641 3 => proptest::collection::vec(inner.clone(), 0..4).prop_map(FilterExpr::And),
642 3 => proptest::collection::vec(inner.clone(), 0..4).prop_map(FilterExpr::Or),
643 1 => inner.prop_map(FilterExpr::negate),
644 ]
645 })
646 }
647
648 #[test]
657 fn eval_agrees_with_lance_sql() {
658 let rt = tokio::runtime::Runtime::new().expect("runtime");
659 let tmp = tempfile::tempdir().expect("tempdir");
660 let uri = tmp.path().join("t.lance").to_string_lossy().to_string();
661 let batch = batch();
662 let schema = batch.schema();
663 rt.block_on(async {
664 lance::Dataset::write(
665 RecordBatchIterator::new(vec![Ok(batch.clone())], schema),
666 &uri,
667 None,
668 )
669 .await
670 .expect("write");
671 });
672 let ds = rt.block_on(lance::Dataset::open(&uri)).expect("open");
673
674 proptest!(ProptestConfig::with_cases(200), |(expr in expr_strategy())| {
675 let sql = expr.to_sql().expect("renderable");
676 let native = eval_rids(&expr, &batch).expect("evaluable");
677 let lance = rt.block_on(sql_rids(&ds, &sql)).expect("scan");
678 prop_assert_eq!(
679 native, lance,
680 "\nexpr: {:?}\nsql: {}\n", expr, sql
681 );
682 });
683 }
684
685 #[test]
694 fn in_list_with_null_renders_as_explicit_unknown() {
695 let all_null = FilterExpr::In {
696 column: "i".into(),
697 values: vec![Scalar::Null],
698 };
699 assert_eq!(all_null.to_sql().unwrap(), "CAST(NULL AS BOOLEAN)");
700
701 let mixed = FilterExpr::In {
702 column: "i".into(),
703 values: vec![Scalar::Int(0), Scalar::Null],
704 };
705 assert_eq!(
706 mixed.to_sql().unwrap(),
707 "(i IN (0) OR CAST(NULL AS BOOLEAN))"
708 );
709
710 let plain = FilterExpr::In {
712 column: "i".into(),
713 values: vec![Scalar::Int(0), Scalar::Int(1)],
714 };
715 assert_eq!(plain.to_sql().unwrap(), "i IN (0, 1)");
716 }
717
718 #[test]
721 fn not_and_of_two_in_lists_with_null_agrees_with_lance() {
722 let rt = tokio::runtime::Runtime::new().expect("runtime");
723 let tmp = tempfile::tempdir().expect("tempdir");
724 let uri = tmp.path().join("t.lance").to_string_lossy().to_string();
725 let b = batch();
726 let schema = b.schema();
727 rt.block_on(async {
728 lance::Dataset::write(
729 RecordBatchIterator::new(vec![Ok(b.clone())], schema),
730 &uri,
731 None,
732 )
733 .await
734 .expect("write");
735 });
736 let ds = rt.block_on(lance::Dataset::open(&uri)).expect("open");
737
738 let expr = FilterExpr::Not(Box::new(FilterExpr::And(vec![
739 FilterExpr::In {
740 column: "i".into(),
741 values: vec![Scalar::Null],
742 },
743 FilterExpr::In {
744 column: "i".into(),
745 values: vec![Scalar::Int(0)],
746 },
747 ])));
748
749 let native = eval_rids(&expr, &b).expect("evaluable");
750 let lance = rt
751 .block_on(sql_rids(&ds, &expr.to_sql().expect("renderable")))
752 .expect("scan");
753 assert_eq!(native, lance);
754
755 assert_eq!(native.len(), 80, "expected the non-null, non-zero rows");
758 }
759
760 #[test]
761 fn null_yields_unknown_not_false() {
762 let b = batch();
763 let expr = FilterExpr::Compare {
765 column: "s".to_string(),
766 op: CmpOp::NotEq,
767 value: Scalar::Str("a".to_string()),
768 };
769 let kept = eval_rids(&expr, &b).unwrap();
770 let s = b.column(4).as_string::<i32>();
771 for r in 0..b.num_rows() {
772 if s.is_null(r) {
773 assert!(!kept.contains(&(r as u64)), "NULL row {r} must not survive");
774 }
775 }
776 assert!(!kept.is_empty(), "non-NULL non-'a' rows must survive");
777
778 let with_null = FilterExpr::all([
780 FilterExpr::Compare {
781 column: "i".to_string(),
782 op: CmpOp::Eq,
783 value: Scalar::Null,
784 },
785 FilterExpr::Literal(true),
786 ]);
787 assert!(eval_rids(&with_null, &b).unwrap().is_empty());
788 }
789
790 #[test]
791 fn empty_in_is_false_and_renders() {
792 let expr = FilterExpr::In {
793 column: "i".to_string(),
794 values: vec![],
795 };
796 assert_eq!(expr.to_sql().unwrap(), "false");
797 assert!(eval_rids(&expr, &batch()).unwrap().is_empty());
798 }
799
800 #[test]
801 fn in_with_null_is_unknown_only_on_miss() {
802 let b = batch();
803 let hit = FilterExpr::In {
804 column: "i".to_string(),
805 values: vec![Scalar::Int(1), Scalar::Null],
806 };
807 assert!(!eval_rids(&hit, &b).unwrap().is_empty());
809
810 let miss = FilterExpr::In {
811 column: "i".to_string(),
812 values: vec![Scalar::Int(99), Scalar::Null],
813 };
814 assert!(eval_rids(&miss, &b).unwrap().is_empty());
815 }
816
817 #[test]
818 fn mixed_numeric_compares_by_value_not_representation() {
819 assert_ne!(Scalar::Int(1), Scalar::Float(1.0));
822 assert_eq!(cmp_num(Num::I(1), Num::F(1.0)), Some(Ordering::Equal));
823 assert_eq!(cmp_num(Num::U(1), Num::F(1.5)), Some(Ordering::Less));
824 assert_eq!(cmp_num(Num::I(-1), Num::U(0)), Some(Ordering::Less));
825 }
826
827 #[test]
828 fn u64_sentinels_survive_the_int_domain() {
829 assert_eq!(
831 cmp_num(Num::U(u64::MAX), Num::I(-1)),
832 Some(Ordering::Greater)
833 );
834 assert_eq!(
835 FilterExpr::version_at_most(u64::MAX).to_sql().unwrap(),
836 "_version <= 18446744073709551615"
837 );
838 }
839
840 #[test]
841 fn large_integers_compare_exactly_against_floats() {
842 let big = (1u64 << 53) + 1;
844 assert_eq!(
845 cmp_num(Num::U(big), Num::F((1u64 << 53) as f64)),
846 Some(Ordering::Greater)
847 );
848 assert_eq!(
849 cmp_num(Num::I(i64::MAX), Num::F(f64::MAX)),
850 Some(Ordering::Less)
851 );
852 assert_eq!(
853 cmp_num(Num::I(i64::MIN), Num::F(f64::NEG_INFINITY)),
854 Some(Ordering::Greater)
855 );
856 }
857
858 #[test]
859 fn nan_is_unordered_so_every_comparison_is_unknown() {
860 assert_eq!(cmp_num(Num::F(f64::NAN), Num::F(1.0)), None);
861 assert_eq!(cmp_num(Num::I(1), Num::F(f64::NAN)), None);
862 let unknown = compare(&Cell::Float(f64::NAN), CmpOp::Eq, &Scalar::Float(f64::NAN));
863 assert_eq!(unknown.unwrap(), None);
864 }
865
866 #[test]
867 fn raw_and_cross_domain_refuse_loudly() {
868 let b = batch();
869 let raw = FilterExpr::Raw("i > 0".to_string());
870 assert!(matches!(
871 raw.eval(&ArrowRow::new(&b, 0)),
872 Err(EvalError::Unsupported(_))
873 ));
874
875 let crossed = FilterExpr::Compare {
876 column: "i".to_string(),
877 op: CmpOp::Eq,
878 value: Scalar::Str("a".to_string()),
879 };
880 assert!(matches!(
881 crossed.eval(&ArrowRow::new(&b, 0)),
882 Err(EvalError::Unsupported(_))
883 ));
884
885 let missing = FilterExpr::Compare {
886 column: "nope".to_string(),
887 op: CmpOp::Eq,
888 value: Scalar::Int(1),
889 };
890 assert!(matches!(
891 missing.eval(&ArrowRow::new(&b, 0)),
892 Err(EvalError::UnknownColumn(_))
893 ));
894 }
895
896 #[test]
897 fn apostrophes_round_trip_through_the_single_escaper() {
898 let expr = FilterExpr::Compare {
899 column: "s".to_string(),
900 op: CmpOp::Eq,
901 value: Scalar::Str("it's".to_string()),
902 };
903 assert_eq!(expr.to_sql().unwrap(), "s = 'it''s'");
904 assert!(!eval_rids(&expr, &batch()).unwrap().is_empty());
905 }
906
907 #[test]
908 fn wildcard_pattern_refuses_in_sql_but_evaluates_exactly() {
909 let b = batch();
910 let expr = FilterExpr::StringMatch {
913 column: "s".to_string(),
914 kind: StringMatchKind::Contains,
915 pattern: "%b_".to_string(),
916 };
917 assert!(matches!(expr.to_sql(), Err(ToSqlError::Unsupported(_))));
918
919 let kept = eval_rids(&expr, &b).unwrap();
921 assert!(
922 !kept.is_empty(),
923 "the literal substring `%b_` occurs in `a%b_c` rows"
924 );
925 let s = b.column(4).as_string::<i32>();
926 for r in 0..b.num_rows() {
927 let expect = !s.is_null(r) && s.value(r).contains("%b_");
928 assert_eq!(kept.contains(&(r as u64)), expect, "row {r}");
929 }
930 }
931
932 #[test]
933 fn sql_pushable_drops_an_or_whole_never_one_branch() {
934 let bad = FilterExpr::StringMatch {
935 column: "s".to_string(),
936 kind: StringMatchKind::Contains,
937 pattern: "%".to_string(),
938 };
939 let good = FilterExpr::equals("i", Scalar::Int(1));
940
941 let anded = FilterExpr::And(vec![good.clone(), bad.clone()]);
944 assert_eq!(anded.sql_pushable(), good);
945
946 let ored = FilterExpr::Or(vec![good.clone(), bad.clone()]);
949 assert_eq!(ored.sql_pushable(), FilterExpr::Literal(true));
950
951 let nested = FilterExpr::And(vec![good.clone(), ored]);
953 assert_eq!(nested.sql_pushable(), good);
954
955 let fine = FilterExpr::And(vec![good.clone(), FilterExpr::IsNull("s".to_string())]);
957 assert_eq!(fine.sql_pushable(), fine);
958 }
959
960 #[test]
961 fn scalar_from_value_excludes_null_so_producers_keep_bailing() {
962 use uni_common::Value;
963 assert_eq!(
964 Scalar::from_value(&Value::String("x".into())),
965 Some(Scalar::Str("x".into()))
966 );
967 assert_eq!(Scalar::from_value(&Value::Int(1)), Some(Scalar::Int(1)));
968 assert_eq!(
969 Scalar::from_value(&Value::Bool(true)),
970 Some(Scalar::Bool(true))
971 );
972 assert_eq!(Scalar::from_value(&Value::Null), None);
975 assert_eq!(Scalar::from_value(&Value::List(vec![])), None);
976 }
977
978 #[test]
986 fn rendered_compare_narrows_a_real_scan() {
987 let rt = tokio::runtime::Runtime::new().unwrap();
988 let tmp = tempfile::tempdir().unwrap();
989 let uri = tmp.path().join("c.lance").to_string_lossy().to_string();
990 let schema = Arc::new(Schema::new(vec![
991 Field::new("rid", arrow_schema::DataType::UInt64, false),
992 Field::new("createdAt", arrow_schema::DataType::Int64, true),
993 ]));
994 let b = RecordBatch::try_new(
995 schema.clone(),
996 vec![
997 Arc::new(UInt64Array::from_iter_values(0..5u64)),
998 Arc::new(Int64Array::from_iter_values(1..6i64)),
999 ],
1000 )
1001 .unwrap();
1002 rt.block_on(async {
1003 lance::Dataset::write(RecordBatchIterator::new(vec![Ok(b)], schema), &uri, None)
1004 .await
1005 .unwrap();
1006 });
1007 let ds = rt.block_on(lance::Dataset::open(&uri)).unwrap();
1008
1009 let range = FilterExpr::all([
1010 FilterExpr::compare("createdAt", CmpOp::GtEq, Scalar::Int(2)),
1011 FilterExpr::compare("createdAt", CmpOp::LtEq, Scalar::Int(4)),
1012 ]);
1013 let sql = range.to_sql().unwrap();
1014 assert!(!sql.contains('"'), "column must be bare: {sql}");
1015 assert_eq!(rt.block_on(sql_rids(&ds, &sql)).unwrap(), vec![1, 2, 3]);
1016
1017 let quoted = "\"createdAt\" >= 2 AND \"createdAt\" <= 4";
1022 assert_eq!(
1023 rt.block_on(sql_rids(&ds, quoted)).unwrap(),
1024 Vec::<u64>::new(),
1025 "a double-quoted column is a string literal, not an identifier"
1026 );
1027 }
1028}