Skip to main content

common_recordbatch/
recordbatch.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16use std::slice;
17use std::sync::Arc;
18
19use datafusion::arrow::util::pretty::pretty_format_batches;
20use datafusion_common::arrow::array::ArrayRef;
21use datafusion_common::arrow::compute;
22use datafusion_common::arrow::datatypes::{DataType as ArrowDataType, SchemaRef as ArrowSchemaRef};
23use datatypes::arrow::array::{Array, AsArray, RecordBatchOptions};
24use datatypes::prelude::DataType;
25use datatypes::schema::SchemaRef;
26use datatypes::vectors::{Helper, VectorRef};
27use serde::ser::{Error, SerializeStruct};
28use serde::{Serialize, Serializer};
29use snafu::{OptionExt, ResultExt, ensure};
30
31use crate::DfRecordBatch;
32use crate::error::{
33    self, ArrowComputeSnafu, ColumnNotExistsSnafu, DataTypesSnafu, ProjectArrowRecordBatchSnafu,
34    Result,
35};
36
37/// A two-dimensional batch of column-oriented data with a defined schema.
38#[derive(Clone, Debug, PartialEq)]
39pub struct RecordBatch {
40    pub schema: SchemaRef,
41    df_record_batch: DfRecordBatch,
42}
43
44impl RecordBatch {
45    /// Create a new [`RecordBatch`] from `schema` and `columns`.
46    pub fn new<I: IntoIterator<Item = VectorRef>>(
47        schema: SchemaRef,
48        columns: I,
49    ) -> Result<RecordBatch> {
50        let columns: Vec<_> = columns.into_iter().collect();
51        let arrow_arrays = columns.iter().map(|v| v.to_arrow_array()).collect();
52
53        // Casting the arrays here to match the schema, is a temporary solution to support Arrow's
54        // view array types (`StringViewArray` and `BinaryViewArray`).
55        // As to "support": the arrays here are created from vectors, which do not have types
56        // corresponding to view arrays. What we can do is to only cast them.
57        // As to "temporary": we are planing to use Arrow's RecordBatch directly in the read path.
58        // the casting here will be removed in the end.
59        // TODO(LFC): Remove the casting here once `Batch` is no longer used.
60        let arrow_arrays = Self::cast_view_arrays(schema.arrow_schema(), arrow_arrays)?;
61
62        let df_record_batch = DfRecordBatch::try_new(schema.arrow_schema().clone(), arrow_arrays)
63            .context(error::NewDfRecordBatchSnafu)?;
64
65        Ok(RecordBatch {
66            schema,
67            df_record_batch,
68        })
69    }
70
71    pub fn to_df_record_batch<I: IntoIterator<Item = VectorRef>>(
72        arrow_schema: ArrowSchemaRef,
73        columns: I,
74    ) -> Result<DfRecordBatch> {
75        let columns: Vec<_> = columns.into_iter().collect();
76        let arrow_arrays = columns.iter().map(|v| v.to_arrow_array()).collect();
77
78        // Casting the arrays here to match the schema, is a temporary solution to support Arrow's
79        // view array types (`StringViewArray` and `BinaryViewArray`).
80        // As to "support": the arrays here are created from vectors, which do not have types
81        // corresponding to view arrays. What we can do is to only cast them.
82        // As to "temporary": we are planing to use Arrow's RecordBatch directly in the read path.
83        // the casting here will be removed in the end.
84        // TODO(LFC): Remove the casting here once `Batch` is no longer used.
85        let arrow_arrays = Self::cast_view_arrays(&arrow_schema, arrow_arrays)?;
86
87        let df_record_batch = DfRecordBatch::try_new(arrow_schema, arrow_arrays)
88            .context(error::NewDfRecordBatchSnafu)?;
89
90        Ok(df_record_batch)
91    }
92
93    fn cast_view_arrays(
94        schema: &ArrowSchemaRef,
95        mut arrays: Vec<ArrayRef>,
96    ) -> Result<Vec<ArrayRef>> {
97        for (f, a) in schema.fields().iter().zip(arrays.iter_mut()) {
98            let expected = f.data_type();
99            let actual = a.data_type();
100            if matches!(
101                (expected, actual),
102                (ArrowDataType::Utf8View, ArrowDataType::Utf8)
103                    | (ArrowDataType::BinaryView, ArrowDataType::Binary)
104            ) {
105                *a = compute::cast(a, expected).context(ArrowComputeSnafu)?;
106            }
107        }
108        Ok(arrays)
109    }
110
111    /// Create an empty [`RecordBatch`] from `schema`.
112    pub fn new_empty(schema: SchemaRef) -> RecordBatch {
113        let df_record_batch = DfRecordBatch::new_empty(schema.arrow_schema().clone());
114        RecordBatch {
115            schema,
116            df_record_batch,
117        }
118    }
119
120    /// Create an empty [`RecordBatch`] from `schema` with `num_rows`.
121    pub fn new_with_count(schema: SchemaRef, num_rows: usize) -> Result<Self> {
122        let df_record_batch = DfRecordBatch::try_new_with_options(
123            schema.arrow_schema().clone(),
124            vec![],
125            &RecordBatchOptions::new().with_row_count(Some(num_rows)),
126        )
127        .context(error::NewDfRecordBatchSnafu)?;
128        Ok(RecordBatch {
129            schema,
130            df_record_batch,
131        })
132    }
133
134    pub fn try_project(&self, indices: &[usize]) -> Result<Self> {
135        let schema = Arc::new(self.schema.try_project(indices).context(DataTypesSnafu)?);
136        let df_record_batch = self.df_record_batch.project(indices).with_context(|_| {
137            ProjectArrowRecordBatchSnafu {
138                schema: self.schema.clone(),
139                projection: indices.to_vec(),
140            }
141        })?;
142
143        Ok(Self {
144            schema,
145            df_record_batch,
146        })
147    }
148
149    /// Create a new [`RecordBatch`] from `schema` and `df_record_batch`.
150    ///
151    /// This method doesn't check the schema.
152    pub fn from_df_record_batch(schema: SchemaRef, df_record_batch: DfRecordBatch) -> RecordBatch {
153        RecordBatch {
154            schema,
155            df_record_batch,
156        }
157    }
158
159    #[inline]
160    pub fn df_record_batch(&self) -> &DfRecordBatch {
161        &self.df_record_batch
162    }
163
164    #[inline]
165    pub fn into_df_record_batch(self) -> DfRecordBatch {
166        self.df_record_batch
167    }
168
169    #[inline]
170    pub fn columns(&self) -> &[ArrayRef] {
171        self.df_record_batch.columns()
172    }
173
174    #[inline]
175    pub fn column(&self, idx: usize) -> &ArrayRef {
176        self.df_record_batch.column(idx)
177    }
178
179    pub fn column_by_name(&self, name: &str) -> Option<&ArrayRef> {
180        self.df_record_batch.column_by_name(name)
181    }
182
183    #[inline]
184    pub fn num_columns(&self) -> usize {
185        self.df_record_batch.num_columns()
186    }
187
188    #[inline]
189    pub fn num_rows(&self) -> usize {
190        self.df_record_batch.num_rows()
191    }
192
193    pub fn column_vectors(
194        &self,
195        table_name: &str,
196        table_schema: SchemaRef,
197    ) -> Result<HashMap<String, VectorRef>> {
198        let mut vectors = HashMap::with_capacity(self.num_columns());
199
200        // column schemas in recordbatch must match its vectors, otherwise it's corrupted
201        for (field, array) in self
202            .df_record_batch
203            .schema()
204            .fields()
205            .iter()
206            .zip(self.df_record_batch.columns().iter())
207        {
208            let column_name = field.name();
209            let column_schema =
210                table_schema
211                    .column_schema_by_name(column_name)
212                    .context(ColumnNotExistsSnafu {
213                        table_name,
214                        column_name,
215                    })?;
216            let vector = if field.data_type() != &column_schema.data_type.as_arrow_type() {
217                let array = compute::cast(array, &column_schema.data_type.as_arrow_type())
218                    .context(ArrowComputeSnafu)?;
219                Helper::try_into_vector(array).context(DataTypesSnafu)?
220            } else {
221                Helper::try_into_vector(array).context(DataTypesSnafu)?
222            };
223
224            let _ = vectors.insert(column_name.clone(), vector);
225        }
226
227        Ok(vectors)
228    }
229
230    /// Pretty display this record batch like a table
231    pub fn pretty_print(&self) -> String {
232        pretty_format_batches(slice::from_ref(&self.df_record_batch))
233            .map(|t| t.to_string())
234            .unwrap_or("failed to pretty display a record batch".to_string())
235    }
236
237    /// Return a slice record batch starts from offset, with len rows
238    pub fn slice(&self, offset: usize, len: usize) -> Result<RecordBatch> {
239        ensure!(
240            offset + len <= self.num_rows(),
241            error::RecordBatchSliceIndexOverflowSnafu {
242                size: self.num_rows(),
243                visit_index: offset + len
244            }
245        );
246        let sliced = self.df_record_batch.slice(offset, len);
247        Ok(RecordBatch::from_df_record_batch(
248            self.schema.clone(),
249            sliced,
250        ))
251    }
252
253    /// Returns the total number of bytes of memory pointed to by the arrays in this `RecordBatch`.
254    ///
255    /// The buffers store bytes in the Arrow memory format, and include the data as well as the validity map.
256    /// Note that this does not always correspond to the exact memory usage of an array,
257    /// since multiple arrays can share the same buffers or slices thereof.
258    pub fn buffer_memory_size(&self) -> usize {
259        self.df_record_batch
260            .columns()
261            .iter()
262            .map(|array| array.get_buffer_memory_size())
263            .sum()
264    }
265
266    /// Returns the logical memory size of this batch's array slices.
267    ///
268    /// This sums Arrow's logical visible slice buffers rather than the capacity of their shared
269    /// backing buffers. View out-of-line payloads and nested custom payloads are not separately
270    /// traversed or accounted. It is not an exact measure of live physical memory. If Arrow cannot
271    /// calculate a slice's size, the full buffer size is used conservatively.
272    ///
273    /// Mito's current scan paths do not produce top-level View arrays. If they do in the future,
274    /// their out-of-line payload accounting must be reassessed here.
275    pub fn logical_slice_memory_size(&self) -> usize {
276        self.df_record_batch
277            .columns()
278            .iter()
279            .fold(0, |total, array| {
280                let array_size = array
281                    .to_data()
282                    .get_slice_memory_size()
283                    .unwrap_or_else(|_| array.get_buffer_memory_size());
284                total.saturating_add(array_size)
285            })
286    }
287
288    /// Iterate the values as strings in the column at index `i`.
289    ///
290    /// Note that if the underlying array is not a valid GreptimeDB vector, an empty iterator is
291    /// returned.
292    ///
293    /// # Panics
294    /// if index `i` is out of bound.
295    pub fn iter_column_as_string(&self, i: usize) -> Box<dyn Iterator<Item = Option<String>> + '_> {
296        macro_rules! iter {
297            ($column: ident) => {
298                Box::new(
299                    (0..$column.len())
300                        .map(|i| $column.is_valid(i).then(|| $column.value(i).to_string())),
301                )
302            };
303        }
304
305        let column = self.df_record_batch.column(i);
306        match column.data_type() {
307            ArrowDataType::Utf8 => {
308                let column = column.as_string::<i32>();
309                let iter = iter!(column);
310                iter as _
311            }
312            ArrowDataType::LargeUtf8 => {
313                let column = column.as_string::<i64>();
314                iter!(column)
315            }
316            ArrowDataType::Utf8View => {
317                let column = column.as_string_view();
318                iter!(column)
319            }
320            _ => {
321                if let Ok(column) = Helper::try_into_vector(column) {
322                    Box::new(
323                        (0..column.len())
324                            .map(move |i| (!column.is_null(i)).then(|| column.get(i).to_string())),
325                    )
326                } else {
327                    Box::new(std::iter::empty())
328                }
329            }
330        }
331    }
332}
333
334impl Serialize for RecordBatch {
335    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
336    where
337        S: Serializer,
338    {
339        // TODO(yingwen): arrow and arrow2's schemas have different fields, so
340        // it might be better to use our `RawSchema` as serialized field.
341        let mut s = serializer.serialize_struct("record", 2)?;
342        s.serialize_field("schema", &**self.schema.arrow_schema())?;
343
344        let columns = self.df_record_batch.columns();
345        let columns = Helper::try_into_vectors(columns).map_err(Error::custom)?;
346        let vec = columns
347            .iter()
348            .map(|c| c.serialize_to_json())
349            .collect::<std::result::Result<Vec<_>, _>>()
350            .map_err(S::Error::custom)?;
351
352        s.serialize_field("columns", &vec)?;
353        s.end()
354    }
355}
356
357/// merge multiple recordbatch into a single
358pub fn merge_record_batches(schema: SchemaRef, batches: &[RecordBatch]) -> Result<RecordBatch> {
359    let batches_len = batches.len();
360    if batches_len == 0 {
361        return Ok(RecordBatch::new_empty(schema));
362    }
363
364    let record_batch = compute::concat_batches(
365        schema.arrow_schema(),
366        batches.iter().map(|x| x.df_record_batch()),
367    )
368    .context(ArrowComputeSnafu)?;
369
370    // Create a new RecordBatch with merged columns
371    Ok(RecordBatch::from_df_record_batch(schema, record_batch))
372}
373
374#[cfg(test)]
375mod tests {
376    use std::sync::Arc;
377
378    use datatypes::arrow::array::{AsArray, StringArray, StringViewArray, UInt32Array};
379    use datatypes::arrow::datatypes::{DataType, Field, Schema as ArrowSchema, UInt32Type};
380    use datatypes::data_type::ConcreteDataType;
381    use datatypes::extension::json::JsonExtensionType;
382    use datatypes::schema::{ColumnSchema, Schema};
383    use datatypes::vectors::{BinaryVector, StringVector, UInt32Vector};
384
385    use super::*;
386
387    #[test]
388    fn test_record_batch() {
389        let arrow_schema = Arc::new(ArrowSchema::new(vec![
390            Field::new("c1", DataType::UInt32, false),
391            Field::new("c2", DataType::UInt32, false),
392        ]));
393        let schema = Arc::new(Schema::try_from(arrow_schema).unwrap());
394
395        let c1 = Arc::new(UInt32Vector::from_slice([1, 2, 3]));
396        let c2 = Arc::new(UInt32Vector::from_slice([4, 5, 6]));
397        let columns: Vec<VectorRef> = vec![c1, c2];
398
399        let expected = vec![
400            Arc::new(UInt32Array::from_iter_values([1, 2, 3])) as ArrayRef,
401            Arc::new(UInt32Array::from_iter_values([4, 5, 6])),
402        ];
403
404        let batch = RecordBatch::new(schema.clone(), columns.clone()).unwrap();
405        assert_eq!(3, batch.num_rows());
406        assert_eq!(expected, batch.df_record_batch().columns());
407        assert_eq!(schema, batch.schema);
408
409        assert_eq!(&expected[0], batch.column_by_name("c1").unwrap());
410        assert_eq!(&expected[1], batch.column_by_name("c2").unwrap());
411        assert!(batch.column_by_name("c3").is_none());
412
413        let converted = RecordBatch::from_df_record_batch(schema, batch.df_record_batch().clone());
414        assert_eq!(batch, converted);
415        assert_eq!(*batch.df_record_batch(), converted.into_df_record_batch());
416    }
417
418    #[test]
419    pub fn test_serialize_recordbatch() {
420        let column_schemas = vec![ColumnSchema::new(
421            "number",
422            ConcreteDataType::uint32_datatype(),
423            false,
424        )];
425        let schema = Arc::new(Schema::try_new(column_schemas).unwrap());
426
427        let numbers: Vec<u32> = (0..10).collect();
428        let columns = vec![Arc::new(UInt32Vector::from_slice(numbers)) as VectorRef];
429        let batch = RecordBatch::new(schema, columns).unwrap();
430
431        let output = serde_json::to_string(&batch).unwrap();
432        assert_eq!(
433            r#"{"schema":{"fields":[{"name":"number","data_type":"UInt32","nullable":false,"dict_id":0,"dict_is_ordered":false,"metadata":{}}],"metadata":{"greptime:version":"0"}},"columns":[[0,1,2,3,4,5,6,7,8,9]]}"#,
434            output
435        );
436    }
437
438    #[test]
439    fn test_record_batch_slice() {
440        let column_schemas = vec![
441            ColumnSchema::new("numbers", ConcreteDataType::uint32_datatype(), false),
442            ColumnSchema::new("strings", ConcreteDataType::string_datatype(), true),
443        ];
444        let schema = Arc::new(Schema::new(column_schemas));
445        let columns: Vec<VectorRef> = vec![
446            Arc::new(UInt32Vector::from_slice(vec![1, 2, 3, 4])),
447            Arc::new(StringVector::from(vec![
448                None,
449                Some("hello"),
450                Some("greptime"),
451                None,
452            ])),
453        ];
454        let recordbatch = RecordBatch::new(schema, columns).unwrap();
455        let recordbatch = recordbatch.slice(1, 2).expect("recordbatch slice");
456
457        let expected = &UInt32Array::from_iter_values([2u32, 3]);
458        let array = recordbatch.column(0);
459        let actual = array.as_primitive::<UInt32Type>();
460        assert_eq!(expected, actual);
461
462        let expected = &StringArray::from(vec!["hello", "greptime"]);
463        let array = recordbatch.column(1);
464        let actual = array.as_string::<i32>();
465        assert_eq!(expected, actual);
466
467        assert!(recordbatch.slice(1, 5).is_err());
468    }
469
470    #[test]
471    fn test_logical_slice_memory_size_for_visible_primitive_string_binary_slices() {
472        let schema = Arc::new(Schema::new(vec![
473            ColumnSchema::new("numbers", ConcreteDataType::uint32_datatype(), false),
474            ColumnSchema::new("strings", ConcreteDataType::string_datatype(), true),
475            ColumnSchema::new("binary", ConcreteDataType::binary_datatype(), true),
476        ]));
477        let numbers: Vec<_> = (0..1024).collect();
478        let strings = (0..1024)
479            .map(|value| (value % 3 != 0).then(|| format!("value-{value}")))
480            .collect::<Vec<_>>();
481        let binary = (0_u32..1024)
482            .map(|value| (value % 3 != 0).then(|| value.to_le_bytes().to_vec()))
483            .collect::<Vec<_>>();
484        let columns: Vec<VectorRef> = vec![
485            Arc::new(UInt32Vector::from_slice(numbers)),
486            Arc::new(StringVector::from(strings)),
487            Arc::new(BinaryVector::from(binary)),
488        ];
489        let batch = RecordBatch::new(schema, columns).unwrap();
490        let slice = batch.slice(511, 3).unwrap();
491
492        assert!(slice.columns().iter().any(|column| column.null_count() > 0));
493        assert!(slice.logical_slice_memory_size() < slice.buffer_memory_size());
494        assert_eq!(
495            slice.logical_slice_memory_size(),
496            slice
497                .columns()
498                .iter()
499                .map(|column| column.to_data().get_slice_memory_size().unwrap())
500                .sum::<usize>()
501        );
502    }
503
504    #[test]
505    fn test_logical_slice_memory_size_for_many_shared_buffer_slices() {
506        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
507            "strings",
508            ConcreteDataType::string_datatype(),
509            false,
510        )]));
511        let strings = (0..1024)
512            .map(|value| format!("shared-value-{value}"))
513            .collect::<Vec<_>>();
514        let backing = RecordBatch::new(
515            schema,
516            vec![Arc::new(StringVector::from(strings)) as VectorRef],
517        )
518        .unwrap();
519        let slices = (0..128)
520            .map(|index| backing.slice(index * 4, 2).unwrap())
521            .collect::<Vec<_>>();
522        let logical_total = slices
523            .iter()
524            .map(RecordBatch::logical_slice_memory_size)
525            .sum::<usize>();
526        let buffer_total = slices
527            .iter()
528            .map(RecordBatch::buffer_memory_size)
529            .sum::<usize>();
530
531        assert!(logical_total < buffer_total);
532        assert!(slices.iter().all(|slice| {
533            slice.logical_slice_memory_size()
534                == slice.column(0).to_data().get_slice_memory_size().unwrap()
535        }));
536    }
537
538    #[test]
539    fn test_logical_slice_memory_size_uses_arrow_slice_scope_for_views() {
540        let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
541            "strings",
542            ConcreteDataType::utf8_view_datatype(),
543            false,
544        )]));
545        let columns: Vec<VectorRef> =
546            vec![Arc::new(StringVector::from(StringViewArray::from(vec![
547                "unrelated backing payload",
548                "visible string view payload",
549            ])))];
550        let batch = RecordBatch::new(schema, columns)
551            .unwrap()
552            .slice(1, 1)
553            .unwrap();
554
555        assert_eq!(
556            batch.column(0).to_data().get_slice_memory_size().unwrap(),
557            batch.logical_slice_memory_size()
558        );
559    }
560
561    #[test]
562    fn test_merge_record_batch() {
563        let column_schemas = vec![
564            ColumnSchema::new("numbers", ConcreteDataType::uint32_datatype(), false),
565            ColumnSchema::new("strings", ConcreteDataType::string_datatype(), true),
566        ];
567        let schema = Arc::new(Schema::new(column_schemas));
568        let columns: Vec<VectorRef> = vec![
569            Arc::new(UInt32Vector::from_slice(vec![1, 2, 3, 4])),
570            Arc::new(StringVector::from(vec![
571                None,
572                Some("hello"),
573                Some("greptime"),
574                None,
575            ])),
576        ];
577        let recordbatch = RecordBatch::new(schema.clone(), columns).unwrap();
578
579        let columns: Vec<VectorRef> = vec![
580            Arc::new(UInt32Vector::from_slice(vec![1, 2, 3, 4])),
581            Arc::new(StringVector::from(vec![
582                None,
583                Some("hello"),
584                Some("greptime"),
585                None,
586            ])),
587        ];
588        let recordbatch2 = RecordBatch::new(schema.clone(), columns).unwrap();
589
590        let merged = merge_record_batches(schema.clone(), &[recordbatch, recordbatch2])
591            .expect("merge recordbatch");
592        assert_eq!(merged.num_rows(), 8);
593    }
594
595    #[test]
596    fn test_legacy_json_with_extension_does_not_align_as_structured_json() {
597        let field = Field::new("j", DataType::Binary, true).with_extension_type(JsonExtensionType);
598        let arrow_schema = Arc::new(ArrowSchema::new(vec![field]));
599        let schema = Arc::new(Schema::try_from(arrow_schema).unwrap());
600        let columns: Vec<VectorRef> = vec![Arc::new(BinaryVector::from(vec![Some(
601            br#"{"a":1}"#.to_vec(),
602        )]))];
603        let batch = RecordBatch::new(schema, columns).unwrap();
604        assert_eq!(batch.column(0).data_type(), &DataType::Binary);
605    }
606}