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