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, .. } | 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
217pub(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
809fn 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}