1use std::collections::{HashMap, HashSet};
10
11use crate::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 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 #[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}