Skip to main content

uqa_planner/unified_plan/
query.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! SELECT, source, projection, order, CTE, and mutation-child lowering.
8
9use super::rewrite::rewrite_query_scalars;
10use super::scalar::{is_builtin_aggregate, lower_scalar_expression};
11use super::{
12    AccessPathPlan, AggregateClassifier, AssignmentPlan, ComputePlan, CteCyclePlan, CtePlan,
13    CteSearchPlan, Expr, ExpressionPlan, FromClause, JoinExecutionStrategy, MergeWhenPlan,
14    NoRegisteredAggregates, OrderBy, OrderPlan, Projection, ProjectionPlan, QueryBlockPlan,
15    QueryPlan, RelationalPlan, ScalarExpr, SelectStmt, SourcePlan, TableFunctionPlan, CTE,
16};
17
18impl QueryPlan {
19    /// Rewrite every physical scalar node owned by this query exactly once,
20    /// including CTEs, relational sources, and scalar-subquery plans.
21    pub fn rewrite_scalar_expressions(&mut self, rewrite: &mut dyn FnMut(&mut ScalarExpr)) {
22        rewrite_query_scalars(self, rewrite);
23    }
24
25    #[must_use]
26    pub fn lower(statement: SelectStmt) -> Self {
27        Self::lower_with(statement, &NoRegisteredAggregates)
28    }
29
30    #[must_use]
31    pub fn lower_with(mut statement: SelectStmt, aggregates: &dyn AggregateClassifier) -> Self {
32        let ctes = lower_ctes(&statement.with, aggregates);
33        statement.with.clear();
34        let root = lower_relational_root(statement, aggregates);
35        Self { ctes, root }
36    }
37}
38
39pub(super) fn lower_ctes(ctes: &[CTE], aggregates: &dyn AggregateClassifier) -> Vec<CtePlan> {
40    ctes.iter()
41        .map(|cte| CtePlan {
42            name: cte.name.clone(),
43            columns: cte.columns.clone(),
44            recursive: cte.recursive,
45            materialization: cte.materialization,
46            search: cte.search.as_ref().map(|search| CteSearchPlan {
47                columns: search.columns.clone(),
48                breadth_first: search.breadth_first,
49                sequence_column: search.sequence_column.clone(),
50            }),
51            cycle: cte.cycle.as_ref().map(|cycle| CteCyclePlan {
52                columns: cycle.columns.clone(),
53                mark_column: cycle.mark_column.clone(),
54                mark_value: lower_scalar_expression(
55                    cycle.mark_value.clone(),
56                    aggregates,
57                    &mut Vec::new(),
58                ),
59                mark_default: lower_scalar_expression(
60                    cycle.mark_default.clone(),
61                    aggregates,
62                    &mut Vec::new(),
63                ),
64                path_column: cycle.path_column.clone(),
65            }),
66            query: Box::new(QueryPlan::lower_with((*cte.query).clone(), aggregates)),
67        })
68        .collect()
69}
70
71pub(super) fn lower_assignments(
72    assignments: Vec<(String, Expr)>,
73    aggregates: &dyn AggregateClassifier,
74    subqueries: &mut Vec<QueryPlan>,
75) -> Vec<AssignmentPlan> {
76    assignments
77        .into_iter()
78        .map(|(column, expression)| AssignmentPlan {
79            column,
80            value: lower_scalar_expression(expression, aggregates, subqueries),
81        })
82        .collect()
83}
84
85pub(super) fn lower_merge_when(
86    clause: uqa_sql::ast::MergeWhen,
87    aggregates: &dyn AggregateClassifier,
88    subqueries: &mut Vec<QueryPlan>,
89) -> MergeWhenPlan {
90    let mut lower_optional = |expression: Option<Expr>| {
91        expression.map(|expression| lower_scalar_expression(expression, aggregates, subqueries))
92    };
93    match clause {
94        uqa_sql::ast::MergeWhen::UpdateMatched {
95            condition,
96            assignments,
97        } => {
98            let condition = lower_optional(condition);
99            let assignments = lower_assignments(assignments, aggregates, subqueries);
100            MergeWhenPlan::UpdateMatched {
101                condition,
102                assignments,
103            }
104        }
105        uqa_sql::ast::MergeWhen::DeleteMatched { condition } => MergeWhenPlan::DeleteMatched {
106            condition: lower_optional(condition),
107        },
108        uqa_sql::ast::MergeWhen::UpdateNotMatchedBySource {
109            condition,
110            assignments,
111        } => {
112            let condition = lower_optional(condition);
113            let assignments = lower_assignments(assignments, aggregates, subqueries);
114            MergeWhenPlan::UpdateNotMatchedBySource {
115                condition,
116                assignments,
117            }
118        }
119        uqa_sql::ast::MergeWhen::DeleteNotMatchedBySource { condition } => {
120            MergeWhenPlan::DeleteNotMatchedBySource {
121                condition: lower_optional(condition),
122            }
123        }
124        uqa_sql::ast::MergeWhen::InsertNotMatched {
125            condition,
126            columns,
127            values,
128        } => {
129            let condition = lower_optional(condition);
130            let values = values
131                .into_iter()
132                .map(|value| lower_scalar_expression(value, aggregates, subqueries))
133                .collect();
134            MergeWhenPlan::InsertNotMatched {
135                condition,
136                columns,
137                values,
138            }
139        }
140        uqa_sql::ast::MergeWhen::NothingMatched { condition } => MergeWhenPlan::NothingMatched {
141            condition: lower_optional(condition),
142        },
143        uqa_sql::ast::MergeWhen::NothingNotMatched { condition } => {
144            MergeWhenPlan::NothingNotMatched {
145                condition: lower_optional(condition),
146            }
147        }
148        uqa_sql::ast::MergeWhen::NothingNotMatchedBySource { condition } => {
149            MergeWhenPlan::NothingNotMatchedBySource {
150                condition: lower_optional(condition),
151            }
152        }
153    }
154}
155pub(super) fn lower_relational_root(
156    mut statement: SelectStmt,
157    aggregates: &dyn AggregateClassifier,
158) -> RelationalPlan {
159    if statement.set_op.is_none() && !statement.values.is_empty() {
160        let mut subqueries = Vec::new();
161        let rows = statement
162            .values
163            .into_iter()
164            .map(|row| {
165                row.into_iter()
166                    .map(|expr| lower_scalar_expression(expr, aggregates, &mut subqueries))
167                    .collect()
168            })
169            .collect();
170        return RelationalPlan::Values { rows, subqueries };
171    }
172    let Some(set_op) = statement.set_op.take() else {
173        return RelationalPlan::QueryBlock(Box::new(QueryBlockPlan::lower_with(
174            statement, aggregates,
175        )));
176    };
177
178    let left = if let Some(left) = set_op.left {
179        QueryPlan::lower_with(*left, aggregates)
180    } else {
181        QueryPlan {
182            ctes: Vec::new(),
183            root: RelationalPlan::QueryBlock(Box::new(QueryBlockPlan::lower_with(
184                statement, aggregates,
185            ))),
186        }
187    };
188    let right = QueryPlan::lower_with(set_op.right, aggregates);
189    let mut subqueries = Vec::new();
190    RelationalPlan::SetOp {
191        kind: set_op.kind,
192        all: set_op.all,
193        left: Box::new(left),
194        right: Box::new(right),
195        order_by: set_op
196            .combined_order_by
197            .into_iter()
198            .map(|order| OrderPlan::lower_with(order, aggregates, &mut subqueries))
199            .collect(),
200        limit: set_op
201            .combined_limit
202            .map(|expr| Box::new(lower_scalar_expression(expr, aggregates, &mut subqueries))),
203        with_ties: set_op.combined_with_ties,
204        offset: set_op
205            .combined_offset
206            .map(|expr| Box::new(lower_scalar_expression(expr, aggregates, &mut subqueries))),
207        subqueries,
208    }
209}
210
211impl QueryBlockPlan {
212    fn lower_with(statement: SelectStmt, aggregates: &dyn AggregateClassifier) -> Self {
213        debug_assert!(statement.with.is_empty());
214        debug_assert!(statement.set_op.is_none());
215        let mut subqueries = Vec::new();
216        let projections: Vec<ProjectionPlan> = statement
217            .projections
218            .into_iter()
219            .map(|projection| ProjectionPlan::lower_with(projection, aggregates, &mut subqueries))
220            .collect();
221        let is_aggregate =
222            |name: &str| is_builtin_aggregate(name) || aggregates.is_registered_aggregate(name);
223        let has_aggregate = !statement.group_by.is_empty()
224            || !statement.grouping_sets.is_empty()
225            || statement.having.is_some()
226            || projections
227                .iter()
228                .any(|projection| projection.expr.contains_aggregate(&is_aggregate));
229        let has_window = projections
230            .iter()
231            .any(|projection| projection.expr.contains_window());
232        let compute = if has_aggregate {
233            ComputePlan::Aggregate
234        } else if has_window {
235            ComputePlan::Window
236        } else {
237            ComputePlan::Project
238        };
239        Self {
240            projections,
241            from: statement
242                .from
243                .map(|source| SourcePlan::lower_with(source, aggregates, &mut subqueries)),
244            r#where: statement
245                .r#where
246                .map(|expr| lower_scalar_expression(expr, aggregates, &mut subqueries)),
247            compute,
248            group_by: statement
249                .group_by
250                .into_iter()
251                .map(|expr| lower_scalar_expression(expr, aggregates, &mut subqueries))
252                .collect(),
253            grouping_sets: statement
254                .grouping_sets
255                .into_iter()
256                .map(|set| {
257                    set.into_iter()
258                        .map(|expr| lower_scalar_expression(expr, aggregates, &mut subqueries))
259                        .collect()
260                })
261                .collect(),
262            group_distinct: statement.group_distinct,
263            having: statement
264                .having
265                .map(|expr| lower_scalar_expression(expr, aggregates, &mut subqueries)),
266            order_by: statement
267                .order_by
268                .into_iter()
269                .map(|order| OrderPlan::lower_with(order, aggregates, &mut subqueries))
270                .collect(),
271            limit: statement
272                .limit
273                .map(|expr| lower_scalar_expression(expr, aggregates, &mut subqueries)),
274            with_ties: statement.with_ties,
275            offset: statement
276                .offset
277                .map(|expr| lower_scalar_expression(expr, aggregates, &mut subqueries)),
278            distinct: statement.distinct,
279            distinct_on: statement
280                .distinct_on
281                .into_iter()
282                .map(|expr| lower_scalar_expression(expr, aggregates, &mut subqueries))
283                .collect(),
284            subqueries,
285            access: AccessPathPlan::Row,
286            locking: statement.locking,
287        }
288    }
289
290    /// Expression nodes evaluated while executing this query block. Query
291    /// bodies under `FROM (SELECT ...)` are excluded because their child plan
292    /// installs its own expression scope when it executes.
293    #[must_use]
294    pub fn expressions(&self) -> Vec<&ScalarExpr> {
295        let mut expressions = Vec::new();
296        if let Some(source) = &self.from {
297            source.push_expressions(&mut expressions);
298        }
299        if let Some(filter) = &self.r#where {
300            expressions.push(filter);
301        }
302        for projection in &self.projections {
303            expressions.push(&projection.expr);
304        }
305        expressions.extend(&self.group_by);
306        for set in &self.grouping_sets {
307            expressions.extend(set);
308        }
309        if let Some(having) = &self.having {
310            expressions.push(having);
311        }
312        expressions.extend(self.order_by.iter().map(|order| &order.expr));
313        if let Some(limit) = &self.limit {
314            expressions.push(limit);
315        }
316        if let Some(offset) = &self.offset {
317            expressions.push(offset);
318        }
319        expressions.extend(&self.distinct_on);
320        expressions
321    }
322}
323
324impl SourcePlan {
325    /// SQL-visible relation qualifier for a non-join FROM item. PostgreSQL uses the local function name, not its schema-qualified lookup identity, when a table function has no explicit alias.
326    #[must_use]
327    pub fn visible_qualifier(&self) -> Option<&str> {
328        match self {
329            Self::Table {
330                qualifier, alias, ..
331            } => Some(alias.as_deref().unwrap_or(qualifier)),
332            Self::Function {
333                output_name, alias, ..
334            } => Some(alias.as_deref().unwrap_or(output_name)),
335            Self::FunctionGroup {
336                functions, alias, ..
337            } => alias.as_deref().or_else(|| {
338                functions
339                    .first()
340                    .map(|function| function.output_name.as_str())
341            }),
342            Self::Values {
343                alias,
344                internal_relation,
345                ..
346            } => internal_relation
347                .is_none()
348                .then_some(alias.as_deref())
349                .flatten(),
350            Self::Subquery { alias, .. } => alias.as_deref(),
351            Self::Join { alias, .. } => alias.as_deref(),
352        }
353    }
354
355    pub(super) fn lower_with(
356        source: FromClause,
357        aggregates: &dyn AggregateClassifier,
358        subqueries: &mut Vec<QueryPlan>,
359    ) -> Self {
360        match source {
361            FromClause::Table {
362                name,
363                qualifier,
364                alias,
365                include_descendants,
366            } => Self::Table {
367                name,
368                qualifier,
369                alias,
370                include_descendants,
371            },
372            FromClause::Join {
373                left,
374                right,
375                kind,
376                on,
377                using,
378                natural,
379                alias,
380                column_aliases,
381                lateral,
382            } => Self::Join {
383                left: Box::new(Self::lower_with(*left, aggregates, subqueries)),
384                right: Box::new(Self::lower_with(*right, aggregates, subqueries)),
385                kind,
386                on: on.map(|expr| lower_scalar_expression(expr, aggregates, subqueries)),
387                using,
388                natural,
389                alias,
390                column_aliases,
391                lateral,
392                strategy: JoinExecutionStrategy::Auto,
393            },
394            FromClause::Values {
395                rows,
396                alias,
397                column_aliases,
398                internal_relation,
399                internal_column_types,
400            } => Self::Values {
401                rows: rows
402                    .into_iter()
403                    .map(|row| {
404                        row.into_iter()
405                            .map(|expr| lower_scalar_expression(expr, aggregates, subqueries))
406                            .collect()
407                    })
408                    .collect(),
409                alias,
410                column_aliases,
411                internal_relation,
412                internal_column_types,
413            },
414            FromClause::Function {
415                name,
416                output_name,
417                relation,
418                args,
419                alias,
420                column_aliases,
421                ordinality,
422                column_types,
423            } => Self::Function {
424                name,
425                binding: None,
426                output_name,
427                relation,
428                args: args
429                    .into_iter()
430                    .map(|expr| lower_scalar_expression(expr, aggregates, subqueries))
431                    .collect(),
432                alias,
433                column_aliases,
434                ordinality,
435                column_types,
436            },
437            FromClause::FunctionGroup {
438                functions,
439                alias,
440                column_aliases,
441                ordinality,
442            } => Self::FunctionGroup {
443                functions: functions
444                    .into_iter()
445                    .map(|function| TableFunctionPlan {
446                        name: function.name,
447                        binding: None,
448                        output_name: function.output_name,
449                        relation: function.relation,
450                        args: function
451                            .args
452                            .into_iter()
453                            .map(|expr| lower_scalar_expression(expr, aggregates, subqueries))
454                            .collect(),
455                        column_aliases: function.column_aliases,
456                        column_types: function.column_types,
457                    })
458                    .collect(),
459                alias,
460                column_aliases,
461                ordinality,
462            },
463            FromClause::Subquery {
464                body,
465                alias,
466                column_aliases,
467            } => Self::Subquery {
468                body: Box::new(QueryPlan::lower_with(*body, aggregates)),
469                alias,
470                column_aliases,
471            },
472        }
473    }
474
475    fn push_expressions<'a>(&'a self, output: &mut Vec<&'a ScalarExpr>) {
476        match self {
477            Self::Table { .. } | Self::Subquery { .. } => {}
478            Self::Join {
479                left, right, on, ..
480            } => {
481                left.push_expressions(output);
482                right.push_expressions(output);
483                if let Some(on) = on {
484                    output.push(on);
485                }
486            }
487            Self::Values { rows, .. } => {
488                for row in rows {
489                    output.extend(row);
490                }
491            }
492            Self::Function { args, .. } => output.extend(args),
493            Self::FunctionGroup { functions, .. } => {
494                for function in functions {
495                    output.extend(&function.args);
496                }
497            }
498        }
499    }
500
501    pub fn collect_tables(&self, output: &mut Vec<(String, Option<String>)>) {
502        match self {
503            Self::Table {
504                name,
505                qualifier,
506                alias,
507                ..
508            } => output.push((
509                name.clone(),
510                Some(alias.as_ref().unwrap_or(qualifier).clone()),
511            )),
512            Self::Join { left, right, .. } => {
513                left.collect_tables(output);
514                right.collect_tables(output);
515            }
516            Self::Values { .. }
517            | Self::Function { .. }
518            | Self::FunctionGroup { .. }
519            | Self::Subquery { .. } => {}
520        }
521    }
522}
523
524impl ProjectionPlan {
525    pub(super) fn lower_with(
526        projection: Projection,
527        aggregates: &dyn AggregateClassifier,
528        subqueries: &mut Vec<QueryPlan>,
529    ) -> Self {
530        Self {
531            expr: lower_scalar_expression(projection.expr, aggregates, subqueries),
532            alias: projection.alias,
533        }
534    }
535}
536
537impl OrderPlan {
538    fn lower_with(
539        order: OrderBy,
540        aggregates: &dyn AggregateClassifier,
541        subqueries: &mut Vec<QueryPlan>,
542    ) -> Self {
543        Self {
544            expr: lower_scalar_expression(order.expr, aggregates, subqueries),
545            descending: order.descending,
546            nulls: order.nulls,
547        }
548    }
549}
550
551impl ExpressionPlan {
552    #[must_use]
553    pub fn lower(expression: Expr) -> Self {
554        Self::lower_with(expression, &NoRegisteredAggregates)
555    }
556
557    pub(super) fn lower_with(expression: Expr, aggregates: &dyn AggregateClassifier) -> Self {
558        let mut subqueries = Vec::new();
559        let scalar = lower_scalar_expression(expression, aggregates, &mut subqueries);
560        Self { scalar, subqueries }
561    }
562}