Skip to main content

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