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