Skip to main content

uqa_sql/schema/
type_lookup.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Scalar type lookup borrows declared columns without constructing physical row indexes.
8
9use super::RowSchema;
10use crate::ast::{ColumnDef, ColumnType, InternalColumnRef};
11
12/// Borrowed schema operations used by scalar type propagation and binding. Physical schemas retain their complete alias, ambiguity, open-column and executor-attribute behavior; declared column slices supply an unqualified positional namespace without allocating another schema generation.
13pub trait ScalarTypeSchema {
14    /// Reuse an already analyzed expression occurrence in this scope. `Some(None)` preserves an observed unknown type; `None` asks the caller to infer the expression normally. Borrowed observations must not outlive their original expressions or parameter environment.
15    fn observed_expression_type(&self, _: &crate::ScalarExpr) -> Option<Option<&ColumnType>> {
16        None
17    }
18
19    fn has_unqualified_column(&self, name: &str) -> bool;
20    fn column_is_ambiguous(&self, name: &str) -> bool;
21    fn type_of(&self, name: &str) -> Option<&ColumnType>;
22    fn column_type(&self, position: usize) -> Option<&ColumnType>;
23    fn internal_type(&self, column: InternalColumnRef) -> Option<&ColumnType>;
24    fn has_qualifier(&self, qualifier: &str) -> bool;
25    /// The named composite identity of a relation's whole row, when the caller retains it. Query-only scopes may expose an anonymous record instead.
26    fn whole_row_type(&self, _qualifier: &str) -> Option<&ColumnType> {
27        None
28    }
29    fn has_qualified_column(&self, qualifier: &str, name: &str) -> bool;
30    fn qualified_column_is_ambiguous(&self, qualifier: &str, name: &str) -> bool;
31    fn qualified_type(&self, qualifier: &str, name: &str) -> Option<&ColumnType>;
32    fn columns_are_open(&self, qualifier: Option<&str>) -> bool;
33
34    fn column_collation(&self, position: usize) -> Option<u32> {
35        self.column_type(position).map(default_collation)
36    }
37    fn collation_of(&self, name: &str) -> Option<u32> {
38        self.type_of(name).map(default_collation)
39    }
40    fn qualified_collation(&self, qualifier: &str, name: &str) -> Option<u32> {
41        self.qualified_type(qualifier, name).map(default_collation)
42    }
43    fn internal_collation(&self, column: InternalColumnRef) -> Option<u32> {
44        self.internal_type(column).map(default_collation)
45    }
46
47    /// Preserve the physical outer-row interface used by existing custom scalar-subquery resolvers. Declared column views have no query arena or physical outer row.
48    fn physical_schema(&self) -> Option<&RowSchema>;
49}
50
51fn default_collation(ty: &ColumnType) -> u32 {
52    u32::try_from(crate::catalog::type_metadata::pg_type_collation_oid(ty))
53        .expect("type collation identity")
54}
55
56impl ScalarTypeSchema for RowSchema {
57    fn column_collation(&self, position: usize) -> Option<u32> {
58        Self::column_collation(self, position)
59    }
60    fn collation_of(&self, name: &str) -> Option<u32> {
61        Self::collation_of(self, name)
62    }
63    fn qualified_collation(&self, qualifier: &str, name: &str) -> Option<u32> {
64        Self::qualified_collation(self, qualifier, name)
65    }
66    fn internal_collation(&self, column: InternalColumnRef) -> Option<u32> {
67        Self::internal_collation(self, column)
68    }
69
70    fn has_unqualified_column(&self, name: &str) -> bool {
71        Self::has_unqualified_column(self, name)
72    }
73    fn column_is_ambiguous(&self, name: &str) -> bool {
74        Self::column_is_ambiguous(self, name)
75    }
76    fn type_of(&self, name: &str) -> Option<&ColumnType> {
77        Self::type_of(self, name)
78    }
79    fn column_type(&self, position: usize) -> Option<&ColumnType> {
80        Self::column_type(self, position)
81    }
82    fn internal_type(&self, column: InternalColumnRef) -> Option<&ColumnType> {
83        Self::internal_type(self, column)
84    }
85    fn has_qualifier(&self, qualifier: &str) -> bool {
86        Self::has_qualifier(self, qualifier)
87    }
88    fn whole_row_type(&self, qualifier: &str) -> Option<&ColumnType> {
89        self.index.cold.row_types.get(qualifier)
90    }
91    fn has_qualified_column(&self, qualifier: &str, name: &str) -> bool {
92        Self::has_qualified_column(self, qualifier, name)
93    }
94    fn qualified_column_is_ambiguous(&self, qualifier: &str, name: &str) -> bool {
95        Self::qualified_column_is_ambiguous(self, qualifier, name)
96    }
97    fn qualified_type(&self, qualifier: &str, name: &str) -> Option<&ColumnType> {
98        Self::qualified_type(self, qualifier, name)
99    }
100    fn columns_are_open(&self, qualifier: Option<&str>) -> bool {
101        Self::columns_are_open(self, qualifier)
102    }
103    fn physical_schema(&self) -> Option<&RowSchema> {
104        Some(self)
105    }
106}
107
108/// Zero-allocation view matching `RowSchema::with_types` for the supplied unqualified column definitions. Names and declared types, including domains and nested arrays, stay borrowed from their existing owner. Duplicate exact names remain ambiguous; dots in an identifier do not create a relation qualifier.
109#[derive(Clone, Copy)]
110pub struct ColumnTypeSchema<'a> {
111    columns: &'a [ColumnDef],
112    relation: Option<(&'a str, &'a ColumnType)>,
113}
114
115impl<'a> ColumnTypeSchema<'a> {
116    pub fn new(columns: &'a [ColumnDef]) -> Self {
117        Self {
118            columns,
119            relation: None,
120        }
121    }
122
123    /// Retain the declaring relation's actual row type without copying its columns or representing system attributes as user columns.
124    pub fn with_relation(
125        columns: &'a [ColumnDef],
126        name: &'a str,
127        row_type: &'a ColumnType,
128    ) -> Result<Self, crate::SQLError> {
129        if !matches!(row_type, ColumnType::Composite(_)) {
130            return Err(crate::SQLError::Internal(
131                "a relation scope requires its named composite row type".into(),
132            ));
133        }
134        Ok(Self {
135            columns,
136            relation: Some((name, row_type)),
137        })
138    }
139
140    pub(crate) fn system_column_type(&self, name: &str) -> Option<&ColumnType> {
141        (self.relation.is_some() && name == "tableoid").then_some(&ColumnType::Oid)
142    }
143
144    /// Original declarations, including stable attribute numbers and selected collations, borrowed from the same catalog generation as type lookup.
145    pub fn columns(&self) -> &'a [ColumnDef] {
146        self.columns
147    }
148}
149
150impl ScalarTypeSchema for ColumnTypeSchema<'_> {
151    fn column_collation(&self, position: usize) -> Option<u32> {
152        self.columns.get(position).map(|column| {
153            column
154                .collation_oid
155                .unwrap_or_else(|| default_collation(&column.ty))
156        })
157    }
158    fn collation_of(&self, name: &str) -> Option<u32> {
159        let mut matching = self
160            .columns
161            .iter()
162            .enumerate()
163            .filter(|(_, column)| column.name == name);
164        let Some((position, _)) = matching.next() else {
165            return self.system_column_type(name).map(default_collation);
166        };
167        matching
168            .next()
169            .is_none()
170            .then(|| self.column_collation(position))
171            .flatten()
172    }
173
174    fn has_unqualified_column(&self, name: &str) -> bool {
175        self.columns.iter().any(|column| column.name == name)
176            || self.system_column_type(name).is_some()
177    }
178    fn column_is_ambiguous(&self, name: &str) -> bool {
179        self.columns
180            .iter()
181            .filter(|column| column.name == name)
182            .nth(1)
183            .is_some()
184    }
185    fn type_of(&self, name: &str) -> Option<&ColumnType> {
186        let mut matching = self.columns.iter().filter(|column| column.name == name);
187        let Some(first) = matching.next() else {
188            return self.system_column_type(name);
189        };
190        matching.next().is_none().then_some(&first.ty)
191    }
192    fn column_type(&self, position: usize) -> Option<&ColumnType> {
193        self.columns.get(position).map(|column| &column.ty)
194    }
195    fn internal_type(&self, _: InternalColumnRef) -> Option<&ColumnType> {
196        None
197    }
198    fn has_qualifier(&self, qualifier: &str) -> bool {
199        self.whole_row_type(qualifier).is_some()
200    }
201    fn whole_row_type(&self, qualifier: &str) -> Option<&ColumnType> {
202        let (name, row_type) = self.relation?;
203        let ColumnType::Composite(reference) = row_type else {
204            unreachable!("relation scope validates its row type");
205        };
206        (qualifier == name || qualifier == reference.name).then_some(row_type)
207    }
208    fn has_qualified_column(&self, qualifier: &str, name: &str) -> bool {
209        self.has_qualifier(qualifier) && self.has_unqualified_column(name)
210    }
211    fn qualified_column_is_ambiguous(&self, qualifier: &str, name: &str) -> bool {
212        self.has_qualifier(qualifier) && self.column_is_ambiguous(name)
213    }
214    fn qualified_type(&self, qualifier: &str, name: &str) -> Option<&ColumnType> {
215        self.has_qualifier(qualifier)
216            .then(|| self.type_of(name))
217            .flatten()
218    }
219    fn qualified_collation(&self, qualifier: &str, name: &str) -> Option<u32> {
220        self.has_qualifier(qualifier)
221            .then(|| self.collation_of(name))
222            .flatten()
223    }
224    fn columns_are_open(&self, _: Option<&str>) -> bool {
225        false
226    }
227    fn physical_schema(&self) -> Option<&RowSchema> {
228        None
229    }
230}
231
232#[cfg(test)]
233mod tests;