Skip to main content

uqa_execution/batch/
outer_scope.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Correlated outer-scope schema composition.
8
9use std::collections::{HashMap, HashSet};
10
11use uqa_sql::ast::ColumnType;
12
13use super::{ColumnIdentity, RowSchema, SchemaBuildMetadata, NULL_SLOT};
14
15impl RowSchema {
16    /// Overlay a statically typed correlated outer scope. The outer columns remain hidden from schema iteration and star expansion, but both qualified and unqualified lookup aliases retain their declared SQL types for expression binding.
17    pub fn with_typed_outer_scope(
18        input: &Self,
19        outer_columns: &[(String, Option<ColumnType>)],
20    ) -> Self {
21        let identities = outer_columns
22            .iter()
23            .map(|(column, ty)| (ColumnIdentity::unqualified(column), ty.clone()))
24            .collect::<Vec<_>>();
25        Self::with_typed_outer_identities(input, &identities)
26    }
27
28    /// Overlay a correlated outer scope carried as structured identities.
29    pub fn with_typed_outer_identities(
30        input: &Self,
31        outer_columns: &[(ColumnIdentity, Option<ColumnType>)],
32    ) -> Self {
33        let outer_base = input.physical_width();
34        let current_qualifiers = input
35            .index
36            .qualified
37            .keys()
38            .chain(input.index.aliases.keys())
39            .filter_map(ColumnIdentity::qualifier)
40            .collect::<HashSet<_>>();
41        let mut current_unqualified = input
42            .identities()
43            .iter()
44            .map(ColumnIdentity::column)
45            .collect::<HashSet<_>>();
46        current_unqualified.extend(
47            input
48                .index
49                .aliases
50                .keys()
51                .filter(|identity| identity.qualifier().is_none())
52                .map(ColumnIdentity::column),
53        );
54
55        let mut aliases = input.index.aliases.clone();
56        let mut alias_types = input.index.cold.aliases.clone();
57        let mut ambiguous_qualified = input.index.ambiguous_qualified.clone();
58        let mut outer_exact = HashSet::<&str>::new();
59        let mut outer_qualified = HashMap::<&str, Vec<usize>>::new();
60        let mut outer_qualified_counts = HashMap::<ColumnIdentity, usize>::new();
61        for (identity, _) in outer_columns {
62            if identity
63                .qualifier()
64                .is_some_and(|qualifier| !current_qualifiers.contains(qualifier))
65            {
66                *outer_qualified_counts.entry(identity.clone()).or_default() += 1;
67            }
68        }
69        for (position, (identity, ty)) in outer_columns.iter().enumerate() {
70            let slot = outer_base + position;
71            if let Some(qualifier) = identity.qualifier() {
72                if !current_qualifiers.contains(qualifier) {
73                    if outer_qualified_counts.get(identity) == Some(&1) {
74                        aliases.insert(identity.clone(), slot);
75                        alias_types.insert(identity.clone(), ty.clone());
76                    } else {
77                        aliases.remove(identity);
78                        alias_types.remove(identity);
79                        ambiguous_qualified.insert(identity.clone());
80                    }
81                }
82                outer_qualified
83                    .entry(identity.column())
84                    .or_default()
85                    .push(position);
86            } else {
87                outer_exact.insert(identity.column());
88                if !current_unqualified.contains(identity.column()) {
89                    aliases.insert(identity.clone(), slot);
90                    alias_types.insert(identity.clone(), ty.clone());
91                }
92            }
93        }
94
95        let mut outer_ambiguous = HashSet::new();
96        for (column, positions) in outer_qualified {
97            if current_unqualified.contains(column) || outer_exact.contains(column) {
98                continue;
99            }
100            match positions.as_slice() {
101                [position] => {
102                    let identity = ColumnIdentity::unqualified(column);
103                    aliases.insert(identity.clone(), outer_base + position);
104                    alias_types.insert(identity, outer_columns[*position].1.clone());
105                }
106                [_, _, ..] => {
107                    outer_ambiguous.insert(Box::<str>::from(column));
108                }
109                [] => {}
110            }
111        }
112
113        Self::from_typed_parts_with_aliases_and_exact_precedence(
114            input.columns().to_vec(),
115            input.identities().to_vec(),
116            input.column_types().to_vec(),
117            input.index.slots.to_vec(),
118            input.physical_width() + outer_columns.len(),
119            SchemaBuildMetadata {
120                aliases,
121                alias_types,
122                extra_ambiguous_unqualified: outer_ambiguous,
123                extra_ambiguous_qualified: ambiguous_qualified,
124                ..SchemaBuildMetadata::default()
125            },
126        )
127    }
128
129    /// Overlay an existing positional outer scope while preserving its complete structured lookup layout and sharing its physical value fragments.
130    pub(crate) fn with_outer_schema(input: &Self, outer: &Self) -> Self {
131        let outer_base = input.physical_width();
132        let current_qualifiers = input
133            .index
134            .qualified
135            .keys()
136            .chain(input.index.aliases.keys())
137            .filter_map(ColumnIdentity::qualifier)
138            .collect::<HashSet<_>>();
139        let mut current_unqualified = input
140            .index
141            .unqualified
142            .keys()
143            .map(std::convert::AsRef::as_ref)
144            .collect::<HashSet<_>>();
145        current_unqualified.extend(
146            input
147                .index
148                .aliases
149                .keys()
150                .filter(|identity| identity.qualifier().is_none())
151                .map(ColumnIdentity::column),
152        );
153
154        let shifted_slot = |slot: usize| {
155            if slot == NULL_SLOT {
156                NULL_SLOT
157            } else {
158                outer_base + slot
159            }
160        };
161        let mut aliases = input.index.aliases.clone();
162        let mut alias_types = input.index.cold.aliases.clone();
163        let mut ambiguous_unqualified = input.index.ambiguous_unqualified.clone();
164        let mut ambiguous_qualified = input.index.ambiguous_qualified.clone();
165
166        let qualified_identities = outer
167            .index
168            .qualified
169            .keys()
170            .chain(
171                outer
172                    .index
173                    .aliases
174                    .keys()
175                    .filter(|identity| identity.qualifier().is_some()),
176            )
177            .chain(outer.index.ambiguous_qualified.iter())
178            .cloned()
179            .collect::<HashSet<_>>();
180        for identity in qualified_identities {
181            let qualifier = identity
182                .qualifier()
183                .expect("qualified outer identity has a qualifier");
184            if current_qualifiers.contains(qualifier) {
185                continue;
186            }
187            if outer.qualified_column_is_ambiguous(qualifier, identity.column()) {
188                aliases.remove(&identity);
189                alias_types.remove(&identity);
190                ambiguous_qualified.insert(identity);
191                continue;
192            }
193            let slot = outer
194                .qualified_slot(qualifier, identity.column())
195                .map_or(NULL_SLOT, shifted_slot);
196            let ty = outer.qualified_type(qualifier, identity.column()).cloned();
197            aliases.insert(identity.clone(), slot);
198            alias_types.insert(identity, ty);
199        }
200
201        let outer_unqualified = outer
202            .index
203            .unqualified
204            .keys()
205            .map(std::convert::AsRef::as_ref)
206            .chain(
207                outer
208                    .index
209                    .aliases
210                    .keys()
211                    .filter(|identity| identity.qualifier().is_none())
212                    .map(ColumnIdentity::column),
213            )
214            .chain(
215                outer
216                    .index
217                    .ambiguous_unqualified
218                    .iter()
219                    .map(std::convert::AsRef::as_ref),
220            )
221            .collect::<HashSet<_>>();
222        for column in outer_unqualified {
223            if current_unqualified.contains(column) {
224                continue;
225            }
226            let identity = ColumnIdentity::unqualified(column);
227            if outer.column_is_ambiguous(column) {
228                aliases.remove(&identity);
229                alias_types.remove(&identity);
230                ambiguous_unqualified.insert(Box::<str>::from(column));
231                continue;
232            }
233            let slot = outer.column_slot(column).map_or(NULL_SLOT, shifted_slot);
234            aliases.insert(identity.clone(), slot);
235            alias_types.insert(identity, outer.type_of(column).cloned());
236        }
237
238        Self::from_typed_parts_with_aliases_and_exact_precedence(
239            input.columns().to_vec(),
240            input.identities().to_vec(),
241            input.column_types().to_vec(),
242            input.index.slots.to_vec(),
243            input.physical_width() + outer.physical_width(),
244            SchemaBuildMetadata {
245                aliases,
246                alias_types,
247                extra_ambiguous_unqualified: ambiguous_unqualified,
248                extra_ambiguous_qualified: ambiguous_qualified,
249                ..SchemaBuildMetadata::default()
250            },
251        )
252    }
253}