Skip to main content

uqa_sql/schema/
record_fields.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Statically known anonymous-record descriptors follow their physical value slots through query analysis.
8
9use super::{Arc, ColumnType, HashMap, RowSchema};
10
11/// A record's analyzed field types. Runtime values cannot distinguish SQL integer widths or a NULL field's declared type.
12pub type RecordFields = Arc<[Option<ColumnType>]>;
13
14impl RowSchema {
15    pub fn record_fields(&self, logical: usize) -> Option<&RecordFields> {
16        self.slot(logical)
17            .and_then(|slot| self.physical_record_fields(slot))
18    }
19
20    pub fn physical_record_fields(&self, slot: usize) -> Option<&RecordFields> {
21        self.index.cold.record_fields.get(&slot)
22    }
23
24    /// Attach analyzed descriptors to logical columns without changing their type or slot identity.
25    pub fn with_record_fields(
26        mut self,
27        fields: impl IntoIterator<Item = (usize, RecordFields)>,
28    ) -> Self {
29        for (logical, fields) in fields {
30            if let Some(slot) = self.slot(logical) {
31                Arc::make_mut(&mut self.index)
32                    .cold
33                    .record_fields
34                    .insert(slot, fields);
35            }
36        }
37        self
38    }
39
40    /// Preserve descriptors when a namespace boundary rebuilds the same logical columns in a compact physical row.
41    pub fn with_record_fields_from(self, input: &Self) -> Self {
42        let width = self.len().min(input.len());
43        self.with_record_fields((0..width).filter_map(|index| {
44            input
45                .record_fields(index)
46                .cloned()
47                .map(|fields| (index, fields))
48        }))
49    }
50
51    pub(super) fn joined_record_fields(
52        left: &Self,
53        right: &Self,
54        right_base: usize,
55    ) -> HashMap<usize, RecordFields> {
56        let mut fields = left.index.cold.record_fields.clone();
57        fields.extend(
58            right
59                .index
60                .cold
61                .record_fields
62                .iter()
63                .map(|(slot, fields)| (right_base + slot, fields.clone())),
64        );
65        fields
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn projection_alias_join_and_compaction_preserve_record_field_slots() {
75        let fields: RecordFields =
76            vec![Some(ColumnType::Integer), Some(ColumnType::BigInteger)].into();
77        let input = RowSchema::with_types(
78            vec!["discard".into(), "value".into()],
79            vec![Some(ColumnType::Text), Some(ColumnType::Record)],
80        )
81        .with_record_fields([(1, fields.clone())]);
82        let selected = RowSchema::select(&input, &[("renamed".into(), "value".into())]);
83        assert_eq!(selected.record_fields(0), Some(&fields));
84        let qualified = RowSchema::with_relation_qualifier(&selected, "r");
85        assert_eq!(
86            qualified.physical_record_fields(qualified.qualified_slot("r", "renamed").unwrap()),
87            Some(&fields)
88        );
89        let (compact, _) = qualified.canonical_projection();
90        assert_eq!(compact.record_fields(0), Some(&fields));
91        let outer = RowSchema::with_outer_schema(&RowSchema::default(), &compact);
92        assert_eq!(
93            outer.physical_record_fields(outer.qualified_slot("r", "renamed").unwrap()),
94            Some(&fields)
95        );
96    }
97}