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 source from its catalog declarations without discarding selected column collations. Attribute numbers remain a catalog identity, independent of these query positions.
72    pub fn with_column_definitions(
73        qualifier: Option<&str>,
74        columns: &[crate::ast::ColumnDef],
75    ) -> Self {
76        Self::with_column_metadata(
77            qualifier,
78            columns.iter().map(|column| {
79                (
80                    column.name.clone(),
81                    Some(column.ty.clone()),
82                    column.collation_oid,
83                )
84            }),
85        )
86    }
87
88    /// Build a positional source while selecting names, types and collations together. This accepts projected, aliased and system columns without a second declaration lookup or a dense collation buffer.
89    pub fn with_column_metadata(
90        qualifier: Option<&str>,
91        columns: impl IntoIterator<Item = (String, Option<ColumnType>, Option<u32>)>,
92    ) -> Self {
93        let columns = columns.into_iter();
94        let capacity = columns.size_hint().0;
95        let mut names = Vec::with_capacity(capacity);
96        let mut identities = Vec::with_capacity(capacity);
97        let mut types = Vec::with_capacity(capacity);
98        let mut metadata = SchemaBuildMetadata::default();
99        for (name, ty, oid) in columns {
100            assert!(
101                oid.is_none_or(|oid| oid == 0
102                    || ty
103                        .as_ref()
104                        .is_some_and(super::composites::type_is_collatable)),
105                "collation requires a collatable column type"
106            );
107            metadata.collations.column(names.len(), oid, ty.as_ref());
108            identities.push(qualifier.map_or_else(
109                || ColumnIdentity::unqualified(&name),
110                |qualifier| ColumnIdentity::qualified(qualifier, &name),
111            ));
112            names.push(name);
113            types.push(ty);
114        }
115        let width = names.len();
116        Self::from_typed_parts_with_aliases_and_exact_precedence(
117            names,
118            identities,
119            types,
120            (0..width).collect(),
121            width,
122            metadata,
123        )
124    }
125
126    /// Build a positional schema from explicit structured identities.
127    pub fn with_identities(
128        columns: Vec<String>,
129        identities: Vec<ColumnIdentity>,
130        types: Vec<Option<ColumnType>>,
131    ) -> Self {
132        let width = columns.len();
133        assert_eq!(
134            width,
135            identities.len(),
136            "row schema column/identity width mismatch"
137        );
138        assert_eq!(width, types.len(), "row schema column/type width mismatch");
139        Self::from_typed_parts_with_aliases_and_exact_precedence(
140            columns,
141            identities,
142            types,
143            (0..width).collect(),
144            width,
145            SchemaBuildMetadata::default(),
146        )
147    }
148
149    /// Build a physical row layout whose values are addressable only through
150    /// an opaque internal relation identity. It contributes no SQL-visible
151    /// columns and therefore cannot affect `*` expansion or name binding.
152    pub fn with_internal_relation_types(
153        relation: InternalRelationId,
154        types: Vec<Option<ColumnType>>,
155    ) -> Self {
156        Self::with_internal_relation_metadata(relation, types.into_iter().map(|ty| (ty, None)))
157    }
158
159    /// Build executor-only inputs with the selected collation of each logical attribute, including NULL values.
160    pub fn with_internal_relation_metadata(
161        relation: InternalRelationId,
162        columns: impl IntoIterator<Item = (Option<ColumnType>, Option<u32>)>,
163    ) -> Self {
164        let mut metadata = SchemaBuildMetadata::default();
165        let mut width = 0;
166        for (position, (ty, oid)) in columns.into_iter().enumerate() {
167            assert!(
168                oid.is_none_or(|oid| oid == 0
169                    || ty
170                        .as_ref()
171                        .is_some_and(super::composites::type_is_collatable)),
172                "collation requires a collatable column type"
173            );
174            let column = relation.column(position);
175            metadata.collations.internal(column, oid, ty.as_ref());
176            metadata.internal.insert(column, position);
177            metadata.internal_types.insert(column, ty);
178            width += 1;
179        }
180        Self::from_typed_parts_with_aliases_and_exact_precedence(
181            Vec::new(),
182            Vec::new(),
183            Vec::new(),
184            Vec::new(),
185            width,
186            metadata,
187        )
188    }
189
190    /// Build the lookup semantics of a named compatibility row. An exact bare
191    /// key in a map is authoritative even when qualified metadata keys share
192    /// its suffix; physical relational schemas continue to treat multiple
193    /// visible owners as ambiguous.
194    pub fn from_named_columns(columns: Vec<String>) -> Self {
195        let width = columns.len();
196        let identities = columns
197            .iter()
198            .cloned()
199            .map(ColumnIdentity::unqualified)
200            .collect();
201        Self::from_parts_with_aliases_and_exact_precedence(
202            columns,
203            identities,
204            (0..width).collect(),
205            width,
206            SchemaBuildMetadata {
207                exact_unqualified_precedence: true,
208                ..SchemaBuildMetadata::default()
209            },
210        )
211    }
212
213    fn from_parts(
214        columns: Vec<String>,
215        identities: Vec<ColumnIdentity>,
216        slots: Vec<usize>,
217        physical_width: usize,
218    ) -> Self {
219        Self::from_parts_with_aliases(columns, identities, slots, physical_width, HashMap::new())
220    }
221
222    fn from_parts_with_aliases(
223        columns: Vec<String>,
224        identities: Vec<ColumnIdentity>,
225        slots: Vec<usize>,
226        physical_width: usize,
227        aliases: HashMap<ColumnIdentity, usize>,
228    ) -> Self {
229        Self::from_parts_with_aliases_and_exact_precedence(
230            columns,
231            identities,
232            slots,
233            physical_width,
234            SchemaBuildMetadata {
235                aliases,
236                ..SchemaBuildMetadata::default()
237            },
238        )
239    }
240
241    fn from_parts_with_aliases_and_exact_precedence(
242        columns: Vec<String>,
243        identities: Vec<ColumnIdentity>,
244        slots: Vec<usize>,
245        physical_width: usize,
246        metadata: SchemaBuildMetadata,
247    ) -> Self {
248        let types = vec![None; columns.len()];
249        Self::from_typed_parts_with_aliases_and_exact_precedence(
250            columns,
251            identities,
252            types,
253            slots,
254            physical_width,
255            metadata,
256        )
257    }
258
259    pub(super) fn from_typed_parts_with_aliases_and_exact_precedence(
260        columns: Vec<String>,
261        identities: Vec<ColumnIdentity>,
262        types: Vec<Option<ColumnType>>,
263        slots: Vec<usize>,
264        physical_width: usize,
265        metadata: SchemaBuildMetadata,
266    ) -> Self {
267        let SchemaBuildMetadata {
268            mut collations,
269            row_types,
270            record_fields,
271            aliases,
272            alias_types,
273            internal,
274            internal_types,
275            score_sources,
276            wildcard_hidden,
277            binding_only,
278            open_qualifiers,
279            exact_unqualified_precedence,
280            extra_ambiguous_unqualified,
281            extra_ambiguous_qualified,
282            local_width,
283        } = metadata;
284        collations.retain(&types, &alias_types, &internal_types, &binding_only);
285        debug_assert_eq!(columns.len(), slots.len());
286        debug_assert_eq!(columns.len(), identities.len());
287        debug_assert_eq!(columns.len(), types.len());
288        debug_assert!(wildcard_hidden
289            .iter()
290            .all(|position| *position < columns.len()));
291        let identity_layout = physical_width == columns.len()
292            && slots
293                .iter()
294                .enumerate()
295                .all(|(position, slot)| position == *slot);
296        let mut exact = HashMap::with_capacity(columns.len());
297        let mut unqualified = HashMap::with_capacity(columns.len());
298        let mut qualified = HashMap::with_capacity(columns.len());
299        let mut unqualified_counts: HashMap<Box<str>, usize> = HashMap::new();
300        let mut qualified_counts: HashMap<ColumnIdentity, usize> = HashMap::new();
301
302        for (logical, (name, identity)) in columns.iter().zip(&identities).enumerate() {
303            // Later writes to the same named field replace the value in a
304            // ResultRow. Schema transforms preserve that contract.
305            exact.insert(Box::<str>::from(name.as_str()), logical);
306            *unqualified_counts
307                .entry(identity.column.clone())
308                .or_default() += 1;
309            unqualified.insert(identity.column.clone(), logical);
310            if identity.qualifier.is_some() {
311                *qualified_counts.entry(identity.clone()).or_default() += 1;
312                qualified.insert(identity.clone(), logical);
313            }
314        }
315        for slot in aliases.values() {
316            debug_assert!(*slot == NULL_SLOT || *slot < physical_width);
317        }
318        for slot in internal.values() {
319            debug_assert!(*slot == NULL_SLOT || *slot < physical_width);
320        }
321        let mut ambiguous_unqualified: HashSet<Box<str>> = unqualified_counts
322            .into_iter()
323            .filter_map(|(column, count)| {
324                (count > 1
325                    && !(exact_unqualified_precedence
326                        && identities.iter().any(|identity| {
327                            identity.qualifier.is_none() && identity.column == column
328                        })))
329                .then_some(column)
330            })
331            .collect();
332        ambiguous_unqualified.extend(extra_ambiguous_unqualified);
333        let mut ambiguous_qualified = qualified_counts
334            .into_iter()
335            .filter_map(|(identity, count)| (count > 1).then_some(identity))
336            .collect::<HashSet<_>>();
337        ambiguous_qualified.extend(extra_ambiguous_qualified);
338        Self {
339            index: Arc::new(SchemaIndex {
340                columns: columns.into_boxed_slice(),
341                identities: identities.into_boxed_slice(),
342                slots: slots.into_boxed_slice(),
343                physical_width,
344                exact,
345                unqualified,
346                qualified,
347                aliases,
348                executor_attributes: internal,
349                ambiguous_unqualified,
350                ambiguous_qualified,
351                cold: Box::new(SchemaColdMetadata {
352                    collations,
353                    row_types,
354                    record_fields,
355                    columns: types.into_boxed_slice(),
356                    aliases: alias_types,
357                    executor_attribute_types: internal_types,
358                    score_sources,
359                    wildcard_hidden,
360                    binding_only,
361                    open_qualifiers,
362                    identity_layout,
363                    local_width,
364                }),
365            }),
366        }
367    }
368}