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 { value, ty } => ScalarExpr::TypedLiteral {
91                value: self.value(value)?,
92                ty: self.text(ty)?,
93                bound_type: None,
94                parameter_index: None,
95            },
96            Node::Param(index) => ScalarExpr::Param(index),
97            Node::Func {
98                name,
99                binding,
100                args,
101                distinct,
102                order_by,
103                order_syntax,
104                filter,
105            } => ScalarExpr::Func {
106                name: self.text(name)?,
107                binding: binding.map(|binding| self.binding(binding)).transpose()?,
108                args: self.map(args, |this, argument| {
109                    this.expression(argument, aggregates, subqueries)
110                })?,
111                distinct,
112                order_by: self.map(order_by, |this, order| {
113                    this.order(order, aggregates, subqueries)
114                })?,
115                order_syntax,
116                filter: filter
117                    .map(|filter| self.child(filter, aggregates, subqueries))
118                    .transpose()?,
119            },
120            Node::Array(items) => ScalarExpr::Array(self.map(items, |this, item| {
121                this.expression(item, aggregates, subqueries)
122            })?),
123            Node::CompositeRow { items, binding } => ScalarExpr::CompositeRow {
124                items: self.map(items, |this, item| {
125                    this.expression(item, aggregates, subqueries)
126                })?,
127                binding: self.composite_binding(binding)?,
128                bound_type: None,
129            },
130            Node::Row(items) => ScalarExpr::Row(self.map(items, |this, item| {
131                this.expression(item, aggregates, subqueries)
132            })?),
133            Node::Binary { op, lhs, rhs } => ScalarExpr::Binary {
134                op,
135                lhs: self.child(lhs, aggregates, subqueries)?,
136                rhs: self.child(rhs, aggregates, subqueries)?,
137            },
138            Node::UnaryMinus(expression) => {
139                ScalarExpr::UnaryMinus(self.child(expression, aggregates, subqueries)?)
140            }
141            Node::Not(expression) => {
142                ScalarExpr::Not(self.child(expression, aggregates, subqueries)?)
143            }
144            Node::And(items) => ScalarExpr::And(self.map(items, |this, item| {
145                this.expression(item, aggregates, subqueries)
146            })?),
147            Node::Or(items) => ScalarExpr::Or(self.map(items, |this, item| {
148                this.expression(item, aggregates, subqueries)
149            })?),
150            Node::IsNull { expr, negated } => ScalarExpr::IsNull {
151                expr: self.child(expr, aggregates, subqueries)?,
152                negated,
153            },
154            Node::Between { expr, low, high } => ScalarExpr::Between {
155                expr: self.child(expr, aggregates, subqueries)?,
156                low: self.child(low, aggregates, subqueries)?,
157                high: self.child(high, aggregates, subqueries)?,
158            },
159            Node::InList {
160                expr,
161                list,
162                negated,
163            } => ScalarExpr::InList {
164                expr: self.child(expr, aggregates, subqueries)?,
165                list: self.map(list, |this, item| {
166                    this.expression(item, aggregates, subqueries)
167                })?,
168                negated,
169            },
170            Node::WindowCall {
171                name,
172                args,
173                spec,
174                filter,
175                modifiers,
176            } => ScalarExpr::WindowCall {
177                modifiers,
178                name: self.text(name)?,
179                args: self.map(args, |this, argument| {
180                    this.expression(argument, aggregates, subqueries)
181                })?,
182                spec: self.window(spec, aggregates, subqueries)?,
183                filter: filter
184                    .map(|filter| self.child(filter, aggregates, subqueries))
185                    .transpose()?,
186            },
187            Node::Case {
188                base,
189                when,
190                else_branch,
191            } => ScalarExpr::Case {
192                base: base
193                    .map(|base| self.child(base, aggregates, subqueries))
194                    .transpose()?,
195                when: self.map(when, |this, pair| {
196                    let (condition, result) = pair.pair();
197                    Ok((
198                        this.expression(condition, aggregates, subqueries)?,
199                        this.expression(result, aggregates, subqueries)?,
200                    ))
201                })?,
202                else_branch: else_branch
203                    .map(|branch| self.child(branch, aggregates, subqueries))
204                    .transpose()?,
205            },
206            Node::Cast { expr, ty, implicit } => ScalarExpr::Cast {
207                implicit,
208                expr: self.child(expr, aggregates, subqueries)?,
209                ty: self.text(ty)?,
210            },
211            Node::ScalarSubquery(query) => {
212                ScalarExpr::ScalarSubquery(self.query(query, aggregates, subqueries)?)
213            }
214            Node::Exists { body, negated } => ScalarExpr::Exists {
215                subquery: self.query(body, aggregates, subqueries)?,
216                negated,
217            },
218            Node::InSubquery {
219                expr,
220                body,
221                negated,
222            } => {
223                let expr = self.child(expr, aggregates, subqueries)?;
224                ScalarExpr::InSubquery {
225                    expr,
226                    subquery: self.query(body, aggregates, subqueries)?,
227                    negated,
228                }
229            }
230        })
231    }
232
233    fn child(
234        &mut self,
235        expression: Source<'_, Box<Expr>>,
236        aggregates: &dyn AggregateClassifier,
237        subqueries: &mut Vec<QueryPlan>,
238    ) -> Result<Box<ScalarExpr>> {
239        self.boxed(|this| this.expression(expression.unbox(), aggregates, subqueries))
240    }
241
242    fn query(
243        &self,
244        query: Source<'_, Box<crate::ast::SelectStmt>>,
245        aggregates: &dyn AggregateClassifier,
246        subqueries: &mut Vec<QueryPlan>,
247    ) -> Result<usize> {
248        self.check()?;
249        let Source::Owned(query) = query else {
250            return Err(CatalogRetentionError::UnexpectedSubquery);
251        };
252        let id = subqueries.len();
253        subqueries.push(QueryPlan::lower_with(*query, aggregates));
254        Ok(id)
255    }
256}
257
258pub(crate) fn is_builtin_aggregate(name: &str) -> bool {
259    crate::ast::is_builtin_aggregate_function(name)
260}
261
262#[cfg(test)]
263mod tests;