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
14/// Comparison field types and a selectable tuple descriptor are independent. A set operation can select fields through its left input even when its rows do not share field types.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub(crate) struct RecordMetadata {
17    pub types: Option<RecordFields>,
18    pub descriptor: Option<Arc<RecordDescriptor>>,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub(crate) struct RecordFieldMetadata {
23    pub collation_oid: u32,
24    pub sql_unknown: bool,
25}
26
27/// A tuple shape available to parser field selection. Anonymous names are generated only when looked up, without allocating an `fN` string for every field.
28#[derive(Debug, PartialEq, Eq)]
29pub(crate) struct RecordDescriptor {
30    pub types: RecordFields,
31    pub names: Option<Box<[String]>>,
32    pub fields: Box<[RecordFieldMetadata]>,
33}
34
35impl RecordDescriptor {
36    pub fn position(&self, name: &str) -> Option<usize> {
37        self.names.as_ref().map_or_else(
38            || {
39                crate::type_resolution::field_selection::anonymous_field_position(
40                    name,
41                    self.types.len(),
42                )
43            },
44            |names| names.iter().position(|candidate| candidate == name),
45        )
46    }
47}
48
49impl RowSchema {
50    pub fn record_fields(&self, logical: usize) -> Option<&RecordFields> {
51        self.slot(logical)
52            .and_then(|slot| self.physical_record_fields(slot))
53    }
54
55    pub fn physical_record_fields(&self, slot: usize) -> Option<&RecordFields> {
56        self.physical_record_metadata(slot)?.types.as_ref()
57    }
58
59    pub(crate) fn physical_record_metadata(&self, slot: usize) -> Option<&RecordMetadata> {
60        self.index.cold.record_fields.get(&slot)
61    }
62
63    pub(crate) fn record_metadata(&self, logical: usize) -> Option<&RecordMetadata> {
64        self.slot(logical)
65            .and_then(|slot| self.index.cold.record_fields.get(&slot))
66    }
67
68    pub(crate) fn input_record_metadata(
69        &self,
70        expression: &crate::ScalarExpr,
71    ) -> Option<&RecordMetadata> {
72        let slot = match expression {
73            crate::ScalarExpr::Column(name) => self.column_slot(name),
74            crate::ScalarExpr::QualifiedColumn { qualifier, column } => {
75                self.qualified_slot(qualifier, column)
76            }
77            crate::ScalarExpr::InternalColumn(column) => self.internal_slot(*column),
78            crate::ScalarExpr::Position(position) => self.slot(*position),
79            _ => None,
80        }?;
81        self.index.cold.record_fields.get(&slot)
82    }
83
84    /// Attach comparison field types to logical columns without declaring a selectable tuple descriptor or changing slot identity.
85    pub fn with_record_fields(
86        self,
87        fields: impl IntoIterator<Item = (usize, RecordFields)>,
88    ) -> Self {
89        self.with_record_metadata(fields.into_iter().map(|(position, types)| {
90            (
91                position,
92                RecordMetadata {
93                    types: Some(types),
94                    descriptor: None,
95                },
96            )
97        }))
98    }
99
100    pub(crate) fn with_record_metadata(
101        mut self,
102        fields: impl IntoIterator<Item = (usize, RecordMetadata)>,
103    ) -> Self {
104        for (logical, fields) in fields {
105            if let Some(slot) = self.slot(logical) {
106                Arc::make_mut(&mut self.index)
107                    .cold
108                    .record_fields
109                    .insert(slot, fields);
110            }
111        }
112        self
113    }
114
115    /// Preserve descriptors when a namespace boundary rebuilds the same logical columns in a compact physical row.
116    pub fn with_record_fields_from(self, input: &Self) -> Self {
117        let width = self.len().min(input.len());
118        self.with_record_metadata((0..width).filter_map(|index| {
119            input
120                .record_metadata(index)
121                .cloned()
122                .map(|fields| (index, fields))
123        }))
124    }
125
126    pub(super) fn joined_record_fields(
127        left: &Self,
128        right: &Self,
129        right_base: usize,
130    ) -> HashMap<usize, RecordMetadata> {
131        let mut fields = left.index.cold.record_fields.clone();
132        fields.extend(
133            right
134                .index
135                .cold
136                .record_fields
137                .iter()
138                .map(|(slot, fields)| (right_base + slot, fields.clone())),
139        );
140        fields
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn projection_alias_join_and_compaction_preserve_record_field_slots() {
150        let fields: RecordFields =
151            vec![Some(ColumnType::Integer), Some(ColumnType::BigInteger)].into();
152        let descriptor = Arc::new(RecordDescriptor {
153            types: fields.clone(),
154            names: None,
155            fields: vec![
156                RecordFieldMetadata {
157                    collation_oid: 0,
158                    sql_unknown: false
159                };
160                2
161            ]
162            .into(),
163        });
164        let input = RowSchema::with_types(
165            vec!["discard".into(), "value".into()],
166            vec![Some(ColumnType::Text), Some(ColumnType::Record)],
167        )
168        .with_record_metadata([(
169            1,
170            RecordMetadata {
171                types: Some(fields.clone()),
172                descriptor: Some(descriptor.clone()),
173            },
174        )]);
175        let selected = RowSchema::select(&input, &[("renamed".into(), "value".into())]);
176        assert_eq!(selected.record_fields(0), Some(&fields));
177        let qualified = RowSchema::with_relation_qualifier(&selected, "r");
178        assert_eq!(
179            qualified.physical_record_fields(qualified.qualified_slot("r", "renamed").unwrap()),
180            Some(&fields)
181        );
182        let (compact, _) = qualified.canonical_projection();
183        assert_eq!(compact.record_fields(0), Some(&fields));
184        let public = qualified.for_result_columns(qualified.columns());
185        assert_eq!(public.physical_width(), 1);
186        assert_eq!(public.qualified_slot("r", "renamed"), None);
187        assert_eq!(public.position("discard"), None);
188        let outer = RowSchema::with_outer_schema(&RowSchema::default(), &compact);
189        assert_eq!(
190            outer.physical_record_fields(outer.qualified_slot("r", "renamed").unwrap()),
191            Some(&fields)
192        );
193        for schema in [&selected, &qualified, &compact, &public] {
194            assert!(Arc::ptr_eq(
195                &descriptor,
196                schema
197                    .record_metadata(0)
198                    .unwrap()
199                    .descriptor
200                    .as_ref()
201                    .unwrap()
202            ));
203            assert!(Arc::ptr_eq(&fields, schema.record_fields(0).unwrap()));
204        }
205        assert!(Arc::ptr_eq(
206            &descriptor,
207            outer
208                .physical_record_metadata(outer.qualified_slot("r", "renamed").unwrap())
209                .unwrap()
210                .descriptor
211                .as_ref()
212                .unwrap()
213        ));
214    }
215}