Skip to main content

uqa_sql/schema/
schema_layout.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Logical-to-physical layout inspection and wildcard expansion.
8
9use super::{ColumnIdentity, ColumnType, HashSet, RowSchema, NULL_SLOT};
10
11impl RowSchema {
12    /// Compare the complete physical row contract while the original schema retains analysis-only namespaces and record descriptors. A restored row must match this layout before it can reuse the original binding context.
13    pub fn has_same_physical_layout(&self, other: &Self) -> bool {
14        let left = &self.index;
15        let right = &other.index;
16        left.columns == right.columns
17            && left.identities == right.identities
18            && left.slots == right.slots
19            && left.physical_width == right.physical_width
20            && left.aliases == right.aliases
21            && left.executor_attributes == right.executor_attributes
22            && left.cold.columns == right.cold.columns
23            && left.cold.aliases == right.cold.aliases
24            && left.cold.executor_attribute_types == right.cold.executor_attribute_types
25            && left.cold.score_sources == right.cold.score_sources
26            && left.cold.wildcard_hidden == right.cold.wildcard_hidden
27            && left
28                .cold
29                .collations
30                .same_physical_bindings(&right.cold.collations)
31    }
32
33    /// Whether one logical source attribute participates in `*` expansion.
34    #[must_use]
35    pub fn wildcard_position_visible(&self, position: usize) -> bool {
36        position < self.len() && !self.index.cold.wildcard_hidden.contains(&position)
37    }
38
39    pub fn columns(&self) -> &[String] {
40        &self.index.columns
41    }
42
43    pub fn len(&self) -> usize {
44        self.index.columns.len()
45    }
46
47    pub fn is_empty(&self) -> bool {
48        self.index.columns.is_empty()
49    }
50
51    pub fn iter(&self) -> std::slice::Iter<'_, String> {
52        self.index.columns.iter()
53    }
54
55    /// Static SQL type at one logical output position.
56    pub fn column_type(&self, logical: usize) -> Option<&ColumnType> {
57        self.index
58            .cold
59            .columns
60            .get(logical)
61            .and_then(Option::as_ref)
62    }
63
64    /// Static SQL types aligned with [`Self::columns`].
65    pub fn column_types(&self) -> &[Option<ColumnType>] {
66        &self.index.cold.columns
67    }
68
69    /// Structured SQL identities aligned with [`Self::columns`].
70    pub fn identities(&self) -> &[ColumnIdentity] {
71        &self.index.identities
72    }
73
74    pub fn identity(&self, logical: usize) -> Option<&ColumnIdentity> {
75        self.index.identities.get(logical)
76    }
77
78    pub fn public_name(&self, logical: usize) -> Option<&str> {
79        self.identity(logical).map(ColumnIdentity::column)
80    }
81
82    pub fn position(&self, name: &str) -> Option<usize> {
83        self.index.exact.get(name).copied()
84    }
85
86    /// Resolve one visible unqualified SQL identity to its logical position. Ambiguous names deliberately do not select an arbitrary owner.
87    pub fn unqualified_position(&self, column: &str) -> Option<usize> {
88        if self.index.ambiguous_unqualified.contains(column) {
89            return None;
90        }
91        self.index.unqualified.get(column).copied()
92    }
93
94    /// Resolve one visible qualified identity to its logical position.
95    pub fn qualified_position(&self, qualifier: &str, column: &str) -> Option<usize> {
96        let identity = ColumnIdentity::qualified(qualifier, column);
97        if self.index.ambiguous_qualified.contains(&identity) {
98            return None;
99        }
100        self.index.qualified.get(&identity).copied()
101    }
102
103    pub fn physical_width(&self) -> usize {
104        self.index.physical_width
105    }
106
107    /// Resolve one logical output position to its flattened physical slot.
108    pub fn physical_slot(&self, logical: usize) -> Option<usize> {
109        self.slot(logical)
110    }
111
112    /// Static type attached to one flattened physical slot, including
113    /// executor-only internal attributes that have no logical SQL position.
114    pub fn physical_type(&self, physical: usize) -> Option<&ColumnType> {
115        self.index
116            .slots
117            .iter()
118            .position(|slot| *slot == physical)
119            .and_then(|logical| self.column_type(logical))
120            .or_else(|| {
121                self.index.aliases.iter().find_map(|(identity, slot)| {
122                    (*slot == physical)
123                        .then(|| {
124                            self.index
125                                .cold
126                                .aliases
127                                .get(identity)
128                                .and_then(Option::as_ref)
129                        })
130                        .flatten()
131                })
132            })
133            .or_else(|| {
134                self.index
135                    .executor_attributes
136                    .iter()
137                    .find_map(|(column, slot)| {
138                        (*slot == physical)
139                            .then(|| {
140                                self.index
141                                    .cold
142                                    .executor_attribute_types
143                                    .get(column)
144                                    .and_then(Option::as_ref)
145                            })
146                            .flatten()
147                    })
148            })
149    }
150
151    /// Resolve a structured SQL identity, including a hidden JOIN USING alias, to its flattened physical slot. Ambiguous identities deliberately return `None` rather than selecting an arbitrary source value.
152    pub fn physical_slot_for_identity(&self, identity: &ColumnIdentity) -> Option<usize> {
153        match identity.qualifier() {
154            Some(qualifier) => self.qualified_slot(qualifier, identity.column()),
155            None => self.column_slot(identity.column()),
156        }
157    }
158
159    pub fn slot(&self, logical: usize) -> Option<usize> {
160        self.index
161            .slots
162            .get(logical)
163            .copied()
164            .filter(|slot| *slot != NULL_SLOT)
165    }
166
167    pub fn exact_slot(&self, name: &str) -> Option<usize> {
168        self.index
169            .exact
170            .get(name)
171            .and_then(|logical| self.slot(*logical))
172            .filter(|slot| *slot != NULL_SLOT)
173    }
174
175    pub(super) fn exact_type(&self, name: &str) -> Option<&ColumnType> {
176        self.index
177            .exact
178            .get(name)
179            .and_then(|logical| self.column_type(*logical))
180    }
181
182    pub fn column_slot(&self, name: &str) -> Option<usize> {
183        if self.index.ambiguous_unqualified.contains(name) {
184            return None;
185        }
186        self.index
187            .unqualified
188            .get(name)
189            .and_then(|logical| self.slot(*logical))
190            .or_else(|| {
191                self.index
192                    .aliases
193                    .get(&ColumnIdentity::unqualified(name))
194                    .copied()
195            })
196            .filter(|slot| *slot != NULL_SLOT)
197    }
198
199    /// Physical projection layout for `qualifier.*` in relation-column order. Hidden identities introduced by `JOIN ... USING` remain selectable, so each side's wildcard retains its own merged-column value.
200    pub fn qualified_star_layout(
201        &self,
202        qualifier: &str,
203    ) -> Vec<(String, usize, Option<ColumnType>)> {
204        self.qualified_star_position_layout(qualifier)
205            .into_iter()
206            .map(|(column, _, slot, ty)| (column, slot, ty))
207            .collect()
208    }
209
210    /// Bound layout for `qualifier.*`. Visible columns retain their logical positions; hidden aliases such as the suppressed side of `JOIN ... USING` expose only their physical slot.
211    pub fn qualified_star_position_layout(
212        &self,
213        qualifier: &str,
214    ) -> Vec<(String, Option<usize>, usize, Option<ColumnType>)> {
215        let mut entries = Vec::new();
216        let mut visible_layout = HashSet::new();
217        for (logical, identity) in self.identities().iter().enumerate() {
218            if identity.qualifier() == Some(qualifier) {
219                let slot = self.slot(logical).unwrap_or(NULL_SLOT);
220                visible_layout.insert((identity.clone(), slot));
221                entries.push((
222                    identity.clone(),
223                    Some(logical),
224                    slot,
225                    self.column_type(logical).cloned(),
226                ));
227            }
228        }
229        for (identity, slot) in &self.index.aliases {
230            if identity.qualifier() == Some(qualifier)
231                && !visible_layout.contains(&(identity.clone(), *slot))
232            {
233                entries.push((
234                    identity.clone(),
235                    None,
236                    *slot,
237                    self.index.cold.aliases.get(identity).cloned().flatten(),
238                ));
239            }
240        }
241        entries.sort_by_key(|(_, _, slot, _)| *slot);
242        entries
243            .into_iter()
244            .map(|(identity, logical, slot, ty)| (identity.column().to_string(), logical, slot, ty))
245            .collect()
246    }
247
248    pub fn qualified_slot(&self, qualifier: &str, column: &str) -> Option<usize> {
249        let identity = ColumnIdentity::qualified(qualifier, column);
250        if self.index.ambiguous_qualified.contains(&identity) {
251            return None;
252        }
253        self.index
254            .qualified
255            .get(&identity)
256            .and_then(|logical| self.slot(*logical))
257            .or_else(|| self.index.aliases.get(&identity).copied())
258            .filter(|slot| *slot != NULL_SLOT)
259    }
260}
261
262impl<'a> IntoIterator for &'a RowSchema {
263    type Item = &'a String;
264    type IntoIter = std::slice::Iter<'a, String>;
265
266    fn into_iter(self) -> Self::IntoIter {
267        self.iter()
268    }
269}