Skip to main content

rudb_exec/
schema.rs

1//! What an operator produces: names, types, and the bindings downstream expressions use.
2
3use rudb_common::{Error, Field, LogicalType, Result};
4use rudb_plan::ColumnBinding;
5
6/// One operator's output columns.
7///
8/// The fields and the bindings are parallel and always the same length, which [`Schema::new`]
9/// checks. They are two vectors rather than a vector of pairs because a caller almost always wants
10/// one of them whole: the result set wants the names and the types, and the expression evaluator
11/// wants to search the bindings.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Schema {
14    fields: Vec<Field>,
15    bindings: Vec<ColumnBinding>,
16}
17
18impl Schema {
19    /// A schema of `fields`, bound at `bindings`.
20    ///
21    /// # Errors
22    ///
23    /// If the two are not the same length, which would make a binding resolve to the wrong column
24    /// or to no column at all.
25    pub fn new(fields: Vec<Field>, bindings: Vec<ColumnBinding>) -> Result<Self> {
26        if fields.len() != bindings.len() {
27            return Err(Error::internal(format!(
28                "a schema of {} fields and {} bindings",
29                fields.len(),
30                bindings.len()
31            )));
32        }
33        Ok(Self { fields, bindings })
34    }
35
36    /// A schema of `fields` numbered from zero against `table`.
37    ///
38    /// The common case, since an operator that introduces columns numbers them in the order it
39    /// produces them and the binder does the same.
40    ///
41    /// # Panics
42    ///
43    /// If there are more than `u32::MAX` fields, which is the bound a binding's position already
44    /// carries.
45    #[must_use]
46    pub fn numbered(fields: Vec<Field>, table: u32) -> Self {
47        let bindings = (0..fields.len())
48            .map(|at| {
49                ColumnBinding::new(
50                    table,
51                    u32::try_from(at).expect("a schema this wide cannot be built"),
52                )
53            })
54            .collect();
55        Self { fields, bindings }
56    }
57
58    /// A schema with no columns, which is what [`Node::Dummy`](rudb_plan::Node::Dummy) produces.
59    #[must_use]
60    pub fn empty() -> Self {
61        Self { fields: Vec::new(), bindings: Vec::new() }
62    }
63
64    /// The fields, in output order.
65    #[must_use]
66    pub fn fields(&self) -> &[Field] {
67        &self.fields
68    }
69
70    /// The bindings, in output order.
71    #[must_use]
72    pub fn bindings(&self) -> &[ColumnBinding] {
73        &self.bindings
74    }
75
76    /// How many columns.
77    #[must_use]
78    pub fn width(&self) -> usize {
79        self.fields.len()
80    }
81
82    /// Whether there are no columns.
83    #[must_use]
84    pub fn is_empty(&self) -> bool {
85        self.fields.is_empty()
86    }
87
88    /// The column types, in order.
89    #[must_use]
90    pub fn types(&self) -> Vec<LogicalType> {
91        self.fields.iter().map(|field| field.ty.clone()).collect()
92    }
93
94    /// The column names, in order.
95    #[must_use]
96    pub fn names(&self) -> Vec<String> {
97        self.fields.iter().map(|field| field.name.clone()).collect()
98    }
99
100    /// Where a binding sits in the output.
101    ///
102    /// A linear scan. An operator's schema is as wide as the query is, which is tens of columns on
103    /// the widest ClickBench query, and a hash map of tens of entries rebuilt per operator costs
104    /// more than the scans it saves.
105    #[must_use]
106    pub fn position_of(&self, binding: ColumnBinding) -> Option<usize> {
107        self.bindings.iter().position(|held| *held == binding)
108    }
109
110    /// The left schema's columns followed by the right schema's, which is what a join produces.
111    #[must_use]
112    pub fn concat(left: &Self, right: &Self) -> Self {
113        let mut fields = left.fields.clone();
114        fields.extend(right.fields.iter().cloned());
115        let mut bindings = left.bindings.clone();
116        bindings.extend(right.bindings.iter().copied());
117        Self { fields, bindings }
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    fn two() -> Schema {
126        Schema::numbered(
127            vec![Field::new("a", LogicalType::Integer), Field::new("b", LogicalType::Varchar)],
128            7,
129        )
130    }
131
132    #[test]
133    fn a_numbered_schema_binds_its_columns_in_order() {
134        let schema = two();
135        assert_eq!(schema.position_of(ColumnBinding::new(7, 0)), Some(0));
136        assert_eq!(schema.position_of(ColumnBinding::new(7, 1)), Some(1));
137        assert_eq!(schema.position_of(ColumnBinding::new(7, 2)), None);
138        assert_eq!(schema.position_of(ColumnBinding::new(6, 0)), None);
139    }
140
141    #[test]
142    fn a_schema_reports_its_names_and_types_in_output_order() {
143        let schema = two();
144        assert_eq!(schema.names(), vec!["a".to_string(), "b".to_string()]);
145        assert_eq!(schema.types(), vec![LogicalType::Integer, LogicalType::Varchar]);
146        assert_eq!(schema.width(), 2);
147    }
148
149    /// Two tables that both number their columns from zero is the ordinary case, and it is the case
150    /// a join would get wrong if the position were the whole of the answer.
151    #[test]
152    fn concatenating_keeps_both_sides_distinguishable() {
153        let left = Schema::numbered(vec![Field::new("id", LogicalType::Integer)], 0);
154        let right = Schema::numbered(vec![Field::new("id", LogicalType::Integer)], 1);
155        let joined = Schema::concat(&left, &right);
156        assert_eq!(joined.width(), 2);
157        assert_eq!(joined.position_of(ColumnBinding::new(0, 0)), Some(0));
158        assert_eq!(joined.position_of(ColumnBinding::new(1, 0)), Some(1));
159    }
160
161    #[test]
162    fn a_schema_whose_halves_disagree_is_caught() {
163        let error = Schema::new(vec![Field::new("a", LogicalType::Integer)], Vec::new())
164            .expect_err("one field and no bindings");
165        assert!(error.message().contains("1 fields and 0 bindings"), "{error}");
166    }
167}