uqa_execution/batch/
outer_scope.rs1use std::collections::{HashMap, HashSet};
10
11use uqa_sql::ast::ColumnType;
12
13use super::{ColumnIdentity, RowSchema, SchemaBuildMetadata, NULL_SLOT};
14
15impl RowSchema {
16 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 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 extra_ambiguous_unqualified: outer_ambiguous,
128 extra_ambiguous_qualified: ambiguous_qualified,
129 ..SchemaBuildMetadata::default()
130 },
131 )
132 }
133
134 #[expect(
136 clippy::too_many_lines,
137 reason = "outer-scope merge keeps schema and value precedence aligned"
138 )]
139 pub fn with_outer_schema(input: &Self, outer: &Self) -> Self {
140 let outer_base = input.physical_width();
141 let current_qualifiers = input
142 .index
143 .qualified
144 .keys()
145 .chain(input.index.aliases.keys())
146 .filter_map(ColumnIdentity::qualifier)
147 .collect::<HashSet<_>>();
148 let mut current_unqualified = input
149 .index
150 .unqualified
151 .keys()
152 .map(std::convert::AsRef::as_ref)
153 .collect::<HashSet<_>>();
154 current_unqualified.extend(
155 input
156 .index
157 .aliases
158 .keys()
159 .filter(|identity| identity.qualifier().is_none())
160 .map(ColumnIdentity::column),
161 );
162
163 let shifted_slot = |slot: usize| {
164 if slot == NULL_SLOT {
165 NULL_SLOT
166 } else {
167 outer_base + slot
168 }
169 };
170 let mut aliases = input.index.aliases.clone();
171 let mut alias_types = input.index.cold.aliases.clone();
172 let mut ambiguous_unqualified = input.index.ambiguous_unqualified.clone();
173 let mut ambiguous_qualified = input.index.ambiguous_qualified.clone();
174
175 let qualified_identities = outer
176 .index
177 .qualified
178 .keys()
179 .chain(
180 outer
181 .index
182 .aliases
183 .keys()
184 .filter(|identity| identity.qualifier().is_some()),
185 )
186 .chain(outer.index.ambiguous_qualified.iter())
187 .cloned()
188 .collect::<HashSet<_>>();
189 for identity in qualified_identities {
190 let qualifier = identity
191 .qualifier()
192 .expect("qualified outer identity has a qualifier");
193 if current_qualifiers.contains(qualifier) {
194 continue;
195 }
196 if outer.qualified_column_is_ambiguous(qualifier, identity.column()) {
197 aliases.remove(&identity);
198 alias_types.remove(&identity);
199 ambiguous_qualified.insert(identity);
200 continue;
201 }
202 let slot = outer
203 .qualified_slot(qualifier, identity.column())
204 .map_or(NULL_SLOT, shifted_slot);
205 let ty = outer.qualified_type(qualifier, identity.column()).cloned();
206 aliases.insert(identity.clone(), slot);
207 alias_types.insert(identity, ty);
208 }
209
210 let outer_unqualified = outer
211 .index
212 .unqualified
213 .keys()
214 .map(std::convert::AsRef::as_ref)
215 .chain(
216 outer
217 .index
218 .aliases
219 .keys()
220 .filter(|identity| identity.qualifier().is_none())
221 .map(ColumnIdentity::column),
222 )
223 .chain(
224 outer
225 .index
226 .ambiguous_unqualified
227 .iter()
228 .map(std::convert::AsRef::as_ref),
229 )
230 .collect::<HashSet<_>>();
231 for column in outer_unqualified {
232 if current_unqualified.contains(column) {
233 continue;
234 }
235 let identity = ColumnIdentity::unqualified(column);
236 if outer.column_is_ambiguous(column) {
237 aliases.remove(&identity);
238 alias_types.remove(&identity);
239 ambiguous_unqualified.insert(Box::<str>::from(column));
240 continue;
241 }
242 let slot = outer.column_slot(column).map_or(NULL_SLOT, shifted_slot);
243 aliases.insert(identity.clone(), slot);
244 alias_types.insert(identity, outer.type_of(column).cloned());
245 }
246
247 let mut internal = input.index.executor_attributes.clone();
248 let mut internal_types = input.index.cold.executor_attribute_types.clone();
249 for (column, slot) in &outer.index.executor_attributes {
250 internal.insert(*column, shifted_slot(*slot));
251 }
252 for (column, ty) in &outer.index.cold.executor_attribute_types {
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
265 let schema = Self::from_typed_parts_with_aliases_and_exact_precedence(
266 input.columns().to_vec(),
267 input.identities().to_vec(),
268 input.column_types().to_vec(),
269 input.index.slots.to_vec(),
270 input.physical_width() + outer.physical_width(),
271 SchemaBuildMetadata {
272 aliases,
273 alias_types,
274 internal,
275 internal_types,
276 score_sources,
277 wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
278 binding_only: input.index.cold.binding_only.clone(),
279 extra_ambiguous_unqualified: ambiguous_unqualified,
280 extra_ambiguous_qualified: ambiguous_qualified,
281 ..SchemaBuildMetadata::default()
282 },
283 );
284 let virtual_identities = outer
285 .typed_virtual_identities()
286 .filter(|(identity, _)| {
287 identity
288 .qualifier()
289 .is_none_or(|qualifier| !current_qualifiers.contains(qualifier))
290 })
291 .map(|(identity, ty)| (identity.clone(), ty.cloned()))
292 .collect::<Vec<_>>();
293 Self::with_typed_virtual_identities(&schema, &virtual_identities)
294 }
295}