Skip to main content

uqa_execution/batch/
schema_construction.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Construction and indexing of immutable row schemas.
8
9use super::{
10    Arc, ColumnIdentity, ColumnType, HashMap, HashSet, RowSchema, SchemaBuildMetadata,
11    SchemaColdMetadata, SchemaIndex, NULL_SLOT,
12};
13use uqa_sql::ast::InternalRelationId;
14
15impl Default for RowSchema {
16    fn default() -> Self {
17        Self::new(Vec::new())
18    }
19}
20
21impl From<Vec<String>> for RowSchema {
22    fn from(columns: Vec<String>) -> Self {
23        Self::new(columns)
24    }
25}
26
27impl RowSchema {
28    pub fn new(columns: Vec<String>) -> Self {
29        let width = columns.len();
30        let identities = columns
31            .iter()
32            .cloned()
33            .map(ColumnIdentity::unqualified)
34            .collect();
35        Self::from_parts(columns, identities, (0..width).collect(), width)
36    }
37
38    /// Build a positional schema with statically bound SQL types.
39    pub fn with_types(columns: Vec<String>, types: Vec<Option<ColumnType>>) -> Self {
40        let width = columns.len();
41        assert_eq!(width, types.len(), "row schema column/type width mismatch");
42        let identities = columns
43            .iter()
44            .cloned()
45            .map(ColumnIdentity::unqualified)
46            .collect();
47        Self::from_typed_parts_with_aliases_and_exact_precedence(
48            columns,
49            identities,
50            types,
51            (0..width).collect(),
52            width,
53            SchemaBuildMetadata::default(),
54        )
55    }
56
57    /// Build a positional schema whose visible columns all belong to one relation qualifier while retaining their public names verbatim.
58    pub fn with_qualified_types(
59        qualifier: &str,
60        columns: Vec<String>,
61        types: Vec<Option<ColumnType>>,
62    ) -> Self {
63        let identities = columns
64            .iter()
65            .cloned()
66            .map(|column| ColumnIdentity::qualified(qualifier, column))
67            .collect();
68        Self::with_identities(columns, identities, types)
69    }
70
71    /// Build a positional schema from explicit structured identities.
72    pub fn with_identities(
73        columns: Vec<String>,
74        identities: Vec<ColumnIdentity>,
75        types: Vec<Option<ColumnType>>,
76    ) -> Self {
77        let width = columns.len();
78        assert_eq!(
79            width,
80            identities.len(),
81            "row schema column/identity width mismatch"
82        );
83        assert_eq!(width, types.len(), "row schema column/type width mismatch");
84        Self::from_typed_parts_with_aliases_and_exact_precedence(
85            columns,
86            identities,
87            types,
88            (0..width).collect(),
89            width,
90            SchemaBuildMetadata::default(),
91        )
92    }
93
94    /// Build a physical row layout whose values are addressable only through
95    /// an opaque internal relation identity. It contributes no SQL-visible
96    /// columns and therefore cannot affect `*` expansion or name binding.
97    pub fn with_internal_relation_types(
98        relation: InternalRelationId,
99        types: Vec<Option<ColumnType>>,
100    ) -> Self {
101        let internal = (0..types.len())
102            .map(|position| (relation.column(position), position))
103            .collect();
104        let internal_types = types
105            .iter()
106            .enumerate()
107            .map(|(position, ty)| (relation.column(position), ty.clone()))
108            .collect();
109        Self::from_typed_parts_with_aliases_and_exact_precedence(
110            Vec::new(),
111            Vec::new(),
112            Vec::new(),
113            Vec::new(),
114            types.len(),
115            SchemaBuildMetadata {
116                internal,
117                internal_types,
118                ..SchemaBuildMetadata::default()
119            },
120        )
121    }
122
123    /// Build the lookup semantics of a named compatibility row. An exact bare
124    /// key in a map is authoritative even when qualified metadata keys share
125    /// its suffix; physical relational schemas continue to treat multiple
126    /// visible owners as ambiguous.
127    pub fn from_named_columns(columns: Vec<String>) -> Self {
128        let width = columns.len();
129        let identities = columns
130            .iter()
131            .cloned()
132            .map(ColumnIdentity::unqualified)
133            .collect();
134        Self::from_parts_with_aliases_and_exact_precedence(
135            columns,
136            identities,
137            (0..width).collect(),
138            width,
139            SchemaBuildMetadata {
140                exact_unqualified_precedence: true,
141                ..SchemaBuildMetadata::default()
142            },
143        )
144    }
145
146    fn from_parts(
147        columns: Vec<String>,
148        identities: Vec<ColumnIdentity>,
149        slots: Vec<usize>,
150        physical_width: usize,
151    ) -> Self {
152        Self::from_parts_with_aliases(columns, identities, slots, physical_width, HashMap::new())
153    }
154
155    fn from_parts_with_aliases(
156        columns: Vec<String>,
157        identities: Vec<ColumnIdentity>,
158        slots: Vec<usize>,
159        physical_width: usize,
160        aliases: HashMap<ColumnIdentity, usize>,
161    ) -> Self {
162        Self::from_parts_with_aliases_and_exact_precedence(
163            columns,
164            identities,
165            slots,
166            physical_width,
167            SchemaBuildMetadata {
168                aliases,
169                ..SchemaBuildMetadata::default()
170            },
171        )
172    }
173
174    fn from_parts_with_aliases_and_exact_precedence(
175        columns: Vec<String>,
176        identities: Vec<ColumnIdentity>,
177        slots: Vec<usize>,
178        physical_width: usize,
179        metadata: SchemaBuildMetadata,
180    ) -> Self {
181        let types = vec![None; columns.len()];
182        Self::from_typed_parts_with_aliases_and_exact_precedence(
183            columns,
184            identities,
185            types,
186            slots,
187            physical_width,
188            metadata,
189        )
190    }
191
192    pub(super) fn from_typed_parts_with_aliases_and_exact_precedence(
193        columns: Vec<String>,
194        identities: Vec<ColumnIdentity>,
195        types: Vec<Option<ColumnType>>,
196        slots: Vec<usize>,
197        physical_width: usize,
198        metadata: SchemaBuildMetadata,
199    ) -> Self {
200        let SchemaBuildMetadata {
201            aliases,
202            alias_types,
203            internal,
204            internal_types,
205            score_sources,
206            wildcard_hidden,
207            binding_only,
208            exact_unqualified_precedence,
209            extra_ambiguous_unqualified,
210            extra_ambiguous_qualified,
211        } = metadata;
212        debug_assert_eq!(columns.len(), slots.len());
213        debug_assert_eq!(columns.len(), identities.len());
214        debug_assert_eq!(columns.len(), types.len());
215        debug_assert!(wildcard_hidden
216            .iter()
217            .all(|position| *position < columns.len()));
218        let identity_layout = physical_width == columns.len()
219            && slots
220                .iter()
221                .enumerate()
222                .all(|(position, slot)| position == *slot);
223        let mut exact = HashMap::with_capacity(columns.len());
224        let mut unqualified = HashMap::with_capacity(columns.len());
225        let mut qualified = HashMap::with_capacity(columns.len());
226        let mut unqualified_counts: HashMap<Box<str>, usize> = HashMap::new();
227        let mut qualified_counts: HashMap<ColumnIdentity, usize> = HashMap::new();
228
229        for (logical, (name, identity)) in columns.iter().zip(&identities).enumerate() {
230            // Later writes to the same named field replace the value in a
231            // ResultRow. Schema transforms preserve that contract.
232            exact.insert(Box::<str>::from(name.as_str()), logical);
233            *unqualified_counts
234                .entry(identity.column.clone())
235                .or_default() += 1;
236            unqualified.insert(identity.column.clone(), logical);
237            if identity.qualifier.is_some() {
238                *qualified_counts.entry(identity.clone()).or_default() += 1;
239                qualified.insert(identity.clone(), logical);
240            }
241        }
242        for slot in aliases.values() {
243            debug_assert!(*slot == NULL_SLOT || *slot < physical_width);
244        }
245        for slot in internal.values() {
246            debug_assert!(*slot == NULL_SLOT || *slot < physical_width);
247        }
248        let mut ambiguous_unqualified: HashSet<Box<str>> = unqualified_counts
249            .into_iter()
250            .filter_map(|(column, count)| {
251                (count > 1
252                    && !(exact_unqualified_precedence
253                        && identities.iter().any(|identity| {
254                            identity.qualifier.is_none() && identity.column == column
255                        })))
256                .then_some(column)
257            })
258            .collect();
259        ambiguous_unqualified.extend(extra_ambiguous_unqualified);
260        let mut ambiguous_qualified = qualified_counts
261            .into_iter()
262            .filter_map(|(identity, count)| (count > 1).then_some(identity))
263            .collect::<HashSet<_>>();
264        ambiguous_qualified.extend(extra_ambiguous_qualified);
265        Self {
266            index: Arc::new(SchemaIndex {
267                columns: columns.into_boxed_slice(),
268                identities: identities.into_boxed_slice(),
269                slots: slots.into_boxed_slice(),
270                physical_width,
271                exact,
272                unqualified,
273                qualified,
274                aliases,
275                executor_attributes: internal,
276                ambiguous_unqualified,
277                ambiguous_qualified,
278                cold: Box::new(SchemaColdMetadata {
279                    columns: types.into_boxed_slice(),
280                    aliases: alias_types,
281                    executor_attribute_types: internal_types,
282                    score_sources,
283                    wildcard_hidden,
284                    binding_only,
285                    identity_layout,
286                }),
287            }),
288        }
289    }
290}