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 resources;
23mod source;
24mod window;
25
26impl super::ExpressionPlan {
27    /// 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.
28    pub fn lower_column_budgeted(
29        expression: &Expr,
30        budget: &MemoryBudget,
31        original: &CancellationToken,
32        invoking: &CancellationToken,
33    ) -> Result<Budgeted<ScalarExpr>> {
34        let mut lowering = Lowering {
35            control: Some(Control::new(budget, original, invoking)),
36        };
37        let scalar = lowering.expression(
38            Source::Borrowed(expression),
39            &super::NoRegisteredAggregates,
40            &mut Vec::new(),
41        )?;
42        lowering.finish(scalar)
43    }
44}
45
46pub(super) fn lower_scalar_expression(
47    expression: Expr,
48    aggregates: &dyn AggregateClassifier,
49    subqueries: &mut Vec<QueryPlan>,
50) -> ScalarExpr {
51    Lowering { control: None }
52        .expression(Source::Owned(expression), aggregates, subqueries)
53        .expect("owned lowering has no admission failure")
54}
55
56impl Lowering<'_> {
57    #[expect(
58        clippy::too_many_lines,
59        reason = "plan lowering preserves exhaustive variants and structural identities"
60    )]
61    fn expression(
62        &mut self,
63        expression: Source<'_, Expr>,
64        aggregates: &dyn AggregateClassifier,
65        subqueries: &mut Vec<QueryPlan>,
66    ) -> Result<ScalarExpr> {
67        self.check()?;
68        Ok(match expression.node() {
69            Node::Star => ScalarExpr::Star,
70            Node::QualifiedStar(name) => ScalarExpr::QualifiedStar(self.text(name)?),
71            Node::Default => ScalarExpr::Default,
72            Node::Column(name) => ScalarExpr::Column(self.text(name)?),
73            Node::QualifiedColumn { qualifier, column } => ScalarExpr::QualifiedColumn {
74                qualifier: self.text(qualifier)?,
75                column: self.text(column)?,
76            },
77            Node::InternalColumn(column) => ScalarExpr::InternalColumn(column),
78            Node::Literal(value) => ScalarExpr::Literal(self.value(value)?),
79            Node::TypedLiteral { value, ty } => ScalarExpr::TypedLiteral {
80                value: self.value(value)?,
81                ty: self.text(ty)?,
82                bound_type: None,
83                parameter_index: None,
84            },
85            Node::Param(index) => ScalarExpr::Param(index),
86            Node::Func {
87                name,
88                binding,
89                args,
90                distinct,
91                order_by,
92                filter,
93            } => ScalarExpr::Func {
94                name: self.text(name)?,
95                binding: binding.map(|binding| self.binding(binding)).transpose()?,
96                args: self.map(args, |this, argument| {
97                    this.expression(argument, aggregates, subqueries)
98                })?,
99                distinct,
100                order_by: self.map(order_by, |this, order| {
101                    this.order(order, aggregates, subqueries)
102                })?,
103                filter: filter
104                    .map(|filter| self.child(filter, aggregates, subqueries))
105                    .transpose()?,
106            },
107            Node::Array(items) => ScalarExpr::Array(self.map(items, |this, item| {
108                this.expression(item, aggregates, subqueries)
109            })?),
110            Node::Row(items) => ScalarExpr::Row(self.map(items, |this, item| {
111                this.expression(item, aggregates, subqueries)
112            })?),
113            Node::Binary { op, lhs, rhs } => ScalarExpr::Binary {
114                op,
115                lhs: self.child(lhs, aggregates, subqueries)?,
116                rhs: self.child(rhs, aggregates, subqueries)?,
117            },
118            Node::UnaryMinus(expression) => {
119                ScalarExpr::UnaryMinus(self.child(expression, aggregates, subqueries)?)
120            }
121            Node::Not(expression) => {
122                ScalarExpr::Not(self.child(expression, aggregates, subqueries)?)
123            }
124            Node::And(items) => ScalarExpr::And(self.map(items, |this, item| {
125                this.expression(item, aggregates, subqueries)
126            })?),
127            Node::Or(items) => ScalarExpr::Or(self.map(items, |this, item| {
128                this.expression(item, aggregates, subqueries)
129            })?),
130            Node::IsNull { expr, negated } => ScalarExpr::IsNull {
131                expr: self.child(expr, aggregates, subqueries)?,
132                negated,
133            },
134            Node::Between { expr, low, high } => ScalarExpr::Between {
135                expr: self.child(expr, aggregates, subqueries)?,
136                low: self.child(low, aggregates, subqueries)?,
137                high: self.child(high, aggregates, subqueries)?,
138            },
139            Node::InList {
140                expr,
141                list,
142                negated,
143            } => ScalarExpr::InList {
144                expr: self.child(expr, aggregates, subqueries)?,
145                list: self.map(list, |this, item| {
146                    this.expression(item, aggregates, subqueries)
147                })?,
148                negated,
149            },
150            Node::WindowCall { name, args, spec } => ScalarExpr::WindowCall {
151                name: self.text(name)?,
152                args: self.map(args, |this, argument| {
153                    this.expression(argument, aggregates, subqueries)
154                })?,
155                spec: self.window(spec, aggregates, subqueries)?,
156            },
157            Node::Case {
158                base,
159                when,
160                else_branch,
161            } => ScalarExpr::Case {
162                base: base
163                    .map(|base| self.child(base, aggregates, subqueries))
164                    .transpose()?,
165                when: self.map(when, |this, pair| {
166                    let (condition, result) = pair.pair();
167                    Ok((
168                        this.expression(condition, aggregates, subqueries)?,
169                        this.expression(result, aggregates, subqueries)?,
170                    ))
171                })?,
172                else_branch: else_branch
173                    .map(|branch| self.child(branch, aggregates, subqueries))
174                    .transpose()?,
175            },
176            Node::Cast { expr, ty } => ScalarExpr::Cast {
177                expr: self.child(expr, aggregates, subqueries)?,
178                ty: self.text(ty)?,
179            },
180            Node::ScalarSubquery(query) => {
181                ScalarExpr::ScalarSubquery(self.query(query, aggregates, subqueries)?)
182            }
183            Node::Exists { body, negated } => ScalarExpr::Exists {
184                subquery: self.query(body, aggregates, subqueries)?,
185                negated,
186            },
187            Node::InSubquery {
188                expr,
189                body,
190                negated,
191            } => {
192                let expr = self.child(expr, aggregates, subqueries)?;
193                ScalarExpr::InSubquery {
194                    expr,
195                    subquery: self.query(body, aggregates, subqueries)?,
196                    negated,
197                }
198            }
199        })
200    }
201
202    fn child(
203        &mut self,
204        expression: Source<'_, Box<Expr>>,
205        aggregates: &dyn AggregateClassifier,
206        subqueries: &mut Vec<QueryPlan>,
207    ) -> Result<Box<ScalarExpr>> {
208        self.boxed(|this| this.expression(expression.unbox(), aggregates, subqueries))
209    }
210
211    fn query(
212        &self,
213        query: Source<'_, Box<crate::ast::SelectStmt>>,
214        aggregates: &dyn AggregateClassifier,
215        subqueries: &mut Vec<QueryPlan>,
216    ) -> Result<usize> {
217        self.check()?;
218        let Source::Owned(query) = query else {
219            return Err(CatalogRetentionError::UnexpectedSubquery);
220        };
221        let id = subqueries.len();
222        subqueries.push(QueryPlan::lower_with(*query, aggregates));
223        Ok(id)
224    }
225}
226
227pub(crate) fn is_builtin_aggregate(name: &str) -> bool {
228    crate::ast::is_builtin_aggregate_function(name)
229}
230
231#[cfg(test)]
232mod tests;