use rudb_common::{Error, Result};
use rudb_vector::Chunk;
use crate::array::Array;
use crate::types::{Field, Schema};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecordBatch {
schema: Schema,
len: usize,
columns: Vec<Array>,
}
impl RecordBatch {
pub fn of(chunk: &Chunk, names: &[String]) -> Result<Self> {
if names.len() != chunk.width() {
return Err(Error::internal(format!(
"{} names for a chunk of {} columns",
names.len(),
chunk.width()
)));
}
let mut columns = Vec::with_capacity(chunk.width());
let mut fields = Vec::with_capacity(chunk.width());
for (index, name) in names.iter().enumerate() {
let array = Array::of(chunk.column(index)?)?;
fields.push(Field::new(name.clone(), array.data_type().clone()));
columns.push(array);
}
Ok(Self { schema: Schema::new(fields), len: chunk.len(), columns })
}
pub fn empty(schema: Schema) -> Result<Self> {
let columns =
schema.fields.iter().map(|field| Array::empty(field.data_type.clone())).collect();
Ok(Self { schema, len: 0, columns })
}
#[must_use]
pub fn schema(&self) -> &Schema {
&self.schema
}
#[must_use]
pub fn len(&self) -> usize {
self.len
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
pub fn width(&self) -> usize {
self.columns.len()
}
#[must_use]
pub fn columns(&self) -> &[Array] {
&self.columns
}
#[must_use]
pub fn column(&self, index: usize) -> Option<&Array> {
self.columns.get(index)
}
}
#[cfg(test)]
mod tests {
use rudb_common::{LogicalType, Value};
use rudb_vector::{Chunk, Vector};
use super::RecordBatch;
use crate::types::{DataType, Field, Schema};
fn chunk() -> Chunk {
let ids = Vector::from_values(
LogicalType::Integer,
&[Value::Integer(1), Value::Integer(2), Value::Null],
)
.expect("the values are integers");
let names = Vector::from_values(
LogicalType::Varchar,
&[
Value::Varchar("ann".to_string()),
Value::Varchar("bo".to_string()),
Value::Varchar("cy".to_string()),
],
)
.expect("the values are strings");
Chunk::new(vec![ids, names]).expect("both columns are three long")
}
fn names() -> Vec<String> {
vec!["id".to_string(), "name".to_string()]
}
#[test]
fn a_chunk_becomes_one_array_per_column_with_the_names_it_was_given() {
let batch = RecordBatch::of(&chunk(), &names()).expect("both types map onto Arrow");
assert_eq!(batch.len(), 3);
assert_eq!(batch.width(), 2);
assert_eq!(
batch.schema().fields,
vec![Field::new("id", DataType::Int32), Field::new("name", DataType::Utf8)]
);
assert_eq!(batch.column(0).expect("the first column").null_count(), 1);
assert_eq!(batch.column(1).expect("the second column").values(), b"annbocy");
}
#[test]
fn the_wrong_number_of_names_is_refused_rather_than_padded_out() {
let error = RecordBatch::of(&chunk(), &["id".to_string()])
.expect_err("one name for two columns is a caller mistake");
assert!(error.to_string().contains("1 names for a chunk of 2 columns"), "{error}");
}
#[test]
fn a_result_with_no_rows_still_has_its_columns_and_their_types() {
let schema = Schema::new(vec![
Field::new("id", DataType::Int32),
Field::new("name", DataType::Utf8),
]);
let batch = RecordBatch::empty(schema.clone()).expect("an empty batch is always buildable");
assert!(batch.is_empty());
assert_eq!(batch.width(), 2);
assert_eq!(batch.schema(), &schema);
}
#[test]
fn a_chunk_of_no_columns_is_a_batch_of_no_columns() {
let batch = RecordBatch::of(&Chunk::empty(&[]), &[]).expect("nothing to convert");
assert_eq!(batch.width(), 0);
assert!(batch.is_empty());
}
}