Skip to main content

linkrs_query/executor/expression/evaluator/
compiled.rs

1//! Compiled expression evaluation
2//!
3//! Compiles an [`Expression`] tree once into a closure tree that evaluates
4//! against a raw row slice.  The compiled tree eliminates the per-row
5//! recursive `match` over the full expression enum, pre-resolves variable
6//! references to slot IDs and function references to the global registry at
7//! compile time, and folds constant subtrees inline.
8//!
9//! Nodes that require the full runtime context (subqueries, predicates,
10//! labels, list comprehensions, aggregates, ...) are kept as a fallback node
11//! that delegates to the original per-row interpreter with a
12//! [`BorrowedRowContext`], preserving the exact semantics of the scalar
13//! evaluator.
14
15use std::sync::Arc;
16
17use crate::executor::expression::evaluator::collection_operations::CollectionOperationEvaluator;
18use crate::executor::expression::evaluator::expression_evaluator::ExpressionEvaluator;
19use crate::executor::expression::evaluator::functions::FunctionEvaluator;
20use crate::executor::expression::evaluator::operations::{
21    BinaryOperationEvaluator, UnaryOperationEvaluator,
22};
23use crate::executor::expression::functions::{
24    global_registry, global_registry_ref, OwnedFunctionRef,
25};
26use crate::executor::expression::ExpressionError;
27use crate::executor::streaming::chunk::TypedColumn;
28use crate::executor::streaming::context::BorrowedRowContext;
29use crate::executor::streaming::slot::{SlotId, SlotLayout};
30use crate::executor::streaming::subquery::EvalEnv;
31use linkrs_core::types::expr::Expression;
32use linkrs_core::types::operators::{BinaryOperator, UnaryOperator};
33use linkrs_core::value::list::List;
34use linkrs_core::value::NullType;
35use linkrs_core::DataType;
36use linkrs_core::Value;
37
38/// Global runtime switch for the compiled evaluation path.
39///
40/// Keep the scalar path selectable so problems can be pinned down to the
41/// compiled evaluator without rebuilding.
42static COMPILED_EVAL_ENABLED: std::sync::atomic::AtomicBool =
43    std::sync::atomic::AtomicBool::new(true);
44
45/// Enable or disable the compiled expression evaluation path.
46pub fn set_compiled_eval_enabled(enabled: bool) {
47    COMPILED_EVAL_ENABLED.store(enabled, std::sync::atomic::Ordering::Relaxed);
48}
49
50/// Whether compiled expression evaluation is currently enabled.
51pub fn compiled_eval_enabled() -> bool {
52    COMPILED_EVAL_ENABLED.load(std::sync::atomic::Ordering::Relaxed)
53}
54
55/// A columnar value: either a constant scalar (never materialized as a
56/// column) or a full column of values.
57#[derive(Debug, Clone)]
58pub enum ColumnarValue {
59    /// A constant scalar shared by every row.
60    Const(Value),
61    /// One value per row of the chunk.
62    Column(Vec<Value>),
63}
64
65impl ColumnarValue {
66    /// Materialize into a full-length column, broadcasting a constant.
67    pub fn into_values(self, len: usize) -> Vec<Value> {
68        match self {
69            ColumnarValue::Const(v) => vec![v; len],
70            ColumnarValue::Column(col) => col,
71        }
72    }
73}
74
75/// Compiled closure-tree node.
76///
77/// Mirrors the hot subset of [`Expression`] with slot-resolved variables and
78/// pre-resolved function references; anything else is kept verbatim in
79/// [`CompiledExpr::Fallback`] and evaluated through the scalar interpreter.
80#[derive(Debug, Clone)]
81pub enum CompiledExpr {
82    /// Compile-time folded constant.
83    Const(Value),
84    /// A variable resolved to its slot at compile time.
85    Slot(SlotId),
86    /// A `var.prop` property access resolved to its compound slot at compile
87    /// time (flat column layout).
88    CompoundSlot(SlotId),
89    /// Unary operation over a compiled operand.
90    Unary {
91        op: UnaryOperator,
92        operand: Box<CompiledExpr>,
93    },
94    /// Binary operation over two compiled operands.
95    Binary {
96        op: BinaryOperator,
97        left: Box<CompiledExpr>,
98        right: Box<CompiledExpr>,
99    },
100    /// CASE expression with short-circuit evaluation.
101    Case {
102        test: Option<Box<CompiledExpr>>,
103        conditions: Vec<(CompiledExpr, CompiledExpr)>,
104        default: Option<Box<CompiledExpr>>,
105    },
106    /// Function call with compile-time resolved function reference.
107    Function {
108        name: String,
109        func: Option<OwnedFunctionRef>,
110        args: Vec<CompiledExpr>,
111    },
112    /// Type cast over a compiled operand.
113    TypeCast {
114        expr: Box<CompiledExpr>,
115        target_type: DataType,
116    },
117    /// List literal.
118    List(Vec<CompiledExpr>),
119    /// Map literal.
120    Map(Vec<(String, CompiledExpr)>),
121    /// Subscript access.
122    Subscript {
123        collection: Box<CompiledExpr>,
124        index: Box<CompiledExpr>,
125    },
126    /// Range access.
127    Range {
128        collection: Box<CompiledExpr>,
129        start: Option<Box<CompiledExpr>>,
130        end: Option<Box<CompiledExpr>>,
131    },
132    /// Path expression (evaluates to a list).
133    Path(Vec<CompiledExpr>),
134    /// Query parameter reference, resolved from the environment at runtime.
135    Parameter(String),
136    /// Session variable reference, resolved from the environment at runtime.
137    SessionVariable(String),
138    /// Everything that requires the full runtime context — evaluated per row
139    /// through the original interpreter with a [`BorrowedRowContext`].
140    Fallback(Expression),
141}
142
143impl CompiledExpr {
144    /// Compile an expression tree against a slot layout.
145    ///
146    /// Variable references resolve to slot IDs, `var.prop` accesses to their
147    /// compound slots, function references to the global registry, and
148    /// constant subtrees are folded inline.  Any node that needs the runtime
149    /// context stays as [`CompiledExpr::Fallback`].
150    pub fn compile(expression: &Expression, layout: &SlotLayout) -> Self {
151        match expression {
152            Expression::Literal(v) => CompiledExpr::Const(v.clone()),
153            Expression::Vector(d) => CompiledExpr::Const(Value::vector(d.clone())),
154
155            Expression::Variable(name) => match layout.slot_id(name) {
156                Some(slot) => CompiledExpr::Slot(slot),
157                None => CompiledExpr::Fallback(expression.clone()),
158            },
159
160            // Flat property access: `var.prop` resolves to the compound slot
161            // when the layout carries it; otherwise delegate to the runtime
162            // interpreter (which falls back to Vertex/Map extraction).
163            Expression::Property { object, property } => {
164                if let Expression::Variable(var_name) = object.as_ref() {
165                    let compound = format!("{}.{}", var_name, property);
166                    if let Some(slot) = layout.slot_id(&compound) {
167                        return CompiledExpr::CompoundSlot(slot);
168                    }
169                }
170                CompiledExpr::Fallback(expression.clone())
171            }
172
173            Expression::Binary { left, op, right } => {
174                let left = Self::compile(left, layout);
175                let right = Self::compile(right, layout);
176                match (left, right) {
177                    (CompiledExpr::Const(l), CompiledExpr::Const(r)) => {
178                        match BinaryOperationEvaluator::evaluate(&l, op, &r) {
179                            Ok(v) => CompiledExpr::Const(v),
180                            // Preserve runtime error semantics: keep the
181                            // unfolded node so the error surfaces per row.
182                            Err(_) => CompiledExpr::Binary {
183                                op: *op,
184                                left: Box::new(CompiledExpr::Const(l)),
185                                right: Box::new(CompiledExpr::Const(r)),
186                            },
187                        }
188                    }
189                    (left, right) => CompiledExpr::Binary {
190                        op: *op,
191                        left: Box::new(left),
192                        right: Box::new(right),
193                    },
194                }
195            }
196
197            Expression::Unary { op, operand } => {
198                let operand = Self::compile(operand, layout);
199                match operand {
200                    CompiledExpr::Const(v) => match UnaryOperationEvaluator::evaluate(op, &v) {
201                        Ok(folded) => CompiledExpr::Const(folded),
202                        Err(_) => CompiledExpr::Unary {
203                            op: *op,
204                            operand: Box::new(CompiledExpr::Const(v)),
205                        },
206                    },
207                    operand => CompiledExpr::Unary {
208                        op: *op,
209                        operand: Box::new(operand),
210                    },
211                }
212            }
213
214            Expression::Function { name, args } => {
215                // Higher-order lambda calls must run through the scalar
216                // interpreter, which binds lambda parameters per element.
217                // Compiling them would eager-evaluate the Lambda node and
218                // fail before the callee sees the closure.
219                if args
220                    .iter()
221                    .any(|a| matches!(a.as_expr(), Expression::Lambda { .. }))
222                {
223                    return CompiledExpr::Fallback(expression.clone());
224                }
225                // Sequence functions carry catalog state (`next_val`
226                // advances the sequence) and resolve their manager through
227                // the row evaluation environment. The compiled tree has no
228                // env access at function nodes, so keep them on the scalar
229                // interpreter path with the exact per-row semantics.
230                if global_registry_ref()
231                    .get_builtin(name.as_str())
232                    .is_some_and(|f| {
233                        matches!(
234                            f,
235                            crate::executor::expression::functions::BuiltinFunction::Sequence(_)
236                        )
237                    })
238                {
239                    return CompiledExpr::Fallback(expression.clone());
240                }
241                let args: Vec<CompiledExpr> = args
242                    .iter()
243                    .map(|a| Self::compile(a.as_expr(), layout))
244                    .collect();
245                // Compile-time folding only for pure functions with constant
246                // arguments; unregistered functions are conservatively kept
247                // for runtime resolution (identical error semantics).
248                if args.iter().all(|a| matches!(a, CompiledExpr::Const(_)))
249                    && global_registry_ref()
250                        .get_builtin(name.as_str())
251                        .is_some_and(|f| f.is_pure())
252                {
253                    let values: Vec<Value> = args
254                        .iter()
255                        .map(|a| match a {
256                            CompiledExpr::Const(v) => v.clone(),
257                            _ => unreachable!("guarded by all-Const check"),
258                        })
259                        .collect();
260                    if let Ok(v) = global_registry().execute(name, &values) {
261                        return CompiledExpr::Const(v);
262                    }
263                }
264                let func = global_registry_ref()
265                    .get_builtin(name.as_str())
266                    .map(|f| OwnedFunctionRef::Builtin(f.clone()))
267                    .or_else(|| {
268                        global_registry_ref()
269                            .get_custom(name.as_str())
270                            .map(|f| OwnedFunctionRef::Custom(f.clone()))
271                    });
272                CompiledExpr::Function {
273                    name: name.clone(),
274                    func,
275                    args,
276                }
277            }
278
279            Expression::Aggregate {
280                func,
281                args,
282                distinct,
283                filter,
284            } => {
285                let args: Vec<CompiledExpr> =
286                    args.iter().map(|a| Self::compile(a, layout)).collect();
287                let filter = filter.as_ref().map(|f| Box::new(Self::compile(f, layout)));
288                // Aggregates need a row-group context; fold only when the
289                // whole call is constant and pure (mirrors the heuristic
290                // constant-folding rule).
291                if filter.is_none() && args.iter().all(|a| matches!(a, CompiledExpr::Const(_))) {
292                    let values: Vec<Value> = args
293                        .iter()
294                        .map(|a| match a {
295                            CompiledExpr::Const(v) => v.clone(),
296                            _ => unreachable!("guarded by all-Const check"),
297                        })
298                        .collect();
299                    if let Ok(v) =
300                        FunctionEvaluator::eval_aggregate_function(func, &values, *distinct)
301                    {
302                        return CompiledExpr::Const(v);
303                    }
304                }
305                CompiledExpr::Fallback(expression.clone())
306            }
307
308            Expression::List(items) => {
309                CompiledExpr::List(items.iter().map(|e| Self::compile(e, layout)).collect())
310            }
311
312            Expression::Map(pairs) => CompiledExpr::Map(
313                pairs
314                    .iter()
315                    .map(|(k, v)| (k.clone(), Self::compile(v, layout)))
316                    .collect(),
317            ),
318
319            Expression::Case {
320                test_expr,
321                conditions,
322                default,
323            } => CompiledExpr::Case {
324                test: test_expr
325                    .as_ref()
326                    .map(|e| Box::new(Self::compile(e, layout))),
327                conditions: conditions
328                    .iter()
329                    .map(|(c, v)| (Self::compile(c, layout), Self::compile(v, layout)))
330                    .collect(),
331                default: default.as_ref().map(|e| Box::new(Self::compile(e, layout))),
332            },
333
334            Expression::TypeCast {
335                expression,
336                target_type,
337            } => {
338                let expr = Self::compile(expression, layout);
339                match expr {
340                    CompiledExpr::Const(v) => {
341                        match ExpressionEvaluator::eval_type_cast(&v, target_type) {
342                            Ok(folded) => CompiledExpr::Const(folded),
343                            Err(_) => CompiledExpr::TypeCast {
344                                expr: Box::new(CompiledExpr::Const(v)),
345                                target_type: target_type.clone(),
346                            },
347                        }
348                    }
349                    expr => CompiledExpr::TypeCast {
350                        expr: Box::new(expr),
351                        target_type: target_type.clone(),
352                    },
353                }
354            }
355
356            Expression::Subscript { collection, index } => CompiledExpr::Subscript {
357                collection: Box::new(Self::compile(collection, layout)),
358                index: Box::new(Self::compile(index, layout)),
359            },
360
361            Expression::Range {
362                collection,
363                start,
364                end,
365            } => CompiledExpr::Range {
366                collection: Box::new(Self::compile(collection, layout)),
367                start: start.as_ref().map(|e| Box::new(Self::compile(e, layout))),
368                end: end.as_ref().map(|e| Box::new(Self::compile(e, layout))),
369            },
370
371            Expression::Path(items) => {
372                CompiledExpr::Path(items.iter().map(|e| Self::compile(e, layout)).collect())
373            }
374
375            Expression::Parameter(name) => CompiledExpr::Parameter(name.clone()),
376            Expression::SessionVariable(name) => CompiledExpr::SessionVariable(name.clone()),
377
378            // Everything else requires the runtime context.
379            _ => CompiledExpr::Fallback(expression.clone()),
380        }
381    }
382
383    /// Evaluate the compiled expression against a single row.
384    ///
385    /// `layout` and `env` are only used by fallback nodes that delegate to the
386    /// scalar interpreter; the compiled nodes read the row slice directly.
387    pub fn evaluate(
388        &self,
389        row: &[Value],
390        layout: Arc<SlotLayout>,
391        env: Option<&EvalEnv>,
392    ) -> Result<Value, ExpressionError> {
393        match self {
394            CompiledExpr::Const(v) => Ok(v.clone()),
395            CompiledExpr::Slot(slot) => row
396                .get(*slot)
397                .cloned()
398                .ok_or_else(|| ExpressionError::undefined_variable("column slot")),
399            CompiledExpr::CompoundSlot(slot) => row
400                .get(*slot)
401                .cloned()
402                .ok_or_else(|| ExpressionError::undefined_variable("column slot")),
403
404            CompiledExpr::Unary { op, operand } => {
405                let value = operand.evaluate(row, layout, env)?;
406                UnaryOperationEvaluator::evaluate(op, &value)
407            }
408
409            CompiledExpr::Binary { op, left, right } => {
410                let left_value = left.evaluate(row, layout.clone(), env)?;
411                let right_value = right.evaluate(row, layout, env)?;
412                BinaryOperationEvaluator::evaluate(&left_value, op, &right_value)
413            }
414
415            CompiledExpr::Case {
416                test,
417                conditions,
418                default,
419            } => {
420                if let Some(test_expr) = test {
421                    let test_value = test_expr.evaluate(row, layout.clone(), env)?;
422                    for (condition, value) in conditions {
423                        let condition_result = condition.evaluate(row, layout.clone(), env)?;
424                        if test_value == condition_result {
425                            return value.evaluate(row, layout, env);
426                        }
427                    }
428                } else {
429                    for (condition, value) in conditions {
430                        let condition_result = condition.evaluate(row, layout.clone(), env)?;
431                        match condition_result {
432                            Value::Bool(true) => return value.evaluate(row, layout, env),
433                            Value::Bool(false) => continue,
434                            _ => {
435                                return Err(ExpressionError::type_error(
436                                    "CASE conditions must be Boolean",
437                                ))
438                            }
439                        }
440                    }
441                }
442                match default {
443                    Some(default_expression) => default_expression.evaluate(row, layout, env),
444                    None => Ok(Value::Null(NullType::Null)),
445                }
446            }
447
448            CompiledExpr::Function { name, func, args } => {
449                let mut arg_values = Vec::with_capacity(args.len());
450                for arg in args {
451                    arg_values.push(arg.evaluate(row, layout.clone(), env)?);
452                }
453                match func {
454                    Some(f) => f.execute(&arg_values),
455                    None => global_registry().execute(name, &arg_values),
456                }
457            }
458
459            CompiledExpr::TypeCast { expr, target_type } => {
460                let value = expr.evaluate(row, layout, env)?;
461                ExpressionEvaluator::eval_type_cast(&value, target_type)
462            }
463
464            CompiledExpr::List(items) => {
465                let mut values = Vec::with_capacity(items.len());
466                for item in items {
467                    values.push(item.evaluate(row, layout.clone(), env)?);
468                }
469                Ok(Value::list(List::from(values)))
470            }
471
472            CompiledExpr::Map(entries) => {
473                let mut map_values = std::collections::HashMap::new();
474                for (key, value_expression) in entries {
475                    let value = value_expression.evaluate(row, layout.clone(), env)?;
476                    map_values.insert(Value::string(key.clone()), value);
477                }
478                Ok(Value::map(map_values))
479            }
480
481            CompiledExpr::Subscript { collection, index } => {
482                let collection_value = collection.evaluate(row, layout.clone(), env)?;
483                let index_value = index.evaluate(row, layout, env)?;
484                CollectionOperationEvaluator::eval_subscript_access(&collection_value, &index_value)
485            }
486
487            CompiledExpr::Range {
488                collection,
489                start,
490                end,
491            } => {
492                let collection_value = collection.evaluate(row, layout.clone(), env)?;
493                let start_value = start
494                    .as_ref()
495                    .map(|e| e.evaluate(row, layout.clone(), env))
496                    .transpose()?;
497                let end_value = end
498                    .as_ref()
499                    .map(|e| e.evaluate(row, layout.clone(), env))
500                    .transpose()?;
501                CollectionOperationEvaluator::eval_range_access(
502                    &collection_value,
503                    start_value.as_ref(),
504                    end_value.as_ref(),
505                )
506            }
507
508            CompiledExpr::Path(items) => {
509                let mut values = Vec::with_capacity(items.len());
510                for item in items {
511                    values.push(item.evaluate(row, layout.clone(), env)?);
512                }
513                Ok(Value::list(List::from(values)))
514            }
515
516            CompiledExpr::Parameter(name) => env
517                .and_then(|e| e.params.as_ref())
518                .and_then(|p| p.get(name).cloned())
519                .ok_or_else(|| ExpressionError::undefined_parameter(name)),
520
521            CompiledExpr::SessionVariable(name) => env
522                .and_then(|e| e.session_variables.as_ref())
523                .and_then(|v| v.get(name).cloned())
524                .ok_or_else(|| {
525                    ExpressionError::type_error(format!(
526                        "Session variable `{}` is not defined",
527                        name
528                    ))
529                }),
530
531            CompiledExpr::Fallback(expression) => {
532                let mut ctx = match env {
533                    Some(env) => BorrowedRowContext::with_env(row, layout, env),
534                    None => BorrowedRowContext::new(row, layout),
535                };
536                ExpressionEvaluator::evaluate(expression, &mut ctx)
537            }
538        }
539    }
540
541    /// Batch-evaluate the compiled expression over every row of a chunk.
542    ///
543    /// The columnar entry point: constants are never materialized into
544    /// columns, binary/unary operators run elementwise over the produced
545    /// columns, and fallback nodes delegate per row to the scalar
546    /// interpreter.
547    ///
548    /// `typed_columns` is an optional zero-clone read path for slot leaves:
549    /// when present, `Slot`/`CompoundSlot` nodes materialize their column
550    /// from the typed layout instead of cloning per-row values out of
551    /// `rows`. Non-slot subtrees still evaluate over `rows`, so mixed
552    /// expressions keep exact semantics while their slot leaves skip the
553    /// per-row clone.
554    pub fn evaluate_batch(
555        &self,
556        rows: &[Vec<Value>],
557        layout: Arc<SlotLayout>,
558        env: Option<&EvalEnv>,
559        typed_columns: Option<&[TypedColumn]>,
560    ) -> Result<ColumnarValue, ExpressionError> {
561        let len = rows.len();
562        match self {
563            CompiledExpr::Const(v) => Ok(ColumnarValue::Const(v.clone())),
564            CompiledExpr::Slot(slot) | CompiledExpr::CompoundSlot(slot) => {
565                // Fast path: read the slot column from the typed layout
566                // without per-row cloning; fall back to rows when the
567                // layout is absent or does not cover the slot.
568                if let Some(cols) = typed_columns {
569                    if let Some(col) = cols.get(*slot) {
570                        return Ok(ColumnarValue::Column(col.to_values()));
571                    }
572                }
573                let mut col = Vec::with_capacity(len);
574                for row in rows {
575                    col.push(
576                        row.get(*slot)
577                            .cloned()
578                            .ok_or_else(|| ExpressionError::undefined_variable("column slot"))?,
579                    );
580                }
581                Ok(ColumnarValue::Column(col))
582            }
583
584            CompiledExpr::Unary { op, operand } => {
585                let operand = operand.evaluate_batch(rows, layout, env, typed_columns)?;
586                match operand {
587                    ColumnarValue::Const(v) => Ok(ColumnarValue::Const(
588                        UnaryOperationEvaluator::evaluate(op, &v)?,
589                    )),
590                    ColumnarValue::Column(col) => {
591                        let mut out = Vec::with_capacity(col.len());
592                        for v in col {
593                            out.push(UnaryOperationEvaluator::evaluate(op, &v)?);
594                        }
595                        Ok(ColumnarValue::Column(out))
596                    }
597                }
598            }
599
600            CompiledExpr::Binary { op, left, right } => {
601                let left = left.evaluate_batch(rows, layout.clone(), env, typed_columns)?;
602                let right = right.evaluate_batch(rows, layout, env, typed_columns)?;
603                match (left, right) {
604                    (ColumnarValue::Const(l), ColumnarValue::Const(r)) => Ok(ColumnarValue::Const(
605                        BinaryOperationEvaluator::evaluate(&l, op, &r)?,
606                    )),
607                    (ColumnarValue::Const(l), ColumnarValue::Column(r)) => {
608                        let mut out = Vec::with_capacity(r.len());
609                        for v in r {
610                            out.push(BinaryOperationEvaluator::evaluate(&l, op, &v)?);
611                        }
612                        Ok(ColumnarValue::Column(out))
613                    }
614                    (ColumnarValue::Column(l), ColumnarValue::Const(r)) => {
615                        let mut out = Vec::with_capacity(l.len());
616                        for v in l {
617                            out.push(BinaryOperationEvaluator::evaluate(&v, op, &r)?);
618                        }
619                        Ok(ColumnarValue::Column(out))
620                    }
621                    (ColumnarValue::Column(l), ColumnarValue::Column(r)) => {
622                        let mut out = Vec::with_capacity(l.len());
623                        for (lv, rv) in l.into_iter().zip(r) {
624                            out.push(BinaryOperationEvaluator::evaluate(&lv, op, &rv)?);
625                        }
626                        Ok(ColumnarValue::Column(out))
627                    }
628                }
629            }
630
631            // Short-circuit semantics (CASE, subqueries, per-row collection
632            // access) run through the per-row evaluator. Parameters resolve
633            // once to a shared constant; a missing parameter keeps the exact
634            // per-row error by evaluating the first row through the scalar
635            // path.
636            CompiledExpr::Parameter(name) => match env
637                .and_then(|e| e.params.as_ref())
638                .and_then(|p| p.get(name).cloned())
639            {
640                Some(value) => Ok(ColumnarValue::Const(value)),
641                None => {
642                    let first = rows.first().cloned().unwrap_or_default();
643                    self.evaluate(&first, layout, env)?;
644                    Ok(ColumnarValue::Column(Vec::new()))
645                }
646            },
647            CompiledExpr::SessionVariable(name) => match env
648                .and_then(|e| e.session_variables.as_ref())
649                .and_then(|v| v.get(name).cloned())
650            {
651                Some(value) => Ok(ColumnarValue::Const(value)),
652                None => {
653                    let first = rows.first().cloned().unwrap_or_default();
654                    self.evaluate(&first, layout, env)?;
655                    Ok(ColumnarValue::Column(Vec::new()))
656                }
657            },
658            _ => {
659                let mut out = Vec::with_capacity(len);
660                for row in rows {
661                    out.push(self.evaluate(row, layout.clone(), env)?);
662                }
663                Ok(ColumnarValue::Column(out))
664            }
665        }
666    }
667}
668
669#[cfg(test)]
670mod tests {
671    use super::*;
672    use crate::executor::streaming::chunk::DataChunk;
673    use crate::executor::streaming::slot::SlotLayout;
674    use linkrs_core::types::operators::{BinaryOperator, UnaryOperator};
675
676    fn layout(names: &[&str]) -> Arc<SlotLayout> {
677        Arc::new(SlotLayout::from_names(
678            &names.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
679        ))
680    }
681
682    fn eval_once(expr: &Expression, row: Vec<Value>, names: &[&str]) -> Value {
683        let layout = layout(names);
684        let compiled = CompiledExpr::compile(expr, &layout);
685        compiled
686            .evaluate(&row, layout, None)
687            .expect("evaluation should succeed")
688    }
689
690    #[test]
691    fn literal_compiles_to_const() {
692        let expr = Expression::Literal(Value::Int(42));
693        let layout = layout(&["a"]);
694        let compiled = CompiledExpr::compile(&expr, &layout);
695        assert!(matches!(compiled, CompiledExpr::Const(_)));
696        assert_eq!(
697            eval_once(&expr, vec![Value::Int(1)], &["a"]),
698            Value::Int(42)
699        );
700    }
701
702    #[test]
703    fn variable_resolves_to_slot() {
704        let expr = Expression::Variable("a".to_string());
705        let layout = layout(&["a", "b"]);
706        let compiled = CompiledExpr::compile(&expr, &layout);
707        assert!(matches!(compiled, CompiledExpr::Slot(0)));
708        assert_eq!(
709            eval_once(&expr, vec![Value::Int(7), Value::Int(9)], &["a", "b"]),
710            Value::Int(7)
711        );
712    }
713
714    #[test]
715    fn property_resolves_to_compound_slot() {
716        let expr = Expression::Property {
717            object: Box::new(Expression::Variable("p".to_string())),
718            property: "age".to_string(),
719        };
720        let layout = layout(&["id", "p.age"]);
721        let compiled = CompiledExpr::compile(&expr, &layout);
722        assert!(matches!(compiled, CompiledExpr::CompoundSlot(1)));
723        assert_eq!(
724            eval_once(&expr, vec![Value::Int(1), Value::Int(30)], &["id", "p.age"]),
725            Value::Int(30)
726        );
727    }
728
729    #[test]
730    fn binary_folds_constants_at_compile_time() {
731        let expr = Expression::Binary {
732            left: Box::new(Expression::Literal(Value::Int(2))),
733            op: BinaryOperator::Add,
734            right: Box::new(Expression::Literal(Value::Int(3))),
735        };
736        let layout = layout(&["a"]);
737        let compiled = CompiledExpr::compile(&expr, &layout);
738        assert!(matches!(compiled, CompiledExpr::Const(Value::Int(5))));
739        assert_eq!(eval_once(&expr, vec![Value::Int(0)], &["a"]), Value::Int(5));
740    }
741
742    #[test]
743    fn binary_with_variable_stays_unfolded() {
744        let expr = Expression::Binary {
745            left: Box::new(Expression::Variable("a".to_string())),
746            op: BinaryOperator::Add,
747            right: Box::new(Expression::Literal(Value::Int(1))),
748        };
749        let layout = layout(&["a"]);
750        let compiled = CompiledExpr::compile(&expr, &layout);
751        assert!(matches!(compiled, CompiledExpr::Binary { .. }));
752        assert_eq!(
753            eval_once(&expr, vec![Value::Int(10)], &["a"]),
754            Value::Int(11)
755        );
756    }
757
758    #[test]
759    fn null_semantics_match_scalar_evaluator() {
760        // The compiled path reuses the exact binary evaluator, so NULL
761        // handling must be identical to the scalar path.  Assert against the
762        // scalar result rather than assuming three-valued logic.
763        let expr = Expression::Binary {
764            left: Box::new(Expression::Variable("a".to_string())),
765            op: BinaryOperator::Equal,
766            right: Box::new(Expression::Literal(Value::Int(1))),
767        };
768        let layout = layout(&["a"]);
769        let row = vec![Value::Null(NullType::Null)];
770        let compiled = CompiledExpr::compile(&expr, &layout);
771        let compiled_result = compiled
772            .evaluate(&row, layout.clone(), None)
773            .expect("evaluation should succeed");
774
775        let mut ctx = BorrowedRowContext::new(&row, layout);
776        let scalar_result =
777            ExpressionEvaluator::evaluate(&expr, &mut ctx).expect("evaluation should succeed");
778        assert_eq!(compiled_result, scalar_result);
779    }
780
781    #[test]
782    fn unary_folds_constant() {
783        let expr = Expression::Unary {
784            op: UnaryOperator::Minus,
785            operand: Box::new(Expression::Literal(Value::Int(4))),
786        };
787        let layout = layout(&["a"]);
788        let compiled = CompiledExpr::compile(&expr, &layout);
789        assert!(matches!(compiled, CompiledExpr::Const(Value::Int(-4))));
790    }
791
792    #[test]
793    fn impure_function_is_not_folded() {
794        let expr = Expression::Function {
795            name: "rand".to_string(),
796            args: vec![],
797        };
798        let layout = layout(&["a"]);
799        let compiled = CompiledExpr::compile(&expr, &layout);
800        // `rand` is impure: the call stays as a Function node, never a Const.
801        assert!(matches!(compiled, CompiledExpr::Function { .. }));
802    }
803
804    #[test]
805    fn unknown_variable_falls_back_to_interpreter() {
806        // A variable that is not in the layout keeps the original expression
807        // so the interpreter reports the same undefined-variable error.
808        let expr = Expression::Variable("missing".to_string());
809        let layout = layout(&["a"]);
810        let compiled = CompiledExpr::compile(&expr, &layout);
811        assert!(matches!(compiled, CompiledExpr::Fallback(_)));
812        let err = compiled
813            .evaluate(&[Value::Int(1)], layout, None)
814            .unwrap_err();
815        assert!(err.to_string().contains("missing"));
816    }
817
818    #[test]
819    fn batch_evaluation_matches_per_row() {
820        let expr = Expression::Binary {
821            left: Box::new(Expression::Variable("a".to_string())),
822            op: BinaryOperator::Add,
823            right: Box::new(Expression::Literal(Value::Int(10))),
824        };
825        let layout = layout(&["a"]);
826        let rows = vec![
827            vec![Value::Int(1)],
828            vec![Value::Int(2)],
829            vec![Value::Int(3)],
830        ];
831        let compiled = CompiledExpr::compile(&expr, &layout);
832        let batch = compiled
833            .evaluate_batch(&rows, layout.clone(), None, None)
834            .expect("batch evaluation should succeed")
835            .into_values(rows.len());
836        assert_eq!(batch, vec![Value::Int(11), Value::Int(12), Value::Int(13)]);
837    }
838
839    #[test]
840    fn batch_const_never_materializes() {
841        let expr = Expression::Literal(Value::Bool(true));
842        let layout = layout(&["a"]);
843        let rows = vec![vec![Value::Int(1)], vec![Value::Int(2)]];
844        let compiled = CompiledExpr::compile(&expr, &layout);
845        let batch = compiled
846            .evaluate_batch(&rows, layout, None, None)
847            .expect("batch evaluation should succeed");
848        assert!(matches!(batch, ColumnarValue::Const(_)));
849    }
850
851    #[test]
852    fn batch_parameter_broadcasts_const() {
853        use crate::executor::streaming::subquery::EvalEnv;
854        use std::collections::HashMap;
855        use std::sync::Arc;
856
857        let expr = Expression::Parameter("x".to_string());
858        let layout = layout(&["a"]);
859        let rows = vec![vec![Value::Int(1)], vec![Value::Int(2)]];
860        let compiled = CompiledExpr::compile(&expr, &layout);
861        let mut params = HashMap::new();
862        params.insert("x".into(), Value::Int(9));
863        let env = EvalEnv {
864            params: Some(Arc::new(params)),
865            session_variables: None,
866            subquery_executor: None,
867            sequence_manager: None,
868        };
869        let batch = compiled
870            .evaluate_batch(&rows, layout, Some(&env), None)
871            .expect("batch evaluation should succeed");
872        assert!(matches!(batch, ColumnarValue::Const(Value::Int(9))));
873    }
874
875    #[test]
876    fn batch_missing_parameter_preserves_error() {
877        let expr = Expression::Parameter("missing".to_string());
878        let layout = layout(&["a"]);
879        let rows = vec![vec![Value::Int(1)]];
880        let compiled = CompiledExpr::compile(&expr, &layout);
881        let err = compiled
882            .evaluate_batch(&rows, layout, None, None)
883            .expect_err("missing parameter must fail");
884        assert!(err.to_string().contains("missing"));
885    }
886
887    #[test]
888    fn fallback_switch_toggles_compiled_path() {
889        assert!(compiled_eval_enabled());
890        set_compiled_eval_enabled(false);
891        assert!(!compiled_eval_enabled());
892        set_compiled_eval_enabled(true);
893        assert!(compiled_eval_enabled());
894    }
895
896    #[test]
897    fn subquery_stays_fallback() {
898        let expr = Expression::Exists {
899            body: Box::new(linkrs_core::types::expr::SubqueryBody {
900                id: 0,
901                patterns: vec![],
902                where_clause: None,
903                return_expr: None,
904            }),
905        };
906        let layout = layout(&["a"]);
907        let compiled = CompiledExpr::compile(&expr, &layout);
908        assert!(matches!(compiled, CompiledExpr::Fallback(_)));
909    }
910
911    #[test]
912    fn case_short_circuits() {
913        let expr = Expression::Case {
914            test_expr: None,
915            conditions: vec![
916                (
917                    Expression::Binary {
918                        left: Box::new(Expression::Variable("a".to_string())),
919                        op: BinaryOperator::GreaterThan,
920                        right: Box::new(Expression::Literal(Value::Int(5))),
921                    },
922                    Expression::Literal(Value::string("big")),
923                ),
924                (
925                    Expression::Literal(Value::Bool(true)),
926                    Expression::Literal(Value::string("small")),
927                ),
928            ],
929            default: Some(Box::new(Expression::Literal(Value::Null(NullType::Null)))),
930        };
931        assert_eq!(
932            eval_once(&expr, vec![Value::Int(9)], &["a"]),
933            Value::string("big")
934        );
935        assert_eq!(
936            eval_once(&expr, vec![Value::Int(1)], &["a"]),
937            Value::string("small")
938        );
939    }
940
941    #[test]
942    fn batch_mixed_expression_reads_typed_slots() {
943        // Mixed expression: both slot leaves are typed, the binary node
944        // itself stays on the rows path. Typed slot reads must agree with
945        // the rows-only baseline exactly.
946        let expr = Expression::Binary {
947            left: Box::new(Expression::Variable("x".to_string())),
948            op: BinaryOperator::Add,
949            right: Box::new(Expression::Variable("y".to_string())),
950        };
951        let layout = layout(&["x", "y"]);
952        let rows = vec![
953            vec![Value::Int(1), Value::Int(10)],
954            vec![Value::Int(2), Value::Int(20)],
955        ];
956        let compiled = CompiledExpr::compile(&expr, &layout);
957        let baseline = compiled
958            .evaluate_batch(&rows, layout.clone(), None, None)
959            .expect("batch evaluation should succeed")
960            .into_values(rows.len());
961        let mut chunk = DataChunk::new_with_layout(rows.clone(), layout.clone());
962        chunk.build_typed_columns(true);
963        assert!(
964            chunk.typed_columns.is_some(),
965            "typed layout must be built for the test"
966        );
967        let typed = compiled
968            .evaluate_batch(&rows, layout, None, chunk.typed_columns.as_deref())
969            .expect("batch evaluation should succeed")
970            .into_values(rows.len());
971        assert_eq!(typed, baseline);
972        assert_eq!(typed, vec![Value::Int(11), Value::Int(22)]);
973    }
974}