use std::sync::Arc;
use crate::array::Array;
use crate::error::{Error, ErrorContext, Result};
use crate::schema::{Column, LogicalType, Schema};
#[derive(Debug, Clone, PartialEq)]
pub struct RecordBatch {
schema: Arc<Schema>,
columns: Vec<Array>,
row_count: usize,
}
impl RecordBatch {
pub fn try_new(schema: Arc<Schema>, columns: Vec<Array>, row_count: usize) -> Result<Self> {
if columns.len() != schema.column_count() {
return Err(invalid("the column count does not match the schema"));
}
for (column, array) in schema.columns().iter().zip(&columns) {
if array.len() != row_count {
return Err(invalid(format!(
"column {} has {} values, expected {row_count}",
column.name(),
array.len()
)));
}
if !holds(column, array) {
return Err(invalid(format!(
"column {} was decoded as {array:?}, which is not {:?}",
column.name(),
column.logical_type()
)));
}
if let Some(validity) = validity(array) {
if validity.len() != row_count {
return Err(invalid(format!(
"column {} has a validity bitmap with {} bits, expected {row_count}",
column.name(),
validity.len()
)));
}
if !column.is_nullable() {
return Err(invalid(format!(
"non-nullable column {} has a validity bitmap",
column.name()
)));
}
}
}
Ok(Self {
schema,
columns,
row_count,
})
}
pub fn schema(&self) -> &Schema {
&self.schema
}
pub fn columns(&self) -> &[Array] {
&self.columns
}
pub fn row_count(&self) -> usize {
self.row_count
}
pub fn column_by_name(&self, name: &str) -> Option<&Array> {
self.schema
.columns()
.iter()
.position(|column| column.name() == name)
.and_then(|index| self.columns.get(index))
}
pub fn column(&self, index: usize) -> Option<&Array> {
self.columns.get(index)
}
pub(crate) fn slice(&self, start: usize, end: usize) -> Self {
assert!(start <= end && end <= self.row_count);
Self {
schema: Arc::clone(&self.schema),
columns: self
.columns
.iter()
.map(|column| column.slice(start, end))
.collect(),
row_count: end - start,
}
}
pub(crate) fn take(&self, indices: &[usize]) -> Self {
assert!(indices.iter().all(|&index| index < self.row_count));
Self {
schema: Arc::clone(&self.schema),
columns: self
.columns
.iter()
.map(|column| column.take(indices))
.collect(),
row_count: indices.len(),
}
}
}
fn holds(column: &Column, array: &Array) -> bool {
matches!(
(column.logical_type(), array),
(LogicalType::Bool, Array::Bool(_))
| (LogicalType::Int8, Array::Int8(_))
| (LogicalType::Int16, Array::Int16(_))
| (LogicalType::Int32, Array::Int32(_))
| (LogicalType::Int64, Array::Int64(_))
| (LogicalType::UInt8, Array::UInt8(_))
| (LogicalType::UInt16, Array::UInt16(_))
| (LogicalType::UInt32, Array::UInt32(_))
| (LogicalType::UInt64, Array::UInt64(_))
| (LogicalType::Float32, Array::Float32(_))
| (LogicalType::Float64, Array::Float64(_))
| (LogicalType::Decimal { .. }, Array::Decimal(_))
| (LogicalType::Timestamp { .. }, Array::Timestamp(_))
| (LogicalType::Utf8, Array::Utf8(_))
| (LogicalType::Categorical { .. }, Array::Categorical(_))
| (LogicalType::Binary, Array::Binary(_))
| (LogicalType::FixedBinary { .. }, Array::FixedBinary(_))
| (LogicalType::Date32, Array::Date32(_))
)
}
fn validity(array: &Array) -> Option<&[bool]> {
match array {
Array::Bool(array) => array.validity(),
Array::Int8(array) => array.validity(),
Array::Int16(array) => array.validity(),
Array::Int32(array) => array.validity(),
Array::Int64(array) => array.validity(),
Array::UInt8(array) => array.validity(),
Array::UInt16(array) => array.validity(),
Array::UInt32(array) => array.validity(),
Array::UInt64(array) => array.validity(),
Array::Float32(array) => array.validity(),
Array::Float64(array) => array.validity(),
Array::Decimal(array) => array.validity(),
Array::Timestamp(array) => array.validity(),
Array::Utf8(array) | Array::Categorical(array) => array.validity(),
Array::Binary(array) | Array::FixedBinary(array) => array.validity(),
Array::Date32(array) => array.validity(),
}
}
fn invalid(message: impl Into<String>) -> Error {
Error::invalid_argument(message).with_context(ErrorContext::Payload)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::array::{PrimitiveArray, ScalarValue, Utf8Array};
fn schema() -> Arc<Schema> {
Arc::new(Schema::new(
1,
vec![
Column::new(1, "value", LogicalType::Int64, true),
Column::new(2, "label", LogicalType::Utf8, false),
],
None,
))
}
fn batch() -> RecordBatch {
RecordBatch::try_new(
schema(),
vec![
Array::Int64(PrimitiveArray::new(
vec![1, 2, 3, 4],
Some(vec![true, false, true, true]),
)),
Array::Utf8(Utf8Array::new(
vec!["a".into(), "b".into(), "c".into(), "d".into()],
None,
)),
],
4,
)
.expect("a well-formed batch")
}
#[test]
fn a_slice_reports_the_rows_of_its_range() {
assert_eq!(batch().slice(1, 3).row_count(), 2);
}
#[test]
fn a_slice_keeps_every_column() {
assert_eq!(batch().slice(1, 3).columns().len(), 2);
}
#[test]
fn a_slice_keeps_its_rows_aligned_across_columns() {
let sliced = batch().slice(1, 3);
assert_eq!(
(
sliced.column(0).unwrap().value_at(1),
sliced.column(1).unwrap().value_at(1)
),
(Some(ScalarValue::Int64(3)), Some(ScalarValue::Utf8("c")))
);
}
#[test]
fn a_slice_keeps_the_nulls_of_its_range() {
let sliced = batch().slice(1, 3);
assert_eq!(sliced.column(0).unwrap().value_at(0), None);
}
#[test]
fn a_slice_shares_the_schema_it_came_from() {
assert_eq!(batch().slice(0, 1).schema(), schema().as_ref());
}
#[test]
fn slicing_a_whole_batch_reproduces_it() {
assert_eq!(batch().slice(0, 4), batch());
}
}