Skip to main content

teaql_runtime/
inmemory_engine.rs

1use std::cmp::Ordering;
2use std::time::SystemTime;
3
4use teaql_core::{
5    Aggregate, AggregateFunction, BinaryOp, Expr, OrderBy, Record, SelectQuery, SortDirection,
6    Value,
7};
8use teaql_data_service::{DataServiceOperation, ExecutionMetadata, QueryResult};
9
10/// A general-purpose in-memory query engine that executes [`SelectQuery`] against a
11/// `Vec<Record>`. This replaces the database engine for non-SQL data sources.
12pub struct InMemoryQueryEngine;
13
14impl InMemoryQueryEngine {
15    /// Execute a [`SelectQuery`] against the given rows and return a [`QueryResult`].
16    ///
17    /// Processing order: filter → aggregation (if any) → sort → paginate → project.
18    pub fn execute(query: &SelectQuery, mut rows: Vec<Record>) -> QueryResult {
19        let started_at = SystemTime::now();
20
21        // 1. Filter
22        if let Some(filter) = &query.filter {
23            Self::filter(&mut rows, filter);
24        }
25
26        // 2. Aggregation short-circuits the normal pipeline.
27        if !query.aggregates.is_empty() {
28            let mut result = Self::aggregate(query, rows);
29            result.metadata.started_at = started_at;
30            result.metadata.ended_at = SystemTime::now();
31            return result;
32        }
33
34        // 3. Sort
35        if !query.order_by.is_empty() {
36            Self::sort(&mut rows, &query.order_by);
37        }
38
39        // 4. Paginate
40        if let Some(slice) = &query.slice {
41            rows = Self::paginate(rows, slice);
42        }
43
44        // 5. Project
45        if !query.projection.is_empty() {
46            rows = Self::project(rows, &query.projection);
47        }
48
49        let count = rows.len();
50        QueryResult {
51            rows,
52            metadata: ExecutionMetadata {
53                debug_query: None,
54                backend: "memory".to_owned(),
55                operation: DataServiceOperation::Query,
56                started_at,
57                ended_at: SystemTime::now(),
58                affected_rows: None,
59                result_count: Some(count),
60                trace_chain: Vec::new(),
61                comment: None,
62                backend_request_id: None,
63            },
64        }
65    }
66
67    /// Retain only the rows for which the expression evaluates to `true`.
68    fn filter(rows: &mut Vec<Record>, expr: &Expr) {
69        rows.retain(|row| ExprEvaluator::eval(expr, row));
70    }
71
72    /// Sort rows in-place according to the given [`OrderBy`] list (multi-column).
73    fn sort(rows: &mut Vec<Record>, order_by: &[OrderBy]) {
74        rows.sort_by(|a, b| {
75            for ob in order_by {
76                let va = a.get(&ob.field).unwrap_or(&Value::Null);
77                let vb = b.get(&ob.field).unwrap_or(&Value::Null);
78                let ord = compare_values(va, vb);
79                let ord = match ob.direction {
80                    SortDirection::Asc => ord,
81                    SortDirection::Desc => ord.reverse(),
82                };
83                if ord != Ordering::Equal {
84                    return ord;
85                }
86            }
87            Ordering::Equal
88        });
89    }
90
91    /// Apply offset/limit pagination.
92    fn paginate(rows: Vec<Record>, slice: &teaql_core::Slice) -> Vec<Record> {
93        let offset = slice.offset as usize;
94        let iter = rows.into_iter().skip(offset);
95        match slice.limit {
96            Some(limit) => iter.take(limit as usize).collect(),
97            None => iter.collect(),
98        }
99    }
100
101    /// Keep only the specified fields in each record.
102    fn project(rows: Vec<Record>, projection: &[String]) -> Vec<Record> {
103        rows.into_iter()
104            .map(|row| {
105                row.into_iter()
106                    .filter(|(key, _)| projection.contains(key))
107                    .collect()
108            })
109            .collect()
110    }
111
112    /// Compute aggregations over the (already-filtered) rows and return the result
113    /// as a single-row [`QueryResult`].
114    fn aggregate(query: &SelectQuery, rows: Vec<Record>) -> QueryResult {
115        let started_at = SystemTime::now();
116
117        // If there are group-by fields, partition the rows into groups.
118        if !query.group_by.is_empty() {
119            return Self::aggregate_grouped(query, rows, started_at);
120        }
121
122        // No group-by: single aggregation over all rows.
123        let mut result_row = Record::new();
124        for agg in &query.aggregates {
125            let value = compute_aggregate(agg, &rows);
126            result_row.insert(agg.alias.clone(), value);
127        }
128
129        let result_rows = vec![result_row];
130        let count = result_rows.len();
131        QueryResult {
132            rows: result_rows,
133            metadata: ExecutionMetadata {
134                debug_query: None,
135                backend: "memory".to_owned(),
136                operation: DataServiceOperation::Query,
137                started_at,
138                ended_at: SystemTime::now(),
139                affected_rows: None,
140                result_count: Some(count),
141                trace_chain: Vec::new(),
142                comment: None,
143                backend_request_id: None,
144            },
145        }
146    }
147
148    /// Aggregate with GROUP BY support.
149    fn aggregate_grouped(
150        query: &SelectQuery,
151        rows: Vec<Record>,
152        started_at: SystemTime,
153    ) -> QueryResult {
154        // Build groups keyed by the group-by field values.
155        let mut groups: Vec<(Vec<Value>, Vec<Record>)> = Vec::new();
156
157        for row in rows {
158            let key: Vec<Value> = query
159                .group_by
160                .iter()
161                .map(|gb| row.get(gb).cloned().unwrap_or(Value::Null))
162                .collect();
163
164            match groups.iter_mut().find(|(k, _)| k == &key) {
165                Some((_k, group)) => group.push(row),
166                None => groups.push((key, vec![row])),
167            }
168        }
169
170        let mut result_rows = Vec::with_capacity(groups.len());
171        for (key_values, group_rows) in &groups {
172            let mut result_row = Record::new();
173
174            // Include group-by fields in the output.
175            for (i, gb) in query.group_by.iter().enumerate() {
176                result_row.insert(gb.clone(), key_values[i].clone());
177            }
178
179            // Compute each aggregate over this group.
180            for agg in &query.aggregates {
181                let value = compute_aggregate(agg, group_rows);
182                result_row.insert(agg.alias.clone(), value);
183            }
184
185            result_rows.push(result_row);
186        }
187
188        let count = result_rows.len();
189        QueryResult {
190            rows: result_rows,
191            metadata: ExecutionMetadata {
192                debug_query: None,
193                backend: "memory".to_owned(),
194                operation: DataServiceOperation::Query,
195                started_at,
196                ended_at: SystemTime::now(),
197                affected_rows: None,
198                result_count: Some(count),
199                trace_chain: Vec::new(),
200                comment: None,
201                backend_request_id: None,
202            },
203        }
204    }
205}
206
207/// Evaluates [`Expr`] trees against a single [`Record`].
208pub struct ExprEvaluator;
209
210impl ExprEvaluator {
211    /// Evaluate an expression as a boolean predicate against a row.
212    pub fn eval(expr: &Expr, row: &Record) -> bool {
213        match expr {
214            Expr::Binary { left, op, right } => {
215                let lv = Self::resolve(left, row);
216                let rv = Self::resolve(right, row);
217                Self::compare_op(&lv, op, &rv)
218            }
219            Expr::And(parts) => parts.iter().all(|p| Self::eval(p, row)),
220            Expr::Or(parts) => parts.iter().any(|p| Self::eval(p, row)),
221            Expr::Not(inner) => !Self::eval(inner, row),
222            Expr::IsNull(inner) => Self::resolve(inner, row) == Value::Null,
223            Expr::IsNotNull(inner) => Self::resolve(inner, row) != Value::Null,
224            Expr::Between {
225                expr: inner,
226                lower,
227                upper,
228            } => {
229                let v = Self::resolve(inner, row);
230                let lo = Self::resolve(lower, row);
231                let hi = Self::resolve(upper, row);
232                compare_values(&v, &lo) != Ordering::Less
233                    && compare_values(&v, &hi) != Ordering::Greater
234            }
235            // SubQuery is not supported for in-memory evaluation; always false.
236            Expr::SubQuery { .. } => false,
237            // Function expressions are not boolean predicates in general.
238            Expr::Function { .. } => false,
239            // A bare column or value is truthy if it is a Bool(true).
240            Expr::Column(_) | Expr::Value(_) => {
241                matches!(Self::resolve(expr, row), Value::Bool(true))
242            }
243        }
244    }
245
246    /// Resolve an expression to a concrete [`Value`] given a row.
247    pub fn resolve(expr: &Expr, row: &Record) -> Value {
248        match expr {
249            Expr::Column(name) => row.get(name).cloned().unwrap_or(Value::Null),
250            Expr::Value(v) => v.clone(),
251            Expr::Binary { left, op, right } => {
252                let lv = Self::resolve(left, row);
253                let rv = Self::resolve(right, row);
254                Value::Bool(Self::compare_op(&lv, op, &rv))
255            }
256            Expr::And(parts) => Value::Bool(parts.iter().all(|p| Self::eval(p, row))),
257            Expr::Or(parts) => Value::Bool(parts.iter().any(|p| Self::eval(p, row))),
258            Expr::Not(inner) => Value::Bool(!Self::eval(inner, row)),
259            Expr::IsNull(inner) => Value::Bool(Self::resolve(inner, row) == Value::Null),
260            Expr::IsNotNull(inner) => Value::Bool(Self::resolve(inner, row) != Value::Null),
261            Expr::Between {
262                expr: inner,
263                lower,
264                upper,
265            } => {
266                let v = Self::resolve(inner, row);
267                let lo = Self::resolve(lower, row);
268                let hi = Self::resolve(upper, row);
269                Value::Bool(
270                    compare_values(&v, &lo) != Ordering::Less
271                        && compare_values(&v, &hi) != Ordering::Greater,
272                )
273            }
274            Expr::SubQuery { .. } => Value::Null,
275            Expr::Function { .. } => Value::Null,
276        }
277    }
278
279    /// Compare two values according to a [`BinaryOp`].
280    fn compare_op(left: &Value, op: &BinaryOp, right: &Value) -> bool {
281        match op {
282            BinaryOp::Eq => left == right,
283            BinaryOp::Ne => left != right,
284            BinaryOp::Gt => compare_values(left, right) == Ordering::Greater,
285            BinaryOp::Gte => matches!(
286                compare_values(left, right),
287                Ordering::Greater | Ordering::Equal
288            ),
289            BinaryOp::Lt => compare_values(left, right) == Ordering::Less,
290            BinaryOp::Lte => matches!(
291                compare_values(left, right),
292                Ordering::Less | Ordering::Equal
293            ),
294            BinaryOp::Like => match (left, right) {
295                (Value::Text(text), Value::Text(pattern)) => Self::like_match(text, pattern),
296                _ => false,
297            },
298            BinaryOp::NotLike => match (left, right) {
299                (Value::Text(text), Value::Text(pattern)) => !Self::like_match(text, pattern),
300                _ => true,
301            },
302            BinaryOp::In | BinaryOp::InLarge => match right {
303                Value::List(items) => items.contains(left),
304                _ => left == right,
305            },
306            BinaryOp::NotIn | BinaryOp::NotInLarge => match right {
307                Value::List(items) => !items.contains(left),
308                _ => left != right,
309            },
310        }
311    }
312
313    /// SQL LIKE matching without regex.
314    ///
315    /// - `%` matches any sequence of characters (including empty).
316    /// - `_` matches exactly one character.
317    fn like_match(text: &str, pattern: &str) -> bool {
318        let text_chars: Vec<char> = text.chars().collect();
319        let pattern_chars: Vec<char> = pattern.chars().collect();
320        like_match_recursive(&text_chars, 0, &pattern_chars, 0)
321    }
322}
323
324/// Recursive helper for SQL LIKE matching with memoisation-free DP-style
325/// backtracking via iterative `%` expansion.
326fn like_match_recursive(text: &[char], ti: usize, pattern: &[char], pi: usize) -> bool {
327    let mut ti = ti;
328    let mut pi = pi;
329
330    loop {
331        if pi == pattern.len() {
332            return ti == text.len();
333        }
334
335        match pattern[pi] {
336            '%' => {
337                // Skip consecutive '%' characters.
338                while pi < pattern.len() && pattern[pi] == '%' {
339                    pi += 1;
340                }
341                // If '%' was the last character in pattern, match everything.
342                if pi == pattern.len() {
343                    return true;
344                }
345                // Try matching the rest of the pattern from every position.
346                for start in ti..=text.len() {
347                    if like_match_recursive(text, start, pattern, pi) {
348                        return true;
349                    }
350                }
351                return false;
352            }
353            '_' => {
354                if ti >= text.len() {
355                    return false;
356                }
357                ti += 1;
358                pi += 1;
359            }
360            ch => {
361                if ti >= text.len() || text[ti] != ch {
362                    return false;
363                }
364                ti += 1;
365                pi += 1;
366            }
367        }
368    }
369}
370
371/// Compare a signed `i64` against an unsigned `u64`, handling the negative case.
372fn compare_i64_u64(a: i64, b: u64) -> Ordering {
373    match a < 0 {
374        true => Ordering::Less,
375        false => (a as u64).cmp(&b),
376    }
377}
378
379/// Compare two [`Value`]s for ordering. Nulls sort first.
380fn compare_values(a: &Value, b: &Value) -> Ordering {
381    match (a, b) {
382        (Value::Null, Value::Null) => Ordering::Equal,
383        (Value::Null, _) => Ordering::Less,
384        (_, Value::Null) => Ordering::Greater,
385        (Value::Bool(a), Value::Bool(b)) => a.cmp(b),
386        (Value::I64(a), Value::I64(b)) => a.cmp(b),
387        (Value::U64(a), Value::U64(b)) => a.cmp(b),
388        (Value::I64(a), Value::U64(b)) => compare_i64_u64(*a, *b),
389        (Value::U64(a), Value::I64(b)) => compare_i64_u64(*b, *a).reverse(),
390        (Value::F64(a), Value::F64(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
391        (Value::Decimal(a), Value::Decimal(b)) => a.cmp(b),
392        (Value::Text(a), Value::Text(b)) => a.cmp(b),
393        (Value::Date(a), Value::Date(b)) => a.cmp(b),
394        (Value::Timestamp(a), Value::Timestamp(b)) => a.cmp(b),
395        // Cross-type numeric comparisons via f64.
396        _ => value_to_f64(a)
397            .zip(value_to_f64(b))
398            .and_then(|(fa, fb)| fa.partial_cmp(&fb))
399            .unwrap_or(Ordering::Equal),
400    }
401}
402
403/// Best-effort conversion of a [`Value`] to `f64` for cross-type numeric comparison.
404fn value_to_f64(v: &Value) -> Option<f64> {
405    v.try_f64()
406}
407
408/// Count rows, treating `"*"` as counting all rows and other fields as counting non-null values.
409fn count_rows(rows: &[Record], field: &str) -> Value {
410    let count = match field {
411        "*" => rows.len(),
412        _ => rows
413            .iter()
414            .filter(|r| r.get(field).map(|v| v != &Value::Null).unwrap_or(false))
415            .count(),
416    };
417    Value::I64(count as i64)
418}
419
420/// Compute a single aggregate over a slice of rows.
421fn compute_aggregate(agg: &Aggregate, rows: &[Record]) -> Value {
422    match agg.function {
423        AggregateFunction::Count => count_rows(rows, &agg.field),
424        AggregateFunction::Sum => {
425            let mut sum: f64 = 0.0;
426            let mut found = false;
427            for row in rows {
428                if let Some(v) = row.get(&agg.field) {
429                    if let Some(f) = v.try_f64() {
430                        sum += f;
431                        found = true;
432                    }
433                }
434            }
435            found.then(|| Value::F64(sum)).unwrap_or(Value::Null)
436        }
437        AggregateFunction::Avg => {
438            let mut sum: f64 = 0.0;
439            let mut count: u64 = 0;
440            for row in rows {
441                if let Some(v) = row.get(&agg.field) {
442                    if let Some(f) = v.try_f64() {
443                        sum += f;
444                        count += 1;
445                    }
446                }
447            }
448            (count > 0)
449                .then(|| Value::F64(sum / count as f64))
450                .unwrap_or(Value::Null)
451        }
452        AggregateFunction::Max => {
453            let mut max: Option<&Value> = None;
454            for row in rows {
455                if let Some(v) = row.get(&agg.field) {
456                    if v == &Value::Null {
457                        continue;
458                    }
459                    max = Some(match max {
460                        Some(current) if compare_values(v, current) == Ordering::Greater => v,
461                        Some(current) => current,
462                        None => v,
463                    });
464                }
465            }
466            max.cloned().unwrap_or(Value::Null)
467        }
468        AggregateFunction::Min => {
469            let mut min: Option<&Value> = None;
470            for row in rows {
471                if let Some(v) = row.get(&agg.field) {
472                    if v == &Value::Null {
473                        continue;
474                    }
475                    min = Some(match min {
476                        Some(current) if compare_values(v, current) == Ordering::Less => v,
477                        Some(current) => current,
478                        None => v,
479                    });
480                }
481            }
482            min.cloned().unwrap_or(Value::Null)
483        }
484        // Unsupported aggregate functions return Null.
485        AggregateFunction::Stddev
486        | AggregateFunction::StddevPop
487        | AggregateFunction::VarSamp
488        | AggregateFunction::VarPop
489        | AggregateFunction::BitAnd
490        | AggregateFunction::BitOr
491        | AggregateFunction::BitXor => Value::Null,
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498    use teaql_core::{Aggregate, AggregateFunction, Record, SelectQuery, Value};
499
500    fn make_row(pairs: Vec<(&str, Value)>) -> Record {
501        pairs.into_iter().map(|(k, v)| (k.to_owned(), v)).collect()
502    }
503
504    fn sample_rows() -> Vec<Record> {
505        vec![
506            make_row(vec![
507                ("id", Value::U64(1)),
508                ("name", Value::Text("Alice".to_owned())),
509                ("age", Value::I64(30)),
510            ]),
511            make_row(vec![
512                ("id", Value::U64(2)),
513                ("name", Value::Text("Bob".to_owned())),
514                ("age", Value::I64(25)),
515            ]),
516            make_row(vec![
517                ("id", Value::U64(3)),
518                ("name", Value::Text("Charlie".to_owned())),
519                ("age", Value::I64(35)),
520            ]),
521        ]
522    }
523
524    #[test]
525    fn test_execute_no_filter() {
526        let query = SelectQuery::new("User");
527        let result = InMemoryQueryEngine::execute(&query, sample_rows());
528        assert_eq!(result.rows.len(), 3);
529        assert_eq!(result.metadata.backend, "memory");
530    }
531
532    #[test]
533    fn test_execute_with_eq_filter() {
534        let query = SelectQuery::new("User").filter(Expr::eq("name", "Bob"));
535        let result = InMemoryQueryEngine::execute(&query, sample_rows());
536        assert_eq!(result.rows.len(), 1);
537        assert_eq!(
538            result.rows[0].get("name"),
539            Some(&Value::Text("Bob".to_owned()))
540        );
541    }
542
543    #[test]
544    fn test_execute_with_gt_filter() {
545        let query = SelectQuery::new("User").filter(Expr::gt("age", 28_i64));
546        let result = InMemoryQueryEngine::execute(&query, sample_rows());
547        assert_eq!(result.rows.len(), 2); // Alice(30) and Charlie(35)
548    }
549
550    #[test]
551    fn test_sort_ascending() {
552        let query = SelectQuery::new("User").order_by(teaql_core::OrderBy::asc("age"));
553        let result = InMemoryQueryEngine::execute(&query, sample_rows());
554        let ages: Vec<_> = result
555            .rows
556            .iter()
557            .map(|r| r.get("age").unwrap().clone())
558            .collect();
559        assert_eq!(ages, vec![Value::I64(25), Value::I64(30), Value::I64(35)]);
560    }
561
562    #[test]
563    fn test_sort_descending() {
564        let query = SelectQuery::new("User").order_by(teaql_core::OrderBy::desc("age"));
565        let result = InMemoryQueryEngine::execute(&query, sample_rows());
566        let ages: Vec<_> = result
567            .rows
568            .iter()
569            .map(|r| r.get("age").unwrap().clone())
570            .collect();
571        assert_eq!(ages, vec![Value::I64(35), Value::I64(30), Value::I64(25)]);
572    }
573
574    #[test]
575    fn test_paginate() {
576        let query = SelectQuery::new("User").page(1, 1);
577        let result = InMemoryQueryEngine::execute(&query, sample_rows());
578        assert_eq!(result.rows.len(), 1);
579        assert_eq!(
580            result.rows[0].get("name"),
581            Some(&Value::Text("Bob".to_owned()))
582        );
583    }
584
585    #[test]
586    fn test_projection() {
587        let query = SelectQuery::new("User").projects(["name"]);
588        let result = InMemoryQueryEngine::execute(&query, sample_rows());
589        for row in &result.rows {
590            assert!(row.contains_key("name"));
591            assert!(!row.contains_key("id"));
592            assert!(!row.contains_key("age"));
593        }
594    }
595
596    #[test]
597    fn test_count_aggregate() {
598        let query = SelectQuery::new("User").aggregate(Aggregate::count("total"));
599        let result = InMemoryQueryEngine::execute(&query, sample_rows());
600        assert_eq!(result.rows.len(), 1);
601        assert_eq!(result.rows[0].get("total"), Some(&Value::I64(3)));
602    }
603
604    #[test]
605    fn test_sum_aggregate() {
606        let query = SelectQuery::new("User").aggregate(Aggregate::sum("age", "age_sum"));
607        let result = InMemoryQueryEngine::execute(&query, sample_rows());
608        assert_eq!(result.rows[0].get("age_sum"), Some(&Value::F64(90.0)));
609    }
610
611    #[test]
612    fn test_avg_aggregate() {
613        let query = SelectQuery::new("User").aggregate(Aggregate::avg("age", "age_avg"));
614        let result = InMemoryQueryEngine::execute(&query, sample_rows());
615        assert_eq!(result.rows[0].get("age_avg"), Some(&Value::F64(30.0)));
616    }
617
618    #[test]
619    fn test_max_aggregate() {
620        let query = SelectQuery::new("User").aggregate(Aggregate::max("age", "age_max"));
621        let result = InMemoryQueryEngine::execute(&query, sample_rows());
622        assert_eq!(result.rows[0].get("age_max"), Some(&Value::I64(35)));
623    }
624
625    #[test]
626    fn test_min_aggregate() {
627        let query = SelectQuery::new("User").aggregate(Aggregate::min("age", "age_min"));
628        let result = InMemoryQueryEngine::execute(&query, sample_rows());
629        assert_eq!(result.rows[0].get("age_min"), Some(&Value::I64(25)));
630    }
631
632    #[test]
633    fn test_like_match_percent() {
634        assert!(ExprEvaluator::like_match("hello world", "%world"));
635        assert!(ExprEvaluator::like_match("hello world", "hello%"));
636        assert!(ExprEvaluator::like_match("hello world", "%lo wo%"));
637        assert!(ExprEvaluator::like_match("hello world", "%"));
638        assert!(!ExprEvaluator::like_match("hello world", "%xyz%"));
639    }
640
641    #[test]
642    fn test_like_match_underscore() {
643        assert!(ExprEvaluator::like_match("abc", "a_c"));
644        assert!(!ExprEvaluator::like_match("abbc", "a_c"));
645        assert!(ExprEvaluator::like_match("abc", "___"));
646        assert!(!ExprEvaluator::like_match("ab", "___"));
647    }
648
649    #[test]
650    fn test_like_match_combined() {
651        assert!(ExprEvaluator::like_match("foobar", "f%r"));
652        assert!(ExprEvaluator::like_match("foobar", "f__b%"));
653        assert!(!ExprEvaluator::like_match("foobar", "f__x%"));
654    }
655
656    #[test]
657    fn test_and_or_not() {
658        let row = make_row(vec![("a", Value::I64(10)), ("b", Value::I64(20))]);
659        let expr_and = Expr::and([Expr::eq("a", 10_i64), Expr::eq("b", 20_i64)]);
660        assert!(ExprEvaluator::eval(&expr_and, &row));
661
662        let expr_or = Expr::or([Expr::eq("a", 99_i64), Expr::eq("b", 20_i64)]);
663        assert!(ExprEvaluator::eval(&expr_or, &row));
664
665        let expr_not = Expr::negate(Expr::eq("a", 99_i64));
666        assert!(ExprEvaluator::eval(&expr_not, &row));
667    }
668
669    #[test]
670    fn test_is_null_is_not_null() {
671        let row = make_row(vec![("x", Value::Null), ("y", Value::I64(1))]);
672        assert!(ExprEvaluator::eval(&Expr::is_null("x"), &row));
673        assert!(!ExprEvaluator::eval(&Expr::is_not_null("x"), &row));
674        assert!(ExprEvaluator::eval(&Expr::is_not_null("y"), &row));
675    }
676
677    #[test]
678    fn test_between() {
679        let row = make_row(vec![("age", Value::I64(30))]);
680        assert!(ExprEvaluator::eval(
681            &Expr::between("age", Value::I64(25), Value::I64(35)),
682            &row
683        ));
684        assert!(!ExprEvaluator::eval(
685            &Expr::between("age", Value::I64(31), Value::I64(35)),
686            &row
687        ));
688    }
689
690    #[test]
691    fn test_in_list() {
692        let row = make_row(vec![("status", Value::Text("active".to_owned()))]);
693        let expr = Expr::in_list(
694            "status",
695            vec![
696                Value::Text("active".to_owned()),
697                Value::Text("pending".to_owned()),
698            ],
699        );
700        assert!(ExprEvaluator::eval(&expr, &row));
701
702        let expr_miss = Expr::in_list("status", vec![Value::Text("closed".to_owned())]);
703        assert!(!ExprEvaluator::eval(&expr_miss, &row));
704    }
705}