Skip to main content

uqa_sql/plan/
scalar.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! SQL AST to executable scalar IR lowering and aggregate classification.
8
9use super::{
10    AggregateClassifier, Expr, FrameBound, OrderBy, QueryPlan, ScalarExpr, ScalarFrameBound,
11    ScalarOrder, ScalarWindowFrame, ScalarWindowSpec, WindowSpec,
12};
13use crate::schema::retention::CatalogRetentionError;
14use resources::{Control, Lowering, Result};
15use source::{Node, Source};
16use uqa_core::{
17    memory::{Budgeted, MemoryBudget},
18    CancellationToken,
19};
20
21mod binding;
22mod copy;
23mod resources;
24mod source;
25mod window;
26
27impl super::ExpressionPlan {
28    /// Lower a borrowed, validated column expression directly into admitted scalar IR. Destination strings, value payloads, bindings, vector capacities and boxes acquire the supplied allowance before allocation, and the result retains those leases. Both the retained definition's original cancellation and the invoking reader's cancellation remain active during lowering. This controls AST-to-IR production only; subsequent type binding and evaluation require their own resource contracts. Query children violate the validated column-expression invariant.
29    pub fn lower_column_budgeted(
30        expression: &Expr,
31        budget: &MemoryBudget,
32        original: &CancellationToken,
33        invoking: &CancellationToken,
34    ) -> Result<Budgeted<ScalarExpr>> {
35        let mut lowering = Lowering {
36            control: Some(Control::new(budget, original, invoking)),
37        };
38        let scalar = lowering.expression(
39            Source::Borrowed(expression),
40            &super::NoRegisteredAggregates,
41            &mut Vec::new(),
42        )?;
43        lowering.finish(scalar)
44    }
45}
46
47pub(super) fn lower_scalar_expression(
48    expression: Expr,
49    aggregates: &dyn AggregateClassifier,
50    subqueries: &mut Vec<QueryPlan>,
51) -> ScalarExpr {
52    Lowering { control: None }
53        .expression(Source::Owned(expression), aggregates, subqueries)
54        .expect("owned lowering has no admission failure")
55}
56
57pub(super) fn lower_window_spec(
58    specification: WindowSpec,
59    aggregates: &dyn AggregateClassifier,
60    subqueries: &mut Vec<QueryPlan>,
61) -> ScalarWindowSpec {
62    Lowering { control: None }
63        .window(Source::Owned(specification), aggregates, subqueries)
64        .expect("owned lowering has no admission failure")
65}
66
67impl Lowering<'_> {
68    #[expect(
69        clippy::too_many_lines,
70        reason = "plan lowering preserves exhaustive variants and structural identities"
71    )]
72    fn expression(
73        &mut self,
74        expression: Source<'_, Expr>,
75        aggregates: &dyn AggregateClassifier,
76        subqueries: &mut Vec<QueryPlan>,
77    ) -> Result<ScalarExpr> {
78        self.check()?;
79        Ok(match expression.node() {
80            Node::Star => ScalarExpr::Star,
81            Node::QualifiedStar(name) => ScalarExpr::QualifiedStar(self.text(name)?),
82            Node::Default => ScalarExpr::Default,
83            Node::Column(name) => ScalarExpr::Column(self.text(name)?),
84            Node::QualifiedColumn { qualifier, column } => ScalarExpr::QualifiedColumn {
85                qualifier: self.text(qualifier)?,
86                column: self.text(column)?,
87            },
88            Node::InternalColumn(column) => ScalarExpr::InternalColumn(column),
89            Node::Literal(value) => ScalarExpr::Literal(self.value(value)?),
90            Node::TypedLiteral {
91                value,
92                ty,
93                composite_source,
94            } => ScalarExpr::TypedLiteral {
95                composite_source: self.composite_source(composite_source)?,
96                value: self.value(value)?,
97                ty: self.text(ty)?,
98                bound_type: None,
99                parameter_index: None,
100            },
101            Node::Param(index) => ScalarExpr::Param(index),
102            Node::Func {
103                name,
104                binding,
105                args,
106                distinct,
107                order_by,
108                order_syntax,
109                filter,
110            } => ScalarExpr::Func {
111                name: self.text(name)?,
112                binding: binding.map(|binding| self.binding(binding)).transpose()?,
113                args: self.map(args, |this, argument| {
114                    this.expression(argument, aggregates, subqueries)
115                })?,
116                distinct,
117                order_by: self.map(order_by, |this, order| {
118                    this.order(order, aggregates, subqueries)
119                })?,
120                order_syntax,
121                filter: filter
122                    .map(|filter| self.child(filter, aggregates, subqueries))
123                    .transpose()?,
124            },
125            Node::Array(items) => ScalarExpr::Array(self.map(items, |this, item| {
126                this.expression(item, aggregates, subqueries)
127            })?),
128            Node::CompositeRow { items, binding } => ScalarExpr::CompositeRow {
129                items: self.map(items, |this, item| {
130                    this.expression(item, aggregates, subqueries)
131                })?,
132                binding: self.composite_binding(binding)?,
133                bound_type: None,
134            },
135            Node::Row(items) => ScalarExpr::Row(self.map(items, |this, item| {
136                this.expression(item, aggregates, subqueries)
137            })?),
138            Node::Binary { op, lhs, rhs } => ScalarExpr::Binary {
139                op,
140                lhs: self.child(lhs, aggregates, subqueries)?,
141                rhs: self.child(rhs, aggregates, subqueries)?,
142            },
143            Node::UnaryMinus(expression) => {
144                ScalarExpr::UnaryMinus(self.child(expression, aggregates, subqueries)?)
145            }
146            Node::Not(expression) => {
147                ScalarExpr::Not(self.child(expression, aggregates, subqueries)?)
148            }
149            Node::And(items) => ScalarExpr::And(self.map(items, |this, item| {
150                this.expression(item, aggregates, subqueries)
151            })?),
152            Node::Or(items) => ScalarExpr::Or(self.map(items, |this, item| {
153                this.expression(item, aggregates, subqueries)
154            })?),
155            Node::IsNull { expr, negated } => ScalarExpr::IsNull {
156                expr: self.child(expr, aggregates, subqueries)?,
157                negated,
158            },
159            Node::Between { expr, low, high } => ScalarExpr::Between {
160                expr: self.child(expr, aggregates, subqueries)?,
161                low: self.child(low, aggregates, subqueries)?,
162                high: self.child(high, aggregates, subqueries)?,
163            },
164            Node::InList {
165                expr,
166                list,
167                negated,
168            } => ScalarExpr::InList {
169                expr: self.child(expr, aggregates, subqueries)?,
170                list: self.map(list, |this, item| {
171                    this.expression(item, aggregates, subqueries)
172                })?,
173                negated,
174            },
175            Node::WindowCall {
176                name,
177                args,
178                spec,
179                filter,
180                modifiers,
181            } => ScalarExpr::WindowCall {
182                modifiers,
183                name: self.text(name)?,
184                args: self.map(args, |this, argument| {
185                    this.expression(argument, aggregates, subqueries)
186                })?,
187                spec: self.window(spec, aggregates, subqueries)?,
188                filter: filter
189                    .map(|filter| self.child(filter, aggregates, subqueries))
190                    .transpose()?,
191            },
192            Node::Case {
193                base,
194                when,
195                else_branch,
196            } => ScalarExpr::Case {
197                base: base
198                    .map(|base| self.child(base, aggregates, subqueries))
199                    .transpose()?,
200                when: self.map(when, |this, pair| {
201                    let (condition, result) = pair.pair();
202                    Ok((
203                        this.expression(condition, aggregates, subqueries)?,
204                        this.expression(result, aggregates, subqueries)?,
205                    ))
206                })?,
207                else_branch: else_branch
208                    .map(|branch| self.child(branch, aggregates, subqueries))
209                    .transpose()?,
210            },
211            Node::Cast { expr, ty, implicit } => ScalarExpr::Cast {
212                implicit,
213                expr: self.child(expr, aggregates, subqueries)?,
214                ty: self.text(ty)?,
215            },
216            Node::ScalarSubquery(query) => {
217                ScalarExpr::ScalarSubquery(self.query(query, aggregates, subqueries)?)
218            }
219            Node::Exists { body, negated } => ScalarExpr::Exists {
220                subquery: self.query(body, aggregates, subqueries)?,
221                negated,
222            },
223            Node::InSubquery {
224                expr,
225                body,
226                negated,
227            } => {
228                let expr = self.child(expr, aggregates, subqueries)?;
229                ScalarExpr::InSubquery {
230                    expr,
231                    subquery: self.query(body, aggregates, subqueries)?,
232                    negated,
233                }
234            }
235        })
236    }
237
238    fn child(
239        &mut self,
240        expression: Source<'_, Box<Expr>>,
241        aggregates: &dyn AggregateClassifier,
242        subqueries: &mut Vec<QueryPlan>,
243    ) -> Result<Box<ScalarExpr>> {
244        self.boxed(|this| this.expression(expression.unbox(), aggregates, subqueries))
245    }
246
247    fn query(
248        &self,
249        query: Source<'_, Box<crate::ast::SelectStmt>>,
250        aggregates: &dyn AggregateClassifier,
251        subqueries: &mut Vec<QueryPlan>,
252    ) -> Result<usize> {
253        self.check()?;
254        let Source::Owned(query) = query else {
255            return Err(CatalogRetentionError::UnexpectedSubquery);
256        };
257        let id = subqueries.len();
258        subqueries.push(QueryPlan::lower_with(*query, aggregates));
259        Ok(id)
260    }
261}
262
263pub(crate) fn is_builtin_aggregate(name: &str) -> bool {
264    crate::ast::is_builtin_aggregate_function(name)
265}
266
267#[cfg(test)]
268mod tests;