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#[expect(
216    clippy::too_many_lines,
217    reason = "preserves scope and subquery identity"
218)]
219fn query_has_external_reference(
220    context: CorrelationContext<'_>,
221    plan: &QueryPlan,
222    scopes: &mut Vec<QueryScope>,
223) -> Result<bool, SQLError> {
224    let mut ctes = BTreeMap::new();
225    if plan.ctes.iter().any(|cte| cte.recursive) {
226        for cte in &plan.ctes {
227            let columns = if cte.columns.is_empty() {
228                cte.body
229                    .query()
230                    .map_or_else(RelationColumns::default, query_output_columns)
231            } else {
232                RelationColumns::known(cte.columns.clone())
233            };
234            ctes.insert(cte.name.clone(), columns);
235        }
236    }
237    for cte in &plan.ctes {
238        let columns = if cte.columns.is_empty() {
239            cte.body
240                .query()
241                .map_or_else(RelationColumns::default, query_output_columns)
242        } else {
243            RelationColumns::known(cte.columns.clone())
244        };
245        if cte.recursive {
246            ctes.insert(cte.name.clone(), columns.clone());
247        }
248        if cte.body.query().map_or(Ok(true), |query| {
249            query_has_external_reference(context, query, scopes)
250        })? {
251            return Ok(true);
252        }
253        ctes.insert(cte.name.clone(), columns);
254    }
255
256    match &plan.root {
257        RelationalPlan::QueryBlock(block) => {
258            let scope = match block.from.as_ref() {
259                Some(source) => source_scope(context, source, &ctes)?,
260                None => QueryScope {
261                    qualifiers: BTreeSet::new(),
262                    internal_relations: BTreeSet::new(),
263                    columns: RelationColumns::empty_known(),
264                },
265            };
266            scopes.push(scope);
267            let result = (|| {
268                for expression in block.expressions() {
269                    if expression_has_external_reference(expression, scopes) {
270                        return Ok(true);
271                    }
272                }
273                if let Some(source) = block.from.as_ref() {
274                    if source_has_external_reference(context, source, scopes)? {
275                        return Ok(true);
276                    }
277                }
278                for subquery in &block.subqueries {
279                    if query_has_external_reference(context, subquery, scopes)? {
280                        return Ok(true);
281                    }
282                }
283                Ok(false)
284            })();
285            scopes.pop();
286            result
287        }
288        RelationalPlan::SetOp {
289            left,
290            right,
291            order_by,
292            limit,
293            offset,
294            subqueries,
295            ..
296        } => {
297            if query_has_external_reference(context, left, scopes)?
298                || query_has_external_reference(context, right, scopes)?
299            {
300                return Ok(true);
301            }
302            scopes.push(QueryScope {
303                qualifiers: BTreeSet::new(),
304                internal_relations: BTreeSet::new(),
305                columns: query_output_columns(left),
306            });
307            let result = (|| {
308                for expression in order_by.iter().map(|order| &order.expr) {
309                    if expression_has_external_reference(expression, scopes) {
310                        return Ok(true);
311                    }
312                }
313                if limit
314                    .as_deref()
315                    .is_some_and(|expr| expression_has_external_reference(expr, scopes))
316                    || offset
317                        .as_deref()
318                        .is_some_and(|expr| expression_has_external_reference(expr, scopes))
319                {
320                    return Ok(true);
321                }
322                for subquery in subqueries {
323                    if query_has_external_reference(context, subquery, scopes)? {
324                        return Ok(true);
325                    }
326                }
327                Ok(false)
328            })();
329            scopes.pop();
330            result
331        }
332        RelationalPlan::Values { rows, subqueries } => {
333            scopes.push(QueryScope {
334                qualifiers: BTreeSet::new(),
335                internal_relations: BTreeSet::new(),
336                columns: RelationColumns::empty_known(),
337            });
338            let result = (|| {
339                for expression in rows.iter().flatten() {
340                    if expression_has_external_reference(expression, scopes) {
341                        return Ok(true);
342                    }
343                }
344                for subquery in subqueries {
345                    if query_has_external_reference(context, subquery, scopes)? {
346                        return Ok(true);
347                    }
348                }
349                Ok(false)
350            })();
351            scopes.pop();
352            result
353        }
354    }
355}
356
357#[expect(
358    clippy::too_many_lines,
359    reason = "preserves scope and subquery identity"
360)]
361fn source_scope(
362    context: CorrelationContext<'_>,
363    source: &SourcePlan,
364    ctes: &BTreeMap<String, RelationColumns>,
365) -> Result<QueryScope, SQLError> {
366    match source {
367        SourcePlan::Table {
368            bound_columns,
369            name,
370            qualifier,
371            alias,
372            column_aliases,
373            ..
374        } => {
375            let mut qualifiers = BTreeSet::new();
376            qualifiers.insert(alias.as_ref().unwrap_or(qualifier).clone());
377            let mut columns = match bound_columns {
378                Some(columns) => RelationColumns::known(columns.clone()),
379                None => relation_columns(context, name, ctes)?,
380            };
381            columns.apply_positional_aliases(column_aliases);
382            Ok(QueryScope {
383                qualifiers,
384                internal_relations: BTreeSet::new(),
385                columns,
386            })
387        }
388        SourcePlan::Join {
389            left,
390            right,
391            alias,
392            column_aliases,
393            ..
394        } => {
395            let left = source_scope(context, left, ctes)?;
396            let right = source_scope(context, right, ctes)?;
397            let complete = left.columns.complete && right.columns.complete;
398            let mut names = left.columns.names;
399            names.extend(right.columns.names);
400            let mut ordered = left.columns.ordered;
401            ordered.extend(right.columns.ordered);
402            let mut internal_relations = left.internal_relations;
403            internal_relations.extend(right.internal_relations);
404            if let Some(alias) = alias {
405                if !column_aliases.is_empty() {
406                    names = column_aliases.iter().cloned().collect();
407                    ordered.clone_from(column_aliases);
408                }
409                return Ok(QueryScope {
410                    qualifiers: [alias.clone()].into_iter().collect(),
411                    internal_relations,
412                    columns: RelationColumns {
413                        names,
414                        ordered,
415                        complete: complete && column_aliases.is_empty(),
416                    },
417                });
418            }
419            let mut qualifiers = left.qualifiers;
420            qualifiers.extend(right.qualifiers);
421            Ok(QueryScope {
422                qualifiers,
423                internal_relations,
424                columns: RelationColumns {
425                    names,
426                    ordered,
427                    complete,
428                },
429            })
430        }
431        SourcePlan::Values {
432            rows,
433            alias,
434            column_aliases,
435            internal_relation,
436            ..
437        } => {
438            if let Some(internal_relation) = internal_relation {
439                return Ok(QueryScope {
440                    qualifiers: BTreeSet::new(),
441                    internal_relations: [*internal_relation].into_iter().collect(),
442                    columns: RelationColumns::empty_known(),
443                });
444            }
445            let qualifiers = alias.iter().cloned().collect();
446            let columns = if column_aliases.is_empty() {
447                (0..rows.first().map_or(0, Vec::len))
448                    .map(|index| format!("column{}", index + 1))
449                    .collect::<Vec<_>>()
450            } else {
451                column_aliases.clone()
452            };
453            Ok(QueryScope {
454                qualifiers,
455                internal_relations: BTreeSet::new(),
456                columns: RelationColumns::known(columns),
457            })
458        }
459        SourcePlan::Function {
460            name,
461            output_name,
462            alias,
463            column_aliases,
464            ..
465        } => {
466            let qualifiers = [alias.as_ref().unwrap_or(output_name).clone()]
467                .into_iter()
468                .collect();
469            let mut names: BTreeSet<String> = column_aliases.iter().cloned().collect();
470            let complete = !column_aliases.is_empty()
471                || matches!(
472                    name.to_ascii_lowercase().as_str(),
473                    "generate_series" | "unnest" | "regexp_split_to_table" | "string_to_table"
474                );
475            if names.is_empty() && complete {
476                names.insert(output_name.clone());
477            }
478            Ok(QueryScope {
479                qualifiers,
480                internal_relations: BTreeSet::new(),
481                columns: RelationColumns {
482                    ordered: names.iter().cloned().collect(),
483                    names,
484                    complete,
485                },
486            })
487        }
488        SourcePlan::FunctionGroup {
489            functions,
490            alias,
491            column_aliases,
492            ordinality,
493        } => {
494            let qualifier = alias.clone().or_else(|| {
495                functions
496                    .first()
497                    .map(|function| function.output_name.clone())
498            });
499            let qualifiers = qualifier.into_iter().collect();
500            let mut names = Vec::new();
501            let mut complete = true;
502            for function in functions {
503                if function.column_aliases.is_empty() {
504                    let local = crate::semantics::builtin_function_dispatch_name(&function.name);
505                    if matches!(
506                        local.as_str(),
507                        "generate_series" | "unnest" | "regexp_split_to_table" | "string_to_table"
508                    ) {
509                        names.push(function.output_name.clone());
510                    } else {
511                        complete = false;
512                    }
513                } else {
514                    names.extend(function.column_aliases.iter().cloned());
515                }
516            }
517            if *ordinality {
518                names.push("ordinality".into());
519            }
520            for (name, alias) in names.iter_mut().zip(column_aliases) {
521                name.clone_from(alias);
522            }
523            let column_names = names.iter().cloned().collect();
524            Ok(QueryScope {
525                qualifiers,
526                internal_relations: BTreeSet::new(),
527                columns: RelationColumns {
528                    names: column_names,
529                    ordered: names,
530                    complete,
531                },
532            })
533        }
534        SourcePlan::Subquery {
535            body,
536            alias,
537            column_aliases,
538        } => {
539            let qualifiers = alias.iter().cloned().collect();
540            let columns = if column_aliases.is_empty() {
541                query_output_columns(body)
542            } else {
543                RelationColumns {
544                    names: column_aliases.iter().cloned().collect(),
545                    ordered: column_aliases.clone(),
546                    complete: true,
547                }
548            };
549            Ok(QueryScope {
550                qualifiers,
551                internal_relations: BTreeSet::new(),
552                columns,
553            })
554        }
555    }
556}
557
558fn relation_columns(
559    context: CorrelationContext<'_>,
560    name: &str,
561    ctes: &BTreeMap<String, RelationColumns>,
562) -> Result<RelationColumns, SQLError> {
563    if let Some(columns) = ctes
564        .iter()
565        .find(|(cte, _)| cte.eq_ignore_ascii_case(name))
566        .map(|(_, columns)| columns)
567    {
568        return Ok(columns.clone());
569    }
570    let catalog = context.catalog;
571    let resolution = context.resolution;
572    if let Some(columns) = catalog.virtual_relation_schema(resolution, name)? {
573        return Ok(RelationColumns::known(
574            columns.into_iter().map(|(name, _)| name).collect(),
575        ));
576    }
577    if let Ok(Some(table)) = catalog.table_resolved(resolution, name) {
578        return Ok(RelationColumns::known(
579            table
580                .columns
581                .iter()
582                .map(|column| column.name.clone())
583                .collect(),
584        ));
585    }
586    if let Some(view) = catalog.view_resolved(resolution, name)? {
587        return Ok(if view.materialized {
588            RelationColumns::known(view.output_columns.unwrap_or_default())
589        } else {
590            query_output_columns(&view.query)
591        });
592    }
593    if let Ok(Some(table)) = catalog.foreign_table_resolved(resolution, name) {
594        return Ok(RelationColumns::known(
595            table
596                .columns
597                .iter()
598                .map(|column| column.name.clone())
599                .collect(),
600        ));
601    }
602    Ok(RelationColumns::default())
603}
604
605fn source_has_external_reference(
606    context: CorrelationContext<'_>,
607    source: &SourcePlan,
608    scopes: &mut Vec<QueryScope>,
609) -> Result<bool, SQLError> {
610    match source {
611        SourcePlan::Join {
612            left, right, on, ..
613        } => Ok(source_has_external_reference(context, left, scopes)?
614            || source_has_external_reference(context, right, scopes)?
615            || on
616                .as_ref()
617                .is_some_and(|expr| expression_has_external_reference(expr, scopes))),
618        SourcePlan::Values { rows, .. } => Ok(rows
619            .iter()
620            .flatten()
621            .any(|expr| expression_has_external_reference(expr, scopes))),
622        SourcePlan::Function { args, .. } => Ok(args
623            .iter()
624            .any(|expr| expression_has_external_reference(expr, scopes))),
625        SourcePlan::FunctionGroup { functions, .. } => Ok(functions.iter().any(|function| {
626            function
627                .args
628                .iter()
629                .any(|expr| expression_has_external_reference(expr, scopes))
630        })),
631        SourcePlan::Subquery { body, .. } => query_has_external_reference(context, body, scopes),
632        SourcePlan::Table { .. } => Ok(false),
633    }
634}
635
636fn query_output_columns(plan: &QueryPlan) -> RelationColumns {
637    match &plan.root {
638        RelationalPlan::QueryBlock(block) => {
639            let ordered = projection_columns(&block.projections);
640            RelationColumns {
641                names: ordered.iter().cloned().collect(),
642                ordered,
643                complete: !block
644                    .projections
645                    .iter()
646                    .any(|projection| matches!(projection.expr, ScalarExpr::Star)),
647            }
648        }
649        RelationalPlan::SetOp { left, .. } => query_output_columns(left),
650        RelationalPlan::Values { rows, .. } => RelationColumns::known(
651            (0..rows.first().map_or(0, Vec::len))
652                .map(|index| format!("column{}", index + 1))
653                .collect(),
654        ),
655    }
656}
657
658fn expression_has_external_reference(expr: &ScalarExpr, scopes: &[QueryScope]) -> bool {
659    match expr {
660        ScalarExpr::Column(column) => !resolves_unqualified(column, scopes),
661        ScalarExpr::QualifiedColumn { qualifier, .. } => !scopes.iter().rev().any(|scope| {
662            scope
663                .qualifiers
664                .iter()
665                .any(|local| local.eq_ignore_ascii_case(qualifier))
666        }),
667        ScalarExpr::QualifiedStar(qualifier) => !scopes.iter().rev().any(|scope| {
668            scope
669                .qualifiers
670                .iter()
671                .any(|local| local.eq_ignore_ascii_case(qualifier))
672        }),
673        ScalarExpr::InternalColumn(column) => !scopes
674            .iter()
675            .rev()
676            .any(|scope| scope.internal_relations.contains(&column.relation())),
677        ScalarExpr::Func {
678            args,
679            order_by,
680            filter,
681            ..
682        } => {
683            args.iter()
684                .any(|expr| expression_has_external_reference(expr, scopes))
685                || order_by
686                    .iter()
687                    .any(|order| expression_has_external_reference(&order.expr, scopes))
688                || filter
689                    .as_deref()
690                    .is_some_and(|expr| expression_has_external_reference(expr, scopes))
691        }
692        ScalarExpr::Array(items)
693        | ScalarExpr::Row(items)
694        | ScalarExpr::And(items)
695        | ScalarExpr::Or(items) => items
696            .iter()
697            .any(|expr| expression_has_external_reference(expr, scopes)),
698        ScalarExpr::Binary { lhs, rhs, .. } => {
699            expression_has_external_reference(lhs, scopes)
700                || expression_has_external_reference(rhs, scopes)
701        }
702        ScalarExpr::Not(inner)
703        | ScalarExpr::UnaryMinus(inner)
704        | ScalarExpr::IsNull { expr: inner, .. }
705        | ScalarExpr::Cast { expr: inner, .. } => expression_has_external_reference(inner, scopes),
706        ScalarExpr::Between { expr, low, high } => {
707            expression_has_external_reference(expr, scopes)
708                || expression_has_external_reference(low, scopes)
709                || expression_has_external_reference(high, scopes)
710        }
711        ScalarExpr::InList { expr, list, .. } => {
712            expression_has_external_reference(expr, scopes)
713                || list
714                    .iter()
715                    .any(|item| expression_has_external_reference(item, scopes))
716        }
717        ScalarExpr::WindowCall { args, spec, .. } => {
718            args.iter()
719                .any(|expr| expression_has_external_reference(expr, scopes))
720                || spec
721                    .partition_by
722                    .iter()
723                    .any(|expr| expression_has_external_reference(expr, scopes))
724                || spec
725                    .order_by
726                    .iter()
727                    .any(|order| expression_has_external_reference(&order.expr, scopes))
728                || spec.frame.as_ref().is_some_and(|frame| {
729                    frame_bound_has_external_reference(&frame.start, scopes)
730                        || frame_bound_has_external_reference(&frame.end, scopes)
731                })
732        }
733        ScalarExpr::Case {
734            base,
735            when,
736            else_branch,
737        } => {
738            base.as_deref()
739                .is_some_and(|expr| expression_has_external_reference(expr, scopes))
740                || when.iter().any(|(condition, result)| {
741                    expression_has_external_reference(condition, scopes)
742                        || expression_has_external_reference(result, scopes)
743                })
744                || else_branch
745                    .as_deref()
746                    .is_some_and(|expr| expression_has_external_reference(expr, scopes))
747        }
748        ScalarExpr::InSubquery { expr, .. } => expression_has_external_reference(expr, scopes),
749        ScalarExpr::Default
750        | ScalarExpr::Star
751        | ScalarExpr::Position(_)
752        | ScalarExpr::Literal(_)
753        | ScalarExpr::TypedLiteral { .. }
754        | ScalarExpr::Param(_)
755        | ScalarExpr::ScalarSubquery(_)
756        | ScalarExpr::Exists { .. } => false,
757    }
758}
759
760fn resolves_unqualified(column: &str, scopes: &[QueryScope]) -> bool {
761    for scope in scopes.iter().rev() {
762        if scope
763            .columns
764            .names
765            .iter()
766            .any(|local| local.eq_ignore_ascii_case(column))
767            || scope
768                .qualifiers
769                .iter()
770                .any(|local| local.eq_ignore_ascii_case(column))
771        {
772            return true;
773        }
774        if !scope.columns.complete {
775            return false;
776        }
777    }
778    false
779}
780
781fn frame_bound_has_external_reference(bound: &ScalarFrameBound, scopes: &[QueryScope]) -> bool {
782    match bound {
783        ScalarFrameBound::Preceding(expr) | ScalarFrameBound::Following(expr) => {
784            expression_has_external_reference(expr, scopes)
785        }
786        ScalarFrameBound::UnboundedPreceding
787        | ScalarFrameBound::UnboundedFollowing
788        | ScalarFrameBound::CurrentRow => false,
789    }
790}
791
792#[cfg(test)]
793mod tests {
794    use super::*;
795
796    #[test]
797    fn table_range_aliases_replace_physical_correlation_names() {
798        let crate::ast::Statement::CreateTable(definition) =
799            crate::compile("CREATE TABLE correlation_alias_source(id INTEGER, label TEXT)")
800                .unwrap()
801                .remove(0)
802        else {
803            panic!("expected table definition")
804        };
805        let catalog = crate::binding::fixture::catalog(BTreeMap::from([(
806            crate::RelationIdentity::from_legacy_name("public.correlation_alias_source").unwrap(),
807            crate::binding::fixture::table_definition(definition.columns),
808        )]));
809        let resolution =
810            crate::binding::fixture::resolution(vec!["public".into()], "pg_temp_1".into());
811        let context = CorrelationContext {
812            catalog: catalog.as_ref(),
813            resolution: &resolution,
814        };
815        let source = SourcePlan::Table {
816            bound_columns: None,
817            name: "correlation_alias_source".into(),
818            qualifier: "correlation_alias_source".into(),
819            alias: Some("source".into()),
820            column_aliases: vec!["key".into(), "value".into()],
821            include_descendants: true,
822        };
823        let scope = source_scope(context, &source, &BTreeMap::new()).unwrap();
824        assert_eq!(
825            scope.columns.names,
826            BTreeSet::from(["key".to_string(), "value".to_string()])
827        );
828    }
829}