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