1use rudb_common::{Error, Result};
9use rudb_vector::Chunk;
10
11use crate::array::Array;
12use crate::types::{Field, Schema};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct RecordBatch {
17 schema: Schema,
18 len: usize,
19 columns: Vec<Array>,
20}
21
22impl RecordBatch {
23 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 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 #[must_use]
69 pub fn schema(&self) -> &Schema {
70 &self.schema
71 }
72
73 #[must_use]
75 pub fn len(&self) -> usize {
76 self.len
77 }
78
79 #[must_use]
81 pub fn is_empty(&self) -> bool {
82 self.len == 0
83 }
84
85 #[must_use]
87 pub fn width(&self) -> usize {
88 self.columns.len()
89 }
90
91 #[must_use]
93 pub fn columns(&self) -> &[Array] {
94 &self.columns
95 }
96
97 #[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}