Skip to main content

uqa_execution/batch/
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    /// Whether one logical source attribute participates in `*` expansion.
13    #[must_use]
14    pub fn wildcard_position_visible(&self, position: usize) -> bool {
15        position < self.len() && !self.index.cold.wildcard_hidden.contains(&position)
16    }
17
18    pub fn columns(&self) -> &[String] {
19        &self.index.columns
20    }
21
22    pub fn len(&self) -> usize {
23        self.index.columns.len()
24    }
25
26    pub fn is_empty(&self) -> bool {
27        self.index.columns.is_empty()
28    }
29
30    pub fn iter(&self) -> std::slice::Iter<'_, String> {
31        self.index.columns.iter()
32    }
33
34    /// Static SQL type at one logical output position.
35    pub fn column_type(&self, logical: usize) -> Option<&ColumnType> {
36        self.index
37            .cold
38            .columns
39            .get(logical)
40            .and_then(Option::as_ref)
41    }
42
43    /// Static SQL types aligned with [`Self::columns`].
44    pub fn column_types(&self) -> &[Option<ColumnType>] {
45        &self.index.cold.columns
46    }
47
48    /// Structured SQL identities aligned with [`Self::columns`].
49    pub fn identities(&self) -> &[ColumnIdentity] {
50        &self.index.identities
51    }
52
53    pub fn identity(&self, logical: usize) -> Option<&ColumnIdentity> {
54        self.index.identities.get(logical)
55    }
56
57    pub fn public_name(&self, logical: usize) -> Option<&str> {
58        self.identity(logical).map(ColumnIdentity::column)
59    }
60
61    pub fn position(&self, name: &str) -> Option<usize> {
62        self.index.exact.get(name).copied()
63    }
64
65    /// Resolve one visible unqualified SQL identity to its logical position. Ambiguous names deliberately do not select an arbitrary owner.
66    pub fn unqualified_position(&self, column: &str) -> Option<usize> {
67        if self.index.ambiguous_unqualified.contains(column) {
68            return None;
69        }
70        self.index.unqualified.get(column).copied()
71    }
72
73    /// Resolve one visible qualified identity to its logical position.
74    pub fn qualified_position(&self, qualifier: &str, column: &str) -> Option<usize> {
75        let identity = ColumnIdentity::qualified(qualifier, column);
76        if self.index.ambiguous_qualified.contains(&identity) {
77            return None;
78        }
79        self.index.qualified.get(&identity).copied()
80    }
81
82    pub fn physical_width(&self) -> usize {
83        self.index.physical_width
84    }
85
86    /// Resolve one logical output position to its flattened physical slot.
87    pub fn physical_slot(&self, logical: usize) -> Option<usize> {
88        self.slot(logical)
89    }
90
91    /// Static type attached to one flattened physical slot, including
92    /// executor-only internal attributes that have no logical SQL position.
93    pub fn physical_type(&self, physical: usize) -> Option<&ColumnType> {
94        self.index
95            .slots
96            .iter()
97            .position(|slot| *slot == physical)
98            .and_then(|logical| self.column_type(logical))
99            .or_else(|| {
100                self.index.aliases.iter().find_map(|(identity, slot)| {
101                    (*slot == physical)
102                        .then(|| {
103                            self.index
104                                .cold
105                                .aliases
106                                .get(identity)
107                                .and_then(Option::as_ref)
108                        })
109                        .flatten()
110                })
111            })
112            .or_else(|| {
113                self.index
114                    .executor_attributes
115                    .iter()
116                    .find_map(|(column, slot)| {
117                        (*slot == physical)
118                            .then(|| {
119                                self.index
120                                    .cold
121                                    .executor_attribute_types
122                                    .get(column)
123                                    .and_then(Option::as_ref)
124                            })
125                            .flatten()
126                    })
127            })
128    }
129
130    /// 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.
131    pub fn physical_slot_for_identity(&self, identity: &ColumnIdentity) -> Option<usize> {
132        match identity.qualifier() {
133            Some(qualifier) => self.qualified_slot(qualifier, identity.column()),
134            None => self.column_slot(identity.column()),
135        }
136    }
137
138    pub(crate) fn slot(&self, logical: usize) -> Option<usize> {
139        self.index
140            .slots
141            .get(logical)
142            .copied()
143            .filter(|slot| *slot != NULL_SLOT)
144    }
145
146    pub(super) fn exact_slot(&self, name: &str) -> Option<usize> {
147        self.index
148            .exact
149            .get(name)
150            .and_then(|logical| self.slot(*logical))
151            .filter(|slot| *slot != NULL_SLOT)
152    }
153
154    pub(super) fn exact_type(&self, name: &str) -> Option<&ColumnType> {
155        self.index
156            .exact
157            .get(name)
158            .and_then(|logical| self.column_type(*logical))
159    }
160
161    pub(super) fn column_slot(&self, name: &str) -> Option<usize> {
162        if self.index.ambiguous_unqualified.contains(name) {
163            return None;
164        }
165        self.index
166            .unqualified
167            .get(name)
168            .and_then(|logical| self.slot(*logical))
169            .or_else(|| {
170                self.index
171                    .aliases
172                    .get(&ColumnIdentity::unqualified(name))
173                    .copied()
174            })
175            .filter(|slot| *slot != NULL_SLOT)
176    }
177
178    /// 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.
179    pub fn qualified_star_layout(
180        &self,
181        qualifier: &str,
182    ) -> Vec<(String, usize, Option<ColumnType>)> {
183        self.qualified_star_position_layout(qualifier)
184            .into_iter()
185            .map(|(column, _, slot, ty)| (column, slot, ty))
186            .collect()
187    }
188
189    /// 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.
190    pub fn qualified_star_position_layout(
191        &self,
192        qualifier: &str,
193    ) -> Vec<(String, Option<usize>, usize, Option<ColumnType>)> {
194        let mut entries = Vec::new();
195        let mut visible_layout = HashSet::new();
196        for (logical, identity) in self.identities().iter().enumerate() {
197            if identity.qualifier() == Some(qualifier) {
198                let slot = self.slot(logical).unwrap_or(NULL_SLOT);
199                visible_layout.insert((identity.clone(), slot));
200                entries.push((
201                    identity.clone(),
202                    Some(logical),
203                    slot,
204                    self.column_type(logical).cloned(),
205                ));
206            }
207        }
208        for (identity, slot) in &self.index.aliases {
209            if identity.qualifier() == Some(qualifier)
210                && !visible_layout.contains(&(identity.clone(), *slot))
211            {
212                entries.push((
213                    identity.clone(),
214                    None,
215                    *slot,
216                    self.index.cold.aliases.get(identity).cloned().flatten(),
217                ));
218            }
219        }
220        entries.sort_by_key(|(_, _, slot, _)| *slot);
221        entries
222            .into_iter()
223            .map(|(identity, logical, slot, ty)| (identity.column().to_string(), logical, slot, ty))
224            .collect()
225    }
226
227    pub(super) fn qualified_slot(&self, qualifier: &str, column: &str) -> Option<usize> {
228        let identity = ColumnIdentity::qualified(qualifier, column);
229        if self.index.ambiguous_qualified.contains(&identity) {
230            return None;
231        }
232        self.index
233            .qualified
234            .get(&identity)
235            .and_then(|logical| self.slot(*logical))
236            .or_else(|| self.index.aliases.get(&identity).copied())
237            .filter(|slot| *slot != NULL_SLOT)
238    }
239}