1use std::cmp::Ordering;
11
12use crate::expr::EvalError;
13use crate::msgpack_scan::field::extract_field;
14use crate::msgpack_scan::index::FieldIndex;
15use crate::msgpack_scan::reader::{
16 array_header, map_header, read_null, read_str, read_value, skip_value,
17};
18use crate::scan_filter::like::sql_like_match;
19use crate::scan_filter::types::column_compare;
20use crate::scan_filter::{FilterOp, ScanFilter};
21
22impl ScanFilter {
23 pub fn all_match_binary(group: &[ScanFilter], doc: &[u8]) -> Result<bool, EvalError> {
33 for f in group {
34 if !f.matches_binary(doc)? {
35 return Ok(false);
36 }
37 }
38 Ok(true)
39 }
40
41 pub fn all_match_binary_indexed(
43 group: &[ScanFilter],
44 doc: &[u8],
45 idx: &FieldIndex,
46 ) -> Result<bool, EvalError> {
47 for f in group {
48 if !f.matches_binary_indexed(doc, idx)? {
49 return Ok(false);
50 }
51 }
52 Ok(true)
53 }
54}
55
56impl ScanFilter {
57 pub fn matches_binary(&self, doc: &[u8]) -> Result<bool, EvalError> {
67 match self.op {
68 FilterOp::MatchAll | FilterOp::Exists | FilterOp::NotExists => return Ok(true),
69 FilterOp::Or => {
70 for clause in &self.clauses {
71 if Self::all_match_binary(clause, doc)? {
72 return Ok(true);
73 }
74 }
75 return Ok(false);
76 }
77 FilterOp::Expr => {
78 return match (self.expr.as_ref(), nodedb_types::value_from_msgpack(doc)) {
79 (Some(expr), Ok(value)) => Ok(crate::value_ops::is_truthy(&expr.eval(&value)?)),
80 _ => Ok(false),
81 };
82 }
83 FilterOp::Eq
84 | FilterOp::Ne
85 | FilterOp::Gt
86 | FilterOp::Gte
87 | FilterOp::Lt
88 | FilterOp::Lte
89 | FilterOp::Contains
90 | FilterOp::Like
91 | FilterOp::NotLike
92 | FilterOp::Ilike
93 | FilterOp::NotIlike
94 | FilterOp::In
95 | FilterOp::NotIn
96 | FilterOp::IsNull
97 | FilterOp::IsNotNull
98 | FilterOp::ArrayContains
99 | FilterOp::ArrayContainsAll
100 | FilterOp::ArrayOverlap
101 | FilterOp::GtColumn
102 | FilterOp::GteColumn
103 | FilterOp::LtColumn
104 | FilterOp::LteColumn
105 | FilterOp::EqColumn
106 | FilterOp::NeColumn => {}
107 }
108
109 let (start, end) = match extract_field(doc, 0, &self.field) {
110 Some(r) => r,
111 None => {
112 let suffix = format!(".{}", self.field);
114 match find_field_by_suffix(doc, &suffix) {
115 Some(r) => r,
116 None => return Ok(self.op == FilterOp::IsNull),
117 }
118 }
119 };
120
121 Ok(eval_op(self, doc, start, end))
122 }
123
124 pub fn matches_binary_indexed(&self, doc: &[u8], idx: &FieldIndex) -> Result<bool, EvalError> {
128 match self.op {
129 FilterOp::MatchAll | FilterOp::Exists | FilterOp::NotExists => return Ok(true),
130 FilterOp::Or => {
131 for clause in &self.clauses {
132 if Self::all_match_binary_indexed(clause, doc, idx)? {
133 return Ok(true);
134 }
135 }
136 return Ok(false);
137 }
138 FilterOp::Expr => {
139 return match (self.expr.as_ref(), nodedb_types::value_from_msgpack(doc)) {
140 (Some(expr), Ok(value)) => Ok(crate::value_ops::is_truthy(&expr.eval(&value)?)),
141 _ => Ok(false),
142 };
143 }
144 FilterOp::Eq
145 | FilterOp::Ne
146 | FilterOp::Gt
147 | FilterOp::Gte
148 | FilterOp::Lt
149 | FilterOp::Lte
150 | FilterOp::Contains
151 | FilterOp::Like
152 | FilterOp::NotLike
153 | FilterOp::Ilike
154 | FilterOp::NotIlike
155 | FilterOp::In
156 | FilterOp::NotIn
157 | FilterOp::IsNull
158 | FilterOp::IsNotNull
159 | FilterOp::ArrayContains
160 | FilterOp::ArrayContainsAll
161 | FilterOp::ArrayOverlap
162 | FilterOp::GtColumn
163 | FilterOp::GteColumn
164 | FilterOp::LtColumn
165 | FilterOp::LteColumn
166 | FilterOp::EqColumn
167 | FilterOp::NeColumn => {}
168 }
169
170 let (start, end) = match idx
174 .get(&self.field)
175 .or_else(|| find_field_by_suffix(doc, &format!(".{}", self.field)))
176 {
177 Some(r) => r,
178 None => return Ok(self.op == FilterOp::IsNull),
179 };
180
181 Ok(eval_op(self, doc, start, end))
182 }
183}
184
185fn eval_op(filter: &ScanFilter, doc: &[u8], start: usize, _end: usize) -> bool {
187 match filter.op {
188 FilterOp::IsNull => read_null(doc, start),
189 FilterOp::IsNotNull => !read_null(doc, start),
190 FilterOp::Eq => eq_value(doc, start, &filter.value),
191 FilterOp::Ne => !eq_value(doc, start, &filter.value),
192 FilterOp::Gt => cmp_value(doc, start, &filter.value) == Some(Ordering::Greater),
193 FilterOp::Gte => matches!(
194 cmp_value(doc, start, &filter.value),
195 Some(Ordering::Greater | Ordering::Equal)
196 ),
197 FilterOp::Lt => cmp_value(doc, start, &filter.value) == Some(Ordering::Less),
198 FilterOp::Lte => matches!(
199 cmp_value(doc, start, &filter.value),
200 Some(Ordering::Less | Ordering::Equal)
201 ),
202 FilterOp::Contains => {
203 if let (Some(s), Some(pattern)) = (read_str(doc, start), filter.value.as_str()) {
204 s.contains(pattern)
205 } else {
206 false
207 }
208 }
209 FilterOp::Like => str_match(doc, start, &filter.value, false, false),
210 FilterOp::NotLike => str_match(doc, start, &filter.value, false, true),
211 FilterOp::Ilike => str_match(doc, start, &filter.value, true, false),
212 FilterOp::NotIlike => str_match(doc, start, &filter.value, true, true),
213 FilterOp::In => {
214 if let Some(mut iter) = filter.value.as_array_iter() {
215 iter.any(|v| eq_value(doc, start, v))
216 } else {
217 false
218 }
219 }
220 FilterOp::NotIn => {
221 if let Some(mut iter) = filter.value.as_array_iter() {
222 !iter.any(|v| eq_value(doc, start, v))
223 } else {
224 true
225 }
226 }
227 FilterOp::ArrayContains => array_any(doc, start, |elem_start| {
228 eq_value(doc, elem_start, &filter.value)
229 }),
230 FilterOp::ArrayContainsAll => {
231 if let Some(mut needles) = filter.value.as_array_iter() {
232 needles.all(|needle| {
233 array_any(doc, start, |elem_start| eq_value(doc, elem_start, needle))
234 })
235 } else {
236 false
237 }
238 }
239 FilterOp::ArrayOverlap => {
240 if let Some(mut needles) = filter.value.as_array_iter() {
241 needles.any(|needle| {
242 array_any(doc, start, |elem_start| eq_value(doc, elem_start, needle))
243 })
244 } else {
245 false
246 }
247 }
248 FilterOp::GtColumn
250 | FilterOp::GteColumn
251 | FilterOp::LtColumn
252 | FilterOp::LteColumn
253 | FilterOp::EqColumn
254 | FilterOp::NeColumn => {
255 let other_col = match &filter.value {
256 nodedb_types::Value::String(s) => s.as_str(),
257 _ => return false,
258 };
259 let other_range = extract_field(doc, 0, other_col).or_else(|| {
261 let suffix = format!(".{other_col}");
262 find_field_by_suffix(doc, &suffix)
263 });
264 let Some((other_start, _)) = other_range else {
265 return false;
266 };
267 let left = read_value(doc, start).unwrap_or(nodedb_types::Value::Null);
269 let right = read_value(doc, other_start).unwrap_or(nodedb_types::Value::Null);
270 column_compare(filter.op, &left, &right)
271 }
272 FilterOp::MatchAll | FilterOp::Exists | FilterOp::NotExists => true,
274 FilterOp::Or | FilterOp::Expr => false,
275 }
276}
277
278fn find_field_by_suffix(doc: &[u8], suffix: &str) -> Option<(usize, usize)> {
280 let (count, mut pos) = map_header(doc, 0)?;
281 for _ in 0..count {
282 let key = read_str(doc, pos);
283 let key_end = skip_value(doc, pos)?;
284 let val_start = key_end;
285 let val_end = skip_value(doc, val_start)?;
286 if let Some(k) = key
287 && k.ends_with(suffix)
288 {
289 return Some((val_start, val_end));
290 }
291 pos = val_end;
292 }
293 None
294}
295
296#[inline]
301fn eq_value(buf: &[u8], offset: usize, filter_val: &nodedb_types::Value) -> bool {
302 if read_null(buf, offset) {
303 return filter_val.is_null();
304 }
305 match read_value(buf, offset) {
306 Some(field_val) => filter_val.eq_coerced(&field_val),
307 None => false,
308 }
309}
310
311#[inline]
318fn cmp_value(buf: &[u8], offset: usize, filter_val: &nodedb_types::Value) -> Option<Ordering> {
319 read_value(buf, offset)?.partial_cmp_coerced(filter_val)
320}
321
322#[inline]
324fn str_match(
325 buf: &[u8],
326 offset: usize,
327 pattern_val: &nodedb_types::Value,
328 icase: bool,
329 negate: bool,
330) -> bool {
331 let result = if let (Some(s), Some(pattern)) = (read_str(buf, offset), pattern_val.as_str()) {
332 sql_like_match(s, pattern, icase)
333 } else {
334 false
335 };
336 if negate { !result } else { result }
337}
338
339fn array_any(buf: &[u8], start: usize, mut pred: impl FnMut(usize) -> bool) -> bool {
341 let Some((count, mut pos)) = array_header(buf, start) else {
342 return false;
343 };
344 for _ in 0..count {
345 if pred(pos) {
346 return true;
347 }
348 let Some(next) = skip_value(buf, pos) else {
349 return false;
350 };
351 pos = next;
352 }
353 false
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use serde_json::json;
360
361 fn encode(v: &serde_json::Value) -> Vec<u8> {
362 nodedb_types::json_msgpack::json_to_msgpack(v).expect("encode")
363 }
364
365 fn filter(field: &str, op: &str, value: nodedb_types::Value) -> ScanFilter {
366 ScanFilter {
367 field: field.into(),
368 op: FilterOp::parse(op).expect(op),
369 value,
370 clauses: vec![],
371 expr: None,
372 }
373 }
374
375 #[test]
376 fn eq_integer() {
377 let doc = encode(&json!({"age": 25}));
378 assert!(
379 filter("age", "eq", nodedb_types::Value::Integer(25))
380 .matches_binary(&doc)
381 .unwrap()
382 );
383 assert!(
384 !filter("age", "eq", nodedb_types::Value::Integer(30))
385 .matches_binary(&doc)
386 .unwrap()
387 );
388 }
389
390 #[test]
393 fn instant_cell_compares_by_micros() {
394 use nodedb_types::{InstantKind, NdbDateTime, Value};
395 const MICROS: i64 = 1_583_402_400_000_000;
396 let mut doc = Vec::new();
397 crate::msgpack_scan::write_map_header(&mut doc, 1);
398 crate::msgpack_scan::write_kv_instant(&mut doc, "ts", InstantKind::Naive, MICROS);
399
400 let same = Value::NaiveDateTime(NdbDateTime::from_micros(MICROS));
401 let later = Value::NaiveDateTime(NdbDateTime::from_micros(MICROS + 1));
402 assert!(
403 filter("ts", "eq", same.clone())
404 .matches_binary(&doc)
405 .unwrap()
406 );
407 assert!(
408 !filter("ts", "eq", later.clone())
409 .matches_binary(&doc)
410 .unwrap()
411 );
412 assert!(filter("ts", "lt", later).matches_binary(&doc).unwrap());
413 assert!(filter("ts", "gte", same).matches_binary(&doc).unwrap());
414 assert!(
415 filter("ts", "eq", Value::String("2020-03-05T10:00:00Z".into()))
416 .matches_binary(&doc)
417 .unwrap()
418 );
419 assert!(
420 filter("ts", "gt", Value::String("2020-03-05 09:00:00".into()))
421 .matches_binary(&doc)
422 .unwrap()
423 );
424 let idx = FieldIndex::build(&doc, 0).unwrap_or_else(FieldIndex::empty);
425 assert!(
426 filter(
427 "ts",
428 "eq",
429 Value::NaiveDateTime(NdbDateTime::from_micros(MICROS))
430 )
431 .matches_binary_indexed(&doc, &idx)
432 .unwrap()
433 );
434 }
435
436 #[test]
437 fn eq_coerces_string_to_integer() {
438 let doc = encode(&json!({"age": 25}));
439 assert!(
440 filter("age", "eq", nodedb_types::Value::String("25".into()))
441 .matches_binary(&doc)
442 .unwrap()
443 );
444 }
445
446 #[test]
447 fn gt_coerces_string_to_integer() {
448 let doc = encode(&json!({"score": "90"}));
449 assert!(
450 filter("score", "gt", nodedb_types::Value::Integer(80))
451 .matches_binary(&doc)
452 .unwrap()
453 );
454 }
455
456 #[test]
457 fn eq_string() {
458 let doc = encode(&json!({"name": "alice"}));
459 assert!(
460 filter("name", "eq", nodedb_types::Value::String("alice".into()))
461 .matches_binary(&doc)
462 .unwrap()
463 );
464 }
465
466 #[test]
467 fn eq_coercion_int_vs_string() {
468 let doc = encode(&json!({"age": 25}));
469 assert!(
470 filter("age", "eq", nodedb_types::Value::String("25".into()))
471 .matches_binary(&doc)
472 .unwrap()
473 );
474 }
475
476 #[test]
477 fn eq_coercion_string_vs_int() {
478 let doc = encode(&json!({"score": "90"}));
479 assert!(
480 filter("score", "eq", nodedb_types::Value::Integer(90))
481 .matches_binary(&doc)
482 .unwrap()
483 );
484 }
485
486 #[test]
487 fn ne() {
488 let doc = encode(&json!({"x": 1}));
489 assert!(
490 filter("x", "ne", nodedb_types::Value::Integer(2))
491 .matches_binary(&doc)
492 .unwrap()
493 );
494 assert!(
495 !filter("x", "ne", nodedb_types::Value::Integer(1))
496 .matches_binary(&doc)
497 .unwrap()
498 );
499 }
500
501 #[test]
502 fn gt_lt() {
503 let doc = encode(&json!({"v": 10}));
504 assert!(
505 filter("v", "gt", nodedb_types::Value::Integer(5))
506 .matches_binary(&doc)
507 .unwrap()
508 );
509 assert!(
510 !filter("v", "gt", nodedb_types::Value::Integer(15))
511 .matches_binary(&doc)
512 .unwrap()
513 );
514 assert!(
515 filter("v", "lt", nodedb_types::Value::Integer(15))
516 .matches_binary(&doc)
517 .unwrap()
518 );
519 assert!(
520 !filter("v", "lt", nodedb_types::Value::Integer(5))
521 .matches_binary(&doc)
522 .unwrap()
523 );
524 }
525
526 #[test]
527 fn gte_lte() {
528 let doc = encode(&json!({"v": 10}));
529 assert!(
530 filter("v", "gte", nodedb_types::Value::Integer(10))
531 .matches_binary(&doc)
532 .unwrap()
533 );
534 assert!(
535 filter("v", "gte", nodedb_types::Value::Integer(5))
536 .matches_binary(&doc)
537 .unwrap()
538 );
539 assert!(
540 !filter("v", "gte", nodedb_types::Value::Integer(15))
541 .matches_binary(&doc)
542 .unwrap()
543 );
544 assert!(
545 filter("v", "lte", nodedb_types::Value::Integer(10))
546 .matches_binary(&doc)
547 .unwrap()
548 );
549 }
550
551 #[test]
552 fn is_null_not_null() {
553 let doc = encode(&json!({"a": null, "b": 1}));
554 assert!(
555 filter("a", "is_null", nodedb_types::Value::Null)
556 .matches_binary(&doc)
557 .unwrap()
558 );
559 assert!(
560 !filter("b", "is_null", nodedb_types::Value::Null)
561 .matches_binary(&doc)
562 .unwrap()
563 );
564 assert!(
565 filter("b", "is_not_null", nodedb_types::Value::Null)
566 .matches_binary(&doc)
567 .unwrap()
568 );
569 }
570
571 #[test]
572 fn missing_field_is_null() {
573 let doc = encode(&json!({"x": 1}));
574 assert!(
575 filter("missing", "is_null", nodedb_types::Value::Null)
576 .matches_binary(&doc)
577 .unwrap()
578 );
579 }
580
581 #[test]
582 fn contains_str() {
583 let doc = encode(&json!({"msg": "hello world"}));
584 assert!(
585 filter(
586 "msg",
587 "contains",
588 nodedb_types::Value::String("world".into())
589 )
590 .matches_binary(&doc)
591 .unwrap()
592 );
593 }
594
595 #[test]
596 fn like_ilike() {
597 let doc = encode(&json!({"name": "Alice"}));
598 assert!(
599 filter("name", "like", nodedb_types::Value::String("Ali%".into()))
600 .matches_binary(&doc)
601 .unwrap()
602 );
603 assert!(
604 !filter("name", "like", nodedb_types::Value::String("ali%".into()))
605 .matches_binary(&doc)
606 .unwrap()
607 );
608 assert!(
609 filter("name", "ilike", nodedb_types::Value::String("ali%".into()))
610 .matches_binary(&doc)
611 .unwrap()
612 );
613 assert!(
614 filter(
615 "name",
616 "not_like",
617 nodedb_types::Value::String("Bob%".into())
618 )
619 .matches_binary(&doc)
620 .unwrap()
621 );
622 }
623
624 #[test]
625 fn in_not_in() {
626 let doc = encode(&json!({"status": "active"}));
627 let vals = nodedb_types::Value::Array(vec![
628 nodedb_types::Value::String("active".into()),
629 nodedb_types::Value::String("pending".into()),
630 ]);
631 assert!(
632 ScanFilter {
633 field: "status".into(),
634 op: FilterOp::In,
635 value: vals.clone(),
636 clauses: vec![],
637 expr: None
638 }
639 .matches_binary(&doc)
640 .unwrap()
641 );
642
643 let doc2 = encode(&json!({"status": "deleted"}));
644 assert!(
645 ScanFilter {
646 field: "status".into(),
647 op: FilterOp::NotIn,
648 value: vals,
649 clauses: vec![],
650 expr: None
651 }
652 .matches_binary(&doc2)
653 .unwrap()
654 );
655 }
656
657 #[test]
658 fn array_contains() {
659 let doc = encode(&json!({"tags": ["rust", "db", "fast"]}));
660 assert!(
661 filter(
662 "tags",
663 "array_contains",
664 nodedb_types::Value::String("rust".into())
665 )
666 .matches_binary(&doc)
667 .unwrap()
668 );
669 assert!(
670 !filter(
671 "tags",
672 "array_contains",
673 nodedb_types::Value::String("slow".into())
674 )
675 .matches_binary(&doc)
676 .unwrap()
677 );
678 }
679
680 #[test]
681 fn array_contains_all() {
682 let doc = encode(&json!({"tags": ["a", "b", "c"]}));
683 let needles = nodedb_types::Value::Array(vec![
684 nodedb_types::Value::String("a".into()),
685 nodedb_types::Value::String("c".into()),
686 ]);
687 assert!(
688 ScanFilter {
689 field: "tags".into(),
690 op: FilterOp::ArrayContainsAll,
691 value: needles,
692 clauses: vec![],
693 expr: None
694 }
695 .matches_binary(&doc)
696 .unwrap()
697 );
698 }
699
700 #[test]
701 fn array_overlap() {
702 let doc = encode(&json!({"tags": ["x", "y"]}));
703 let needles = nodedb_types::Value::Array(vec![
704 nodedb_types::Value::String("y".into()),
705 nodedb_types::Value::String("z".into()),
706 ]);
707 assert!(
708 ScanFilter {
709 field: "tags".into(),
710 op: FilterOp::ArrayOverlap,
711 value: needles,
712 clauses: vec![],
713 expr: None
714 }
715 .matches_binary(&doc)
716 .unwrap()
717 );
718 }
719
720 #[test]
721 fn or_clauses() {
722 let doc = encode(&json!({"x": 5}));
723 let f = ScanFilter {
724 field: String::new(),
725 op: FilterOp::Or,
726 value: nodedb_types::Value::Null,
727 clauses: vec![
728 vec![filter("x", "eq", nodedb_types::Value::Integer(10))],
729 vec![filter("x", "eq", nodedb_types::Value::Integer(5))],
730 ],
731 expr: None,
732 };
733 assert!(f.matches_binary(&doc).unwrap());
734 }
735
736 #[test]
737 fn match_all() {
738 let doc = encode(&json!({"any": "thing"}));
739 assert!(
740 filter("", "match_all", nodedb_types::Value::Null)
741 .matches_binary(&doc)
742 .unwrap()
743 );
744 }
745
746 #[test]
747 fn float_comparison() {
748 let doc = encode(&json!({"temp": 36.6}));
749 assert!(
750 filter("temp", "gt", nodedb_types::Value::Float(30.0))
751 .matches_binary(&doc)
752 .unwrap()
753 );
754 assert!(
755 filter("temp", "lt", nodedb_types::Value::Float(40.0))
756 .matches_binary(&doc)
757 .unwrap()
758 );
759 }
760
761 #[test]
762 fn bool_eq() {
763 let doc = encode(&json!({"active": true}));
764 assert!(
765 filter("active", "eq", nodedb_types::Value::Bool(true))
766 .matches_binary(&doc)
767 .unwrap()
768 );
769 assert!(
770 !filter("active", "eq", nodedb_types::Value::Bool(false))
771 .matches_binary(&doc)
772 .unwrap()
773 );
774 }
775
776 #[test]
777 fn gt_coercion_string_field() {
778 let doc = encode(&json!({"score": "90"}));
779 assert!(
780 filter("score", "gt", nodedb_types::Value::Integer(80))
781 .matches_binary(&doc)
782 .unwrap()
783 );
784 }
785
786 #[test]
787 fn timestamp_upper_bound_matches() {
788 let doc = encode(&json!({"t": "2026-07-02T13:00:00.000000Z"}));
789 assert!(
790 filter(
791 "t",
792 "lte",
793 nodedb_types::Value::String("2026-07-02 14:00:00".into())
794 )
795 .matches_binary(&doc)
796 .unwrap()
797 );
798 assert!(
799 filter(
800 "t",
801 "lt",
802 nodedb_types::Value::String("2026-07-02 14:00:00".into())
803 )
804 .matches_binary(&doc)
805 .unwrap()
806 );
807 assert!(
808 filter(
809 "t",
810 "gte",
811 nodedb_types::Value::String("2026-07-02 12:00:00".into())
812 )
813 .matches_binary(&doc)
814 .unwrap()
815 );
816 assert!(
817 filter(
818 "t",
819 "eq",
820 nodedb_types::Value::String("2026-07-02 13:00:00".into())
821 )
822 .matches_binary(&doc)
823 .unwrap()
824 );
825 }
826
827 #[test]
830 fn indexed_matches_same_as_sequential() {
831 let doc = encode(&json!({"a": 1, "b": "hello", "c": true, "d": null}));
832 let idx = FieldIndex::build(&doc, 0).unwrap();
833
834 let filters = vec![
835 filter("a", "eq", nodedb_types::Value::Integer(1)),
836 filter("b", "contains", nodedb_types::Value::String("ell".into())),
837 filter("c", "eq", nodedb_types::Value::Bool(true)),
838 filter("d", "is_null", nodedb_types::Value::Null),
839 filter("missing", "is_null", nodedb_types::Value::Null),
840 ];
841
842 for f in &filters {
843 assert_eq!(
844 f.matches_binary(&doc).unwrap(),
845 f.matches_binary_indexed(&doc, &idx).unwrap(),
846 "mismatch for field={} op={:?}",
847 f.field,
848 f.op
849 );
850 }
851 }
852}