1use super::{
10 ColumnIdentity, ColumnType, HashMap, PhysicalLayout, ProjectedSlot, RowSchema,
11 SchemaBuildMetadata, SchemaLayoutError, SchemaLayoutResult, ScoreSource, NULL_SLOT,
12};
13
14impl RowSchema {
15 pub fn without_internal_attributes(
19 input: &Self,
20 columns: &[crate::ast::InternalColumnRef],
21 ) -> Self {
22 let mut internal = input.index.executor_attributes.clone();
23 let mut internal_types = input.index.cold.executor_attribute_types.clone();
24 for column in columns {
25 internal.remove(column);
26 internal_types.remove(column);
27 }
28 let score_sources = input
29 .index
30 .cold
31 .score_sources
32 .iter()
33 .filter(|source| internal.contains_key(&source.column))
34 .cloned()
35 .collect();
36 Self::from_typed_parts_with_aliases_and_exact_precedence(
37 input.columns().to_vec(),
38 input.identities().to_vec(),
39 input.column_types().to_vec(),
40 input.index.slots.to_vec(),
41 input.physical_width(),
42 SchemaBuildMetadata {
43 aliases: input.index.aliases.clone(),
44 alias_types: input.index.cold.aliases.clone(),
45 internal,
46 internal_types,
47 score_sources,
48 wildcard_hidden: input.index.cold.wildcard_hidden.clone(),
49 binding_only: input.index.cold.binding_only.clone(),
50 open_qualifiers: input.index.cold.open_qualifiers.clone(),
51 ..SchemaBuildMetadata::default()
52 },
53 )
54 }
55
56 pub fn select(input: &Self, columns: &[(String, String)]) -> Self {
59 let output_names = columns
60 .iter()
61 .map(|(output, _)| output.clone())
62 .collect::<Vec<_>>();
63 let slots = columns
64 .iter()
65 .map(|(_, source)| input.exact_slot(source).unwrap_or(NULL_SLOT))
66 .collect();
67 let types = columns
68 .iter()
69 .map(|(_, source)| input.exact_type(source).cloned())
70 .collect();
71 let identities = output_names
72 .iter()
73 .cloned()
74 .map(ColumnIdentity::unqualified)
75 .collect();
76 Self::from_typed_parts_with_aliases_and_exact_precedence(
77 output_names,
78 identities,
79 types,
80 slots,
81 input.physical_width(),
82 SchemaBuildMetadata {
83 aliases: HashMap::new(),
84 alias_types: HashMap::new(),
85 internal: input.index.executor_attributes.clone(),
86 internal_types: input.index.cold.executor_attribute_types.clone(),
87 score_sources: input.index.cold.score_sources.clone(),
88 binding_only: HashMap::new(),
89 ..SchemaBuildMetadata::default()
90 },
91 )
92 }
93
94 pub fn project_with_sources(
96 input: &Self,
97 projected: Vec<(String, Option<ColumnType>, ProjectedSlot)>,
98 projected_internal: Vec<(
99 crate::ast::InternalColumnRef,
100 Option<ColumnType>,
101 ProjectedSlot,
102 )>,
103 computed_count: usize,
104 pass_through: bool,
105 ) -> Self {
106 let resolve_slot = |source: ProjectedSlot| match source {
107 ProjectedSlot::Input(slot) => slot.unwrap_or(NULL_SLOT),
108 ProjectedSlot::Computed(position) => input.physical_width() + position,
109 };
110 let physical_width = input.physical_width() + computed_count;
111 let mut internal = input.index.executor_attributes.clone();
112 let mut internal_types = input.index.cold.executor_attribute_types.clone();
113 for (column, ty, source) in projected_internal {
114 internal.insert(column, resolve_slot(source));
115 internal_types.insert(column, ty);
116 }
117
118 if pass_through {
119 let mut columns = input.columns().to_vec();
120 let mut identities = input.identities().to_vec();
121 let mut types = input.column_types().to_vec();
122 let mut slots = input.index.slots.to_vec();
123 let mut wildcard_hidden = input.index.cold.wildcard_hidden.clone();
124 for (name, ty, source) in projected {
125 let slot = resolve_slot(source);
126 if let Some(position) = columns.iter().position(|column| column == &name) {
127 slots[position] = slot;
128 identities[position] = ColumnIdentity::unqualified(name);
129 types[position] = ty;
130 wildcard_hidden.remove(&position);
131 } else {
132 identities.push(ColumnIdentity::unqualified(name.clone()));
133 columns.push(name);
134 types.push(ty);
135 slots.push(slot);
136 }
137 }
138 return Self::from_typed_parts_with_aliases_and_exact_precedence(
139 columns,
140 identities,
141 types,
142 slots,
143 physical_width,
144 SchemaBuildMetadata {
145 aliases: input.index.aliases.clone(),
146 alias_types: input.index.cold.aliases.clone(),
147 internal,
148 internal_types,
149 score_sources: input.index.cold.score_sources.clone(),
150 wildcard_hidden,
151 binding_only: input.index.cold.binding_only.clone(),
152 open_qualifiers: input.index.cold.open_qualifiers.clone(),
153 ..SchemaBuildMetadata::default()
154 },
155 );
156 }
157
158 let mut columns = Vec::with_capacity(projected.len());
159 let mut identities = Vec::with_capacity(projected.len());
160 let mut types = Vec::with_capacity(projected.len());
161 let mut slots = Vec::with_capacity(projected.len());
162 for (name, ty, source) in projected {
163 slots.push(resolve_slot(source));
164 identities.push(ColumnIdentity::unqualified(name.clone()));
165 columns.push(name);
166 types.push(ty);
167 }
168 Self::from_typed_parts_with_aliases_and_exact_precedence(
169 columns,
170 identities,
171 types,
172 slots,
173 physical_width,
174 SchemaBuildMetadata {
175 aliases: HashMap::new(),
176 alias_types: HashMap::new(),
177 internal,
178 internal_types,
179 score_sources: input.index.cold.score_sources.clone(),
180 binding_only: HashMap::new(),
181 ..SchemaBuildMetadata::default()
182 },
183 )
184 }
185
186 pub fn canonical_projection(&self) -> (Self, Vec<usize>) {
191 fn remap_slot(
192 slot: usize,
193 source_slots: &mut Vec<usize>,
194 positions: &mut HashMap<usize, usize>,
195 ) -> usize {
196 if slot == NULL_SLOT {
197 return NULL_SLOT;
198 }
199 if let Some(position) = positions.get(&slot) {
200 return *position;
201 }
202 let position = source_slots.len();
203 source_slots.push(slot);
204 positions.insert(slot, position);
205 position
206 }
207
208 let mut source_slots = Vec::new();
209 let mut positions = HashMap::new();
210 let slots = self
211 .index
212 .slots
213 .iter()
214 .map(|slot| remap_slot(*slot, &mut source_slots, &mut positions))
215 .collect();
216 let mut source_aliases = self.index.aliases.iter().collect::<Vec<_>>();
217 source_aliases.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));
218 let aliases = source_aliases
219 .into_iter()
220 .map(|(name, slot)| {
221 (
222 name.clone(),
223 remap_slot(*slot, &mut source_slots, &mut positions),
224 )
225 })
226 .collect();
227 let mut source_internal = self.index.executor_attributes.iter().collect::<Vec<_>>();
228 source_internal.sort_unstable_by_key(|(column, _)| **column);
229 let internal = source_internal
230 .into_iter()
231 .map(|(column, slot)| {
232 (
233 *column,
234 remap_slot(*slot, &mut source_slots, &mut positions),
235 )
236 })
237 .collect();
238 (
239 Self::from_typed_parts_with_aliases_and_exact_precedence(
240 self.columns().to_vec(),
241 self.identities().to_vec(),
242 self.column_types().to_vec(),
243 slots,
244 source_slots.len(),
245 SchemaBuildMetadata {
246 aliases,
247 alias_types: self.index.cold.aliases.clone(),
248 internal,
249 internal_types: self.index.cold.executor_attribute_types.clone(),
250 score_sources: self.index.cold.score_sources.clone(),
251 wildcard_hidden: self.index.cold.wildcard_hidden.clone(),
252 binding_only: self.index.cold.binding_only.clone(),
253 open_qualifiers: self.index.cold.open_qualifiers.clone(),
254 ..SchemaBuildMetadata::default()
255 },
256 ),
257 source_slots,
258 )
259 }
260
261 #[expect(
267 clippy::too_many_lines,
268 reason = "projection keeps schema and physical column positions aligned"
269 )]
270 pub fn from_physical_layout(layout: PhysicalLayout) -> SchemaLayoutResult<Self> {
271 let PhysicalLayout {
272 columns,
273 identities,
274 types,
275 slots,
276 physical_width,
277 aliases,
278 internal,
279 score_sources,
280 wildcard_hidden,
281 } = layout;
282 if columns.len() != slots.len() {
283 return Err(SchemaLayoutError(format!(
284 "physical schema has {} columns but {} logical slots",
285 columns.len(),
286 slots.len()
287 )));
288 }
289 if columns.len() != types.len() {
290 return Err(SchemaLayoutError(format!(
291 "physical schema has {} columns but {} logical types",
292 columns.len(),
293 types.len()
294 )));
295 }
296 if columns.len() != identities.len() {
297 return Err(SchemaLayoutError(format!(
298 "physical schema has {} columns but {} logical identities",
299 columns.len(),
300 identities.len()
301 )));
302 }
303 if wildcard_hidden
304 .iter()
305 .any(|position| *position >= columns.len())
306 {
307 return Err(SchemaLayoutError(
308 "physical schema wildcard-hidden position is outside logical width".into(),
309 ));
310 }
311 let slots = slots
312 .into_iter()
313 .map(|slot| match slot {
314 Some(slot) if slot < physical_width => Ok(slot),
315 Some(slot) => Err(SchemaLayoutError(format!(
316 "physical schema logical slot {slot} is outside width {physical_width}"
317 ))),
318 None => Ok(NULL_SLOT),
319 })
320 .collect::<SchemaLayoutResult<Vec<_>>>()?;
321 let mut lookup_aliases = HashMap::with_capacity(aliases.len());
322 let mut alias_types = HashMap::with_capacity(aliases.len());
323 for (identity, slot, ty) in aliases {
324 let slot = match slot {
325 Some(slot) if slot < physical_width => slot,
326 Some(slot) => {
327 return Err(SchemaLayoutError(format!(
328 "physical schema alias `{identity:?}` slot {slot} is outside width {physical_width}"
329 )))
330 }
331 None => NULL_SLOT,
332 };
333 if lookup_aliases.insert(identity.clone(), slot).is_some() {
334 return Err(SchemaLayoutError(format!(
335 "physical schema contains duplicate alias `{identity:?}`"
336 )));
337 }
338 alias_types.insert(identity, ty);
339 }
340 let mut internal_slots = HashMap::with_capacity(internal.len());
341 let mut internal_types = HashMap::with_capacity(internal.len());
342 for (column, slot, ty) in internal {
343 let slot = match slot {
344 Some(slot) if slot < physical_width => slot,
345 Some(slot) => {
346 return Err(SchemaLayoutError(format!(
347 "physical schema internal attribute `{column:?}` slot {slot} is outside width {physical_width}"
348 )))
349 }
350 None => NULL_SLOT,
351 };
352 if internal_slots.insert(column, slot).is_some() {
353 return Err(SchemaLayoutError(format!(
354 "physical schema contains duplicate internal attribute `{column:?}`"
355 )));
356 }
357 internal_types.insert(column, ty);
358 }
359 let score_sources = score_sources
360 .into_iter()
361 .map(|(qualifier, column)| {
362 if !internal_slots.contains_key(&column) {
363 return Err(SchemaLayoutError(format!(
364 "physical schema score source references missing internal attribute `{column:?}`"
365 )));
366 }
367 Ok(ScoreSource {
368 qualifier: qualifier.map(Box::<str>::from),
369 column,
370 })
371 })
372 .collect::<SchemaLayoutResult<Vec<_>>>()?;
373 Ok(Self::from_typed_parts_with_aliases_and_exact_precedence(
374 columns,
375 identities,
376 types,
377 slots,
378 physical_width,
379 SchemaBuildMetadata {
380 aliases: lookup_aliases,
381 alias_types,
382 internal: internal_slots,
383 internal_types,
384 score_sources,
385 wildcard_hidden,
386 binding_only: HashMap::new(),
387 ..SchemaBuildMetadata::default()
388 },
389 ))
390 }
391
392 pub fn lookup_aliases(&self) -> Vec<(&ColumnIdentity, Option<usize>)> {
393 let mut aliases = self
394 .index
395 .aliases
396 .iter()
397 .map(|(identity, slot)| (identity, (*slot != NULL_SLOT).then_some(*slot)))
398 .collect::<Vec<_>>();
399 aliases.sort_unstable_by_key(|(identity, _)| *identity);
400 aliases
401 }
402
403 pub fn lookup_aliases_with_types(
404 &self,
405 ) -> Vec<(&ColumnIdentity, Option<usize>, Option<&ColumnType>)> {
406 self.lookup_aliases()
407 .into_iter()
408 .map(|(identity, slot)| {
409 (
410 identity,
411 slot,
412 self.index
413 .cold
414 .aliases
415 .get(identity)
416 .and_then(Option::as_ref),
417 )
418 })
419 .collect()
420 }
421
422 pub fn internal_columns_with_types(
423 &self,
424 ) -> Vec<(
425 crate::ast::InternalColumnRef,
426 Option<usize>,
427 Option<&ColumnType>,
428 )> {
429 let mut columns = self
430 .index
431 .executor_attributes
432 .iter()
433 .map(|(column, slot)| {
434 (
435 *column,
436 (*slot != NULL_SLOT).then_some(*slot),
437 self.index
438 .cold
439 .executor_attribute_types
440 .get(column)
441 .and_then(Option::as_ref),
442 )
443 })
444 .collect::<Vec<_>>();
445 columns.sort_unstable_by_key(|(column, _, _)| *column);
446 columns
447 }
448
449 pub fn score_sources(
450 &self,
451 ) -> impl Iterator<Item = (Option<&str>, crate::ast::InternalColumnRef)> {
452 self.index
453 .cold
454 .score_sources
455 .iter()
456 .map(|source| (source.qualifier.as_deref(), source.column))
457 }
458
459 pub fn wildcard_hidden_positions(&self) -> impl Iterator<Item = usize> + '_ {
460 self.index.cold.wildcard_hidden.iter().copied()
461 }
462
463 pub fn remap_positions(
465 input: &Self,
466 columns: &[(String, usize)],
467 aliases: &[(ColumnIdentity, usize)],
468 ) -> Self {
469 let columns = columns
470 .iter()
471 .map(|(name, logical)| (name.clone(), *logical, input.column_type(*logical).cloned()))
472 .collect::<Vec<_>>();
473 Self::remap_typed_positions(input, &columns, aliases)
474 }
475
476 pub fn remap_typed_positions(
478 input: &Self,
479 columns: &[(String, usize, Option<ColumnType>)],
480 aliases: &[(ColumnIdentity, usize)],
481 ) -> Self {
482 let output_names = columns
483 .iter()
484 .map(|(output, _, _)| output.clone())
485 .collect::<Vec<_>>();
486 let slots = columns
487 .iter()
488 .map(|(_, logical, _)| input.slot(*logical).unwrap_or(NULL_SLOT))
489 .collect();
490 let types = columns.iter().map(|(_, _, ty)| ty.clone()).collect();
491 let identities = output_names
492 .iter()
493 .cloned()
494 .map(ColumnIdentity::unqualified)
495 .collect();
496 let wildcard_hidden = columns
497 .iter()
498 .enumerate()
499 .filter_map(|(output, (_, logical, _))| {
500 input
501 .index
502 .cold
503 .wildcard_hidden
504 .contains(logical)
505 .then_some(output)
506 })
507 .collect();
508 let mut lookup_aliases = input.index.aliases.clone();
509 let mut alias_types = input.index.cold.aliases.clone();
510 for (identity, logical) in aliases {
511 lookup_aliases.insert(identity.clone(), input.slot(*logical).unwrap_or(NULL_SLOT));
512 alias_types.insert(identity.clone(), input.column_type(*logical).cloned());
513 }
514 Self::from_typed_parts_with_aliases_and_exact_precedence(
515 output_names,
516 identities,
517 types,
518 slots,
519 input.physical_width(),
520 SchemaBuildMetadata {
521 aliases: lookup_aliases,
522 alias_types,
523 internal: input.index.executor_attributes.clone(),
524 internal_types: input.index.cold.executor_attribute_types.clone(),
525 score_sources: input.index.cold.score_sources.clone(),
526 wildcard_hidden,
527 binding_only: input.index.cold.binding_only.clone(),
528 open_qualifiers: input.index.cold.open_qualifiers.clone(),
529 ..SchemaBuildMetadata::default()
530 },
531 )
532 }
533}