use rudb_common::{Error, Field, LogicalType, Result, Value};
use rudb_storage::MemoryTable;
use rudb_vector::{Chunk, Form};
use crate::name::{QualifiedName, same_name};
#[derive(Debug, Clone)]
pub struct Table {
name: QualifiedName,
columns: Vec<Field>,
rows: MemoryTable,
}
impl Table {
pub fn new(name: QualifiedName, columns: Vec<Field>) -> Result<Self> {
for (at, column) in columns.iter().enumerate() {
if let Some(earlier) =
columns[..at].iter().find(|held| same_name(&held.name, &column.name))
{
return Err(Error::binder(format!(
"table \"{}\" has a duplicate column name \"{}\"",
name.table, earlier.name
)));
}
}
let types = columns.iter().map(|column| column.ty.clone()).collect();
Ok(Self { name, columns, rows: MemoryTable::new(types) })
}
#[must_use]
pub fn name(&self) -> &QualifiedName {
&self.name
}
#[must_use]
pub fn columns(&self) -> &[Field] {
&self.columns
}
#[must_use]
pub fn types(&self) -> Vec<LogicalType> {
self.columns.iter().map(|column| column.ty.clone()).collect()
}
#[must_use]
pub fn column_index(&self, name: &str) -> Option<usize> {
self.columns.iter().position(|column| same_name(&column.name, name))
}
#[must_use]
pub fn rows(&self) -> &MemoryTable {
&self.rows
}
pub fn rows_mut(&mut self) -> &mut MemoryTable {
&mut self.rows
}
pub fn append(&mut self, chunk: Chunk) -> Result<()> {
self.refuse_nulls(&chunk)?;
self.rows.append(chunk)
}
pub fn append_rows(&mut self, rows: &[Vec<Value>]) -> Result<()> {
for row in rows {
for (at, column) in self.columns.iter().enumerate() {
if column.not_null && row.get(at).is_some_and(Value::is_null) {
return Err(self.null_in(&column.name));
}
}
}
self.rows.append_rows(rows)
}
fn refuse_nulls(&self, chunk: &Chunk) -> Result<()> {
for (at, column) in self.columns.iter().enumerate() {
if !column.not_null {
continue;
}
let vector = chunk.column(at)?;
let found = match vector.form() {
Form::Flat | Form::Sequence => {
vector.validity().has_nulls(vector.len())
&& (0..vector.len()).any(|row| !vector.validity().is_valid(row))
}
_ => (0..vector.len()).any(|row| vector.value_at(row).is_null()),
};
if found {
return Err(self.null_in(&column.name));
}
}
Ok(())
}
fn null_in(&self, column: &str) -> Error {
Error::constraint(format!("NOT NULL constraint failed: {}.{}", self.name.table, column))
}
}
#[cfg(test)]
mod tests {
use rudb_vector::Vector;
use super::*;
fn hits() -> Table {
Table::new(
QualifiedName::new("memory", "main", "hits"),
vec![
Field::new("UserID", LogicalType::BigInt),
Field::new("SearchPhrase", LogicalType::Varchar),
],
)
.expect("two columns with different names")
}
#[test]
fn a_column_is_found_however_it_is_spelled() {
let table = hits();
assert_eq!(table.column_index("userid"), Some(0));
assert_eq!(table.column_index("SEARCHPHRASE"), Some(1));
assert_eq!(table.column_index("nope"), None);
}
#[test]
fn two_columns_with_one_name_is_caught() {
let error = Table::new(
QualifiedName::new("memory", "main", "t"),
vec![Field::new("a", LogicalType::Integer), Field::new("A", LogicalType::Varchar)],
)
.expect_err("two columns called a");
assert!(error.message().contains("duplicate column"), "{error}");
}
#[test]
fn a_new_table_is_empty_and_typed() {
let mut table = hits();
assert!(table.rows().is_empty());
assert_eq!(table.rows().types(), table.types());
table
.rows_mut()
.append_rows(&[vec![Value::BigInt(1), Value::Varchar("a".to_string())]])
.expect("a row of the table's own types");
assert_eq!(table.rows().len(), 1);
}
fn required() -> Table {
Table::new(
QualifiedName::new("memory", "main", "hits"),
vec![
Field::required("UserID", LogicalType::BigInt),
Field::new("SearchPhrase", LogicalType::Varchar),
],
)
.expect("two columns with different names")
}
#[test]
fn a_null_in_a_not_null_column_is_refused() {
let mut table = required();
let error = table
.append_rows(&[vec![Value::Null, Value::Varchar("a".to_string())]])
.expect_err("a null in UserID");
assert_eq!(error.message(), "NOT NULL constraint failed: hits.UserID");
assert!(table.rows().is_empty(), "the row was kept anyway");
}
#[test]
fn a_null_in_a_column_that_allows_them_is_kept() {
let mut table = required();
table.append_rows(&[vec![Value::BigInt(7), Value::Null]]).expect("a null in SearchPhrase");
assert_eq!(table.rows().len(), 1);
}
#[test]
fn a_chunk_is_checked_through_its_mask() {
let mut table = required();
let phrase = Vector::constant(LogicalType::Varchar, Value::Varchar("a".to_string()), 2);
let good = Chunk::new(vec![
Vector::from_values(LogicalType::BigInt, &[Value::BigInt(1), Value::BigInt(2)])
.expect("two ids"),
phrase.clone(),
])
.expect("two columns of two rows");
table.append(good).expect("no nulls anywhere");
let bad = Chunk::new(vec![
Vector::from_values(LogicalType::BigInt, &[Value::BigInt(1), Value::Null])
.expect("an id and a null"),
phrase,
])
.expect("two columns of two rows");
let error = table.append(bad).expect_err("a null in UserID");
assert_eq!(error.message(), "NOT NULL constraint failed: hits.UserID");
assert_eq!(table.rows().len(), 2, "the bad chunk was kept anyway");
}
#[test]
fn a_null_hiding_in_a_constant_is_found() {
let mut table = required();
let chunk = Chunk::new(vec![
Vector::constant(LogicalType::BigInt, Value::Null, 4),
Vector::constant(LogicalType::Varchar, Value::Varchar("a".to_string()), 4),
])
.expect("two columns of four rows");
let error = table.append(chunk).expect_err("a constant null in UserID");
assert_eq!(error.message(), "NOT NULL constraint failed: hits.UserID");
}
}