1use std::sync::Arc;
4
5use crate::array::Array;
6use crate::error::{Error, ErrorContext, Result};
7use crate::schema::{Column, LogicalType, Schema};
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct RecordBatch {
12 schema: Arc<Schema>,
13 columns: Vec<Array>,
14 row_count: usize,
15}
16
17impl RecordBatch {
18 pub fn try_new(schema: Arc<Schema>, columns: Vec<Array>, row_count: usize) -> Result<Self> {
24 if columns.len() != schema.column_count() {
25 return Err(invalid("the column count does not match the schema"));
26 }
27 for (column, array) in schema.columns().iter().zip(&columns) {
28 if array.len() != row_count {
29 return Err(invalid(format!(
30 "column {} has {} values, expected {row_count}",
31 column.name(),
32 array.len()
33 )));
34 }
35 if !holds(column, array) {
36 return Err(invalid(format!(
37 "column {} was decoded as {array:?}, which is not {:?}",
38 column.name(),
39 column.logical_type()
40 )));
41 }
42 if let Some(validity) = validity(array) {
43 if validity.len() != row_count {
44 return Err(invalid(format!(
45 "column {} has a validity bitmap with {} bits, expected {row_count}",
46 column.name(),
47 validity.len()
48 )));
49 }
50 if !column.is_nullable() {
51 return Err(invalid(format!(
52 "non-nullable column {} has a validity bitmap",
53 column.name()
54 )));
55 }
56 }
57 }
58 Ok(Self {
59 schema,
60 columns,
61 row_count,
62 })
63 }
64
65 pub fn schema(&self) -> &Schema {
67 &self.schema
68 }
69
70 pub fn columns(&self) -> &[Array] {
72 &self.columns
73 }
74
75 pub fn row_count(&self) -> usize {
77 self.row_count
78 }
79
80 pub fn column_by_name(&self, name: &str) -> Option<&Array> {
82 self.schema
83 .columns()
84 .iter()
85 .position(|column| column.name() == name)
86 .and_then(|index| self.columns.get(index))
87 }
88
89 pub fn column(&self, index: usize) -> Option<&Array> {
91 self.columns.get(index)
92 }
93
94 pub(crate) fn slice(&self, start: usize, end: usize) -> Self {
99 assert!(start <= end && end <= self.row_count);
100 Self {
101 schema: Arc::clone(&self.schema),
102 columns: self
103 .columns
104 .iter()
105 .map(|column| column.slice(start, end))
106 .collect(),
107 row_count: end - start,
108 }
109 }
110
111 pub(crate) fn take(&self, indices: &[usize]) -> Self {
114 assert!(indices.iter().all(|&index| index < self.row_count));
115 Self {
116 schema: Arc::clone(&self.schema),
117 columns: self
118 .columns
119 .iter()
120 .map(|column| column.take(indices))
121 .collect(),
122 row_count: indices.len(),
123 }
124 }
125}
126
127fn holds(column: &Column, array: &Array) -> bool {
129 matches!(
130 (column.logical_type(), array),
131 (LogicalType::Bool, Array::Bool(_))
132 | (LogicalType::Int8, Array::Int8(_))
133 | (LogicalType::Int16, Array::Int16(_))
134 | (LogicalType::Int32, Array::Int32(_))
135 | (LogicalType::Int64, Array::Int64(_))
136 | (LogicalType::UInt8, Array::UInt8(_))
137 | (LogicalType::UInt16, Array::UInt16(_))
138 | (LogicalType::UInt32, Array::UInt32(_))
139 | (LogicalType::UInt64, Array::UInt64(_))
140 | (LogicalType::Float32, Array::Float32(_))
141 | (LogicalType::Float64, Array::Float64(_))
142 | (LogicalType::Decimal { .. }, Array::Decimal(_))
143 | (LogicalType::Timestamp { .. }, Array::Timestamp(_))
144 | (LogicalType::Utf8, Array::Utf8(_))
145 | (LogicalType::Categorical { .. }, Array::Categorical(_))
146 | (LogicalType::Binary, Array::Binary(_))
147 | (LogicalType::FixedBinary { .. }, Array::FixedBinary(_))
148 | (LogicalType::Date32, Array::Date32(_))
149 )
150}
151
152fn validity(array: &Array) -> Option<&[bool]> {
153 match array {
154 Array::Bool(array) => array.validity(),
155 Array::Int8(array) => array.validity(),
156 Array::Int16(array) => array.validity(),
157 Array::Int32(array) => array.validity(),
158 Array::Int64(array) => array.validity(),
159 Array::UInt8(array) => array.validity(),
160 Array::UInt16(array) => array.validity(),
161 Array::UInt32(array) => array.validity(),
162 Array::UInt64(array) => array.validity(),
163 Array::Float32(array) => array.validity(),
164 Array::Float64(array) => array.validity(),
165 Array::Decimal(array) => array.validity(),
166 Array::Timestamp(array) => array.validity(),
167 Array::Utf8(array) | Array::Categorical(array) => array.validity(),
168 Array::Binary(array) | Array::FixedBinary(array) => array.validity(),
169 Array::Date32(array) => array.validity(),
170 }
171}
172
173fn invalid(message: impl Into<String>) -> Error {
174 Error::invalid_argument(message).with_context(ErrorContext::Payload)
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::array::{PrimitiveArray, ScalarValue, Utf8Array};
181
182 fn schema() -> Arc<Schema> {
183 Arc::new(Schema::new(
184 1,
185 vec![
186 Column::new(1, "value", LogicalType::Int64, true),
187 Column::new(2, "label", LogicalType::Utf8, false),
188 ],
189 None,
190 ))
191 }
192
193 fn batch() -> RecordBatch {
194 RecordBatch::try_new(
195 schema(),
196 vec![
197 Array::Int64(PrimitiveArray::new(
198 vec![1, 2, 3, 4],
199 Some(vec![true, false, true, true]),
200 )),
201 Array::Utf8(Utf8Array::new(
202 vec!["a".into(), "b".into(), "c".into(), "d".into()],
203 None,
204 )),
205 ],
206 4,
207 )
208 .expect("a well-formed batch")
209 }
210
211 #[test]
212 fn a_slice_reports_the_rows_of_its_range() {
213 assert_eq!(batch().slice(1, 3).row_count(), 2);
214 }
215
216 #[test]
217 fn a_slice_keeps_every_column() {
218 assert_eq!(batch().slice(1, 3).columns().len(), 2);
219 }
220
221 #[test]
222 fn a_slice_keeps_its_rows_aligned_across_columns() {
223 let sliced = batch().slice(1, 3);
224
225 assert_eq!(
226 (
227 sliced.column(0).unwrap().value_at(1),
228 sliced.column(1).unwrap().value_at(1)
229 ),
230 (Some(ScalarValue::Int64(3)), Some(ScalarValue::Utf8("c")))
231 );
232 }
233
234 #[test]
235 fn a_slice_keeps_the_nulls_of_its_range() {
236 let sliced = batch().slice(1, 3);
237
238 assert_eq!(sliced.column(0).unwrap().value_at(0), None);
239 }
240
241 #[test]
242 fn a_slice_shares_the_schema_it_came_from() {
243 assert_eq!(batch().slice(0, 1).schema(), schema().as_ref());
244 }
245
246 #[test]
247 fn slicing_a_whole_batch_reproduces_it() {
248 assert_eq!(batch().slice(0, 4), batch());
249 }
250}