Skip to main content

uqa_execution/
scalar.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! AST-independent scalar physical IR shared by the planner and executors.
8
9use uqa_core::{ArrayValue, Value};
10use uqa_sql::ast::{BinaryOp, FrameMode, FunctionBinding, NullsOrder};
11use uqa_sql::expr::{
12    cast_value_from, eval_binary_values, eval_binary_values_with_integer_width, eval_function_call,
13    integer_width_for_literal, integer_width_for_type, negate_value, truthy, EngineHook,
14    EvalContext, IntegerWidth, RowLookup, NAMED_ARG_FUNCTION,
15};
16use uqa_sql::{ResultRow, SQLError, SQLParam};
17
18use crate::batch::{OwnedPhysicalRow, PhysicalRow, RowSchema};
19
20/// Index into the query children owned by the enclosing expression plan.
21pub type SubqueryId = usize;
22
23#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
24pub enum ScalarExpr {
25    Star,
26    QualifiedStar(String),
27    Default,
28    Column(String),
29    /// Logical position in an already-bound physical row schema. This variant is introduced only after relational binding so duplicate SQL labels remain independently addressable.
30    Position(usize),
31    QualifiedColumn {
32        qualifier: String,
33        column: String,
34    },
35    Literal(Value),
36    Param(usize),
37    Func {
38        name: String,
39        #[serde(default, skip_serializing_if = "Option::is_none")]
40        binding: Option<FunctionBinding>,
41        args: Vec<Self>,
42        distinct: bool,
43        order_by: Vec<ScalarOrder>,
44        filter: Option<Box<Self>>,
45    },
46    Array(Vec<Self>),
47    Row(Vec<Self>),
48    Binary {
49        op: BinaryOp,
50        lhs: Box<Self>,
51        rhs: Box<Self>,
52    },
53    UnaryMinus(Box<Self>),
54    Not(Box<Self>),
55    And(Vec<Self>),
56    Or(Vec<Self>),
57    IsNull {
58        expr: Box<Self>,
59        negated: bool,
60    },
61    Between {
62        expr: Box<Self>,
63        low: Box<Self>,
64        high: Box<Self>,
65    },
66    InList {
67        expr: Box<Self>,
68        list: Vec<Self>,
69        negated: bool,
70    },
71    WindowCall {
72        name: String,
73        args: Vec<Self>,
74        spec: ScalarWindowSpec,
75    },
76    Case {
77        base: Option<Box<Self>>,
78        when: Vec<(Self, Self)>,
79        else_branch: Option<Box<Self>>,
80    },
81    Cast {
82        expr: Box<Self>,
83        ty: String,
84    },
85    ScalarSubquery(SubqueryId),
86    Exists {
87        subquery: SubqueryId,
88        negated: bool,
89    },
90    InSubquery {
91        expr: Box<Self>,
92        subquery: SubqueryId,
93        negated: bool,
94    },
95}
96
97#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
98pub struct ScalarOrder {
99    pub expr: ScalarExpr,
100    pub descending: bool,
101    pub nulls: Option<NullsOrder>,
102}
103
104#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
105pub struct ScalarWindowSpec {
106    pub partition_by: Vec<ScalarExpr>,
107    pub order_by: Vec<ScalarOrder>,
108    pub frame: Option<ScalarWindowFrame>,
109}
110
111#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
112pub struct ScalarWindowFrame {
113    pub mode: FrameMode,
114    pub start: ScalarFrameBound,
115    pub end: ScalarFrameBound,
116}
117
118#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
119pub enum ScalarFrameBound {
120    UnboundedPreceding,
121    UnboundedFollowing,
122    CurrentRow,
123    Preceding(Box<ScalarExpr>),
124    Following(Box<ScalarExpr>),
125}
126
127impl ScalarExpr {
128    #[must_use]
129    pub fn qualified_column(qualifier: impl Into<String>, column: impl Into<String>) -> Self {
130        Self::QualifiedColumn {
131            qualifier: qualifier.into(),
132            column: column.into(),
133        }
134    }
135
136    /// Collect every column needed to evaluate this expression. Returns
137    /// `false` when evaluation needs row shape or a relational child that a
138    /// projected field scan cannot provide.
139    pub fn collect_columns(&self, output: &mut std::collections::BTreeSet<String>) -> bool {
140        match self {
141            Self::Column(name) | Self::QualifiedColumn { column: name, .. } => {
142                output.insert(name.clone());
143                true
144            }
145            Self::Literal(_) | Self::Param(_) => true,
146            Self::Func {
147                args,
148                order_by,
149                filter,
150                ..
151            } => {
152                args.iter().all(|arg| arg.collect_columns(output))
153                    && order_by
154                        .iter()
155                        .all(|order| order.expr.collect_columns(output))
156                    && filter
157                        .as_deref()
158                        .is_none_or(|filter| filter.collect_columns(output))
159            }
160            Self::Array(items) | Self::Row(items) | Self::And(items) | Self::Or(items) => {
161                items.iter().all(|item| item.collect_columns(output))
162            }
163            Self::Binary { lhs, rhs, .. } => {
164                lhs.collect_columns(output) && rhs.collect_columns(output)
165            }
166            Self::UnaryMinus(expr)
167            | Self::Not(expr)
168            | Self::IsNull { expr, .. }
169            | Self::Cast { expr, .. } => expr.collect_columns(output),
170            Self::Between { expr, low, high } => {
171                expr.collect_columns(output)
172                    && low.collect_columns(output)
173                    && high.collect_columns(output)
174            }
175            Self::InList { expr, list, .. } => {
176                expr.collect_columns(output) && list.iter().all(|item| item.collect_columns(output))
177            }
178            Self::Case {
179                base,
180                when,
181                else_branch,
182            } => {
183                base.as_deref()
184                    .is_none_or(|base| base.collect_columns(output))
185                    && when.iter().all(|(condition, result)| {
186                        condition.collect_columns(output) && result.collect_columns(output)
187                    })
188                    && else_branch
189                        .as_deref()
190                        .is_none_or(|branch| branch.collect_columns(output))
191            }
192            Self::Default
193            | Self::Star
194            | Self::QualifiedStar(_)
195            | Self::Position(_)
196            | Self::WindowCall { .. }
197            | Self::ScalarSubquery(_)
198            | Self::Exists { .. }
199            | Self::InSubquery { .. } => false,
200        }
201    }
202
203    #[must_use]
204    pub fn contains_window(&self) -> bool {
205        match self {
206            Self::WindowCall { .. } => true,
207            Self::Func {
208                args,
209                order_by,
210                filter,
211                ..
212            } => {
213                args.iter().any(Self::contains_window)
214                    || order_by.iter().any(|order| order.expr.contains_window())
215                    || filter.as_deref().is_some_and(Self::contains_window)
216            }
217            Self::Array(items) | Self::Row(items) | Self::And(items) | Self::Or(items) => {
218                items.iter().any(Self::contains_window)
219            }
220            Self::Binary { lhs, rhs, .. } => lhs.contains_window() || rhs.contains_window(),
221            Self::UnaryMinus(expr)
222            | Self::Not(expr)
223            | Self::IsNull { expr, .. }
224            | Self::Cast { expr, .. }
225            | Self::InSubquery { expr, .. } => expr.contains_window(),
226            Self::Between { expr, low, high } => {
227                expr.contains_window() || low.contains_window() || high.contains_window()
228            }
229            Self::InList { expr, list, .. } => {
230                expr.contains_window() || list.iter().any(Self::contains_window)
231            }
232            Self::Case {
233                base,
234                when,
235                else_branch,
236            } => {
237                base.as_deref().is_some_and(Self::contains_window)
238                    || when.iter().any(|(condition, result)| {
239                        condition.contains_window() || result.contains_window()
240                    })
241                    || else_branch.as_deref().is_some_and(Self::contains_window)
242            }
243            Self::Default
244            | Self::Star
245            | Self::QualifiedStar(_)
246            | Self::Column(_)
247            | Self::QualifiedColumn { .. }
248            | Self::Position(_)
249            | Self::Literal(_)
250            | Self::Param(_)
251            | Self::ScalarSubquery(_)
252            | Self::Exists { .. } => false,
253        }
254    }
255
256    #[must_use]
257    pub fn contains_subquery(&self) -> bool {
258        match self {
259            Self::ScalarSubquery(_) | Self::Exists { .. } | Self::InSubquery { .. } => true,
260            Self::Func {
261                args,
262                order_by,
263                filter,
264                ..
265            } => {
266                args.iter().any(Self::contains_subquery)
267                    || order_by.iter().any(|order| order.expr.contains_subquery())
268                    || filter.as_deref().is_some_and(Self::contains_subquery)
269            }
270            Self::Array(items) | Self::Row(items) | Self::And(items) | Self::Or(items) => {
271                items.iter().any(Self::contains_subquery)
272            }
273            Self::Binary { lhs, rhs, .. } => lhs.contains_subquery() || rhs.contains_subquery(),
274            Self::UnaryMinus(expr)
275            | Self::Not(expr)
276            | Self::IsNull { expr, .. }
277            | Self::Cast { expr, .. } => expr.contains_subquery(),
278            Self::Between { expr, low, high } => {
279                expr.contains_subquery() || low.contains_subquery() || high.contains_subquery()
280            }
281            Self::InList { expr, list, .. } => {
282                expr.contains_subquery() || list.iter().any(Self::contains_subquery)
283            }
284            Self::WindowCall { args, spec, .. } => {
285                args.iter().any(Self::contains_subquery)
286                    || spec.partition_by.iter().any(Self::contains_subquery)
287                    || spec
288                        .order_by
289                        .iter()
290                        .any(|order| order.expr.contains_subquery())
291            }
292            Self::Case {
293                base,
294                when,
295                else_branch,
296            } => {
297                base.as_deref().is_some_and(Self::contains_subquery)
298                    || when.iter().any(|(condition, result)| {
299                        condition.contains_subquery() || result.contains_subquery()
300                    })
301                    || else_branch.as_deref().is_some_and(Self::contains_subquery)
302            }
303            Self::Default
304            | Self::Star
305            | Self::QualifiedStar(_)
306            | Self::Column(_)
307            | Self::QualifiedColumn { .. }
308            | Self::Position(_)
309            | Self::Literal(_)
310            | Self::Param(_) => false,
311        }
312    }
313
314    #[must_use]
315    pub fn contains_parameter(&self) -> bool {
316        match self {
317            Self::Param(_) => true,
318            Self::Func {
319                args,
320                order_by,
321                filter,
322                ..
323            } => {
324                args.iter().any(Self::contains_parameter)
325                    || order_by.iter().any(|order| order.expr.contains_parameter())
326                    || filter.as_deref().is_some_and(Self::contains_parameter)
327            }
328            Self::Array(items) | Self::Row(items) | Self::And(items) | Self::Or(items) => {
329                items.iter().any(Self::contains_parameter)
330            }
331            Self::Binary { lhs, rhs, .. } => lhs.contains_parameter() || rhs.contains_parameter(),
332            Self::UnaryMinus(expr)
333            | Self::Not(expr)
334            | Self::IsNull { expr, .. }
335            | Self::Cast { expr, .. }
336            | Self::InSubquery { expr, .. } => expr.contains_parameter(),
337            Self::Between { expr, low, high } => {
338                expr.contains_parameter() || low.contains_parameter() || high.contains_parameter()
339            }
340            Self::InList { expr, list, .. } => {
341                expr.contains_parameter() || list.iter().any(Self::contains_parameter)
342            }
343            Self::WindowCall { args, spec, .. } => {
344                args.iter().any(Self::contains_parameter)
345                    || spec.partition_by.iter().any(Self::contains_parameter)
346                    || spec
347                        .order_by
348                        .iter()
349                        .any(|order| order.expr.contains_parameter())
350                    || spec.frame.as_ref().is_some_and(|frame| {
351                        scalar_frame_bound_contains_parameter(&frame.start)
352                            || scalar_frame_bound_contains_parameter(&frame.end)
353                    })
354            }
355            Self::Case {
356                base,
357                when,
358                else_branch,
359            } => {
360                base.as_deref().is_some_and(Self::contains_parameter)
361                    || when.iter().any(|(condition, result)| {
362                        condition.contains_parameter() || result.contains_parameter()
363                    })
364                    || else_branch.as_deref().is_some_and(Self::contains_parameter)
365            }
366            Self::Default
367            | Self::Star
368            | Self::QualifiedStar(_)
369            | Self::Column(_)
370            | Self::QualifiedColumn { .. }
371            | Self::Position(_)
372            | Self::Literal(_)
373            | Self::ScalarSubquery(_)
374            | Self::Exists { .. } => false,
375        }
376    }
377
378    #[must_use]
379    pub fn contains_aggregate(&self, is_aggregate: &dyn Fn(&str) -> bool) -> bool {
380        match self {
381            Self::Func {
382                name,
383                args,
384                order_by,
385                filter,
386                ..
387            } => {
388                is_aggregate(name)
389                    || args
390                        .iter()
391                        .any(|expression| expression.contains_aggregate(is_aggregate))
392                    || order_by
393                        .iter()
394                        .any(|order| order.expr.contains_aggregate(is_aggregate))
395                    || filter
396                        .as_deref()
397                        .is_some_and(|expression| expression.contains_aggregate(is_aggregate))
398            }
399            Self::Array(items) | Self::Row(items) | Self::And(items) | Self::Or(items) => items
400                .iter()
401                .any(|expression| expression.contains_aggregate(is_aggregate)),
402            Self::Binary { lhs, rhs, .. } => {
403                lhs.contains_aggregate(is_aggregate) || rhs.contains_aggregate(is_aggregate)
404            }
405            Self::UnaryMinus(expr)
406            | Self::Not(expr)
407            | Self::IsNull { expr, .. }
408            | Self::Cast { expr, .. }
409            | Self::InSubquery { expr, .. } => expr.contains_aggregate(is_aggregate),
410            Self::Between { expr, low, high } => {
411                expr.contains_aggregate(is_aggregate)
412                    || low.contains_aggregate(is_aggregate)
413                    || high.contains_aggregate(is_aggregate)
414            }
415            Self::InList { expr, list, .. } => {
416                expr.contains_aggregate(is_aggregate)
417                    || list
418                        .iter()
419                        .any(|item| item.contains_aggregate(is_aggregate))
420            }
421            Self::Case {
422                base,
423                when,
424                else_branch,
425            } => {
426                base.as_deref()
427                    .is_some_and(|expression| expression.contains_aggregate(is_aggregate))
428                    || when.iter().any(|(condition, result)| {
429                        condition.contains_aggregate(is_aggregate)
430                            || result.contains_aggregate(is_aggregate)
431                    })
432                    || else_branch
433                        .as_deref()
434                        .is_some_and(|expression| expression.contains_aggregate(is_aggregate))
435            }
436            Self::Default
437            | Self::Star
438            | Self::QualifiedStar(_)
439            | Self::Column(_)
440            | Self::QualifiedColumn { .. }
441            | Self::Position(_)
442            | Self::Literal(_)
443            | Self::Param(_)
444            | Self::ScalarSubquery(_)
445            | Self::Exists { .. }
446            | Self::WindowCall { .. } => false,
447        }
448    }
449}
450
451fn scalar_frame_bound_contains_parameter(bound: &ScalarFrameBound) -> bool {
452    match bound {
453        ScalarFrameBound::Preceding(expression) | ScalarFrameBound::Following(expression) => {
454            expression.contains_parameter()
455        }
456        ScalarFrameBound::UnboundedPreceding
457        | ScalarFrameBound::UnboundedFollowing
458        | ScalarFrameBound::CurrentRow => false,
459    }
460}
461
462/// Runtime callback for query children referenced by [`SubqueryId`]. The
463/// planner owns the actual query-plan arena; execution only needs this stable
464/// slot interface.
465pub trait ScalarSubqueryRunner {
466    fn execute_subquery(
467        &self,
468        subquery: SubqueryId,
469        outer_row: Option<&dyn RowLookup>,
470        params: &[SQLParam],
471    ) -> Result<SubqueryResult, SQLError>;
472
473    fn execute_subquery_physical(
474        &self,
475        subquery: SubqueryId,
476        outer_schema: &RowSchema,
477        outer_row: &PhysicalRow,
478        params: &[SQLParam],
479    ) -> Result<SubqueryResult, SQLError> {
480        let outer = outer_schema.view(outer_row);
481        self.execute_subquery(subquery, Some(&outer), params)
482    }
483
484    fn scalar_subquery_value(
485        &self,
486        subquery: SubqueryId,
487        outer_row: Option<&dyn RowLookup>,
488        params: &[SQLParam],
489    ) -> Result<Value, SQLError> {
490        self.execute_subquery(subquery, outer_row, params)?
491            .into_scalar_value()
492    }
493
494    fn scalar_subquery_value_physical(
495        &self,
496        subquery: SubqueryId,
497        outer_schema: &RowSchema,
498        outer_row: &PhysicalRow,
499        params: &[SQLParam],
500    ) -> Result<Value, SQLError> {
501        self.execute_subquery_physical(subquery, outer_schema, outer_row, params)?
502            .into_scalar_value()
503    }
504
505    fn subquery_exists(
506        &self,
507        subquery: SubqueryId,
508        outer_row: Option<&dyn RowLookup>,
509        params: &[SQLParam],
510    ) -> Result<bool, SQLError> {
511        self.execute_subquery(subquery, outer_row, params)?
512            .into_exists()
513    }
514
515    fn subquery_exists_physical(
516        &self,
517        subquery: SubqueryId,
518        outer_schema: &RowSchema,
519        outer_row: &PhysicalRow,
520        params: &[SQLParam],
521    ) -> Result<bool, SQLError> {
522        self.execute_subquery_physical(subquery, outer_schema, outer_row, params)?
523            .into_exists()
524    }
525
526    fn subquery_contains(
527        &self,
528        subquery: SubqueryId,
529        needle: &Value,
530        outer_row: Option<&dyn RowLookup>,
531        params: &[SQLParam],
532    ) -> Result<Option<bool>, SQLError> {
533        self.execute_subquery(subquery, outer_row, params)?
534            .contains(needle)
535    }
536
537    fn subquery_contains_physical(
538        &self,
539        subquery: SubqueryId,
540        needle: &Value,
541        outer_schema: &RowSchema,
542        outer_row: &PhysicalRow,
543        params: &[SQLParam],
544    ) -> Result<Option<bool>, SQLError> {
545        self.execute_subquery_physical(subquery, outer_schema, outer_row, params)?
546            .contains(needle)
547    }
548}
549
550/// Pull-based scalar-subquery result. Scalar, EXISTS, and IN consumers never
551/// need to materialize the complete child relation: they respectively inspect
552/// at most two rows, one row, or one row at a time.
553pub struct SubqueryResult {
554    pub columns: Vec<String>,
555    pub rows: Box<dyn Iterator<Item = Result<OwnedPhysicalRow, SQLError>> + Send>,
556}
557
558impl SubqueryResult {
559    pub fn from_rows(columns: Vec<String>, rows: Vec<ResultRow>) -> Self {
560        let schema = RowSchema::new(columns.clone());
561        Self {
562            columns,
563            rows: Box::new(rows.into_iter().map(move |row| {
564                Ok(OwnedPhysicalRow::new(
565                    schema.clone(),
566                    PhysicalRow::from_result_row(&schema, row),
567                ))
568            })),
569        }
570    }
571
572    pub fn into_scalar_value(mut self) -> Result<Value, SQLError> {
573        let Some(first_row) = self.rows.next().transpose()? else {
574            return Ok(Value::Null);
575        };
576        if self.rows.next().transpose()?.is_some() {
577            return Err(SQLError::TypeMismatch(
578                "scalar subquery returned more than one row".into(),
579            ));
580        }
581        if self.columns.is_empty() {
582            return Err(SQLError::TypeMismatch(
583                "scalar subquery returned no columns".into(),
584            ));
585        }
586        Ok(first_row
587            .positional_column(0)
588            .cloned()
589            .unwrap_or(Value::Null))
590    }
591
592    pub fn into_exists(mut self) -> Result<bool, SQLError> {
593        Ok(self.rows.next().transpose()?.is_some())
594    }
595
596    pub fn contains(self, needle: &Value) -> Result<Option<bool>, SQLError> {
597        if self.columns.is_empty() {
598            return Ok(Some(false));
599        }
600        let mut saw_row = false;
601        let mut saw_null = false;
602        for row in self.rows {
603            let row = row?;
604            saw_row = true;
605            match row.positional_column(0) {
606                Some(Value::Null) | None => saw_null = true,
607                Some(value) if !matches!(needle, Value::Null) && value == needle => {
608                    return Ok(Some(true));
609                }
610                Some(_) => {}
611            }
612        }
613        Ok(if !saw_row {
614            Some(false)
615        } else if matches!(needle, Value::Null) || saw_null {
616            None
617        } else {
618            Some(false)
619        })
620    }
621}
622
623pub struct ScalarEvalContext<'a> {
624    row: Option<&'a ResultRow>,
625    row_lookup: Option<&'a dyn RowLookup>,
626    params: &'a [SQLParam],
627    function_hook: Option<&'a dyn EngineHook>,
628    subquery_runner: Option<&'a dyn ScalarSubqueryRunner>,
629    physical_outer_row: Option<(&'a RowSchema, &'a PhysicalRow)>,
630}
631
632impl<'a> ScalarEvalContext<'a> {
633    #[must_use]
634    pub fn new(row: Option<&'a ResultRow>, params: &'a [SQLParam]) -> Self {
635        Self {
636            row,
637            row_lookup: row.map(|row| row as &dyn RowLookup),
638            params,
639            function_hook: None,
640            subquery_runner: None,
641            physical_outer_row: None,
642        }
643    }
644
645    #[must_use]
646    pub fn from_row_lookup(row: &'a dyn RowLookup, params: &'a [SQLParam]) -> Self {
647        Self {
648            row: None,
649            row_lookup: Some(row),
650            params,
651            function_hook: None,
652            subquery_runner: None,
653            physical_outer_row: None,
654        }
655    }
656
657    #[must_use]
658    pub fn with_function_hook(mut self, hook: &'a dyn EngineHook) -> Self {
659        self.function_hook = Some(hook);
660        self
661    }
662
663    #[must_use]
664    pub fn with_subquery_runner(mut self, runner: &'a dyn ScalarSubqueryRunner) -> Self {
665        self.subquery_runner = Some(runner);
666        self
667    }
668
669    #[must_use]
670    pub fn with_physical_outer_row(mut self, schema: &'a RowSchema, row: &'a PhysicalRow) -> Self {
671        self.physical_outer_row = Some((schema, row));
672        self
673    }
674
675    fn sql_context(&self) -> EvalContext<'_> {
676        let context = self.row_lookup.map_or_else(
677            || EvalContext::new(self.row, self.params),
678            |row| EvalContext::from_row_lookup(row, self.params),
679        );
680        match self.function_hook {
681            Some(hook) => context.with_engine(hook),
682            None => context,
683        }
684    }
685
686    fn outer_row(&self) -> Option<&dyn RowLookup> {
687        self.row_lookup
688    }
689}
690
691/// Evaluate the physical scalar tree directly. No parser expression is
692/// reconstructed at this boundary.
693pub fn eval_scalar(
694    expression: &ScalarExpr,
695    context: &ScalarEvalContext<'_>,
696) -> Result<Value, SQLError> {
697    match expression {
698        ScalarExpr::Default => Err(SQLError::Internal(
699            "DEFAULT reached scalar expression evaluation without a mutation target".into(),
700        )),
701        ScalarExpr::Star | ScalarExpr::QualifiedStar(_) => {
702            Err(SQLError::Internal("`*` cannot be evaluated".into()))
703        }
704        ScalarExpr::Column(name) => context.sql_context().column_value(name),
705        ScalarExpr::Position(position) => context
706            .row_lookup
707            .and_then(|row| row.positional_column(*position))
708            .cloned()
709            .ok_or_else(|| {
710                SQLError::Internal(format!(
711                    "bound physical column position {position} is unavailable"
712                ))
713            }),
714        ScalarExpr::QualifiedColumn { qualifier, column } => context
715            .sql_context()
716            .qualified_column_value(qualifier, column),
717        ScalarExpr::Literal(value) => Ok(value.clone()),
718        ScalarExpr::Param(index) => eval_parameter(*index, context.params),
719        ScalarExpr::Func {
720            name,
721            binding,
722            args,
723            ..
724        } => {
725            let arguments = eval_call_arguments(args, context)?;
726            if let Some(binding) = binding {
727                let sql_context = context.sql_context();
728                let engine = sql_context.engine.ok_or_else(|| {
729                    SQLError::Unsupported(
730                        "bound user function requires a logical engine session".into(),
731                    )
732                })?;
733                engine
734                    .call_bound_user_function(binding, &arguments)
735                    .unwrap_or_else(|| Err(SQLError::UnknownFunction(binding.name.clone())))
736            } else {
737                eval_function_call(name, arguments, &context.sql_context())
738            }
739        }
740        ScalarExpr::Array(items) => items
741            .iter()
742            .map(|item| eval_scalar(item, context))
743            .collect::<Result<Vec<_>, _>>()
744            .and_then(|items| {
745                ArrayValue::try_new(items).map(Value::Array).ok_or_else(|| {
746                    SQLError::TypeMismatch(
747                        "multidimensional arrays must have matching dimensions".into(),
748                    )
749                })
750            }),
751        ScalarExpr::Row(items) => items
752            .iter()
753            .map(|item| eval_scalar(item, context))
754            .collect::<Result<Vec<_>, _>>()
755            .map(Value::Row),
756        ScalarExpr::Binary { op, lhs, rhs } => {
757            let left = eval_scalar(lhs, context)?;
758            let right = eval_scalar(rhs, context)?;
759            eval_binary_values_with_integer_width(
760                *op,
761                &left,
762                &right,
763                scalar_integer_binary_width(lhs, rhs),
764            )
765        }
766        ScalarExpr::UnaryMinus(inner) => {
767            let source_ty = scalar_source_type(inner);
768            let value = eval_scalar(inner, context)?;
769            negate_value(&value, source_ty)
770        }
771        ScalarExpr::Not(inner) => {
772            let value = eval_scalar(inner, context)?;
773            if matches!(value, Value::Null) {
774                Ok(Value::Null)
775            } else {
776                Ok(Value::Bool(!truthy(&value)))
777            }
778        }
779        ScalarExpr::And(items) => eval_and(items, context),
780        ScalarExpr::Or(items) => eval_or(items, context),
781        ScalarExpr::IsNull { expr, negated } => {
782            let is_null = matches!(eval_scalar(expr, context)?, Value::Null);
783            Ok(Value::Bool(if *negated { !is_null } else { is_null }))
784        }
785        ScalarExpr::Between { expr, low, high } => eval_between(expr, low, high, context),
786        ScalarExpr::InList {
787            expr,
788            list,
789            negated,
790        } => eval_in_list(expr, list, *negated, context),
791        ScalarExpr::WindowCall { name, .. } => Err(SQLError::Unsupported(format!(
792            "window function `{name}` must be evaluated by the window-aware executor"
793        ))),
794        ScalarExpr::Case {
795            base,
796            when,
797            else_branch,
798        } => eval_case(base.as_deref(), when, else_branch.as_deref(), context),
799        ScalarExpr::Cast { expr, ty } => {
800            let source_ty = scalar_source_type(expr);
801            let value = eval_scalar(expr, context)?;
802            cast_value_from(&value, ty, source_ty)
803        }
804        ScalarExpr::ScalarSubquery(subquery) => execute_scalar_subquery(*subquery, context),
805        ScalarExpr::Exists { subquery, negated } => {
806            let exists = execute_exists_subquery(*subquery, context)?;
807            Ok(Value::Bool(if *negated { !exists } else { exists }))
808        }
809        ScalarExpr::InSubquery {
810            expr,
811            subquery,
812            negated,
813        } => {
814            let needle = eval_scalar(expr, context)?;
815            let found = execute_in_subquery(*subquery, &needle, context)?;
816            Ok(found.map_or(Value::Null, |found| {
817                Value::Bool(if *negated { !found } else { found })
818            }))
819        }
820    }
821}
822
823fn eval_parameter(index: usize, params: &[SQLParam]) -> Result<Value, SQLError> {
824    match index
825        .checked_sub(1)
826        .and_then(|parameter_index| params.get(parameter_index))
827    {
828        Some(SQLParam::Scalar(value)) => Ok(value.clone()),
829        Some(SQLParam::Vector(vector)) => Ok(Value::List(
830            vector
831                .iter()
832                .map(|value| Value::Float(f64::from(*value)))
833                .collect(),
834        )),
835        Some(SQLParam::Tensor(vectors)) => Ok(Value::List(
836            vectors
837                .iter()
838                .map(|vector| {
839                    Value::List(
840                        vector
841                            .iter()
842                            .map(|value| Value::Float(f64::from(*value)))
843                            .collect(),
844                    )
845                })
846                .collect(),
847        )),
848        None => Err(SQLError::MissingParam(index)),
849    }
850}
851
852pub fn eval_call_arguments(
853    arguments: &[ScalarExpr],
854    context: &ScalarEvalContext<'_>,
855) -> Result<Vec<(Option<String>, Value)>, SQLError> {
856    arguments
857        .iter()
858        .map(|argument| match argument {
859            ScalarExpr::Func {
860                name,
861                args: marker_args,
862                ..
863            } if name == NAMED_ARG_FUNCTION => {
864                let Some(ScalarExpr::Literal(Value::Str(argument_name))) = marker_args.first()
865                else {
866                    return Err(SQLError::Internal("named argument without a name".into()));
867                };
868                let value = marker_args
869                    .get(1)
870                    .ok_or_else(|| SQLError::Internal("named argument without a value".into()))?;
871                Ok((Some(argument_name.clone()), eval_scalar(value, context)?))
872            }
873            other => Ok((None, eval_scalar(other, context)?)),
874        })
875        .collect()
876}
877
878fn eval_and(items: &[ScalarExpr], context: &ScalarEvalContext<'_>) -> Result<Value, SQLError> {
879    let mut saw_null = false;
880    for item in items {
881        let value = eval_scalar(item, context)?;
882        if matches!(value, Value::Null) {
883            saw_null = true;
884        } else if !truthy(&value) {
885            return Ok(Value::Bool(false));
886        }
887    }
888    Ok(if saw_null {
889        Value::Null
890    } else {
891        Value::Bool(true)
892    })
893}
894
895fn eval_or(items: &[ScalarExpr], context: &ScalarEvalContext<'_>) -> Result<Value, SQLError> {
896    let mut saw_null = false;
897    for item in items {
898        let value = eval_scalar(item, context)?;
899        if matches!(value, Value::Null) {
900            saw_null = true;
901        } else if truthy(&value) {
902            return Ok(Value::Bool(true));
903        }
904    }
905    Ok(if saw_null {
906        Value::Null
907    } else {
908        Value::Bool(false)
909    })
910}
911
912fn eval_between(
913    expression: &ScalarExpr,
914    low: &ScalarExpr,
915    high: &ScalarExpr,
916    context: &ScalarEvalContext<'_>,
917) -> Result<Value, SQLError> {
918    let value = eval_scalar(expression, context)?;
919    let low = eval_scalar(low, context)?;
920    let high = eval_scalar(high, context)?;
921    let greater_equal = eval_binary_values(BinaryOp::GreaterEqual, &value, &low)?;
922    let less_equal = eval_binary_values(BinaryOp::LessEqual, &value, &high)?;
923    match (greater_equal, less_equal) {
924        (Value::Bool(false), _) | (_, Value::Bool(false)) => Ok(Value::Bool(false)),
925        (Value::Bool(true), Value::Bool(true)) => Ok(Value::Bool(true)),
926        _ => Ok(Value::Null),
927    }
928}
929
930fn eval_in_list(
931    expression: &ScalarExpr,
932    list: &[ScalarExpr],
933    negated: bool,
934    context: &ScalarEvalContext<'_>,
935) -> Result<Value, SQLError> {
936    let needle = eval_scalar(expression, context)?;
937    let mut saw_null = matches!(needle, Value::Null);
938    for item in list {
939        let candidate = eval_scalar(item, context)?;
940        match eval_binary_values(BinaryOp::Equal, &needle, &candidate)? {
941            Value::Bool(true) => return Ok(Value::Bool(!negated)),
942            Value::Null => saw_null = true,
943            _ => {}
944        }
945    }
946    Ok(if saw_null {
947        Value::Null
948    } else {
949        Value::Bool(negated)
950    })
951}
952
953fn eval_case(
954    base: Option<&ScalarExpr>,
955    branches: &[(ScalarExpr, ScalarExpr)],
956    else_branch: Option<&ScalarExpr>,
957    context: &ScalarEvalContext<'_>,
958) -> Result<Value, SQLError> {
959    let base = base
960        .map(|expression| eval_scalar(expression, context))
961        .transpose()?;
962    for (condition, result) in branches {
963        let condition = eval_scalar(condition, context)?;
964        let matched = match &base {
965            Some(base) => matches!(
966                eval_binary_values(BinaryOp::Equal, base, &condition)?,
967                Value::Bool(true)
968            ),
969            None => truthy(&condition),
970        };
971        if matched {
972            return eval_scalar(result, context);
973        }
974    }
975    else_branch.map_or(Ok(Value::Null), |expression| {
976        eval_scalar(expression, context)
977    })
978}
979
980fn execute_scalar_subquery(
981    subquery: SubqueryId,
982    context: &ScalarEvalContext<'_>,
983) -> Result<Value, SQLError> {
984    let runner = context
985        .subquery_runner
986        .ok_or_else(|| SQLError::Unsupported("physical subquery requires a plan runner".into()))?;
987    match context.physical_outer_row {
988        Some((schema, row)) => {
989            runner.scalar_subquery_value_physical(subquery, schema, row, context.params)
990        }
991        None => runner.scalar_subquery_value(subquery, context.outer_row(), context.params),
992    }
993}
994
995fn execute_exists_subquery(
996    subquery: SubqueryId,
997    context: &ScalarEvalContext<'_>,
998) -> Result<bool, SQLError> {
999    let runner = context
1000        .subquery_runner
1001        .ok_or_else(|| SQLError::Unsupported("physical subquery requires a plan runner".into()))?;
1002    match context.physical_outer_row {
1003        Some((schema, row)) => {
1004            runner.subquery_exists_physical(subquery, schema, row, context.params)
1005        }
1006        None => runner.subquery_exists(subquery, context.outer_row(), context.params),
1007    }
1008}
1009
1010fn execute_in_subquery(
1011    subquery: SubqueryId,
1012    needle: &Value,
1013    context: &ScalarEvalContext<'_>,
1014) -> Result<Option<bool>, SQLError> {
1015    let runner = context
1016        .subquery_runner
1017        .ok_or_else(|| SQLError::Unsupported("physical subquery requires a plan runner".into()))?;
1018    match context.physical_outer_row {
1019        Some((schema, row)) => {
1020            runner.subquery_contains_physical(subquery, needle, schema, row, context.params)
1021        }
1022        None => runner.subquery_contains(subquery, needle, context.outer_row(), context.params),
1023    }
1024}
1025
1026fn scalar_source_type(expression: &ScalarExpr) -> Option<&str> {
1027    match expression {
1028        ScalarExpr::Cast { ty, .. } => Some(ty),
1029        ScalarExpr::UnaryMinus(inner) => scalar_source_type(inner),
1030        ScalarExpr::Literal(Value::Int(value)) if i32::try_from(*value).is_ok() => Some("integer"),
1031        ScalarExpr::Literal(Value::Int(_)) => Some("bigint"),
1032        ScalarExpr::Literal(Value::Bytes(_)) => Some("bytea"),
1033        _ => None,
1034    }
1035}
1036
1037fn scalar_integer_width(expression: &ScalarExpr) -> Option<IntegerWidth> {
1038    match expression {
1039        ScalarExpr::Literal(Value::Int(value)) => Some(integer_width_for_literal(*value)),
1040        ScalarExpr::Cast { ty, .. } => integer_width_for_type(ty),
1041        ScalarExpr::UnaryMinus(inner) => scalar_integer_width(inner),
1042        ScalarExpr::Binary {
1043            op: BinaryOp::Add | BinaryOp::Subtract | BinaryOp::Multiply | BinaryOp::Divide,
1044            lhs,
1045            rhs,
1046        } => Some(scalar_integer_width(lhs)?.max(scalar_integer_width(rhs)?)),
1047        _ => None,
1048    }
1049}
1050
1051pub(crate) fn scalar_integer_binary_width(
1052    lhs: &ScalarExpr,
1053    rhs: &ScalarExpr,
1054) -> Option<IntegerWidth> {
1055    Some(scalar_integer_width(lhs)?.max(scalar_integer_width(rhs)?))
1056}
1057
1058#[cfg(test)]
1059mod tests {
1060    use super::{eval_scalar, ScalarEvalContext, ScalarExpr};
1061    use uqa_core::Value;
1062    use uqa_sql::ast::BinaryOp;
1063    use uqa_sql::{SQLError, SQLParam};
1064
1065    #[test]
1066    fn arithmetic_does_not_require_parser_ast() {
1067        let expression = ScalarExpr::Binary {
1068            op: BinaryOp::Multiply,
1069            lhs: Box::new(ScalarExpr::Literal(Value::Int(7))),
1070            rhs: Box::new(ScalarExpr::Literal(Value::Int(3))),
1071        };
1072        assert_eq!(
1073            eval_scalar(&expression, &ScalarEvalContext::new(None, &[])).unwrap(),
1074            Value::Int(21)
1075        );
1076    }
1077
1078    #[test]
1079    fn parameter_zero_is_not_aliased_to_parameter_one() {
1080        let params = [SQLParam::Scalar(Value::Str("secret".into()))];
1081        assert!(matches!(
1082            eval_scalar(
1083                &ScalarExpr::Param(0),
1084                &ScalarEvalContext::new(None, &params)
1085            ),
1086            Err(SQLError::MissingParam(0))
1087        ));
1088    }
1089
1090    #[test]
1091    fn parameter_detection_descends_into_nested_expressions() {
1092        let expression = ScalarExpr::Func {
1093            name: "knn_match".into(),
1094            binding: None,
1095            args: vec![
1096                ScalarExpr::Column("embedding".into()),
1097                ScalarExpr::Array(vec![ScalarExpr::Param(1)]),
1098                ScalarExpr::Literal(Value::Int(3)),
1099            ],
1100            distinct: false,
1101            order_by: Vec::new(),
1102            filter: None,
1103        };
1104
1105        assert!(expression.contains_parameter());
1106        assert!(!ScalarExpr::Literal(Value::Int(3)).contains_parameter());
1107    }
1108}