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