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