Skip to main content

uqa_sql/binding/
correlation.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Physical-plan correlation analysis for scalar subquery initialization.
8
9use std::collections::{BTreeMap, BTreeSet};
10
11use crate::plan::{ComputePlan, ProjectionPlan, QueryPlan, RelationalPlan, SourcePlan};
12use crate::SQLError;
13use crate::{ScalarExpr, ScalarFrameBound};
14
15use crate::catalog::{analysis::AnalysisCatalog, resolution::RelationNameResolution};
16use crate::semantics::projection_columns;
17
18/// A single immutable namespace and catalog view used throughout correlation analysis.
19#[derive(Clone, Copy)]
20pub struct CorrelationContext<'a> {
21    pub catalog: &'a dyn AnalysisCatalog,
22    pub resolution: &'a RelationNameResolution,
23}
24
25#[derive(Clone, Default)]
26struct RelationColumns {
27    names: BTreeSet<String>,
28    ordered: Vec<String>,
29    complete: bool,
30}
31
32impl RelationColumns {
33    fn known(ordered: Vec<String>) -> Self {
34        Self {
35            names: ordered.iter().cloned().collect(),
36            ordered,
37            complete: true,
38        }
39    }
40
41    fn empty_known() -> Self {
42        Self::known(Vec::new())
43    }
44
45    fn apply_positional_aliases(&mut self, aliases: &[String]) {
46        for (column, alias) in self.ordered.iter_mut().zip(aliases) {
47            column.clone_from(alias);
48        }
49        self.names = self.ordered.iter().cloned().collect();
50    }
51}
52
53#[derive(Clone, Default)]
54struct QueryScope {
55    qualifiers: BTreeSet<String>,
56    internal_relations: BTreeSet<crate::ast::InternalRelationId>,
57    columns: RelationColumns,
58}
59
60pub struct DecorrelatedExistsPlan {
61    pub inner: QueryPlan,
62    pub outer_keys: Vec<ScalarExpr>,
63}
64
65/// Turn a simple correlated equality EXISTS into an uncorrelated key query.
66///
67/// The caller materializes the returned inner key rows once and probes them
68/// with `outer_keys`, which is the physical equivalent of a hash semi-join.
69pub fn decorrelate_exists(
70    context: CorrelationContext<'_>,
71    plan: &QueryPlan,
72) -> Result<Option<DecorrelatedExistsPlan>, SQLError> {
73    if !plan.ctes.is_empty() {
74        return Ok(None);
75    }
76    let RelationalPlan::QueryBlock(block) = &plan.root else {
77        return Ok(None);
78    };
79    let Some(source) = block.from.as_ref() else {
80        return Ok(None);
81    };
82    if !matches!(block.compute, ComputePlan::Project)
83        || !block.group_by.is_empty()
84        || !block.grouping_sets.is_empty()
85        || block.having.is_some()
86        || block.limit.is_some()
87        || block.offset.is_some()
88        || block.distinct
89        || !block.distinct_on.is_empty()
90        || !block.subqueries.is_empty()
91    {
92        return Ok(None);
93    }
94
95    let scope = source_scope(context, source, &BTreeMap::new())?;
96    let mut source_scopes = vec![scope.clone()];
97    if source_has_external_reference(context, source, &mut source_scopes)? {
98        return Ok(None);
99    }
100    let Some(predicate) = block.r#where.as_ref() else {
101        return Ok(None);
102    };
103    let conjuncts = match predicate {
104        ScalarExpr::And(items) => items.as_slice(),
105        expression => std::slice::from_ref(expression),
106    };
107    let mut inner_keys = Vec::new();
108    let mut outer_keys = Vec::new();
109    let mut residual = Vec::new();
110    for conjunct in conjuncts {
111        if let ScalarExpr::Binary {
112            op: crate::ast::BinaryOp::Equal,
113            lhs,
114            rhs,
115        } = conjunct
116        {
117            let lhs_scope = correlation_column_scope(lhs, &scope);
118            let rhs_scope = correlation_column_scope(rhs, &scope);
119            match (lhs_scope, rhs_scope) {
120                (Some(ColumnScope::Inner), Some(ColumnScope::Outer)) => {
121                    inner_keys.push((**lhs).clone());
122                    outer_keys.push((**rhs).clone());
123                    continue;
124                }
125                (Some(ColumnScope::Outer), Some(ColumnScope::Inner)) => {
126                    inner_keys.push((**rhs).clone());
127                    outer_keys.push((**lhs).clone());
128                    continue;
129                }
130                _ => {}
131            }
132        }
133        if expression_has_external_reference(conjunct, std::slice::from_ref(&scope)) {
134            return Ok(None);
135        }
136        residual.push(conjunct.clone());
137    }
138    if inner_keys.is_empty() {
139        return Ok(None);
140    }
141
142    let mut inner = plan.clone();
143    let RelationalPlan::QueryBlock(inner_block) = &mut inner.root else {
144        unreachable!("query-block shape checked above");
145    };
146    inner_block.projections = inner_keys
147        .into_iter()
148        .map(|expr| ProjectionPlan { expr, alias: None })
149        .collect();
150    inner_block.r#where = match residual.len() {
151        0 => None,
152        1 => residual.pop(),
153        _ => Some(ScalarExpr::And(residual)),
154    };
155    inner_block.order_by.clear();
156    Ok(Some(DecorrelatedExistsPlan { inner, outer_keys }))
157}
158
159#[derive(Clone, Copy, PartialEq, Eq)]
160enum ColumnScope {
161    Inner,
162    Outer,
163}
164
165fn correlation_column_scope(expression: &ScalarExpr, scope: &QueryScope) -> Option<ColumnScope> {
166    match expression {
167        ScalarExpr::Column(column) => {
168            if scope
169                .columns
170                .names
171                .iter()
172                .any(|local| local.eq_ignore_ascii_case(column))
173                || scope
174                    .qualifiers
175                    .iter()
176                    .any(|local| local.eq_ignore_ascii_case(column))
177            {
178                Some(ColumnScope::Inner)
179            } else if scope.columns.complete {
180                Some(ColumnScope::Outer)
181            } else {
182                None
183            }
184        }
185        ScalarExpr::QualifiedColumn { qualifier, .. } => {
186            if scope
187                .qualifiers
188                .iter()
189                .any(|local| local.eq_ignore_ascii_case(qualifier))
190            {
191                Some(ColumnScope::Inner)
192            } else {
193                Some(ColumnScope::Outer)
194            }
195        }
196        ScalarExpr::InternalColumn(column) => {
197            if scope.internal_relations.contains(&column.relation()) {
198                Some(ColumnScope::Inner)
199            } else {
200                Some(ColumnScope::Outer)
201            }
202        }
203        ScalarExpr::Cast { expr, .. } => correlation_column_scope(expr, scope),
204        _ => None,
205    }
206}
207
208pub fn query_depends_on_outer_row(
209    context: CorrelationContext<'_>,
210    plan: &QueryPlan,
211) -> Result<bool, SQLError> {
212    query_has_external_reference(context, plan, &mut Vec::new())
213}
214
215/// Whether a subquery refers to the caller's own row. Seed enclosing identities so references beyond that row are not mistaken for level-zero variables.
216pub(crate) fn query_depends_on_current_row(
217    context: CorrelationContext<'_>,
218    plan: &QueryPlan,
219    input: &crate::RowSchema,
220) -> Result<bool, SQLError> {
221    let mut enclosing = QueryScope {
222        columns: RelationColumns::empty_known(),
223        ..QueryScope::default()
224    };
225    for (identity, _) in input.typed_physical_alias_identities() {
226        if !input.resolves_local_column(identity.qualifier(), identity.column()) {
227            if let Some(qualifier) = identity.qualifier() {
228                enclosing.qualifiers.insert(qualifier.to_owned());
229            } else {
230                enclosing.columns.names.insert(identity.column().to_owned());
231            }
232        }
233    }
234    for (column, slot) in input.internal_identities() {
235        if !input.slot_is_local(slot) {
236            enclosing.internal_relations.insert(column.relation());
237        }
238    }
239    query_has_external_reference(context, plan, &mut vec![enclosing])
240}
241
242#[expect(
243    clippy::too_many_lines,
244    reason = "preserves scope and subquery identity"
245)]
246fn query_has_external_reference(
247    context: CorrelationContext<'_>,
248    plan: &QueryPlan,
249    scopes: &mut Vec<QueryScope>,
250) -> Result<bool, SQLError> {
251    let mut ctes = BTreeMap::new();
252    if plan.ctes.iter().any(|cte| cte.recursive) {
253        for cte in &plan.ctes {
254            let columns = if cte.columns.is_empty() {
255                cte.body
256                    .query()
257                    .map_or_else(RelationColumns::default, query_output_columns)
258            } else {
259                RelationColumns::known(cte.columns.clone())
260            };
261            ctes.insert(cte.name.clone(), columns);
262        }
263    }
264    for cte in &plan.ctes {
265        let columns = if cte.columns.is_empty() {
266            cte.body
267                .query()
268                .map_or_else(RelationColumns::default, query_output_columns)
269        } else {
270            RelationColumns::known(cte.columns.clone())
271        };
272        if cte.recursive {
273            ctes.insert(cte.name.clone(), columns.clone());
274        }
275        if cte.body.query().map_or(Ok(true), |query| {
276            query_has_external_reference(context, query, scopes)
277        })? {
278            return Ok(true);
279        }
280        ctes.insert(cte.name.clone(), columns);
281    }
282
283    match &plan.root {
284        RelationalPlan::QueryBlock(block) => {
285            let scope = match block.from.as_ref() {
286                Some(source) => source_scope(context, source, &ctes)?,
287                None => QueryScope {
288                    qualifiers: BTreeSet::new(),
289                    internal_relations: BTreeSet::new(),
290                    columns: RelationColumns::empty_known(),
291                },
292            };
293            scopes.push(scope);
294            let result = (|| {
295                for expression in block.expressions() {
296                    if expression_has_external_reference(expression, scopes) {
297                        return Ok(true);
298                    }
299                }
300                if let Some(source) = block.from.as_ref() {
301                    if source_has_external_reference(context, source, scopes)? {
302                        return Ok(true);
303                    }
304                }
305                for subquery in &block.subqueries {
306                    if query_has_external_reference(context, subquery, scopes)? {
307                        return Ok(true);
308                    }
309                }
310                Ok(false)
311            })();
312            scopes.pop();
313            result
314        }
315        RelationalPlan::SetOp {
316            left,
317            right,
318            order_by,
319            limit,
320            offset,
321            subqueries,
322            ..
323        } => {
324            if query_has_external_reference(context, left, scopes)?
325                || query_has_external_reference(context, right, scopes)?
326            {
327                return Ok(true);
328            }
329            scopes.push(QueryScope {
330                qualifiers: BTreeSet::new(),
331                internal_relations: BTreeSet::new(),
332                columns: query_output_columns(left),
333            });
334            let result = (|| {
335                for expression in order_by.iter().map(|order| &order.expr) {
336                    if expression_has_external_reference(expression, scopes) {
337                        return Ok(true);
338                    }
339                }
340                if limit
341                    .as_deref()
342                    .is_some_and(|expr| expression_has_external_reference(expr, scopes))
343                    || offset
344                        .as_deref()
345                        .is_some_and(|expr| expression_has_external_reference(expr, scopes))
346                {
347                    return Ok(true);
348                }
349                for subquery in subqueries {
350                    if query_has_external_reference(context, subquery, scopes)? {
351                        return Ok(true);
352                    }
353                }
354                Ok(false)
355            })();
356            scopes.pop();
357            result
358        }
359        RelationalPlan::Values { rows, subqueries } => {
360            scopes.push(QueryScope {
361                qualifiers: BTreeSet::new(),
362                internal_relations: BTreeSet::new(),
363                columns: RelationColumns::empty_known(),
364            });
365            let result = (|| {
366                for expression in rows.iter().flatten() {
367                    if expression_has_external_reference(expression, scopes) {
368                        return Ok(true);
369                    }
370                }
371                for subquery in subqueries {
372                    if query_has_external_reference(context, subquery, scopes)? {
373                        return Ok(true);
374                    }
375                }
376                Ok(false)
377            })();
378            scopes.pop();
379            result
380        }
381    }
382}
383
384#[expect(
385    clippy::too_many_lines,
386    reason = "preserves scope and subquery identity"
387)]
388fn source_scope(
389    context: CorrelationContext<'_>,
390    source: &SourcePlan,
391    ctes: &BTreeMap<String, RelationColumns>,
392) -> Result<QueryScope, SQLError> {
393    match source {
394        SourcePlan::Table {
395            bound_columns,
396            name,
397            qualifier,
398            alias,
399            column_aliases,
400            ..
401        } => {
402            let mut qualifiers = BTreeSet::new();
403            qualifiers.insert(alias.as_ref().unwrap_or(qualifier).clone());
404            let mut columns = match bound_columns {
405                Some(columns) => RelationColumns::known(columns.clone()),
406                None => relation_columns(context, name, ctes)?,
407            };
408            columns.apply_positional_aliases(column_aliases);
409            Ok(QueryScope {
410                qualifiers,
411                internal_relations: BTreeSet::new(),
412                columns,
413            })
414        }
415        SourcePlan::Join {
416            left,
417            right,
418            alias,
419            column_aliases,
420            ..
421        } => {
422            let left = source_scope(context, left, ctes)?;
423            let right = source_scope(context, right, ctes)?;
424            let complete = left.columns.complete && right.columns.complete;
425            let mut names = left.columns.names;
426            names.extend(right.columns.names);
427            let mut ordered = left.columns.ordered;
428            ordered.extend(right.columns.ordered);
429            let mut internal_relations = left.internal_relations;
430            internal_relations.extend(right.internal_relations);
431            if let Some(alias) = alias {
432                if !column_aliases.is_empty() {
433                    names = column_aliases.iter().cloned().collect();
434                    ordered.clone_from(column_aliases);
435                }
436                return Ok(QueryScope {
437                    qualifiers: [alias.clone()].into_iter().collect(),
438                    internal_relations,
439                    columns: RelationColumns {
440                        names,
441                        ordered,
442                        complete: complete && column_aliases.is_empty(),
443                    },
444                });
445            }
446            let mut qualifiers = left.qualifiers;
447            qualifiers.extend(right.qualifiers);
448            Ok(QueryScope {
449                qualifiers,
450                internal_relations,
451                columns: RelationColumns {
452                    names,
453                    ordered,
454                    complete,
455                },
456            })
457        }
458        SourcePlan::Values {
459            rows,
460            alias,
461            column_aliases,
462            internal_relation,
463            ..
464        } => {
465            if let Some(internal_relation) = internal_relation {
466                return Ok(QueryScope {
467                    qualifiers: BTreeSet::new(),
468                    internal_relations: [*internal_relation].into_iter().collect(),
469                    columns: RelationColumns::empty_known(),
470                });
471            }
472            let qualifiers = alias.iter().cloned().collect();
473            let columns = if column_aliases.is_empty() {
474                (0..rows.first().map_or(0, Vec::len))
475                    .map(|index| format!("column{}", index + 1))
476                    .collect::<Vec<_>>()
477            } else {
478                column_aliases.clone()
479            };
480            Ok(QueryScope {
481                qualifiers,
482                internal_relations: BTreeSet::new(),
483                columns: RelationColumns::known(columns),
484            })
485        }
486        SourcePlan::Function {
487            name,
488            output_name,
489            alias,
490            column_aliases,
491            ..
492        } => {
493            let qualifiers = [alias.as_ref().unwrap_or(output_name).clone()]
494                .into_iter()
495                .collect();
496            let mut names: BTreeSet<String> = column_aliases.iter().cloned().collect();
497            let complete = !column_aliases.is_empty()
498                || matches!(
499                    name.to_ascii_lowercase().as_str(),
500                    "generate_series" | "unnest" | "regexp_split_to_table" | "string_to_table"
501                );
502            if names.is_empty() && complete {
503                names.insert(output_name.clone());
504            }
505            Ok(QueryScope {
506                qualifiers,
507                internal_relations: BTreeSet::new(),
508                columns: RelationColumns {
509                    ordered: names.iter().cloned().collect(),
510                    names,
511                    complete,
512                },
513            })
514        }
515        SourcePlan::FunctionGroup {
516            functions,
517            alias,
518            column_aliases,
519            ordinality,
520        } => {
521            let qualifier = alias.clone().or_else(|| {
522                functions
523                    .first()
524                    .map(|function| function.output_name.clone())
525            });
526            let qualifiers = qualifier.into_iter().collect();
527            let mut names = Vec::new();
528            let mut complete = true;
529            for function in functions {
530                if function.column_aliases.is_empty() {
531                    let local = crate::semantics::builtin_function_dispatch_name(&function.name);
532                    if matches!(
533                        local.as_str(),
534                        "generate_series" | "unnest" | "regexp_split_to_table" | "string_to_table"
535                    ) {
536                        names.push(function.output_name.clone());
537                    } else {
538                        complete = false;
539                    }
540                } else {
541                    names.extend(function.column_aliases.iter().cloned());
542                }
543            }
544            if *ordinality {
545                names.push("ordinality".into());
546            }
547            for (name, alias) in names.iter_mut().zip(column_aliases) {
548                name.clone_from(alias);
549            }
550            let column_names = names.iter().cloned().collect();
551            Ok(QueryScope {
552                qualifiers,
553                internal_relations: BTreeSet::new(),
554                columns: RelationColumns {
555                    names: column_names,
556                    ordered: names,
557                    complete,
558                },
559            })
560        }
561        SourcePlan::Subquery {
562            body,
563            alias,
564            column_aliases,
565        } => {
566            let qualifiers = alias.iter().cloned().collect();
567            let columns = if column_aliases.is_empty() {
568                query_output_columns(body)
569            } else {
570                RelationColumns {
571                    names: column_aliases.iter().cloned().collect(),
572                    ordered: column_aliases.clone(),
573                    complete: true,
574                }
575            };
576            Ok(QueryScope {
577                qualifiers,
578                internal_relations: BTreeSet::new(),
579                columns,
580            })
581        }
582    }
583}
584
585fn relation_columns(
586    context: CorrelationContext<'_>,
587    name: &str,
588    ctes: &BTreeMap<String, RelationColumns>,
589) -> Result<RelationColumns, SQLError> {
590    if let Some(columns) = ctes
591        .iter()
592        .find(|(cte, _)| cte.eq_ignore_ascii_case(name))
593        .map(|(_, columns)| columns)
594    {
595        return Ok(columns.clone());
596    }
597    let catalog = context.catalog;
598    let resolution = context.resolution;
599    if let Some(columns) = catalog.virtual_relation_schema(resolution, name)? {
600        return Ok(RelationColumns::known(
601            columns.into_iter().map(|(name, _)| name).collect(),
602        ));
603    }
604    if let Ok(Some(table)) = catalog.table_resolved(resolution, name) {
605        return Ok(RelationColumns::known(
606            table
607                .columns
608                .iter()
609                .map(|column| column.name.clone())
610                .collect(),
611        ));
612    }
613    if let Some(view) = catalog.view_resolved(resolution, name)? {
614        return Ok(if view.materialized {
615            RelationColumns::known(view.output_columns.unwrap_or_default())
616        } else {
617            query_output_columns(&view.query)
618        });
619    }
620    if let Ok(Some(table)) = catalog.foreign_table_resolved(resolution, name) {
621        return Ok(RelationColumns::known(
622            table
623                .columns
624                .iter()
625                .map(|column| column.name.clone())
626                .collect(),
627        ));
628    }
629    Ok(RelationColumns::default())
630}
631
632fn source_has_external_reference(
633    context: CorrelationContext<'_>,
634    source: &SourcePlan,
635    scopes: &mut Vec<QueryScope>,
636) -> Result<bool, SQLError> {
637    match source {
638        SourcePlan::Join {
639            left, right, on, ..
640        } => Ok(source_has_external_reference(context, left, scopes)?
641            || source_has_external_reference(context, right, scopes)?
642            || on
643                .as_ref()
644                .is_some_and(|expr| expression_has_external_reference(expr, scopes))),
645        SourcePlan::Values { rows, .. } => Ok(rows
646            .iter()
647            .flatten()
648            .any(|expr| expression_has_external_reference(expr, scopes))),
649        SourcePlan::Function { args, .. } => Ok(args
650            .iter()
651            .any(|expr| expression_has_external_reference(expr, scopes))),
652        SourcePlan::FunctionGroup { functions, .. } => Ok(functions.iter().any(|function| {
653            function
654                .args
655                .iter()
656                .any(|expr| expression_has_external_reference(expr, scopes))
657        })),
658        SourcePlan::Subquery { body, .. } => query_has_external_reference(context, body, scopes),
659        SourcePlan::Table { .. } => Ok(false),
660    }
661}
662
663fn query_output_columns(plan: &QueryPlan) -> RelationColumns {
664    match &plan.root {
665        RelationalPlan::QueryBlock(block) => {
666            let ordered = projection_columns(&block.projections);
667            RelationColumns {
668                names: ordered.iter().cloned().collect(),
669                ordered,
670                complete: !block
671                    .projections
672                    .iter()
673                    .any(|projection| matches!(projection.expr, ScalarExpr::Star)),
674            }
675        }
676        RelationalPlan::SetOp { left, .. } => query_output_columns(left),
677        RelationalPlan::Values { rows, .. } => RelationColumns::known(
678            (0..rows.first().map_or(0, Vec::len))
679                .map(|index| format!("column{}", index + 1))
680                .collect(),
681        ),
682    }
683}
684
685fn expression_has_external_reference(expr: &ScalarExpr, scopes: &[QueryScope]) -> bool {
686    match expr {
687        ScalarExpr::Column(column) => !resolves_unqualified(column, scopes),
688        ScalarExpr::QualifiedColumn { qualifier, .. } => !scopes.iter().rev().any(|scope| {
689            scope
690                .qualifiers
691                .iter()
692                .any(|local| local.eq_ignore_ascii_case(qualifier))
693        }),
694        ScalarExpr::QualifiedStar(qualifier) => !scopes.iter().rev().any(|scope| {
695            scope
696                .qualifiers
697                .iter()
698                .any(|local| local.eq_ignore_ascii_case(qualifier))
699        }),
700        ScalarExpr::InternalColumn(column) => !scopes
701            .iter()
702            .rev()
703            .any(|scope| scope.internal_relations.contains(&column.relation())),
704        ScalarExpr::Func {
705            args,
706            order_by,
707            filter,
708            ..
709        } => {
710            args.iter()
711                .any(|expr| expression_has_external_reference(expr, scopes))
712                || order_by
713                    .iter()
714                    .any(|order| expression_has_external_reference(&order.expr, scopes))
715                || filter
716                    .as_deref()
717                    .is_some_and(|expr| expression_has_external_reference(expr, scopes))
718        }
719        ScalarExpr::Array(items)
720        | ScalarExpr::Row(items)
721        | ScalarExpr::CompositeRow { items, .. }
722        | ScalarExpr::And(items)
723        | ScalarExpr::Or(items) => items
724            .iter()
725            .any(|expr| expression_has_external_reference(expr, scopes)),
726        ScalarExpr::Binary { lhs, rhs, .. } => {
727            expression_has_external_reference(lhs, scopes)
728                || expression_has_external_reference(rhs, scopes)
729        }
730        ScalarExpr::Not(inner)
731        | ScalarExpr::UnaryMinus(inner)
732        | ScalarExpr::IsNull { expr: inner, .. }
733        | ScalarExpr::Cast { expr: inner, .. } => expression_has_external_reference(inner, scopes),
734        ScalarExpr::Between { expr, low, high } => {
735            expression_has_external_reference(expr, scopes)
736                || expression_has_external_reference(low, scopes)
737                || expression_has_external_reference(high, scopes)
738        }
739        ScalarExpr::InList { expr, list, .. } => {
740            expression_has_external_reference(expr, scopes)
741                || list
742                    .iter()
743                    .any(|item| expression_has_external_reference(item, scopes))
744        }
745        ScalarExpr::WindowCall {
746            args, spec, filter, ..
747        } => window_has_external_reference(args, filter.as_deref(), spec, scopes),
748        ScalarExpr::Case {
749            base,
750            when,
751            else_branch,
752        } => {
753            base.as_deref()
754                .is_some_and(|expr| expression_has_external_reference(expr, scopes))
755                || when.iter().any(|(condition, result)| {
756                    expression_has_external_reference(condition, scopes)
757                        || expression_has_external_reference(result, scopes)
758                })
759                || else_branch
760                    .as_deref()
761                    .is_some_and(|expr| expression_has_external_reference(expr, scopes))
762        }
763        ScalarExpr::InSubquery { expr, .. } => expression_has_external_reference(expr, scopes),
764        ScalarExpr::Default
765        | ScalarExpr::Star
766        | ScalarExpr::Position(_)
767        | ScalarExpr::Literal(_)
768        | ScalarExpr::TypedLiteral { .. }
769        | ScalarExpr::Param(_)
770        | ScalarExpr::ScalarSubquery(_)
771        | ScalarExpr::Exists { .. } => false,
772    }
773}
774
775fn resolves_unqualified(column: &str, scopes: &[QueryScope]) -> bool {
776    for scope in scopes.iter().rev() {
777        if scope
778            .columns
779            .names
780            .iter()
781            .any(|local| local.eq_ignore_ascii_case(column))
782            || scope
783                .qualifiers
784                .iter()
785                .any(|local| local.eq_ignore_ascii_case(column))
786        {
787            return true;
788        }
789        if !scope.columns.complete {
790            return false;
791        }
792    }
793    false
794}
795
796/// Whether a window call's arguments, `FILTER`, keys or frame offsets refer outside the query scopes.
797fn window_has_external_reference(
798    args: &[ScalarExpr],
799    filter: Option<&ScalarExpr>,
800    spec: &crate::ScalarWindowSpec,
801    scopes: &[QueryScope],
802) -> bool {
803    args.iter()
804        .chain(filter)
805        .chain(&spec.partition_by)
806        .chain(spec.order_by.iter().map(|order| &order.expr))
807        .any(|expr| expression_has_external_reference(expr, scopes))
808        || spec.frame.as_ref().is_some_and(|frame| {
809            frame_bound_has_external_reference(&frame.start, scopes)
810                || frame_bound_has_external_reference(&frame.end, scopes)
811        })
812}
813
814fn frame_bound_has_external_reference(bound: &ScalarFrameBound, scopes: &[QueryScope]) -> bool {
815    match bound {
816        ScalarFrameBound::Preceding(expr) | ScalarFrameBound::Following(expr) => {
817            expression_has_external_reference(expr, scopes)
818        }
819        ScalarFrameBound::UnboundedPreceding
820        | ScalarFrameBound::UnboundedFollowing
821        | ScalarFrameBound::CurrentRow => false,
822    }
823}
824
825#[cfg(test)]
826mod tests {
827    use super::*;
828
829    #[test]
830    fn table_range_aliases_replace_physical_correlation_names() {
831        let crate::ast::Statement::CreateTable(definition) =
832            crate::compile("CREATE TABLE correlation_alias_source(id INTEGER, label TEXT)")
833                .unwrap()
834                .remove(0)
835        else {
836            panic!("expected table definition")
837        };
838        let catalog = crate::binding::fixture::catalog(BTreeMap::from([(
839            crate::RelationIdentity::from_legacy_name("public.correlation_alias_source").unwrap(),
840            crate::binding::fixture::table_definition(definition.columns),
841        )]));
842        let resolution =
843            crate::binding::fixture::resolution(vec!["public".into()], "pg_temp_1".into());
844        let context = CorrelationContext {
845            catalog: catalog.as_ref(),
846            resolution: &resolution,
847        };
848        let source = SourcePlan::Table {
849            bound_columns: None,
850            name: "correlation_alias_source".into(),
851            qualifier: "correlation_alias_source".into(),
852            alias: Some("source".into()),
853            column_aliases: vec!["key".into(), "value".into()],
854            include_descendants: true,
855        };
856        let scope = source_scope(context, &source, &BTreeMap::new()).unwrap();
857        assert_eq!(
858            scope.columns.names,
859            BTreeSet::from(["key".to_string(), "value".to_string()])
860        );
861    }
862}