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