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::{
11    BinaryOp, FrameMode, FunctionBinding, FunctionDispatch, InternalColumnRef, NullsOrder,
12};
13use uqa_sql::expr::{
14    cast_value_with_type_resolution, eval_binary_values, eval_binary_values_with_integer_width,
15    eval_bound_builtin_function_call, eval_function_call, integer_width_for_literal,
16    integer_width_for_type, negate_value, truthy, EngineHook, EvalContext, IntegerWidth, RowLookup,
17};
18use uqa_sql::{ResultRow, SQLError, SQLParam};
19
20use crate::batch::{OwnedPhysicalRow, PhysicalRow, RowSchema};
21
22/// Index into the query children owned by the enclosing expression plan.
23pub type SubqueryId = usize;
24
25#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
26pub enum ScalarExpr {
27    Star,
28    QualifiedStar(String),
29    Default,
30    Column(String),
31    /// Logical position in an already-bound physical row schema. This variant is introduced only after relational binding so duplicate SQL labels remain independently addressable.
32    Position(usize),
33    /// Structural executor-only attribute, resolved independently of SQL
34    /// relation and column names.
35    InternalColumn(InternalColumnRef),
36    QualifiedColumn {
37        qualifier: String,
38        column: String,
39    },
40    Literal(Value),
41    Param(usize),
42    Func {
43        name: String,
44        #[serde(default, skip_serializing_if = "Option::is_none")]
45        binding: Option<FunctionBinding>,
46        args: Vec<Self>,
47        distinct: bool,
48        order_by: Vec<ScalarOrder>,
49        filter: Option<Box<Self>>,
50    },
51    Array(Vec<Self>),
52    Row(Vec<Self>),
53    Binary {
54        op: BinaryOp,
55        lhs: Box<Self>,
56        rhs: Box<Self>,
57    },
58    UnaryMinus(Box<Self>),
59    Not(Box<Self>),
60    And(Vec<Self>),
61    Or(Vec<Self>),
62    IsNull {
63        expr: Box<Self>,
64        negated: bool,
65    },
66    Between {
67        expr: Box<Self>,
68        low: Box<Self>,
69        high: Box<Self>,
70    },
71    InList {
72        expr: Box<Self>,
73        list: Vec<Self>,
74        negated: bool,
75    },
76    WindowCall {
77        name: String,
78        args: Vec<Self>,
79        spec: ScalarWindowSpec,
80    },
81    Case {
82        base: Option<Box<Self>>,
83        when: Vec<(Self, Self)>,
84        else_branch: Option<Box<Self>>,
85    },
86    Cast {
87        expr: Box<Self>,
88        ty: String,
89    },
90    ScalarSubquery(SubqueryId),
91    Exists {
92        subquery: SubqueryId,
93        negated: bool,
94    },
95    InSubquery {
96        expr: Box<Self>,
97        subquery: SubqueryId,
98        negated: bool,
99    },
100}
101
102#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
103pub struct ScalarOrder {
104    pub expr: ScalarExpr,
105    pub descending: bool,
106    pub nulls: Option<NullsOrder>,
107}
108
109#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
110pub struct ScalarWindowSpec {
111    pub partition_by: Vec<ScalarExpr>,
112    pub order_by: Vec<ScalarOrder>,
113    pub frame: Option<ScalarWindowFrame>,
114}
115
116#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
117pub struct ScalarWindowFrame {
118    pub mode: FrameMode,
119    pub start: ScalarFrameBound,
120    pub end: ScalarFrameBound,
121}
122
123#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
124pub enum ScalarFrameBound {
125    UnboundedPreceding,
126    UnboundedFollowing,
127    CurrentRow,
128    Preceding(Box<ScalarExpr>),
129    Following(Box<ScalarExpr>),
130}
131
132impl ScalarExpr {
133    #[must_use]
134    pub fn qualified_column(qualifier: impl Into<String>, column: impl Into<String>) -> Self {
135        Self::QualifiedColumn {
136            qualifier: qualifier.into(),
137            column: column.into(),
138        }
139    }
140
141    /// Collect every column needed to evaluate this expression. Returns
142    /// `false` when evaluation needs row shape or a relational child that a
143    /// projected field scan cannot provide.
144    pub fn collect_columns(&self, output: &mut std::collections::BTreeSet<String>) -> bool {
145        match self {
146            Self::Column(name) | Self::QualifiedColumn { column: name, .. } => {
147                output.insert(name.clone());
148                true
149            }
150            Self::Literal(_) | Self::Param(_) | Self::InternalColumn(_) => true,
151            Self::Func {
152                args,
153                order_by,
154                filter,
155                ..
156            } => {
157                args.iter().all(|arg| arg.collect_columns(output))
158                    && order_by
159                        .iter()
160                        .all(|order| order.expr.collect_columns(output))
161                    && filter
162                        .as_deref()
163                        .is_none_or(|filter| filter.collect_columns(output))
164            }
165            Self::Array(items) | Self::Row(items) | Self::And(items) | Self::Or(items) => {
166                items.iter().all(|item| item.collect_columns(output))
167            }
168            Self::Binary { lhs, rhs, .. } => {
169                lhs.collect_columns(output) && rhs.collect_columns(output)
170            }
171            Self::UnaryMinus(expr)
172            | Self::Not(expr)
173            | Self::IsNull { expr, .. }
174            | Self::Cast { expr, .. } => expr.collect_columns(output),
175            Self::Between { expr, low, high } => {
176                expr.collect_columns(output)
177                    && low.collect_columns(output)
178                    && high.collect_columns(output)
179            }
180            Self::InList { expr, list, .. } => {
181                expr.collect_columns(output) && list.iter().all(|item| item.collect_columns(output))
182            }
183            Self::Case {
184                base,
185                when,
186                else_branch,
187            } => {
188                base.as_deref()
189                    .is_none_or(|base| base.collect_columns(output))
190                    && when.iter().all(|(condition, result)| {
191                        condition.collect_columns(output) && result.collect_columns(output)
192                    })
193                    && else_branch
194                        .as_deref()
195                        .is_none_or(|branch| branch.collect_columns(output))
196            }
197            Self::Default
198            | Self::Star
199            | Self::QualifiedStar(_)
200            | Self::Position(_)
201            | Self::WindowCall { .. }
202            | Self::ScalarSubquery(_)
203            | Self::Exists { .. }
204            | Self::InSubquery { .. } => false,
205        }
206    }
207
208    #[must_use]
209    pub fn contains_window(&self) -> bool {
210        match self {
211            Self::WindowCall { .. } => true,
212            Self::Func {
213                args,
214                order_by,
215                filter,
216                ..
217            } => {
218                args.iter().any(Self::contains_window)
219                    || order_by.iter().any(|order| order.expr.contains_window())
220                    || filter.as_deref().is_some_and(Self::contains_window)
221            }
222            Self::Array(items) | Self::Row(items) | Self::And(items) | Self::Or(items) => {
223                items.iter().any(Self::contains_window)
224            }
225            Self::Binary { lhs, rhs, .. } => lhs.contains_window() || rhs.contains_window(),
226            Self::UnaryMinus(expr)
227            | Self::Not(expr)
228            | Self::IsNull { expr, .. }
229            | Self::Cast { expr, .. }
230            | Self::InSubquery { expr, .. } => expr.contains_window(),
231            Self::Between { expr, low, high } => {
232                expr.contains_window() || low.contains_window() || high.contains_window()
233            }
234            Self::InList { expr, list, .. } => {
235                expr.contains_window() || list.iter().any(Self::contains_window)
236            }
237            Self::Case {
238                base,
239                when,
240                else_branch,
241            } => {
242                base.as_deref().is_some_and(Self::contains_window)
243                    || when.iter().any(|(condition, result)| {
244                        condition.contains_window() || result.contains_window()
245                    })
246                    || else_branch.as_deref().is_some_and(Self::contains_window)
247            }
248            Self::Default
249            | Self::Star
250            | Self::QualifiedStar(_)
251            | Self::Column(_)
252            | Self::QualifiedColumn { .. }
253            | Self::Position(_)
254            | Self::InternalColumn(_)
255            | Self::Literal(_)
256            | Self::Param(_)
257            | Self::ScalarSubquery(_)
258            | Self::Exists { .. } => false,
259        }
260    }
261
262    #[must_use]
263    pub fn contains_subquery(&self) -> bool {
264        match self {
265            Self::ScalarSubquery(_) | Self::Exists { .. } | Self::InSubquery { .. } => true,
266            Self::Func {
267                args,
268                order_by,
269                filter,
270                ..
271            } => {
272                args.iter().any(Self::contains_subquery)
273                    || order_by.iter().any(|order| order.expr.contains_subquery())
274                    || filter.as_deref().is_some_and(Self::contains_subquery)
275            }
276            Self::Array(items) | Self::Row(items) | Self::And(items) | Self::Or(items) => {
277                items.iter().any(Self::contains_subquery)
278            }
279            Self::Binary { lhs, rhs, .. } => lhs.contains_subquery() || rhs.contains_subquery(),
280            Self::UnaryMinus(expr)
281            | Self::Not(expr)
282            | Self::IsNull { expr, .. }
283            | Self::Cast { expr, .. } => expr.contains_subquery(),
284            Self::Between { expr, low, high } => {
285                expr.contains_subquery() || low.contains_subquery() || high.contains_subquery()
286            }
287            Self::InList { expr, list, .. } => {
288                expr.contains_subquery() || list.iter().any(Self::contains_subquery)
289            }
290            Self::WindowCall { args, spec, .. } => {
291                args.iter().any(Self::contains_subquery)
292                    || spec.partition_by.iter().any(Self::contains_subquery)
293                    || spec
294                        .order_by
295                        .iter()
296                        .any(|order| order.expr.contains_subquery())
297            }
298            Self::Case {
299                base,
300                when,
301                else_branch,
302            } => {
303                base.as_deref().is_some_and(Self::contains_subquery)
304                    || when.iter().any(|(condition, result)| {
305                        condition.contains_subquery() || result.contains_subquery()
306                    })
307                    || else_branch.as_deref().is_some_and(Self::contains_subquery)
308            }
309            Self::Default
310            | Self::Star
311            | Self::QualifiedStar(_)
312            | Self::Column(_)
313            | Self::QualifiedColumn { .. }
314            | Self::Position(_)
315            | Self::InternalColumn(_)
316            | Self::Literal(_)
317            | Self::Param(_) => false,
318        }
319    }
320
321    #[must_use]
322    pub fn contains_parameter(&self) -> bool {
323        match self {
324            Self::Param(_) => true,
325            Self::Func {
326                args,
327                order_by,
328                filter,
329                ..
330            } => {
331                args.iter().any(Self::contains_parameter)
332                    || order_by.iter().any(|order| order.expr.contains_parameter())
333                    || filter.as_deref().is_some_and(Self::contains_parameter)
334            }
335            Self::Array(items) | Self::Row(items) | Self::And(items) | Self::Or(items) => {
336                items.iter().any(Self::contains_parameter)
337            }
338            Self::Binary { lhs, rhs, .. } => lhs.contains_parameter() || rhs.contains_parameter(),
339            Self::UnaryMinus(expr)
340            | Self::Not(expr)
341            | Self::IsNull { expr, .. }
342            | Self::Cast { expr, .. }
343            | Self::InSubquery { expr, .. } => expr.contains_parameter(),
344            Self::Between { expr, low, high } => {
345                expr.contains_parameter() || low.contains_parameter() || high.contains_parameter()
346            }
347            Self::InList { expr, list, .. } => {
348                expr.contains_parameter() || list.iter().any(Self::contains_parameter)
349            }
350            Self::WindowCall { args, spec, .. } => {
351                args.iter().any(Self::contains_parameter)
352                    || spec.partition_by.iter().any(Self::contains_parameter)
353                    || spec
354                        .order_by
355                        .iter()
356                        .any(|order| order.expr.contains_parameter())
357                    || spec.frame.as_ref().is_some_and(|frame| {
358                        scalar_frame_bound_contains_parameter(&frame.start)
359                            || scalar_frame_bound_contains_parameter(&frame.end)
360                    })
361            }
362            Self::Case {
363                base,
364                when,
365                else_branch,
366            } => {
367                base.as_deref().is_some_and(Self::contains_parameter)
368                    || when.iter().any(|(condition, result)| {
369                        condition.contains_parameter() || result.contains_parameter()
370                    })
371                    || else_branch.as_deref().is_some_and(Self::contains_parameter)
372            }
373            Self::Default
374            | Self::Star
375            | Self::QualifiedStar(_)
376            | Self::Column(_)
377            | Self::QualifiedColumn { .. }
378            | Self::Position(_)
379            | Self::InternalColumn(_)
380            | Self::Literal(_)
381            | Self::ScalarSubquery(_)
382            | Self::Exists { .. } => false,
383        }
384    }
385
386    #[must_use]
387    pub fn contains_aggregate(&self, is_aggregate: &dyn Fn(&str) -> bool) -> bool {
388        match self {
389            Self::Func {
390                name,
391                args,
392                order_by,
393                filter,
394                ..
395            } => {
396                is_aggregate(name)
397                    || args
398                        .iter()
399                        .any(|expression| expression.contains_aggregate(is_aggregate))
400                    || order_by
401                        .iter()
402                        .any(|order| order.expr.contains_aggregate(is_aggregate))
403                    || filter
404                        .as_deref()
405                        .is_some_and(|expression| expression.contains_aggregate(is_aggregate))
406            }
407            Self::Array(items) | Self::Row(items) | Self::And(items) | Self::Or(items) => items
408                .iter()
409                .any(|expression| expression.contains_aggregate(is_aggregate)),
410            Self::Binary { lhs, rhs, .. } => {
411                lhs.contains_aggregate(is_aggregate) || rhs.contains_aggregate(is_aggregate)
412            }
413            Self::UnaryMinus(expr)
414            | Self::Not(expr)
415            | Self::IsNull { expr, .. }
416            | Self::Cast { expr, .. }
417            | Self::InSubquery { expr, .. } => expr.contains_aggregate(is_aggregate),
418            Self::Between { expr, low, high } => {
419                expr.contains_aggregate(is_aggregate)
420                    || low.contains_aggregate(is_aggregate)
421                    || high.contains_aggregate(is_aggregate)
422            }
423            Self::InList { expr, list, .. } => {
424                expr.contains_aggregate(is_aggregate)
425                    || list
426                        .iter()
427                        .any(|item| item.contains_aggregate(is_aggregate))
428            }
429            Self::Case {
430                base,
431                when,
432                else_branch,
433            } => {
434                base.as_deref()
435                    .is_some_and(|expression| expression.contains_aggregate(is_aggregate))
436                    || when.iter().any(|(condition, result)| {
437                        condition.contains_aggregate(is_aggregate)
438                            || result.contains_aggregate(is_aggregate)
439                    })
440                    || else_branch
441                        .as_deref()
442                        .is_some_and(|expression| expression.contains_aggregate(is_aggregate))
443            }
444            Self::Default
445            | Self::Star
446            | Self::QualifiedStar(_)
447            | Self::Column(_)
448            | Self::QualifiedColumn { .. }
449            | Self::Position(_)
450            | Self::InternalColumn(_)
451            | Self::Literal(_)
452            | Self::Param(_)
453            | Self::ScalarSubquery(_)
454            | Self::Exists { .. }
455            | Self::WindowCall { .. } => false,
456        }
457    }
458}
459
460fn scalar_frame_bound_contains_parameter(bound: &ScalarFrameBound) -> bool {
461    match bound {
462        ScalarFrameBound::Preceding(expression) | ScalarFrameBound::Following(expression) => {
463            expression.contains_parameter()
464        }
465        ScalarFrameBound::UnboundedPreceding
466        | ScalarFrameBound::UnboundedFollowing
467        | ScalarFrameBound::CurrentRow => false,
468    }
469}
470
471/// Runtime callback for query children referenced by [`SubqueryId`]. The
472/// planner owns the actual query-plan arena; execution only needs this stable
473/// slot interface.
474pub trait ScalarSubqueryRunner {
475    fn execute_subquery(
476        &self,
477        subquery: SubqueryId,
478        outer_row: Option<&dyn RowLookup>,
479        params: &[SQLParam],
480    ) -> Result<SubqueryResult, SQLError>;
481
482    fn execute_subquery_physical(
483        &self,
484        subquery: SubqueryId,
485        outer_schema: &RowSchema,
486        outer_row: &PhysicalRow,
487        params: &[SQLParam],
488    ) -> Result<SubqueryResult, SQLError> {
489        let outer = outer_schema.view(outer_row);
490        self.execute_subquery(subquery, Some(&outer), params)
491    }
492
493    fn scalar_subquery_value(
494        &self,
495        subquery: SubqueryId,
496        outer_row: Option<&dyn RowLookup>,
497        params: &[SQLParam],
498    ) -> Result<Value, SQLError> {
499        self.execute_subquery(subquery, outer_row, params)?
500            .into_scalar_value()
501    }
502
503    fn scalar_subquery_value_physical(
504        &self,
505        subquery: SubqueryId,
506        outer_schema: &RowSchema,
507        outer_row: &PhysicalRow,
508        params: &[SQLParam],
509    ) -> Result<Value, SQLError> {
510        self.execute_subquery_physical(subquery, outer_schema, outer_row, params)?
511            .into_scalar_value()
512    }
513
514    fn subquery_exists(
515        &self,
516        subquery: SubqueryId,
517        outer_row: Option<&dyn RowLookup>,
518        params: &[SQLParam],
519    ) -> Result<bool, SQLError> {
520        self.execute_subquery(subquery, outer_row, params)?
521            .into_exists()
522    }
523
524    fn subquery_exists_physical(
525        &self,
526        subquery: SubqueryId,
527        outer_schema: &RowSchema,
528        outer_row: &PhysicalRow,
529        params: &[SQLParam],
530    ) -> Result<bool, SQLError> {
531        self.execute_subquery_physical(subquery, outer_schema, outer_row, params)?
532            .into_exists()
533    }
534
535    fn subquery_contains(
536        &self,
537        subquery: SubqueryId,
538        needle: &Value,
539        outer_row: Option<&dyn RowLookup>,
540        params: &[SQLParam],
541    ) -> Result<Option<bool>, SQLError> {
542        self.execute_subquery(subquery, outer_row, params)?
543            .contains(needle)
544    }
545
546    fn subquery_contains_physical(
547        &self,
548        subquery: SubqueryId,
549        needle: &Value,
550        outer_schema: &RowSchema,
551        outer_row: &PhysicalRow,
552        params: &[SQLParam],
553    ) -> Result<Option<bool>, SQLError> {
554        self.execute_subquery_physical(subquery, outer_schema, outer_row, params)?
555            .contains(needle)
556    }
557}
558
559/// Pull-based scalar-subquery result. Scalar, EXISTS, and IN consumers never
560/// need to materialize the complete child relation: they respectively inspect
561/// at most two rows, one row, or one row at a time.
562pub struct SubqueryResult {
563    pub columns: Vec<String>,
564    pub rows: Box<dyn Iterator<Item = Result<OwnedPhysicalRow, SQLError>> + Send>,
565}
566
567impl SubqueryResult {
568    pub fn from_rows(columns: Vec<String>, rows: Vec<ResultRow>) -> Self {
569        let schema = RowSchema::new(columns.clone());
570        Self {
571            columns,
572            rows: Box::new(rows.into_iter().map(move |row| {
573                Ok(OwnedPhysicalRow::new(
574                    schema.clone(),
575                    PhysicalRow::from_result_row(&schema, row),
576                ))
577            })),
578        }
579    }
580
581    pub fn into_scalar_value(mut self) -> Result<Value, SQLError> {
582        let Some(first_row) = self.rows.next().transpose()? else {
583            return Ok(Value::Null);
584        };
585        if self.rows.next().transpose()?.is_some() {
586            return Err(SQLError::TypeMismatch(
587                "scalar subquery returned more than one row".into(),
588            ));
589        }
590        if self.columns.is_empty() {
591            return Err(SQLError::TypeMismatch(
592                "scalar subquery returned no columns".into(),
593            ));
594        }
595        Ok(first_row
596            .positional_column(0)
597            .cloned()
598            .unwrap_or(Value::Null))
599    }
600
601    pub fn into_exists(mut self) -> Result<bool, SQLError> {
602        Ok(self.rows.next().transpose()?.is_some())
603    }
604
605    pub fn contains(self, needle: &Value) -> Result<Option<bool>, SQLError> {
606        if self.columns.is_empty() {
607            return Ok(Some(false));
608        }
609        let mut saw_row = false;
610        let mut saw_null = false;
611        for row in self.rows {
612            let row = row?;
613            saw_row = true;
614            match row.positional_column(0) {
615                Some(Value::Null) | None => saw_null = true,
616                Some(value) if !matches!(needle, Value::Null) && value == needle => {
617                    return Ok(Some(true));
618                }
619                Some(_) => {}
620            }
621        }
622        Ok(if !saw_row {
623            Some(false)
624        } else if matches!(needle, Value::Null) || saw_null {
625            None
626        } else {
627            Some(false)
628        })
629    }
630}
631
632pub struct ScalarEvalContext<'a> {
633    row: Option<&'a ResultRow>,
634    row_lookup: Option<&'a dyn RowLookup>,
635    row_schema: Option<&'a RowSchema>,
636    params: &'a [SQLParam],
637    function_hook: Option<&'a dyn EngineHook>,
638    subquery_runner: Option<&'a dyn ScalarSubqueryRunner>,
639    physical_outer_row: Option<(&'a RowSchema, &'a PhysicalRow)>,
640}
641
642impl<'a> ScalarEvalContext<'a> {
643    #[must_use]
644    pub fn new(row: Option<&'a ResultRow>, params: &'a [SQLParam]) -> Self {
645        Self {
646            row,
647            row_lookup: row.map(|row| row as &dyn RowLookup),
648            row_schema: None,
649            params,
650            function_hook: None,
651            subquery_runner: None,
652            physical_outer_row: None,
653        }
654    }
655
656    #[must_use]
657    pub fn from_row_lookup(row: &'a dyn RowLookup, params: &'a [SQLParam]) -> Self {
658        Self {
659            row: None,
660            row_lookup: Some(row),
661            row_schema: None,
662            params,
663            function_hook: None,
664            subquery_runner: None,
665            physical_outer_row: None,
666        }
667    }
668
669    #[must_use]
670    pub fn with_function_hook(mut self, hook: &'a dyn EngineHook) -> Self {
671        self.function_hook = Some(hook);
672        self
673    }
674
675    #[must_use]
676    pub fn with_row_schema(mut self, schema: &'a RowSchema) -> Self {
677        self.row_schema = Some(schema);
678        self
679    }
680
681    #[must_use]
682    pub fn with_subquery_runner(mut self, runner: &'a dyn ScalarSubqueryRunner) -> Self {
683        self.subquery_runner = Some(runner);
684        self
685    }
686
687    #[must_use]
688    pub fn with_physical_outer_row(mut self, schema: &'a RowSchema, row: &'a PhysicalRow) -> Self {
689        self.physical_outer_row = Some((schema, row));
690        self
691    }
692
693    fn sql_context(&self) -> EvalContext<'_> {
694        let context = self.row_lookup.map_or_else(
695            || EvalContext::new(self.row, self.params),
696            |row| EvalContext::from_row_lookup(row, self.params),
697        );
698        match self.function_hook {
699            Some(hook) => context.with_engine(hook),
700            None => context,
701        }
702    }
703
704    fn outer_row(&self) -> Option<&dyn RowLookup> {
705        self.row_lookup
706    }
707}
708
709/// Evaluate the physical scalar tree directly. No parser expression is
710/// reconstructed at this boundary.
711pub fn eval_scalar(
712    expression: &ScalarExpr,
713    context: &ScalarEvalContext<'_>,
714) -> Result<Value, SQLError> {
715    match expression {
716        ScalarExpr::Default => Err(SQLError::Internal(
717            "DEFAULT reached scalar expression evaluation without a mutation target".into(),
718        )),
719        ScalarExpr::Star | ScalarExpr::QualifiedStar(_) => {
720            Err(SQLError::Internal("`*` cannot be evaluated".into()))
721        }
722        ScalarExpr::Column(name) => context.sql_context().column_value(name),
723        ScalarExpr::Position(position) => context
724            .row_lookup
725            .and_then(|row| row.positional_column(*position))
726            .cloned()
727            .ok_or_else(|| {
728                SQLError::Internal(format!(
729                    "bound physical column position {position} is unavailable"
730                ))
731            }),
732        ScalarExpr::InternalColumn(column) => context
733            .row_lookup
734            .and_then(|row| row.internal_column(*column))
735            .cloned()
736            .ok_or_else(|| {
737                SQLError::Internal(format!(
738                    "internal relation attribute {column:?} is unavailable"
739                ))
740            }),
741        ScalarExpr::QualifiedColumn { qualifier, column } => context
742            .sql_context()
743            .qualified_column_value(qualifier, column),
744        ScalarExpr::Literal(value) => Ok(value.clone()),
745        ScalarExpr::Param(index) => eval_parameter(*index, context.params),
746        ScalarExpr::Func {
747            name,
748            binding,
749            args,
750            ..
751        } => {
752            let arguments = eval_call_arguments(args, context)?;
753            if let Some(binding) = binding {
754                if let Some(uqa_sql::ast::FunctionResolutionError::UndefinedFunction {
755                    signature,
756                }) = binding.resolution_error.as_ref()
757                {
758                    return Err(SQLError::Routine {
759                        sqlstate: "42883".into(),
760                        message: format!("function {signature} does not exist"),
761                    });
762                }
763                if binding.builtin {
764                    if let Some(result) = context
765                        .function_hook
766                        .and_then(|hook| hook.call_bound_builtin_function(binding, &arguments))
767                    {
768                        return result;
769                    }
770                    return eval_bound_builtin_function_call(
771                        binding,
772                        arguments,
773                        &context.sql_context(),
774                    );
775                }
776                let sql_context = context.sql_context();
777                let engine = sql_context.engine.ok_or_else(|| {
778                    SQLError::Unsupported(
779                        "bound user function requires a logical engine session".into(),
780                    )
781                })?;
782                engine
783                    .call_bound_user_function(binding, &arguments)
784                    .unwrap_or_else(|| Err(SQLError::UnknownFunction(binding.name.clone())))
785            } else {
786                eval_function_call(name, arguments, &context.sql_context())
787            }
788        }
789        ScalarExpr::Array(items) => items
790            .iter()
791            .map(|item| eval_scalar(item, context))
792            .collect::<Result<Vec<_>, _>>()
793            .and_then(|items| {
794                ArrayValue::try_new(items).map(Value::Array).ok_or_else(|| {
795                    SQLError::TypeMismatch(
796                        "multidimensional arrays must have matching dimensions".into(),
797                    )
798                })
799            }),
800        ScalarExpr::Row(items) => items
801            .iter()
802            .map(|item| eval_scalar(item, context))
803            .collect::<Result<Vec<_>, _>>()
804            .map(Value::Row),
805        ScalarExpr::Binary { op, lhs, rhs } => {
806            let left = eval_scalar(lhs, context)?;
807            let right = eval_scalar(rhs, context)?;
808            eval_binary_values_with_integer_width(
809                *op,
810                &left,
811                &right,
812                scalar_integer_binary_width(lhs, rhs),
813            )
814        }
815        ScalarExpr::UnaryMinus(inner) => {
816            let source_ty = scalar_source_type(inner, context);
817            let value = eval_scalar(inner, context)?;
818            negate_value(&value, source_ty.as_deref())
819        }
820        ScalarExpr::Not(inner) => {
821            let value = eval_scalar(inner, context)?;
822            if matches!(value, Value::Null) {
823                Ok(Value::Null)
824            } else {
825                Ok(Value::Bool(!truthy(&value)))
826            }
827        }
828        ScalarExpr::And(items) => eval_and(items, context),
829        ScalarExpr::Or(items) => eval_or(items, context),
830        ScalarExpr::IsNull { expr, negated } => {
831            let is_null = matches!(eval_scalar(expr, context)?, Value::Null);
832            Ok(Value::Bool(if *negated { !is_null } else { is_null }))
833        }
834        ScalarExpr::Between { expr, low, high } => eval_between(expr, low, high, context),
835        ScalarExpr::InList {
836            expr,
837            list,
838            negated,
839        } => eval_in_list(expr, list, *negated, context),
840        ScalarExpr::WindowCall { name, .. } => Err(SQLError::Unsupported(format!(
841            "window function `{name}` must be evaluated by the window-aware executor"
842        ))),
843        ScalarExpr::Case {
844            base,
845            when,
846            else_branch,
847        } => eval_case(base.as_deref(), when, else_branch.as_deref(), context),
848        ScalarExpr::Cast { expr, ty } => {
849            let source_ty = scalar_source_type(expr, context);
850            let value = eval_scalar(expr, context)?;
851            cast_value_with_type_resolution(&value, source_ty.as_deref(), ty, context.function_hook)
852        }
853        ScalarExpr::ScalarSubquery(subquery) => execute_scalar_subquery(*subquery, context),
854        ScalarExpr::Exists { subquery, negated } => {
855            let exists = execute_exists_subquery(*subquery, context)?;
856            Ok(Value::Bool(if *negated { !exists } else { exists }))
857        }
858        ScalarExpr::InSubquery {
859            expr,
860            subquery,
861            negated,
862        } => {
863            let needle = eval_scalar(expr, context)?;
864            let found = execute_in_subquery(*subquery, &needle, context)?;
865            Ok(found.map_or(Value::Null, |found| {
866                Value::Bool(if *negated { !found } else { found })
867            }))
868        }
869    }
870}
871
872fn eval_parameter(index: usize, params: &[SQLParam]) -> Result<Value, SQLError> {
873    match index
874        .checked_sub(1)
875        .and_then(|parameter_index| params.get(parameter_index))
876    {
877        Some(SQLParam::Scalar(value) | SQLParam::TypedScalar { value, .. }) => Ok(value.clone()),
878        Some(SQLParam::Vector(vector)) => Ok(Value::List(
879            vector
880                .iter()
881                .map(|value| Value::Float(f64::from(*value)))
882                .collect(),
883        )),
884        Some(SQLParam::Tensor(vectors)) => Ok(Value::List(
885            vectors
886                .iter()
887                .map(|vector| {
888                    Value::List(
889                        vector
890                            .iter()
891                            .map(|value| Value::Float(f64::from(*value)))
892                            .collect(),
893                    )
894                })
895                .collect(),
896        )),
897        None => Err(SQLError::MissingParam(index)),
898    }
899}
900
901pub fn eval_call_arguments(
902    arguments: &[ScalarExpr],
903    context: &ScalarEvalContext<'_>,
904) -> Result<Vec<(Option<String>, Value)>, SQLError> {
905    scalar_call_arguments(arguments)?
906        .into_iter()
907        .map(|argument| {
908            Ok((
909                argument.name.map(str::to_string),
910                eval_scalar(argument.value, context)?,
911            ))
912        })
913        .collect()
914}
915
916/// A physical call argument after removing the compiler's named and explicit `VARIADIC` syntax markers.
917#[doc(hidden)]
918#[derive(Debug, Clone, Copy, PartialEq)]
919pub struct ScalarCallArgument<'a> {
920    pub name: Option<&'a str>,
921    pub value: &'a ScalarExpr,
922    pub explicit_variadic: bool,
923}
924
925/// Decode and validate all compiler-owned call-argument markers. PostgreSQL permits one explicit `VARIADIC` argument and requires it to be the final argument.
926#[doc(hidden)]
927pub fn scalar_call_arguments(
928    arguments: &[ScalarExpr],
929) -> Result<Vec<ScalarCallArgument<'_>>, SQLError> {
930    let mut decoded = Vec::with_capacity(arguments.len());
931    for argument in arguments {
932        decoded.push(scalar_call_argument(argument)?);
933    }
934    validate_scalar_call_arguments(&decoded)?;
935    Ok(decoded)
936}
937
938/// Validate cross-argument invariants after individual syntax markers have been decoded, returning whether the call used explicit `VARIADIC` syntax.
939#[doc(hidden)]
940pub fn validate_scalar_call_arguments(
941    arguments: &[ScalarCallArgument<'_>],
942) -> Result<bool, SQLError> {
943    let variadic_positions = arguments
944        .iter()
945        .enumerate()
946        .filter_map(|(position, argument)| argument.explicit_variadic.then_some(position))
947        .collect::<Vec<_>>();
948    if variadic_positions.len() > 1 {
949        return Err(malformed_call_argument(
950            "call contains more than one explicit VARIADIC argument",
951        ));
952    }
953    if variadic_positions
954        .first()
955        .is_some_and(|position| position + 1 != arguments.len())
956    {
957        return Err(malformed_call_argument(
958            "explicit VARIADIC argument must be the final call argument",
959        ));
960    }
961    Ok(!variadic_positions.is_empty())
962}
963
964/// Decode one compiler-owned call-argument marker. Use [`scalar_call_arguments`] for a complete call so duplicate and ordering invariants are also checked.
965#[doc(hidden)]
966pub fn scalar_call_argument(expression: &ScalarExpr) -> Result<ScalarCallArgument<'_>, SQLError> {
967    let ScalarExpr::Func {
968        name,
969        args,
970        binding,
971        distinct,
972        order_by,
973        filter,
974    } = expression
975    else {
976        return Ok(ScalarCallArgument {
977            name: None,
978            value: expression,
979            explicit_variadic: false,
980        });
981    };
982    if binding.as_ref().and_then(|binding| binding.dispatch)
983        == Some(FunctionDispatch::NamedArgument)
984    {
985        validate_marker_shape(
986            binding.as_ref(),
987            FunctionDispatch::NamedArgument,
988            *distinct,
989            order_by,
990            filter.as_deref(),
991            name,
992        )?;
993        let [ScalarExpr::Literal(Value::Str(argument_name)), value] = args.as_slice() else {
994            return Err(malformed_call_argument(
995                "named argument marker must contain a string name and one value",
996            ));
997        };
998        let (value, explicit_variadic) = direct_variadic_argument(value)?;
999        if !explicit_variadic
1000            && matches!(
1001                value,
1002                ScalarExpr::Func { binding, .. }
1003                    if binding.as_ref().and_then(|binding| binding.dispatch)
1004                        == Some(FunctionDispatch::NamedArgument)
1005            )
1006        {
1007            return Err(malformed_call_argument(
1008                "call argument contains nested syntax markers",
1009            ));
1010        }
1011        return Ok(ScalarCallArgument {
1012            name: Some(argument_name),
1013            value,
1014            explicit_variadic,
1015        });
1016    }
1017    let (value, explicit_variadic) = direct_variadic_argument(expression)?;
1018    Ok(ScalarCallArgument {
1019        name: None,
1020        value,
1021        explicit_variadic,
1022    })
1023}
1024
1025fn direct_variadic_argument(expression: &ScalarExpr) -> Result<(&ScalarExpr, bool), SQLError> {
1026    let ScalarExpr::Func {
1027        name,
1028        args,
1029        binding,
1030        distinct,
1031        order_by,
1032        filter,
1033    } = expression
1034    else {
1035        return Ok((expression, false));
1036    };
1037    if binding.as_ref().and_then(|binding| binding.dispatch)
1038        != Some(FunctionDispatch::VariadicArgument)
1039    {
1040        return Ok((expression, false));
1041    }
1042    validate_marker_shape(
1043        binding.as_ref(),
1044        FunctionDispatch::VariadicArgument,
1045        *distinct,
1046        order_by,
1047        filter.as_deref(),
1048        name,
1049    )?;
1050    let [value] = args.as_slice() else {
1051        return Err(malformed_call_argument(
1052            "VARIADIC argument marker must contain exactly one value",
1053        ));
1054    };
1055    if matches!(
1056        value,
1057        ScalarExpr::Func { binding, .. }
1058            if matches!(
1059                binding.as_ref().and_then(|binding| binding.dispatch),
1060                Some(FunctionDispatch::VariadicArgument | FunctionDispatch::NamedArgument)
1061            )
1062    ) {
1063        return Err(malformed_call_argument(
1064            "call argument contains nested syntax markers",
1065        ));
1066    }
1067    Ok((value, true))
1068}
1069
1070fn validate_marker_shape(
1071    binding: Option<&FunctionBinding>,
1072    expected_dispatch: FunctionDispatch,
1073    distinct: bool,
1074    order_by: &[ScalarOrder],
1075    filter: Option<&ScalarExpr>,
1076    name: &str,
1077) -> Result<(), SQLError> {
1078    if binding.is_none_or(|binding| {
1079        !binding.builtin
1080            || binding.dispatch != Some(expected_dispatch)
1081            || !binding.argument_types.is_empty()
1082            || binding.invocation.is_some()
1083            || binding.resolution_error.is_some()
1084    }) || distinct
1085        || !order_by.is_empty()
1086        || filter.is_some()
1087    {
1088        return Err(malformed_call_argument(&format!(
1089            "{name} syntax marker contains function-call metadata"
1090        )));
1091    }
1092    Ok(())
1093}
1094
1095fn malformed_call_argument(message: &str) -> SQLError {
1096    SQLError::Internal(format!("malformed call argument: {message}"))
1097}
1098
1099fn eval_and(items: &[ScalarExpr], context: &ScalarEvalContext<'_>) -> Result<Value, SQLError> {
1100    let mut saw_null = false;
1101    for item in items {
1102        let value = eval_scalar(item, context)?;
1103        if matches!(value, Value::Null) {
1104            saw_null = true;
1105        } else if !truthy(&value) {
1106            return Ok(Value::Bool(false));
1107        }
1108    }
1109    Ok(if saw_null {
1110        Value::Null
1111    } else {
1112        Value::Bool(true)
1113    })
1114}
1115
1116fn eval_or(items: &[ScalarExpr], context: &ScalarEvalContext<'_>) -> Result<Value, SQLError> {
1117    let mut saw_null = false;
1118    for item in items {
1119        let value = eval_scalar(item, context)?;
1120        if matches!(value, Value::Null) {
1121            saw_null = true;
1122        } else if truthy(&value) {
1123            return Ok(Value::Bool(true));
1124        }
1125    }
1126    Ok(if saw_null {
1127        Value::Null
1128    } else {
1129        Value::Bool(false)
1130    })
1131}
1132
1133fn eval_between(
1134    expression: &ScalarExpr,
1135    low: &ScalarExpr,
1136    high: &ScalarExpr,
1137    context: &ScalarEvalContext<'_>,
1138) -> Result<Value, SQLError> {
1139    let value = eval_scalar(expression, context)?;
1140    let low = eval_scalar(low, context)?;
1141    let high = eval_scalar(high, context)?;
1142    let greater_equal = eval_binary_values(BinaryOp::GreaterEqual, &value, &low)?;
1143    let less_equal = eval_binary_values(BinaryOp::LessEqual, &value, &high)?;
1144    match (greater_equal, less_equal) {
1145        (Value::Bool(false), _) | (_, Value::Bool(false)) => Ok(Value::Bool(false)),
1146        (Value::Bool(true), Value::Bool(true)) => Ok(Value::Bool(true)),
1147        _ => Ok(Value::Null),
1148    }
1149}
1150
1151fn eval_in_list(
1152    expression: &ScalarExpr,
1153    list: &[ScalarExpr],
1154    negated: bool,
1155    context: &ScalarEvalContext<'_>,
1156) -> Result<Value, SQLError> {
1157    let needle = eval_scalar(expression, context)?;
1158    let mut saw_null = matches!(needle, Value::Null);
1159    for item in list {
1160        let candidate = eval_scalar(item, context)?;
1161        match eval_binary_values(BinaryOp::Equal, &needle, &candidate)? {
1162            Value::Bool(true) => return Ok(Value::Bool(!negated)),
1163            Value::Null => saw_null = true,
1164            _ => {}
1165        }
1166    }
1167    Ok(if saw_null {
1168        Value::Null
1169    } else {
1170        Value::Bool(negated)
1171    })
1172}
1173
1174fn eval_case(
1175    base: Option<&ScalarExpr>,
1176    branches: &[(ScalarExpr, ScalarExpr)],
1177    else_branch: Option<&ScalarExpr>,
1178    context: &ScalarEvalContext<'_>,
1179) -> Result<Value, SQLError> {
1180    let base = base
1181        .map(|expression| eval_scalar(expression, context))
1182        .transpose()?;
1183    for (condition, result) in branches {
1184        let condition = eval_scalar(condition, context)?;
1185        let matched = match &base {
1186            Some(base) => matches!(
1187                eval_binary_values(BinaryOp::Equal, base, &condition)?,
1188                Value::Bool(true)
1189            ),
1190            None => truthy(&condition),
1191        };
1192        if matched {
1193            return eval_scalar(result, context);
1194        }
1195    }
1196    else_branch.map_or(Ok(Value::Null), |expression| {
1197        eval_scalar(expression, context)
1198    })
1199}
1200
1201fn execute_scalar_subquery(
1202    subquery: SubqueryId,
1203    context: &ScalarEvalContext<'_>,
1204) -> Result<Value, SQLError> {
1205    let runner = context
1206        .subquery_runner
1207        .ok_or_else(|| SQLError::Unsupported("physical subquery requires a plan runner".into()))?;
1208    match context.physical_outer_row {
1209        Some((schema, row)) => {
1210            runner.scalar_subquery_value_physical(subquery, schema, row, context.params)
1211        }
1212        None => runner.scalar_subquery_value(subquery, context.outer_row(), context.params),
1213    }
1214}
1215
1216fn execute_exists_subquery(
1217    subquery: SubqueryId,
1218    context: &ScalarEvalContext<'_>,
1219) -> Result<bool, SQLError> {
1220    let runner = context
1221        .subquery_runner
1222        .ok_or_else(|| SQLError::Unsupported("physical subquery requires a plan runner".into()))?;
1223    match context.physical_outer_row {
1224        Some((schema, row)) => {
1225            runner.subquery_exists_physical(subquery, schema, row, context.params)
1226        }
1227        None => runner.subquery_exists(subquery, context.outer_row(), context.params),
1228    }
1229}
1230
1231fn execute_in_subquery(
1232    subquery: SubqueryId,
1233    needle: &Value,
1234    context: &ScalarEvalContext<'_>,
1235) -> Result<Option<bool>, SQLError> {
1236    let runner = context
1237        .subquery_runner
1238        .ok_or_else(|| SQLError::Unsupported("physical subquery requires a plan runner".into()))?;
1239    match context.physical_outer_row {
1240        Some((schema, row)) => {
1241            runner.subquery_contains_physical(subquery, needle, schema, row, context.params)
1242        }
1243        None => runner.subquery_contains(subquery, needle, context.outer_row(), context.params),
1244    }
1245}
1246
1247fn scalar_source_type(expression: &ScalarExpr, context: &ScalarEvalContext<'_>) -> Option<String> {
1248    match expression {
1249        ScalarExpr::Cast { ty, .. } => return Some(ty.clone()),
1250        ScalarExpr::UnaryMinus(inner) => return scalar_source_type(inner, context),
1251        ScalarExpr::Literal(Value::Int(value)) if i32::try_from(*value).is_ok() => {
1252            return Some("integer".into());
1253        }
1254        ScalarExpr::Literal(Value::Int(_)) => return Some("bigint".into()),
1255        ScalarExpr::Literal(Value::Bytes(_)) => return Some("bytea".into()),
1256        ScalarExpr::Literal(Value::Str(_) | Value::FixedChar(_)) => return None,
1257        _ => {}
1258    }
1259    context
1260        .row_schema
1261        .and_then(|schema| {
1262            crate::scalar_type(expression, schema, context.params)
1263                .ok()
1264                .flatten()
1265        })
1266        .map(|ty| ty.sql_name())
1267}
1268
1269fn scalar_integer_width(expression: &ScalarExpr) -> Option<IntegerWidth> {
1270    match expression {
1271        ScalarExpr::Literal(Value::Int(value)) => Some(integer_width_for_literal(*value)),
1272        ScalarExpr::Cast { ty, .. } => integer_width_for_type(ty),
1273        ScalarExpr::UnaryMinus(inner) => scalar_integer_width(inner),
1274        ScalarExpr::Binary {
1275            op: BinaryOp::Add | BinaryOp::Subtract | BinaryOp::Multiply | BinaryOp::Divide,
1276            lhs,
1277            rhs,
1278        } => Some(scalar_integer_width(lhs)?.max(scalar_integer_width(rhs)?)),
1279        _ => None,
1280    }
1281}
1282
1283pub(crate) fn scalar_integer_binary_width(
1284    lhs: &ScalarExpr,
1285    rhs: &ScalarExpr,
1286) -> Option<IntegerWidth> {
1287    Some(scalar_integer_width(lhs)?.max(scalar_integer_width(rhs)?))
1288}
1289
1290#[cfg(test)]
1291mod tests {
1292    use super::{
1293        eval_call_arguments, eval_scalar, scalar_call_arguments, ScalarEvalContext, ScalarExpr,
1294    };
1295    use crate::{PhysicalRow, RowSchema};
1296    use uqa_core::Value;
1297    use uqa_sql::ast::{BinaryOp, ColumnType, FunctionBinding, FunctionDispatch};
1298    use uqa_sql::{SQLError, SQLParam};
1299
1300    #[test]
1301    fn arithmetic_does_not_require_parser_ast() {
1302        let expression = ScalarExpr::Binary {
1303            op: BinaryOp::Multiply,
1304            lhs: Box::new(ScalarExpr::Literal(Value::Int(7))),
1305            rhs: Box::new(ScalarExpr::Literal(Value::Int(3))),
1306        };
1307        assert_eq!(
1308            eval_scalar(&expression, &ScalarEvalContext::new(None, &[])).unwrap(),
1309            Value::Int(21)
1310        );
1311    }
1312
1313    #[test]
1314    fn cast_uses_the_input_schema_declared_source_type() {
1315        let schema = RowSchema::with_types(vec!["support".into()], vec![Some(ColumnType::Regproc)]);
1316        let row = PhysicalRow::from_values(vec![Value::Int(0)]);
1317        let view = schema.view(&row);
1318        let expression = ScalarExpr::Cast {
1319            expr: Box::new(ScalarExpr::Column("support".into())),
1320            ty: "text".into(),
1321        };
1322        assert_eq!(
1323            eval_scalar(
1324                &expression,
1325                &ScalarEvalContext::from_row_lookup(&view, &[]).with_row_schema(&schema),
1326            )
1327            .unwrap(),
1328            Value::Str("-".into())
1329        );
1330    }
1331
1332    #[test]
1333    fn cast_preserves_unknown_type_for_string_literals() {
1334        let expression = ScalarExpr::Cast {
1335            expr: Box::new(ScalarExpr::Literal(Value::Str("[1,5)".into()))),
1336            ty: "int4range".into(),
1337        };
1338        assert_eq!(
1339            eval_scalar(&expression, &ScalarEvalContext::new(None, &[])).unwrap(),
1340            Value::Str("[1,5)".into())
1341        );
1342    }
1343
1344    #[test]
1345    fn parameter_zero_is_not_aliased_to_parameter_one() {
1346        let params = [SQLParam::Scalar(Value::Str("secret".into()))];
1347        assert!(matches!(
1348            eval_scalar(
1349                &ScalarExpr::Param(0),
1350                &ScalarEvalContext::new(None, &params)
1351            ),
1352            Err(SQLError::MissingParam(0))
1353        ));
1354    }
1355
1356    #[test]
1357    fn typed_scalar_parameter_evaluates_like_scalar() {
1358        let params = [SQLParam::typed_scalar(
1359            Value::Int(7),
1360            ColumnType::SmallInteger,
1361        )];
1362        assert_eq!(
1363            eval_scalar(
1364                &ScalarExpr::Param(1),
1365                &ScalarEvalContext::new(None, &params)
1366            )
1367            .unwrap(),
1368            Value::Int(7)
1369        );
1370    }
1371
1372    #[test]
1373    fn parameter_detection_descends_into_nested_expressions() {
1374        let expression = ScalarExpr::Func {
1375            name: "knn_match".into(),
1376            binding: None,
1377            args: vec![
1378                ScalarExpr::Column("embedding".into()),
1379                ScalarExpr::Array(vec![ScalarExpr::Param(1)]),
1380                ScalarExpr::Literal(Value::Int(3)),
1381            ],
1382            distinct: false,
1383            order_by: Vec::new(),
1384            filter: None,
1385        };
1386
1387        assert!(expression.contains_parameter());
1388        assert!(!ScalarExpr::Literal(Value::Int(3)).contains_parameter());
1389    }
1390
1391    #[test]
1392    fn explicit_variadic_call_argument_is_transparent_to_runtime_evaluation() {
1393        let arguments = vec![marker(
1394            FunctionDispatch::NamedArgument,
1395            vec![
1396                ScalarExpr::Literal(Value::Str("items".into())),
1397                marker(
1398                    FunctionDispatch::VariadicArgument,
1399                    vec![ScalarExpr::Literal(Value::Int(42))],
1400                ),
1401            ],
1402        )];
1403
1404        let decoded = scalar_call_arguments(&arguments).unwrap();
1405        assert_eq!(decoded[0].name, Some("items"));
1406        assert!(decoded[0].explicit_variadic);
1407        assert_eq!(decoded[0].value, &ScalarExpr::Literal(Value::Int(42)));
1408        assert_eq!(
1409            eval_call_arguments(&arguments, &ScalarEvalContext::new(None, &[])).unwrap(),
1410            vec![(Some("items".into()), Value::Int(42))]
1411        );
1412    }
1413
1414    #[test]
1415    fn call_argument_markers_reject_duplicates_and_malformed_nesting() {
1416        let duplicate = vec![
1417            marker(
1418                FunctionDispatch::VariadicArgument,
1419                vec![ScalarExpr::Literal(Value::Int(1))],
1420            ),
1421            marker(
1422                FunctionDispatch::VariadicArgument,
1423                vec![ScalarExpr::Literal(Value::Int(2))],
1424            ),
1425        ];
1426        assert!(matches!(
1427            scalar_call_arguments(&duplicate),
1428            Err(SQLError::Internal(message)) if message.contains("more than one")
1429        ));
1430
1431        let nested = vec![marker(
1432            FunctionDispatch::VariadicArgument,
1433            vec![marker(
1434                FunctionDispatch::VariadicArgument,
1435                vec![ScalarExpr::Literal(Value::Int(1))],
1436            )],
1437        )];
1438        assert!(matches!(
1439            scalar_call_arguments(&nested),
1440            Err(SQLError::Internal(message)) if message.contains("nested")
1441        ));
1442    }
1443
1444    fn marker(dispatch: FunctionDispatch, args: Vec<ScalarExpr>) -> ScalarExpr {
1445        let binding = FunctionBinding::dispatched(dispatch);
1446        ScalarExpr::Func {
1447            name: binding.name.clone(),
1448            binding: Some(binding),
1449            args,
1450            distinct: false,
1451            order_by: Vec::new(),
1452            filter: None,
1453        }
1454    }
1455}