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