Skip to main content

uqa_sql/schema/
relation_types.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Named whole-row identities belong to a relation scope, including empty rows.
8
9use super::{Arc, ColumnIdentity, ColumnType, HashMap, RowSchema};
10use crate::SQLError;
11
12/// Empty scopes allocate no map; copies of a retained scope share its identities.
13#[derive(Debug, Default, Clone, PartialEq, Eq)]
14pub(super) struct RelationRowTypes(Option<Arc<NamedRows>>);
15
16#[derive(Debug, Default, Clone, PartialEq, Eq)]
17struct NamedRows {
18    types: HashMap<Box<str>, ColumnType>,
19    /// The complete local row denotes this one source. A join or an outer-only namespace has no such identity.
20    source: Option<Box<str>>,
21}
22
23impl RelationRowTypes {
24    pub(super) fn get(&self, qualifier: &str) -> Option<&ColumnType> {
25        self.0.as_ref()?.types.get(qualifier)
26    }
27
28    pub(super) fn qualifiers(&self) -> impl Iterator<Item = &str> {
29        self.0
30            .iter()
31            .flat_map(|rows| rows.types.keys().map(std::convert::AsRef::as_ref))
32    }
33
34    fn insert(&mut self, qualifier: &str, ty: ColumnType) {
35        let rows = self.0.get_or_insert_with(|| Arc::new(NamedRows::default()));
36        Arc::make_mut(rows).types.insert(qualifier.into(), ty);
37    }
38
39    /// An alias of one retained relation keeps that relation's named type. A boundary over several relations produces an anonymous row.
40    pub(super) fn requalified(&self, input: &RowSchema, qualifier: &str) -> Self {
41        let Some(rows) = self.0.as_ref() else {
42            return Self::default();
43        };
44        let Some(source) = rows.source.as_deref() else {
45            return Self::default();
46        };
47        if input
48            .identities()
49            .iter()
50            .enumerate()
51            .any(|(position, column)| {
52                input.wildcard_position_visible(position) && column.qualifier() != Some(source)
53            })
54        {
55            return Self::default();
56        }
57        let mut result = Self::default();
58        result.insert(qualifier, rows.types[source].clone());
59        Arc::make_mut(result.0.as_mut().unwrap()).source = Some(qualifier.into());
60        result
61    }
62
63    pub(super) fn joined(left: &RowSchema, right: &RowSchema) -> Self {
64        if left.index.cold.row_types.0.is_none() && right.index.cold.row_types.0.is_none() {
65            return Self::default();
66        }
67        let mut result = Self::default();
68        for (source, other) in [(left, right), (right, left)] {
69            if let Some(rows) = &source.index.cold.row_types.0 {
70                for (qualifier, ty) in &rows.types {
71                    if !other.has_qualifier(qualifier) {
72                        result.insert(qualifier, ty.clone());
73                    }
74                }
75            }
76        }
77        result
78    }
79
80    pub(super) fn with_outer(input: &RowSchema, outer: &RowSchema) -> Self {
81        let mut result = input.index.cold.row_types.clone();
82        if let Some(rows) = &outer.index.cold.row_types.0 {
83            for (qualifier, ty) in &rows.types {
84                if !input.has_qualifier(qualifier) {
85                    result.insert(qualifier, ty.clone());
86                }
87            }
88        }
89        result
90    }
91}
92
93impl RowSchema {
94    pub(super) fn relation_qualifiers(&self) -> impl Iterator<Item = &str> {
95        self.index
96            .qualified
97            .keys()
98            .chain(self.index.aliases.keys())
99            .filter_map(ColumnIdentity::qualifier)
100            .chain(self.index.cold.row_types.qualifiers())
101    }
102
103    /// Retain a relation's actual named composite identity without adding an ordinary column or a physical value slot. The existing source namespace must belong to that relation; a zero-column relation acquires its namespace here.
104    pub fn with_relation_row_type(
105        mut self,
106        qualifier: &str,
107        ty: ColumnType,
108    ) -> Result<Self, SQLError> {
109        if !matches!(ty, ColumnType::Composite(_))
110            || (!self.is_empty() && !self.has_qualifier(qualifier))
111        {
112            return Err(SQLError::Internal(
113                "a relation row type requires its named composite and source namespace".into(),
114            ));
115        }
116        let rows = &mut Arc::make_mut(&mut self.index).cold.row_types;
117        rows.insert(qualifier, ty);
118        let rows = Arc::make_mut(rows.0.as_mut().unwrap());
119        rows.source = (rows.types.len() == 1).then(|| qualifier.into());
120        Ok(self)
121    }
122}
123
124#[cfg(test)]
125mod tests;