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.relation_qualifiers().collect::<HashSet<_>>();
35        let mut current_unqualified = input
36            .identities()
37            .iter()
38            .map(ColumnIdentity::column)
39            .collect::<HashSet<_>>();
40        current_unqualified.extend(
41            input
42                .index
43                .aliases
44                .keys()
45                .filter(|identity| identity.qualifier().is_none())
46                .map(ColumnIdentity::column),
47        );
48
49        let mut aliases = input.index.aliases.clone();
50        let mut alias_types = input.index.cold.aliases.clone();
51        let mut ambiguous_qualified = input.index.ambiguous_qualified.clone();
52        let mut outer_exact = HashSet::<&str>::new();
53        let mut outer_qualified = HashMap::<&str, Vec<usize>>::new();
54        let mut outer_qualified_counts = HashMap::<ColumnIdentity, usize>::new();
55        for (identity, _) in outer_columns {
56            if identity
57                .qualifier()
58                .is_some_and(|qualifier| !current_qualifiers.contains(qualifier))
59            {
60                *outer_qualified_counts.entry(identity.clone()).or_default() += 1;
61            }
62        }
63        for (position, (identity, ty)) in outer_columns.iter().enumerate() {
64            let slot = outer_base + position;
65            if let Some(qualifier) = identity.qualifier() {
66                if !current_qualifiers.contains(qualifier) {
67                    if outer_qualified_counts.get(identity) == Some(&1) {
68                        aliases.insert(identity.clone(), slot);
69                        alias_types.insert(identity.clone(), ty.clone());
70                    } else {
71                        aliases.remove(identity);
72                        alias_types.remove(identity);
73                        ambiguous_qualified.insert(identity.clone());
74                    }
75                }
76                outer_qualified
77                    .entry(identity.column())
78                    .or_default()
79                    .push(position);
80            } else {
81                outer_exact.insert(identity.column());
82                if !current_unqualified.contains(identity.column()) {
83                    aliases.insert(identity.clone(), slot);
84                    alias_types.insert(identity.clone(), ty.clone());
85                }
86            }
87        }
88
89        let mut outer_ambiguous = HashSet::new();
90        for (column, positions) in outer_qualified {
91            if current_unqualified.contains(column) || outer_exact.contains(column) {
92                continue;
93            }
94            match positions.as_slice() {
95                [position] => {
96                    let identity = ColumnIdentity::unqualified(column);
97                    aliases.insert(identity.clone(), outer_base + position);
98                    alias_types.insert(identity, outer_columns[*position].1.clone());
99                }
100                [_, _, ..] => {
101                    outer_ambiguous.insert(Box::<str>::from(column));
102                }
103                [] => {}
104            }
105        }
106
107        Self::from_typed_parts_with_aliases_and_exact_precedence(
108            input.columns().to_vec(),
109            input.identities().to_vec(),
110            input.column_types().to_vec(),
111            input.index.slots.to_vec(),
112            input.physical_width() + outer_columns.len(),
113            SchemaBuildMetadata {
114                collations: input.index.cold.collations.clone(),
115                record_fields: input.index.cold.record_fields.clone(),
116                aliases,
117                alias_types,
118                internal: input.index.executor_attributes.clone(),
119                internal_types: input.index.cold.executor_attribute_types.clone(),
120                score_sources: input.index.cold.score_sources.clone(),
121                wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
122                binding_only: input.index.cold.binding_only.clone(),
123                row_types: input.index.cold.row_types.clone(),
124                open_qualifiers: input.index.cold.open_qualifiers.clone(),
125                extra_ambiguous_unqualified: outer_ambiguous,
126                extra_ambiguous_qualified: ambiguous_qualified,
127                ..SchemaBuildMetadata::default()
128            },
129        )
130    }
131
132    /// Overlay an existing positional outer scope while preserving its complete structured lookup layout and sharing its physical value fragments.
133    #[expect(
134        clippy::too_many_lines,
135        reason = "outer-scope merge keeps schema and value precedence aligned"
136    )]
137    pub fn with_outer_schema(input: &Self, outer: &Self) -> Self {
138        let mut collations = input.index.cold.collations.clone();
139        let outer_base = input.physical_width();
140        let current_qualifiers = input.relation_qualifiers().collect::<HashSet<_>>();
141        let mut current_unqualified = input
142            .index
143            .unqualified
144            .keys()
145            .map(std::convert::AsRef::as_ref)
146            .collect::<HashSet<_>>();
147        current_unqualified.extend(
148            input
149                .index
150                .aliases
151                .keys()
152                .filter(|identity| identity.qualifier().is_none())
153                .map(ColumnIdentity::column),
154        );
155
156        let shifted_slot = |slot: usize| {
157            if slot == NULL_SLOT {
158                NULL_SLOT
159            } else {
160                outer_base + slot
161            }
162        };
163        let mut aliases = input.index.aliases.clone();
164        let mut alias_types = input.index.cold.aliases.clone();
165        let mut ambiguous_unqualified = input.index.ambiguous_unqualified.clone();
166        let mut ambiguous_qualified = input.index.ambiguous_qualified.clone();
167
168        let qualified_identities = outer
169            .index
170            .qualified
171            .keys()
172            .chain(
173                outer
174                    .index
175                    .aliases
176                    .keys()
177                    .filter(|identity| identity.qualifier().is_some()),
178            )
179            .chain(outer.index.ambiguous_qualified.iter())
180            .cloned()
181            .collect::<HashSet<_>>();
182        for identity in qualified_identities {
183            let qualifier = identity
184                .qualifier()
185                .expect("qualified outer identity has a qualifier");
186            if current_qualifiers.contains(qualifier) {
187                continue;
188            }
189            if outer.qualified_column_is_ambiguous(qualifier, identity.column()) {
190                aliases.remove(&identity);
191                alias_types.remove(&identity);
192                ambiguous_qualified.insert(identity);
193                continue;
194            }
195            let slot = outer
196                .qualified_slot(qualifier, identity.column())
197                .map_or(NULL_SLOT, shifted_slot);
198            let ty = outer.qualified_type(qualifier, identity.column()).cloned();
199            collations.alias(
200                &identity,
201                outer.qualified_collation(qualifier, identity.column()),
202                ty.as_ref(),
203            );
204            aliases.insert(identity.clone(), slot);
205            alias_types.insert(identity, ty);
206        }
207
208        let outer_unqualified = outer
209            .index
210            .unqualified
211            .keys()
212            .map(std::convert::AsRef::as_ref)
213            .chain(
214                outer
215                    .index
216                    .aliases
217                    .keys()
218                    .filter(|identity| identity.qualifier().is_none())
219                    .map(ColumnIdentity::column),
220            )
221            .chain(
222                outer
223                    .index
224                    .ambiguous_unqualified
225                    .iter()
226                    .map(std::convert::AsRef::as_ref),
227            )
228            .collect::<HashSet<_>>();
229        for column in outer_unqualified {
230            if current_unqualified.contains(column) {
231                continue;
232            }
233            let identity = ColumnIdentity::unqualified(column);
234            if outer.column_is_ambiguous(column) {
235                aliases.remove(&identity);
236                alias_types.remove(&identity);
237                ambiguous_unqualified.insert(Box::<str>::from(column));
238                continue;
239            }
240            let slot = outer.column_slot(column).map_or(NULL_SLOT, shifted_slot);
241            aliases.insert(identity.clone(), slot);
242            collations.alias(&identity, outer.collation_of(column), outer.type_of(column));
243            alias_types.insert(identity, outer.type_of(column).cloned());
244        }
245
246        let mut internal = input.index.executor_attributes.clone();
247        let mut internal_types = input.index.cold.executor_attribute_types.clone();
248        for (column, slot) in &outer.index.executor_attributes {
249            internal.insert(*column, shifted_slot(*slot));
250        }
251        for (column, ty) in &outer.index.cold.executor_attribute_types {
252            collations.internal(*column, outer.internal_collation(*column), ty.as_ref());
253            if let Some(existing) = internal_types.get(column) {
254                assert_eq!(
255                    existing, ty,
256                    "structural identity type changed across correlated scopes"
257                );
258            } else {
259                internal_types.insert(*column, ty.clone());
260            }
261        }
262        let mut score_sources = input.index.cold.score_sources.clone();
263        score_sources.extend(outer.index.cold.score_sources.iter().cloned());
264        let mut open_qualifiers = input.index.cold.open_qualifiers.clone();
265        open_qualifiers.extend(
266            outer
267                .index
268                .cold
269                .open_qualifiers
270                .iter()
271                .filter(|qualifier| {
272                    qualifier
273                        .as_deref()
274                        .is_none_or(|qualifier| !input.has_qualifier(qualifier))
275                })
276                .cloned(),
277        );
278
279        let schema = Self::from_typed_parts_with_aliases_and_exact_precedence(
280            input.columns().to_vec(),
281            input.identities().to_vec(),
282            input.column_types().to_vec(),
283            input.index.slots.to_vec(),
284            input.physical_width() + outer.physical_width(),
285            SchemaBuildMetadata {
286                collations,
287                record_fields: Self::joined_record_fields(input, outer, input.physical_width()),
288                aliases,
289                alias_types,
290                internal,
291                internal_types,
292                score_sources,
293                wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
294                binding_only: input.index.cold.binding_only.clone(),
295                row_types: super::relation_types::RelationRowTypes::with_outer(input, outer),
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        let mut schema = Self::with_typed_virtual_identities(&schema, &virtual_identities);
319        let index = std::sync::Arc::make_mut(&mut schema.index);
320        for (identity, _) in &virtual_identities {
321            if input.index.cold.binding_only.contains_key(identity) {
322                continue;
323            }
324            if let Some(ty) = index.cold.binding_only.get(identity) {
325                index.cold.collations.virtual_column(
326                    identity,
327                    outer.virtual_collation(identity),
328                    ty.as_ref(),
329                );
330            }
331        }
332        schema
333    }
334}