Skip to main content

uqa_sql/schema/
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 crate::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            record_fields,
202            aliases,
203            alias_types,
204            internal,
205            internal_types,
206            score_sources,
207            wildcard_hidden,
208            binding_only,
209            open_qualifiers,
210            exact_unqualified_precedence,
211            extra_ambiguous_unqualified,
212            extra_ambiguous_qualified,
213            local_width,
214        } = metadata;
215        debug_assert_eq!(columns.len(), slots.len());
216        debug_assert_eq!(columns.len(), identities.len());
217        debug_assert_eq!(columns.len(), types.len());
218        debug_assert!(wildcard_hidden
219            .iter()
220            .all(|position| *position < columns.len()));
221        let identity_layout = physical_width == columns.len()
222            && slots
223                .iter()
224                .enumerate()
225                .all(|(position, slot)| position == *slot);
226        let mut exact = HashMap::with_capacity(columns.len());
227        let mut unqualified = HashMap::with_capacity(columns.len());
228        let mut qualified = HashMap::with_capacity(columns.len());
229        let mut unqualified_counts: HashMap<Box<str>, usize> = HashMap::new();
230        let mut qualified_counts: HashMap<ColumnIdentity, usize> = HashMap::new();
231
232        for (logical, (name, identity)) in columns.iter().zip(&identities).enumerate() {
233            // Later writes to the same named field replace the value in a
234            // ResultRow. Schema transforms preserve that contract.
235            exact.insert(Box::<str>::from(name.as_str()), logical);
236            *unqualified_counts
237                .entry(identity.column.clone())
238                .or_default() += 1;
239            unqualified.insert(identity.column.clone(), logical);
240            if identity.qualifier.is_some() {
241                *qualified_counts.entry(identity.clone()).or_default() += 1;
242                qualified.insert(identity.clone(), logical);
243            }
244        }
245        for slot in aliases.values() {
246            debug_assert!(*slot == NULL_SLOT || *slot < physical_width);
247        }
248        for slot in internal.values() {
249            debug_assert!(*slot == NULL_SLOT || *slot < physical_width);
250        }
251        let mut ambiguous_unqualified: HashSet<Box<str>> = unqualified_counts
252            .into_iter()
253            .filter_map(|(column, count)| {
254                (count > 1
255                    && !(exact_unqualified_precedence
256                        && identities.iter().any(|identity| {
257                            identity.qualifier.is_none() && identity.column == column
258                        })))
259                .then_some(column)
260            })
261            .collect();
262        ambiguous_unqualified.extend(extra_ambiguous_unqualified);
263        let mut ambiguous_qualified = qualified_counts
264            .into_iter()
265            .filter_map(|(identity, count)| (count > 1).then_some(identity))
266            .collect::<HashSet<_>>();
267        ambiguous_qualified.extend(extra_ambiguous_qualified);
268        Self {
269            index: Arc::new(SchemaIndex {
270                columns: columns.into_boxed_slice(),
271                identities: identities.into_boxed_slice(),
272                slots: slots.into_boxed_slice(),
273                physical_width,
274                exact,
275                unqualified,
276                qualified,
277                aliases,
278                executor_attributes: internal,
279                ambiguous_unqualified,
280                ambiguous_qualified,
281                cold: Box::new(SchemaColdMetadata {
282                    record_fields,
283                    columns: types.into_boxed_slice(),
284                    aliases: alias_types,
285                    executor_attribute_types: internal_types,
286                    score_sources,
287                    wildcard_hidden,
288                    binding_only,
289                    open_qualifiers,
290                    identity_layout,
291                    local_width,
292                }),
293            }),
294        }
295    }
296}