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