use rudb_common::bounds::Bound;
use rudb_common::{Error, Field, LogicalType, Result, Value};
use rudb_native::{FrequencyOccurrences, Reader as NativeReader};
use rudb_storage::{MemoryTable, Probe};
use rudb_vector::{Chunk, Form, Vector};
use crate::catalog::DETACHED;
use crate::name::{QualifiedName, same_name};
pub fn duplicate_check(columns: &[Field]) -> Result<()> {
for (at, column) in columns.iter().enumerate() {
if columns[..at].iter().any(|held| same_name(&held.name, &column.name)) {
return Err(Error::catalog(format!(
"Column with name {} already exists!",
column.name
)));
}
}
Ok(())
}
#[derive(Debug, Clone)]
pub enum Rows {
Memory(MemoryTable),
Native(NativeReader),
}
impl Rows {
pub fn top_frequencies(&self, column: usize, top: usize) -> Result<Option<Vec<(Value, u64)>>> {
match self {
Self::Memory(_) => Ok(None),
Self::Native(reader) => reader.top_frequencies(column, top),
}
}
pub fn distinct_values(&self, column: usize) -> Result<Option<u64>> {
match self {
Self::Memory(_) => Ok(None),
Self::Native(reader) => reader.distinct_values(column),
}
}
pub fn null_count(&self, column: usize) -> Result<Option<u64>> {
match self {
Self::Memory(_) => Ok(None),
Self::Native(reader) => reader.null_count(column).map(Some),
}
}
pub fn text_extremes(&self, column: usize) -> Result<Option<(Value, Value)>> {
match self {
Self::Memory(_) => Ok(None),
Self::Native(reader) => reader.text_extremes(column),
}
}
pub fn exact_extremes(&self, column: usize) -> Result<Option<(Bound, Bound)>> {
match self {
Self::Memory(_) => Ok(None),
Self::Native(reader) => reader.exact_extremes(column),
}
}
pub fn exact_sum(&self, column: usize) -> Result<Option<(i128, u64)>> {
match self {
Self::Memory(_) => Ok(None),
Self::Native(reader) => reader.exact_sum(column),
}
}
pub fn frequency_occurrences(&self, column: usize) -> Result<Option<FrequencyOccurrences>> {
match self {
Self::Memory(_) => Ok(None),
Self::Native(reader) => reader.frequency_occurrences(column),
}
}
pub fn chunk_len(&self, at: usize) -> Result<usize> {
Ok(match self {
Self::Memory(rows) => rows
.chunk(at)
.ok_or_else(|| Error::internal("row ordinal names a missing chunk"))?
.len(),
Self::Native(reader) => {
if at >= reader.parts() {
return Err(Error::internal("row ordinal names a missing part"));
}
reader.part_rows(at)
}
})
}
pub fn rows_at(
&self,
types: &[LogicalType],
columns: &[usize],
ordinals: &[u64],
) -> Result<Chunk> {
if columns.len() != types.len() {
return Err(Error::internal("a row fetch has a different number of columns and types"));
}
if let Self::Native(reader) = self {
return Self::native_rows_at(reader, types, columns, ordinals);
}
let mut values = vec![Vec::with_capacity(ordinals.len()); columns.len()];
let mut cached: Option<(usize, Chunk)> = None;
for &ordinal in ordinals {
let ordinal = usize::try_from(ordinal)
.map_err(|_| Error::internal("row ordinal does not fit this platform"))?;
let mut start = 0_usize;
let mut found = None;
for chunk in 0..self.chunk_count() {
let len = self.chunk_len(chunk)?;
if ordinal < start.saturating_add(len) {
found = Some((chunk, ordinal - start));
break;
}
start = start.saturating_add(len);
}
let (chunk, row) =
found.ok_or_else(|| Error::internal("row ordinal is past the table"))?;
if cached.as_ref().is_none_or(|(held, _)| *held != chunk) {
cached = Some((chunk, self.read(chunk, columns)?));
}
let Some((_, held)) = &cached else {
return Err(Error::internal("row chunk was not cached"));
};
for (at, values) in values.iter_mut().enumerate() {
values.push(held.value_at(row, at));
}
}
let vectors = values
.into_iter()
.zip(types)
.map(|(values, ty)| Vector::from_values(ty.clone(), &values))
.collect::<Result<Vec<_>>>()?;
Chunk::with_rows(vectors, ordinals.len())
}
fn native_rows_at(
reader: &NativeReader,
types: &[LogicalType],
columns: &[usize],
ordinals: &[u64],
) -> Result<Chunk> {
if columns.is_empty() {
return Chunk::with_rows(Vec::new(), ordinals.len());
}
let mut ends = Vec::with_capacity(reader.parts());
let mut end = 0_usize;
for part in 0..reader.parts() {
end = end.saturating_add(reader.part_rows(part));
ends.push(end);
}
let mut locations = Vec::with_capacity(ordinals.len());
for &ordinal in ordinals {
let ordinal = usize::try_from(ordinal)
.map_err(|_| Error::internal("row ordinal does not fit this platform"))?;
let part = ends.partition_point(|&end| end <= ordinal);
if part == ends.len() {
return Err(Error::internal("row ordinal is past the table"));
}
let start = part.checked_sub(1).map_or(0, |before| ends[before]);
locations.push((part, ordinal - start));
}
let mut distinct = 0_usize;
for (at, location) in locations.iter().enumerate() {
if at == 0 || locations[at - 1].0 != location.0 {
distinct += 1;
}
}
let dense = distinct.saturating_mul(8) >= reader.parts();
const MIN_COLUMNS_PER_WORKER: usize = 16;
const MAX_WORKERS: usize = 8;
let workers = columns.len().div_ceil(MIN_COLUMNS_PER_WORKER).min(MAX_WORKERS);
if workers <= 1 {
let vectors = Self::read_native_columns(reader, columns, types, &locations, dense)?;
return Chunk::with_rows(vectors, ordinals.len());
}
let width = columns.len().div_ceil(workers);
let locations = &locations;
let pieces = std::thread::scope(|scope| {
let handles = columns
.chunks(width)
.zip(types.chunks(width))
.map(|(columns, types)| {
scope.spawn(move || {
Self::read_native_columns(reader, columns, types, locations, dense)
})
})
.collect::<Vec<_>>();
handles
.into_iter()
.map(|handle| {
handle
.join()
.map_err(|_| Error::internal("a native row fetch worker panicked"))?
})
.collect::<Result<Vec<_>>>()
})?;
let mut vectors = Vec::with_capacity(columns.len());
for piece in pieces {
vectors.extend(piece);
}
Chunk::with_rows(vectors, ordinals.len())
}
fn read_native_columns(
reader: &NativeReader,
columns: &[usize],
types: &[LogicalType],
locations: &[(usize, usize)],
dense: bool,
) -> Result<Vec<Vector>> {
let mut values = vec![Vec::with_capacity(locations.len()); columns.len()];
let mut from = 0;
while from < locations.len() {
let part = locations[from].0;
let mut upto = from + 1;
while upto < locations.len() && locations[upto].0 == part {
upto += 1;
}
let held = if dense {
reader.read(part, columns)?
} else {
reader.read_sparse(part, columns)?
};
for &(_, row) in &locations[from..upto] {
for (at, values) in values.iter_mut().enumerate() {
values.push(held.value_at(row, at));
}
}
from = upto;
}
values
.into_iter()
.zip(types)
.map(|(values, ty)| Vector::from_values(ty.clone(), &values))
.collect()
}
#[must_use]
pub fn types(&self) -> Vec<LogicalType> {
match self {
Self::Memory(rows) => rows.types().to_vec(),
Self::Native(reader) => {
reader.table().fields().iter().map(|field| field.ty.clone()).collect()
}
}
}
#[must_use]
pub fn len(&self) -> usize {
match self {
Self::Memory(rows) => rows.len(),
Self::Native(reader) => reader.table().rows(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn is_native(&self) -> bool {
matches!(self, Self::Native(_))
}
#[must_use]
pub fn chunk_count(&self) -> usize {
match self {
Self::Memory(rows) => rows.chunk_count(),
Self::Native(reader) => reader.parts(),
}
}
pub fn read(&self, at: usize, columns: &[usize]) -> Result<Chunk> {
match self {
Self::Memory(rows) => rows.read(at, columns),
Self::Native(reader) => reader.read(at, columns),
}
}
#[must_use]
pub fn skips(&self, at: usize, probes: &[Probe]) -> bool {
match self {
Self::Memory(rows) => rows.skips(at, probes),
Self::Native(reader) => reader.skips(at, probes),
}
}
#[must_use]
pub fn chunk(&self, at: usize) -> Option<&Chunk> {
match self {
Self::Memory(rows) => rows.chunk(at),
Self::Native(_) => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Table {
name: QualifiedName,
columns: Vec<Field>,
rows: Rows,
oid: i64,
}
impl Table {
pub fn new(name: QualifiedName, columns: Vec<Field>) -> Result<Self> {
duplicate_check(&columns)?;
let types = columns.iter().map(|column| column.ty.clone()).collect();
Ok(Self { name, columns, rows: Rows::Memory(MemoryTable::new(types)), oid: DETACHED })
}
pub fn native(name: QualifiedName, reader: NativeReader) -> Result<Self> {
let columns = reader.table().fields().to_vec();
duplicate_check(&columns)?;
Ok(Self { name, columns, rows: Rows::Native(reader), oid: DETACHED })
}
#[must_use]
pub fn oid(&self) -> i64 {
self.oid
}
pub(crate) fn stamp(&mut self, oid: i64) {
self.oid = oid;
}
#[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) -> &Rows {
&self.rows
}
pub fn commit_native(&mut self, reader: NativeReader) -> Result<()> {
if !self.rows.is_empty() {
return Err(Error::not_implemented(
"streaming a native insert into a table that already has rows",
));
}
if reader.table().fields() != self.columns {
return Err(Error::internal("a committed native snapshot changed its table schema"));
}
self.rows = Rows::Native(reader);
Ok(())
}
pub fn rows_mut(&mut self) -> &mut MemoryTable {
match &mut self.rows {
Rows::Memory(rows) => rows,
Rows::Native(_) => panic!("a committed native table is immutable"),
}
}
pub fn append(&mut self, chunk: Chunk) -> Result<()> {
self.refuse_nulls(&chunk)?;
match &mut self.rows {
Rows::Memory(rows) => rows.append(chunk),
Rows::Native(_) => Err(Error::not_implemented("appending to a committed native table")),
}
}
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));
}
}
}
match &mut self.rows {
Rows::Memory(held) => held.append_rows(rows),
Rows::Native(_) => Err(Error::not_implemented("appending to a committed native table")),
}
}
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_eq!(error.to_string(), "Catalog Error: Column with name A already exists!");
}
#[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");
}
}