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 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 #[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}