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
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}