Skip to main content

rudb_arrow/
batch.rs

1//! A chunk of columns, in Arrow's layout.
2//!
3//! Arrow calls this a record batch: a schema, a row count, and one array per field. It is the same
4//! shape as a [`Chunk`](rudb_vector::Chunk), which is not a coincidence. Both are columnar, both
5//! carry the whole batch's length once rather than per column, and both are the unit a consumer
6//! pulls. The difference is only the bytes inside, which is what [`Array`] converts.
7
8use rudb_common::{Error, Result};
9use rudb_vector::Chunk;
10
11use crate::array::Array;
12use crate::types::{Field, Schema};
13
14/// A batch of rows, as Arrow columns.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct RecordBatch {
17    schema: Schema,
18    len: usize,
19    columns: Vec<Array>,
20}
21
22impl RecordBatch {
23    /// The batch a chunk becomes, with these column names.
24    ///
25    /// The names come from the caller because a chunk does not have any. A chunk is a run of
26    /// vectors and the names live on whatever produced it, which for a query result is the plan's
27    /// output list and for a table scan is the catalog.
28    ///
29    /// # Errors
30    ///
31    /// When the number of names is not the number of columns, and for anything [`Array::of`]
32    /// rejects.
33    pub fn of(chunk: &Chunk, names: &[String]) -> Result<Self> {
34        if names.len() != chunk.width() {
35            return Err(Error::internal(format!(
36                "{} names for a chunk of {} columns",
37                names.len(),
38                chunk.width()
39            )));
40        }
41        let mut columns = Vec::with_capacity(chunk.width());
42        let mut fields = Vec::with_capacity(chunk.width());
43        for (index, name) in names.iter().enumerate() {
44            let array = Array::of(chunk.column(index)?)?;
45            fields.push(Field::new(name.clone(), array.data_type().clone()));
46            columns.push(array);
47        }
48        Ok(Self { schema: Schema::new(fields), len: chunk.len(), columns })
49    }
50
51    /// An empty batch of this schema.
52    ///
53    /// A result with no rows still has columns, and a consumer that reads the schema off the first
54    /// batch needs one to read it off. Every array in it has length zero, which is a validity
55    /// bitmap of nothing and a values buffer of nothing.
56    ///
57    /// # Errors
58    ///
59    /// Never, today. It returns a result so that a schema carrying a type with no empty array later
60    /// has somewhere to say so.
61    pub fn empty(schema: Schema) -> Result<Self> {
62        let columns =
63            schema.fields.iter().map(|field| Array::empty(field.data_type.clone())).collect();
64        Ok(Self { schema, len: 0, columns })
65    }
66
67    /// The columns and their names.
68    #[must_use]
69    pub fn schema(&self) -> &Schema {
70        &self.schema
71    }
72
73    /// How many rows.
74    #[must_use]
75    pub fn len(&self) -> usize {
76        self.len
77    }
78
79    /// Whether there are none.
80    #[must_use]
81    pub fn is_empty(&self) -> bool {
82        self.len == 0
83    }
84
85    /// How many columns.
86    #[must_use]
87    pub fn width(&self) -> usize {
88        self.columns.len()
89    }
90
91    /// The columns, left to right.
92    #[must_use]
93    pub fn columns(&self) -> &[Array] {
94        &self.columns
95    }
96
97    /// One column, by position.
98    #[must_use]
99    pub fn column(&self, index: usize) -> Option<&Array> {
100        self.columns.get(index)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use rudb_common::{LogicalType, Value};
107    use rudb_vector::{Chunk, Vector};
108
109    use super::RecordBatch;
110    use crate::types::{DataType, Field, Schema};
111
112    fn chunk() -> Chunk {
113        let ids = Vector::from_values(
114            LogicalType::Integer,
115            &[Value::Integer(1), Value::Integer(2), Value::Null],
116        )
117        .expect("the values are integers");
118        let names = Vector::from_values(
119            LogicalType::Varchar,
120            &[
121                Value::Varchar("ann".to_string()),
122                Value::Varchar("bo".to_string()),
123                Value::Varchar("cy".to_string()),
124            ],
125        )
126        .expect("the values are strings");
127        Chunk::new(vec![ids, names]).expect("both columns are three long")
128    }
129
130    fn names() -> Vec<String> {
131        vec!["id".to_string(), "name".to_string()]
132    }
133
134    #[test]
135    fn a_chunk_becomes_one_array_per_column_with_the_names_it_was_given() {
136        let batch = RecordBatch::of(&chunk(), &names()).expect("both types map onto Arrow");
137        assert_eq!(batch.len(), 3);
138        assert_eq!(batch.width(), 2);
139        assert_eq!(
140            batch.schema().fields,
141            vec![Field::new("id", DataType::Int32), Field::new("name", DataType::Utf8)]
142        );
143        assert_eq!(batch.column(0).expect("the first column").null_count(), 1);
144        assert_eq!(batch.column(1).expect("the second column").values(), b"annbocy");
145    }
146
147    #[test]
148    fn the_wrong_number_of_names_is_refused_rather_than_padded_out() {
149        let error = RecordBatch::of(&chunk(), &["id".to_string()])
150            .expect_err("one name for two columns is a caller mistake");
151        assert!(error.to_string().contains("1 names for a chunk of 2 columns"), "{error}");
152    }
153
154    #[test]
155    fn a_result_with_no_rows_still_has_its_columns_and_their_types() {
156        let schema = Schema::new(vec![
157            Field::new("id", DataType::Int32),
158            Field::new("name", DataType::Utf8),
159        ]);
160        let batch = RecordBatch::empty(schema.clone()).expect("an empty batch is always buildable");
161        assert!(batch.is_empty());
162        assert_eq!(batch.width(), 2);
163        assert_eq!(batch.schema(), &schema);
164    }
165
166    #[test]
167    fn a_chunk_of_no_columns_is_a_batch_of_no_columns() {
168        let batch = RecordBatch::of(&Chunk::empty(&[]), &[]).expect("nothing to convert");
169        assert_eq!(batch.width(), 0);
170        assert!(batch.is_empty());
171    }
172}