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