Skip to main content

rudb_catalog/
table.rs

1//! A table: a name, some columns, and the rows.
2
3use rudb_common::{Error, Field, LogicalType, Result, Value};
4use rudb_storage::MemoryTable;
5use rudb_vector::{Chunk, Form};
6
7use crate::name::{QualifiedName, same_name};
8
9/// Refuses a column list that names the same column twice.
10///
11/// Exported because the binder makes the same check before anything is created. `CREATE OR REPLACE
12/// TABLE` drops the old table on its way to creating the new one, so a check that only happened
13/// inside [`Table::new`] would report the duplicate after the old table was already gone.
14///
15/// # Errors
16///
17/// If two of the columns have the same name, compared the way SQL compares names, which is without
18/// regard to case.
19pub fn duplicate_check(columns: &[Field]) -> Result<()> {
20    for (at, column) in columns.iter().enumerate() {
21        if columns[..at].iter().any(|held| same_name(&held.name, &column.name)) {
22            // The one that arrived second is the one named, spelled the way it was written rather
23            // than the way the first one was. `CREATE TABLE t (Abc INTEGER, aBC VARCHAR)` says aBC.
24            return Err(Error::catalog(format!(
25                "Column with name {} already exists!",
26                column.name
27            )));
28        }
29    }
30    Ok(())
31}
32
33/// One table.
34///
35/// The rows are a [`MemoryTable`] because that is what M0 has. When the storage format arrives the
36/// field changes and this type does not, which is the reason the catalog holds the rows behind a
37/// handle rather than being the rows.
38#[derive(Debug, Clone)]
39pub struct Table {
40    name: QualifiedName,
41    columns: Vec<Field>,
42    rows: MemoryTable,
43}
44
45impl Table {
46    /// A table with no rows in it.
47    ///
48    /// # Errors
49    ///
50    /// If two columns have the same name, which SQL does not allow and which would make a column
51    /// reference ambiguous in a way no error message could explain later. The message is DuckDB's,
52    /// which names the column and not the table and is a catalog error rather than a binder one,
53    /// because the same sentence comes out of `CREATE TABLE t (a INT, a INT)` and out of a
54    /// `CREATE TABLE ... AS` whose column list repeats a name.
55    pub fn new(name: QualifiedName, columns: Vec<Field>) -> Result<Self> {
56        duplicate_check(&columns)?;
57        let types = columns.iter().map(|column| column.ty.clone()).collect();
58        Ok(Self { name, columns, rows: MemoryTable::new(types) })
59    }
60
61    /// The three part name.
62    #[must_use]
63    pub fn name(&self) -> &QualifiedName {
64        &self.name
65    }
66
67    /// The columns, in order.
68    #[must_use]
69    pub fn columns(&self) -> &[Field] {
70        &self.columns
71    }
72
73    /// The column types, in order.
74    #[must_use]
75    pub fn types(&self) -> Vec<LogicalType> {
76        self.columns.iter().map(|column| column.ty.clone()).collect()
77    }
78
79    /// Where a column sits, by name, under the identifier rule.
80    #[must_use]
81    pub fn column_index(&self, name: &str) -> Option<usize> {
82        self.columns.iter().position(|column| same_name(&column.name, name))
83    }
84
85    /// The rows.
86    #[must_use]
87    pub fn rows(&self) -> &MemoryTable {
88        &self.rows
89    }
90
91    /// The rows, to add to.
92    ///
93    /// This is the way past the constraint check, and the two `append` methods here are the way
94    /// through it. A caller that already knows what it is holding, such as the loader that built
95    /// the chunk out of a file the table was declared from, can take this one.
96    pub fn rows_mut(&mut self) -> &mut MemoryTable {
97        &mut self.rows
98    }
99
100    /// Adds a chunk, refusing a null in a column that said it would not have one.
101    ///
102    /// # Errors
103    ///
104    /// If the chunk does not match the table, or if a `NOT NULL` column is handed a null. DuckDB
105    /// raises a constraint error there and so does this, with the same shape of message, because a
106    /// program that catches one by its text is a program rudb has to not surprise.
107    pub fn append(&mut self, chunk: Chunk) -> Result<()> {
108        self.refuse_nulls(&chunk)?;
109        self.rows.append(chunk)
110    }
111
112    /// Adds rows of single values, refusing a null in a column that said it would not have one.
113    ///
114    /// # Errors
115    ///
116    /// If a row is not as wide as the table, if a value will not convert to its column's type, or
117    /// if a `NOT NULL` column is handed a null.
118    pub fn append_rows(&mut self, rows: &[Vec<Value>]) -> Result<()> {
119        for row in rows {
120            for (at, column) in self.columns.iter().enumerate() {
121                if column.not_null && row.get(at).is_some_and(Value::is_null) {
122                    return Err(self.null_in(&column.name));
123                }
124            }
125        }
126        self.rows.append_rows(rows)
127    }
128
129    /// Checks a chunk against the `NOT NULL` columns before any of it is kept.
130    ///
131    /// A table with no such column pays one walk of the column list and touches no data, which is
132    /// most tables. A column that does refuse nulls is checked through its validity mask when the
133    /// mask is the whole story, which is one word per sixty four rows rather than a read per row.
134    /// A dictionary or a constant can hold the null in the body it points at instead, where the
135    /// mask cannot see it, so those two are asked value by value.
136    fn refuse_nulls(&self, chunk: &Chunk) -> Result<()> {
137        for (at, column) in self.columns.iter().enumerate() {
138            if !column.not_null {
139                continue;
140            }
141            let vector = chunk.column(at)?;
142            let found = match vector.form() {
143                Form::Flat | Form::Sequence => {
144                    vector.validity().has_nulls(vector.len())
145                        && (0..vector.len()).any(|row| !vector.validity().is_valid(row))
146                }
147                _ => (0..vector.len()).any(|row| vector.value_at(row).is_null()),
148            };
149            if found {
150                return Err(self.null_in(&column.name));
151            }
152        }
153        Ok(())
154    }
155
156    /// The error DuckDB raises when a null reaches a column that refuses them.
157    fn null_in(&self, column: &str) -> Error {
158        Error::constraint(format!("NOT NULL constraint failed: {}.{}", self.name.table, column))
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use rudb_vector::Vector;
165
166    use super::*;
167
168    fn hits() -> Table {
169        Table::new(
170            QualifiedName::new("memory", "main", "hits"),
171            vec![
172                Field::new("UserID", LogicalType::BigInt),
173                Field::new("SearchPhrase", LogicalType::Varchar),
174            ],
175        )
176        .expect("two columns with different names")
177    }
178
179    #[test]
180    fn a_column_is_found_however_it_is_spelled() {
181        let table = hits();
182        assert_eq!(table.column_index("userid"), Some(0));
183        assert_eq!(table.column_index("SEARCHPHRASE"), Some(1));
184        assert_eq!(table.column_index("nope"), None);
185    }
186
187    #[test]
188    fn two_columns_with_one_name_is_caught() {
189        let error = Table::new(
190            QualifiedName::new("memory", "main", "t"),
191            vec![Field::new("a", LogicalType::Integer), Field::new("A", LogicalType::Varchar)],
192        )
193        .expect_err("two columns called a");
194        // Named after the second of the two and spelled the way it was written there, which is what
195        // duckdb v1.4.1 says for `CREATE TABLE t (a INTEGER, A VARCHAR)`.
196        assert_eq!(error.to_string(), "Catalog Error: Column with name A already exists!");
197    }
198
199    #[test]
200    fn a_new_table_is_empty_and_typed() {
201        let mut table = hits();
202        assert!(table.rows().is_empty());
203        assert_eq!(table.rows().types(), table.types());
204        table
205            .rows_mut()
206            .append_rows(&[vec![Value::BigInt(1), Value::Varchar("a".to_string())]])
207            .expect("a row of the table's own types");
208        assert_eq!(table.rows().len(), 1);
209    }
210
211    /// A table whose first column refuses nulls and whose second does not.
212    fn required() -> Table {
213        Table::new(
214            QualifiedName::new("memory", "main", "hits"),
215            vec![
216                Field::required("UserID", LogicalType::BigInt),
217                Field::new("SearchPhrase", LogicalType::Varchar),
218            ],
219        )
220        .expect("two columns with different names")
221    }
222
223    #[test]
224    fn a_null_in_a_not_null_column_is_refused() {
225        let mut table = required();
226        let error = table
227            .append_rows(&[vec![Value::Null, Value::Varchar("a".to_string())]])
228            .expect_err("a null in UserID");
229        assert_eq!(error.message(), "NOT NULL constraint failed: hits.UserID");
230        assert!(table.rows().is_empty(), "the row was kept anyway");
231    }
232
233    #[test]
234    fn a_null_in_a_column_that_allows_them_is_kept() {
235        let mut table = required();
236        table.append_rows(&[vec![Value::BigInt(7), Value::Null]]).expect("a null in SearchPhrase");
237        assert_eq!(table.rows().len(), 1);
238    }
239
240    #[test]
241    fn a_chunk_is_checked_through_its_mask() {
242        let mut table = required();
243        let phrase = Vector::constant(LogicalType::Varchar, Value::Varchar("a".to_string()), 2);
244        let good = Chunk::new(vec![
245            Vector::from_values(LogicalType::BigInt, &[Value::BigInt(1), Value::BigInt(2)])
246                .expect("two ids"),
247            phrase.clone(),
248        ])
249        .expect("two columns of two rows");
250        table.append(good).expect("no nulls anywhere");
251        let bad = Chunk::new(vec![
252            Vector::from_values(LogicalType::BigInt, &[Value::BigInt(1), Value::Null])
253                .expect("an id and a null"),
254            phrase,
255        ])
256        .expect("two columns of two rows");
257        let error = table.append(bad).expect_err("a null in UserID");
258        assert_eq!(error.message(), "NOT NULL constraint failed: hits.UserID");
259        assert_eq!(table.rows().len(), 2, "the bad chunk was kept anyway");
260    }
261
262    #[test]
263    fn a_null_hiding_in_a_constant_is_found() {
264        let mut table = required();
265        let chunk = Chunk::new(vec![
266            Vector::constant(LogicalType::BigInt, Value::Null, 4),
267            Vector::constant(LogicalType::Varchar, Value::Varchar("a".to_string()), 4),
268        ])
269        .expect("two columns of four rows");
270        let error = table.append(chunk).expect_err("a constant null in UserID");
271        assert_eq!(error.message(), "NOT NULL constraint failed: hits.UserID");
272    }
273}