Skip to main content

uqa_sql/schema/
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 crate::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                internal: input.index.executor_attributes.clone(),
123                internal_types: input.index.cold.executor_attribute_types.clone(),
124                score_sources: input.index.cold.score_sources.clone(),
125                wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
126                binding_only: input.index.cold.binding_only.clone(),
127                open_qualifiers: input.index.cold.open_qualifiers.clone(),
128                extra_ambiguous_unqualified: outer_ambiguous,
129                extra_ambiguous_qualified: ambiguous_qualified,
130                ..SchemaBuildMetadata::default()
131            },
132        )
133    }
134
135    /// Overlay an existing positional outer scope while preserving its complete structured lookup layout and sharing its physical value fragments.
136    #[expect(
137        clippy::too_many_lines,
138        reason = "outer-scope merge keeps schema and value precedence aligned"
139    )]
140    pub fn with_outer_schema(input: &Self, outer: &Self) -> Self {
141        let outer_base = input.physical_width();
142        let current_qualifiers = input
143            .index
144            .qualified
145            .keys()
146            .chain(input.index.aliases.keys())
147            .filter_map(ColumnIdentity::qualifier)
148            .collect::<HashSet<_>>();
149        let mut current_unqualified = input
150            .index
151            .unqualified
152            .keys()
153            .map(std::convert::AsRef::as_ref)
154            .collect::<HashSet<_>>();
155        current_unqualified.extend(
156            input
157                .index
158                .aliases
159                .keys()
160                .filter(|identity| identity.qualifier().is_none())
161                .map(ColumnIdentity::column),
162        );
163
164        let shifted_slot = |slot: usize| {
165            if slot == NULL_SLOT {
166                NULL_SLOT
167            } else {
168                outer_base + slot
169            }
170        };
171        let mut aliases = input.index.aliases.clone();
172        let mut alias_types = input.index.cold.aliases.clone();
173        let mut ambiguous_unqualified = input.index.ambiguous_unqualified.clone();
174        let mut ambiguous_qualified = input.index.ambiguous_qualified.clone();
175
176        let qualified_identities = outer
177            .index
178            .qualified
179            .keys()
180            .chain(
181                outer
182                    .index
183                    .aliases
184                    .keys()
185                    .filter(|identity| identity.qualifier().is_some()),
186            )
187            .chain(outer.index.ambiguous_qualified.iter())
188            .cloned()
189            .collect::<HashSet<_>>();
190        for identity in qualified_identities {
191            let qualifier = identity
192                .qualifier()
193                .expect("qualified outer identity has a qualifier");
194            if current_qualifiers.contains(qualifier) {
195                continue;
196            }
197            if outer.qualified_column_is_ambiguous(qualifier, identity.column()) {
198                aliases.remove(&identity);
199                alias_types.remove(&identity);
200                ambiguous_qualified.insert(identity);
201                continue;
202            }
203            let slot = outer
204                .qualified_slot(qualifier, identity.column())
205                .map_or(NULL_SLOT, shifted_slot);
206            let ty = outer.qualified_type(qualifier, identity.column()).cloned();
207            aliases.insert(identity.clone(), slot);
208            alias_types.insert(identity, ty);
209        }
210
211        let outer_unqualified = outer
212            .index
213            .unqualified
214            .keys()
215            .map(std::convert::AsRef::as_ref)
216            .chain(
217                outer
218                    .index
219                    .aliases
220                    .keys()
221                    .filter(|identity| identity.qualifier().is_none())
222                    .map(ColumnIdentity::column),
223            )
224            .chain(
225                outer
226                    .index
227                    .ambiguous_unqualified
228                    .iter()
229                    .map(std::convert::AsRef::as_ref),
230            )
231            .collect::<HashSet<_>>();
232        for column in outer_unqualified {
233            if current_unqualified.contains(column) {
234                continue;
235            }
236            let identity = ColumnIdentity::unqualified(column);
237            if outer.column_is_ambiguous(column) {
238                aliases.remove(&identity);
239                alias_types.remove(&identity);
240                ambiguous_unqualified.insert(Box::<str>::from(column));
241                continue;
242            }
243            let slot = outer.column_slot(column).map_or(NULL_SLOT, shifted_slot);
244            aliases.insert(identity.clone(), slot);
245            alias_types.insert(identity, outer.type_of(column).cloned());
246        }
247
248        let mut internal = input.index.executor_attributes.clone();
249        let mut internal_types = input.index.cold.executor_attribute_types.clone();
250        for (column, slot) in &outer.index.executor_attributes {
251            internal.insert(*column, shifted_slot(*slot));
252        }
253        for (column, ty) in &outer.index.cold.executor_attribute_types {
254            if let Some(existing) = internal_types.get(column) {
255                assert_eq!(
256                    existing, ty,
257                    "structural identity type changed across correlated scopes"
258                );
259            } else {
260                internal_types.insert(*column, ty.clone());
261            }
262        }
263        let mut score_sources = input.index.cold.score_sources.clone();
264        score_sources.extend(outer.index.cold.score_sources.iter().cloned());
265        let mut open_qualifiers = input.index.cold.open_qualifiers.clone();
266        open_qualifiers.extend(
267            outer
268                .index
269                .cold
270                .open_qualifiers
271                .iter()
272                .filter(|qualifier| {
273                    qualifier
274                        .as_deref()
275                        .is_none_or(|qualifier| !input.has_qualifier(qualifier))
276                })
277                .cloned(),
278        );
279
280        let schema = Self::from_typed_parts_with_aliases_and_exact_precedence(
281            input.columns().to_vec(),
282            input.identities().to_vec(),
283            input.column_types().to_vec(),
284            input.index.slots.to_vec(),
285            input.physical_width() + outer.physical_width(),
286            SchemaBuildMetadata {
287                aliases,
288                alias_types,
289                internal,
290                internal_types,
291                score_sources,
292                wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
293                binding_only: input.index.cold.binding_only.clone(),
294                open_qualifiers,
295                extra_ambiguous_unqualified: ambiguous_unqualified,
296                extra_ambiguous_qualified: ambiguous_qualified,
297                ..SchemaBuildMetadata::default()
298            },
299        );
300        let virtual_identities = outer
301            .typed_virtual_identities()
302            .filter(|(identity, _)| {
303                identity
304                    .qualifier()
305                    .is_none_or(|qualifier| !current_qualifiers.contains(qualifier))
306            })
307            .map(|(identity, ty)| (identity.clone(), ty.cloned()))
308            .collect::<Vec<_>>();
309        Self::with_typed_virtual_identities(&schema, &virtual_identities)
310    }
311}