1use 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#[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
65pub 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
215pub(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
796fn 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}