Skip to main content

orc_rust/
arrow_writer.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::io::Write;
19
20use arrow::{
21    array::RecordBatch,
22    datatypes::{DataType as ArrowDataType, SchemaRef},
23};
24use prost::Message;
25use snafu::{ensure, ResultExt};
26
27use crate::{
28    compression::{CompressionType, Compressor},
29    error::{IoSnafu, Result, UnexpectedSnafu},
30    memory::EstimateMemory,
31    proto,
32    writer::stripe::{StripeInformation, StripeWriter},
33};
34
35/// Construct an [`ArrowWriter`] to encode [`RecordBatch`]es into a single
36/// ORC file.
37pub struct ArrowWriterBuilder<W> {
38    writer: W,
39    schema: SchemaRef,
40    batch_size: usize,
41    stripe_byte_size: usize,
42    compression: Option<CompressionType>,
43    compression_block_size: Option<usize>,
44}
45
46impl<W: Write> ArrowWriterBuilder<W> {
47    /// Create a new [`ArrowWriterBuilder`], which will write an ORC file to
48    /// the provided writer, with the expected Arrow schema.
49    pub fn new(writer: W, schema: SchemaRef) -> Self {
50        Self {
51            writer,
52            schema,
53            batch_size: 1024,
54            // 64 MiB
55            stripe_byte_size: 64 * 1024 * 1024,
56            compression: None,
57            compression_block_size: None,
58        }
59    }
60
61    /// Batch size controls the encoding behaviour, where `batch_size` values
62    /// are encoded at a time. Default is `1024`.
63    pub fn with_batch_size(mut self, batch_size: usize) -> Self {
64        self.batch_size = batch_size;
65        self
66    }
67
68    /// The approximate size of stripes. Default is `64MiB`.
69    pub fn with_stripe_byte_size(mut self, stripe_byte_size: usize) -> Self {
70        self.stripe_byte_size = stripe_byte_size;
71        self
72    }
73
74    /// Set the compression codec for this ORC file.
75    pub fn with_compression(mut self, compression: CompressionType) -> Self {
76        self.compression = Some(compression);
77        self
78    }
79
80    /// Set the compression block size. Default is 256 KiB.
81    pub fn with_compression_block_size(mut self, block_size: usize) -> Self {
82        self.compression_block_size = Some(block_size);
83        self
84    }
85
86    /// Construct an [`ArrowWriter`] ready to encode [`RecordBatch`]es into
87    /// an ORC file.
88    pub fn try_build(mut self) -> Result<ArrowWriter<W>> {
89        let timezone = self
90            .schema
91            .fields()
92            .iter()
93            .any(|field| matches!(field.data_type(), ArrowDataType::Timestamp(_, None)))
94            .then(|| "UTC".to_owned());
95        let compressor = Compressor::new(self.compression, self.compression_block_size)?;
96        // Required magic "ORC" bytes at start of file
97        self.writer.write_all(b"ORC").context(IoSnafu)?;
98        let writer = StripeWriter::new(self.writer, &self.schema, compressor, timezone);
99        Ok(ArrowWriter {
100            writer,
101            schema: self.schema,
102            batch_size: self.batch_size,
103            stripe_byte_size: self.stripe_byte_size,
104            written_stripes: vec![],
105            // Accounting for the 3 magic bytes above
106            total_bytes_written: 3,
107        })
108    }
109}
110
111/// Encodes [`RecordBatch`]es into an ORC file. Will encode `batch_size` rows
112/// at a time into a stripe, flushing the stripe to the underlying writer when
113/// it's estimated memory footprint exceeds the configures `stripe_byte_size`.
114pub struct ArrowWriter<W> {
115    writer: StripeWriter<W>,
116    schema: SchemaRef,
117    batch_size: usize,
118    stripe_byte_size: usize,
119    written_stripes: Vec<StripeInformation>,
120    /// Used to keep track of progress in file so far (instead of needing Seek on the writer)
121    total_bytes_written: u64,
122}
123
124impl<W: Write> ArrowWriter<W> {
125    /// Encode the provided batch at `batch_size` rows at a time, flushing any
126    /// stripes that exceed the configured stripe size.
127    pub fn write(&mut self, batch: &RecordBatch) -> Result<()> {
128        ensure!(
129            batch.schema() == self.schema,
130            UnexpectedSnafu {
131                msg: "RecordBatch doesn't match expected schema"
132            }
133        );
134
135        for offset in (0..batch.num_rows()).step_by(self.batch_size) {
136            let length = self.batch_size.min(batch.num_rows() - offset);
137            let batch = batch.slice(offset, length);
138            self.writer.encode_batch(&batch)?;
139
140            // TODO: be able to flush whilst writing a batch (instead of between batches)
141            // Flush stripe when it exceeds estimated configured size
142            if self.writer.estimate_memory_size() > self.stripe_byte_size {
143                self.flush_stripe()?;
144            }
145        }
146        Ok(())
147    }
148
149    /// Flush any buffered data that hasn't been written, and write the stripe
150    /// footer metadata.
151    pub fn flush_stripe(&mut self) -> Result<()> {
152        let info = self.writer.finish_stripe(self.total_bytes_written)?;
153        self.total_bytes_written += info.total_byte_size();
154        self.written_stripes.push(info);
155        Ok(())
156    }
157
158    /// Flush the current stripe if it is still in progress, and write the tail
159    /// metadata and close the writer.
160    pub fn close(mut self) -> Result<()> {
161        // Flush in-progress stripe
162        if self.writer.row_count > 0 {
163            self.flush_stripe()?;
164        }
165        let footer = serialize_footer(&self.written_stripes, &self.schema).encode_to_vec();
166        let metadata = proto::Metadata::default().encode_to_vec();
167
168        let (mut writer, mut compressor) = self.writer.finish();
169        let metadata = compressor.compress(&metadata)?;
170        let footer = compressor.compress(&footer)?;
171
172        let postscript = serialize_postscript(
173            footer.len() as u64,
174            metadata.len() as u64,
175            compressor.compression(),
176            compressor.block_size().map(|size| size as u64),
177        )
178        .encode_to_vec();
179        let postscript_len = postscript.len() as u8;
180
181        writer.write_all(&metadata).context(IoSnafu)?;
182        writer.write_all(&footer).context(IoSnafu)?;
183        writer.write_all(&postscript).context(IoSnafu)?;
184        // Postscript length as last byte
185        writer.write_all(&[postscript_len]).context(IoSnafu)?;
186
187        // TODO: return file metadata
188        Ok(())
189    }
190}
191
192fn serialize_schema(schema: &SchemaRef) -> Vec<proto::Type> {
193    let mut types = vec![];
194
195    let field_names = schema
196        .fields()
197        .iter()
198        .map(|f| f.name().to_owned())
199        .collect();
200    // TODO: consider nested types
201    let subtypes = (1..(schema.fields().len() as u32 + 1)).collect();
202    let root_type = proto::Type {
203        kind: Some(proto::r#type::Kind::Struct.into()),
204        subtypes,
205        field_names,
206        maximum_length: None,
207        precision: None,
208        scale: None,
209        attributes: vec![],
210    };
211    types.push(root_type);
212    for field in schema.fields() {
213        let t = match field.data_type() {
214            ArrowDataType::Float32 => proto::Type {
215                kind: Some(proto::r#type::Kind::Float.into()),
216                ..Default::default()
217            },
218            ArrowDataType::Float64 => proto::Type {
219                kind: Some(proto::r#type::Kind::Double.into()),
220                ..Default::default()
221            },
222            ArrowDataType::Int8 => proto::Type {
223                kind: Some(proto::r#type::Kind::Byte.into()),
224                ..Default::default()
225            },
226            ArrowDataType::Int16 => proto::Type {
227                kind: Some(proto::r#type::Kind::Short.into()),
228                ..Default::default()
229            },
230            ArrowDataType::Int32 => proto::Type {
231                kind: Some(proto::r#type::Kind::Int.into()),
232                ..Default::default()
233            },
234            ArrowDataType::Int64 => proto::Type {
235                kind: Some(proto::r#type::Kind::Long.into()),
236                ..Default::default()
237            },
238            ArrowDataType::Date32 => proto::Type {
239                kind: Some(proto::r#type::Kind::Date.into()),
240                ..Default::default()
241            },
242            ArrowDataType::Timestamp(_, None) => proto::Type {
243                kind: Some(proto::r#type::Kind::Timestamp.into()),
244                ..Default::default()
245            },
246            ArrowDataType::Timestamp(_, Some(timezone)) if timezone.as_ref() == "UTC" => {
247                proto::Type {
248                    kind: Some(proto::r#type::Kind::TimestampInstant.into()),
249                    ..Default::default()
250                }
251            }
252            ArrowDataType::Utf8 | ArrowDataType::LargeUtf8 => proto::Type {
253                kind: Some(proto::r#type::Kind::String.into()),
254                ..Default::default()
255            },
256            ArrowDataType::Binary | ArrowDataType::LargeBinary => proto::Type {
257                kind: Some(proto::r#type::Kind::Binary.into()),
258                ..Default::default()
259            },
260            ArrowDataType::Boolean => proto::Type {
261                kind: Some(proto::r#type::Kind::Boolean.into()),
262                ..Default::default()
263            },
264            // TODO: support more types
265            _ => unimplemented!("unsupported datatype"),
266        };
267        types.push(t);
268    }
269    types
270}
271
272fn serialize_footer(stripes: &[StripeInformation], schema: &SchemaRef) -> proto::Footer {
273    let body_length = stripes
274        .iter()
275        .map(|s| s.index_length + s.data_length + s.footer_length)
276        .sum::<u64>();
277    let number_of_rows = stripes.iter().map(|s| s.row_count as u64).sum::<u64>();
278    let stripes = stripes.iter().map(From::from).collect();
279    let types = serialize_schema(schema);
280    proto::Footer {
281        header_length: Some(3),
282        content_length: Some(body_length + 3),
283        stripes,
284        types,
285        metadata: vec![],
286        number_of_rows: Some(number_of_rows),
287        statistics: vec![],
288        row_index_stride: None,
289        writer: Some(u32::MAX),
290        encryption: None,
291        calendar: None,
292        software_version: None,
293    }
294}
295
296fn serialize_postscript(
297    footer_length: u64,
298    metadata_length: u64,
299    compression: Option<CompressionType>,
300    compression_block_size: Option<u64>,
301) -> proto::PostScript {
302    proto::PostScript {
303        footer_length: Some(footer_length),
304        compression: Some(
305            compression
306                .map(CompressionType::to_proto)
307                .unwrap_or(proto::CompressionKind::None)
308                .into(),
309        ),
310        compression_block_size,
311        version: vec![0, 12],
312        metadata_length: Some(metadata_length),
313        writer_version: Some(u32::MAX), // TODO: check which version to use
314        stripe_statistics_length: None,
315        magic: Some("ORC".to_string()),
316    }
317}
318
319#[cfg(test)]
320mod tests {
321    use std::{io::Read, sync::Arc};
322
323    use arrow::{
324        array::{
325            Array, ArrayRef, BinaryArray, BooleanArray, Date32Array, Float32Array, Float64Array,
326            Int16Array, Int32Array, Int64Array, Int8Array, LargeBinaryArray, LargeStringArray,
327            RecordBatchReader, StringArray, TimestampMicrosecondArray, TimestampMillisecondArray,
328            TimestampNanosecondArray, TimestampSecondArray,
329        },
330        buffer::NullBuffer,
331        compute::concat_batches,
332        datatypes::{DataType as ArrowDataType, Field, Schema, TimeUnit},
333    };
334    use bytes::Bytes;
335    use prost::Message;
336
337    use crate::{
338        compression::{CompressionType, Decompressor},
339        schema::DataType as OrcDataType,
340        stripe::Stripe,
341        ArrowReaderBuilder,
342    };
343
344    use super::*;
345
346    fn encode(batches: &[RecordBatch]) -> Bytes {
347        let mut f = vec![];
348        let mut writer = ArrowWriterBuilder::new(&mut f, batches[0].schema())
349            .try_build()
350            .unwrap();
351        for batch in batches {
352            writer.write(batch).unwrap();
353        }
354        writer.close().unwrap();
355
356        Bytes::from(f)
357    }
358
359    fn roundtrip(batches: &[RecordBatch]) -> Vec<RecordBatch> {
360        let f = encode(batches);
361        let reader = ArrowReaderBuilder::try_new(f).unwrap().build();
362        reader.collect::<Result<Vec<_>, _>>().unwrap()
363    }
364
365    #[test]
366    fn test_roundtrip_write() {
367        let f32_array = Arc::new(Float32Array::from(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0]));
368        let f64_array = Arc::new(Float64Array::from(vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0]));
369        let int8_array = Arc::new(Int8Array::from(vec![0, 1, 2, 3, 4, 5, 6]));
370        let int16_array = Arc::new(Int16Array::from(vec![0, 1, 2, 3, 4, 5, 6]));
371        let int32_array = Arc::new(Int32Array::from(vec![0, 1, 2, 3, 4, 5, 6]));
372        let int64_array = Arc::new(Int64Array::from(vec![0, 1, 2, 3, 4, 5, 6]));
373        let utf8_array = Arc::new(StringArray::from(vec![
374            "Hello",
375            "there",
376            "楡井希実",
377            "💯",
378            "ORC",
379            "",
380            "123",
381        ]));
382        let binary_array = Arc::new(BinaryArray::from(vec![
383            "Hello".as_bytes(),
384            "there".as_bytes(),
385            "楡井希実".as_bytes(),
386            "💯".as_bytes(),
387            "ORC".as_bytes(),
388            "".as_bytes(),
389            "123".as_bytes(),
390        ]));
391        let boolean_array = Arc::new(BooleanArray::from(vec![
392            true, false, true, false, true, true, false,
393        ]));
394        let schema = Schema::new(vec![
395            Field::new("f32", ArrowDataType::Float32, false),
396            Field::new("f64", ArrowDataType::Float64, false),
397            Field::new("int8", ArrowDataType::Int8, false),
398            Field::new("int16", ArrowDataType::Int16, false),
399            Field::new("int32", ArrowDataType::Int32, false),
400            Field::new("int64", ArrowDataType::Int64, false),
401            Field::new("utf8", ArrowDataType::Utf8, false),
402            Field::new("binary", ArrowDataType::Binary, false),
403            Field::new("boolean", ArrowDataType::Boolean, false),
404        ]);
405
406        let batch = RecordBatch::try_new(
407            Arc::new(schema),
408            vec![
409                f32_array,
410                f64_array,
411                int8_array,
412                int16_array,
413                int32_array,
414                int64_array,
415                utf8_array,
416                binary_array,
417                boolean_array,
418            ],
419        )
420        .unwrap();
421
422        let rows = roundtrip(std::slice::from_ref(&batch));
423        assert_eq!(batch, rows[0]);
424    }
425
426    #[test]
427    fn test_roundtrip_write_large_type() {
428        let large_utf8_array = Arc::new(LargeStringArray::from(vec![
429            "Hello",
430            "there",
431            "楡井希実",
432            "💯",
433            "ORC",
434            "",
435            "123",
436        ]));
437        let large_binary_array = Arc::new(LargeBinaryArray::from(vec![
438            "Hello".as_bytes(),
439            "there".as_bytes(),
440            "楡井希実".as_bytes(),
441            "💯".as_bytes(),
442            "ORC".as_bytes(),
443            "".as_bytes(),
444            "123".as_bytes(),
445        ]));
446        let schema = Schema::new(vec![
447            Field::new("large_utf8", ArrowDataType::LargeUtf8, false),
448            Field::new("large_binary", ArrowDataType::LargeBinary, false),
449        ]);
450        let batch =
451            RecordBatch::try_new(Arc::new(schema), vec![large_utf8_array, large_binary_array])
452                .unwrap();
453
454        let rows = roundtrip(&[batch]);
455
456        // Currently we read all String/Binary columns from ORC as plain StringArray/BinaryArray
457        let utf8_array = Arc::new(StringArray::from(vec![
458            "Hello",
459            "there",
460            "楡井希実",
461            "💯",
462            "ORC",
463            "",
464            "123",
465        ]));
466        let binary_array = Arc::new(BinaryArray::from(vec![
467            "Hello".as_bytes(),
468            "there".as_bytes(),
469            "楡井希実".as_bytes(),
470            "💯".as_bytes(),
471            "ORC".as_bytes(),
472            "".as_bytes(),
473            "123".as_bytes(),
474        ]));
475        let schema = Schema::new(vec![
476            Field::new("large_utf8", ArrowDataType::Utf8, false),
477            Field::new("large_binary", ArrowDataType::Binary, false),
478        ]);
479        let batch = RecordBatch::try_new(Arc::new(schema), vec![utf8_array, binary_array]).unwrap();
480        assert_eq!(batch, rows[0]);
481    }
482
483    #[test]
484    fn test_roundtrip_write_date32() {
485        let schema = Arc::new(Schema::new(vec![Field::new(
486            "date",
487            ArrowDataType::Date32,
488            true,
489        )]));
490
491        let batch_without_nulls = RecordBatch::try_new(
492            schema.clone(),
493            vec![Arc::new(Date32Array::from(vec![-1, 0, 1, 11_016]))],
494        )
495        .unwrap();
496        let batch_with_nulls = RecordBatch::try_new(
497            schema.clone(),
498            vec![Arc::new(Date32Array::from(vec![
499                Some(18_321),
500                None,
501                Some(19_782),
502            ]))],
503        )
504        .unwrap();
505
506        let file = encode(&[batch_without_nulls, batch_with_nulls]);
507        let builder = ArrowReaderBuilder::try_new(file).unwrap();
508        assert!(matches!(
509            builder.file_metadata().root_data_type().children()[0].data_type(),
510            OrcDataType::Date { .. }
511        ));
512
513        let rows = builder.build().collect::<Result<Vec<_>, _>>().unwrap();
514        let actual = concat_batches(&schema, rows.iter()).unwrap();
515        let expected = RecordBatch::try_new(
516            schema,
517            vec![Arc::new(Date32Array::from(vec![
518                Some(-1),
519                Some(0),
520                Some(1),
521                Some(11_016),
522                Some(18_321),
523                None,
524                Some(19_782),
525            ]))],
526        )
527        .unwrap();
528
529        assert_eq!(expected, actual);
530    }
531
532    #[test]
533    fn test_roundtrip_write_timestamps() {
534        let second = vec![Some(-2), Some(-1), Some(0), None, Some(1), Some(2)];
535        let millisecond = vec![
536            Some(-1_001),
537            Some(-1_000),
538            Some(-1_999),
539            None,
540            Some(1_001),
541            Some(1_234),
542        ];
543        let microsecond = vec![
544            Some(-1_001_001),
545            Some(-1_000_000),
546            Some(-999_999),
547            None,
548            Some(1_001_001),
549            Some(1_234_567),
550        ];
551        let nanosecond = vec![
552            Some(-1_001_001_001),
553            Some(-1_000_000_000),
554            Some(-999_999_999),
555            None,
556            Some(1_001_001_001),
557            Some(1_234_567_890),
558        ];
559
560        let arrays: Vec<ArrayRef> = vec![
561            Arc::new(TimestampSecondArray::from(second.clone())),
562            Arc::new(TimestampMillisecondArray::from(millisecond.clone())),
563            Arc::new(TimestampMicrosecondArray::from(microsecond.clone())),
564            Arc::new(TimestampNanosecondArray::from(nanosecond.clone())),
565            Arc::new(TimestampSecondArray::from(second).with_timezone("UTC")),
566            Arc::new(TimestampMillisecondArray::from(millisecond).with_timezone("UTC")),
567            Arc::new(TimestampMicrosecondArray::from(microsecond).with_timezone("UTC")),
568            Arc::new(TimestampNanosecondArray::from(nanosecond).with_timezone("UTC")),
569        ];
570        let schema = Arc::new(Schema::new(
571            arrays
572                .iter()
573                .enumerate()
574                .map(|(index, array)| {
575                    Field::new(
576                        format!("timestamp_{index}"),
577                        array.data_type().clone(),
578                        true,
579                    )
580                })
581                .collect::<Vec<_>>(),
582        ));
583        let batch = RecordBatch::try_new(schema.clone(), arrays).unwrap();
584
585        let mut file = Vec::new();
586        let mut writer = ArrowWriterBuilder::new(&mut file, schema.clone())
587            .try_build()
588            .unwrap();
589        writer.write(&batch.slice(0, 3)).unwrap();
590        writer.flush_stripe().unwrap();
591        writer.write(&batch.slice(3, 3)).unwrap();
592        writer.close().unwrap();
593
594        let file = Bytes::from(file);
595        let builder = ArrowReaderBuilder::try_new(file.clone()).unwrap();
596        let children = builder.file_metadata().root_data_type().children();
597        for child in &children[..4] {
598            assert!(matches!(child.data_type(), OrcDataType::Timestamp { .. }));
599        }
600        for child in &children[4..] {
601            assert!(matches!(
602                child.data_type(),
603                OrcDataType::TimestampWithLocalTimezone { .. }
604            ));
605        }
606
607        let stripes = builder.file_metadata().stripe_metadatas();
608        assert_eq!(stripes.len(), 2);
609        for stripe in stripes {
610            let footer_start = stripe.footer_offset() as usize;
611            let footer_end = footer_start + stripe.footer_length() as usize;
612            let mut decoded_footer = Vec::new();
613            Decompressor::new(
614                file.slice(footer_start..footer_end),
615                builder.file_metadata().compression(),
616                Vec::new(),
617            )
618            .read_to_end(&mut decoded_footer)
619            .unwrap();
620            let footer = proto::StripeFooter::decode(decoded_footer.as_slice()).unwrap();
621            assert_eq!(footer.writer_timezone.as_deref(), Some("UTC"));
622            assert_eq!(
623                footer
624                    .streams
625                    .iter()
626                    .filter(|stream| stream.kind() == proto::stream::Kind::Secondary)
627                    .count(),
628                8
629            );
630        }
631
632        let rows = builder
633            .with_schema(schema.clone())
634            .build()
635            .collect::<Result<Vec<_>, _>>()
636            .unwrap();
637        let actual = concat_batches(&schema, rows.iter()).unwrap();
638        assert_eq!(batch, actual);
639    }
640
641    #[test]
642    fn test_write_timestamps_with_inconsistent_null_buffers() {
643        let schema = Arc::new(Schema::new(vec![Field::new(
644            "timestamp",
645            ArrowDataType::Timestamp(TimeUnit::Nanosecond, None),
646            true,
647        )]));
648
649        let without_nulls = Arc::new(TimestampNanosecondArray::from(vec![1, 2, 3]));
650        let with_nulls = Arc::new(TimestampNanosecondArray::from(vec![None, Some(4), None]));
651        assert!(without_nulls.nulls().is_none());
652        assert!(with_nulls.nulls().is_some());
653
654        let batch1 = RecordBatch::try_new(schema.clone(), vec![without_nulls]).unwrap();
655        let batch2 = RecordBatch::try_new(schema.clone(), vec![with_nulls]).unwrap();
656        let expected = RecordBatch::try_new(
657            schema,
658            vec![Arc::new(TimestampNanosecondArray::from(vec![
659                Some(1),
660                Some(2),
661                Some(3),
662                None,
663                Some(4),
664                None,
665            ]))],
666        )
667        .unwrap();
668
669        let rows = roundtrip(&[batch1, batch2]);
670        assert_eq!(expected, rows[0]);
671    }
672
673    #[test]
674    fn test_write_small_stripes() {
675        // Set small stripe size to ensure writing across multiple stripes works
676        let data: Vec<i64> = (0..1_000_000).collect();
677        let int64_array = Arc::new(Int64Array::from(data));
678        let schema = Schema::new(vec![Field::new("int64", ArrowDataType::Int64, true)]);
679
680        let batch = RecordBatch::try_new(Arc::new(schema), vec![int64_array]).unwrap();
681
682        let mut f = vec![];
683        let mut writer = ArrowWriterBuilder::new(&mut f, batch.schema())
684            .with_stripe_byte_size(256)
685            .try_build()
686            .unwrap();
687        writer.write(&batch).unwrap();
688        writer.close().unwrap();
689
690        let f = Bytes::from(f);
691        let reader = ArrowReaderBuilder::try_new(f).unwrap().build();
692        let schema = reader.schema();
693        // Current reader doesn't read a batch across stripe boundaries, so we expect
694        // more than one batch to prove multiple stripes are being written here
695        let rows = reader.collect::<Result<Vec<_>, _>>().unwrap();
696        assert!(
697            rows.len() > 1,
698            "must have written more than 1 stripe (each stripe read as separate recordbatch)"
699        );
700        let actual = concat_batches(&schema, rows.iter()).unwrap();
701        assert_eq!(batch, actual);
702    }
703
704    #[test]
705    fn test_write_inconsistent_null_buffers() {
706        // When writing arrays where null buffer can appear/disappear between writes
707        let schema = Arc::new(Schema::new(vec![Field::new(
708            "int64",
709            ArrowDataType::Int64,
710            true,
711        )]));
712
713        // Ensure first batch has array with no null buffer
714        let array_no_nulls = Arc::new(Int64Array::from(vec![1, 2, 3]));
715        assert!(array_no_nulls.nulls().is_none());
716        // But subsequent batch has array with null buffer
717        let array_with_nulls = Arc::new(Int64Array::from(vec![None, Some(4), None]));
718        assert!(array_with_nulls.nulls().is_some());
719
720        let batch1 = RecordBatch::try_new(schema.clone(), vec![array_no_nulls]).unwrap();
721        let batch2 = RecordBatch::try_new(schema.clone(), vec![array_with_nulls]).unwrap();
722
723        // ORC writer should be able to handle this gracefully
724        let expected_array = Arc::new(Int64Array::from(vec![
725            Some(1),
726            Some(2),
727            Some(3),
728            None,
729            Some(4),
730            None,
731        ]));
732        let expected_batch = RecordBatch::try_new(schema, vec![expected_array]).unwrap();
733
734        let rows = roundtrip(&[batch1, batch2]);
735        assert_eq!(expected_batch, rows[0]);
736    }
737
738    #[test]
739    fn test_empty_null_buffers() {
740        // Create an ORC file with present streams, but which have no nulls.
741        // When this file is read then the resulting Arrow arrays show have
742        // NO null buffer, even though there is a present stream.
743        let schema = Arc::new(Schema::new(vec![Field::new(
744            "int64",
745            ArrowDataType::Int64,
746            true,
747        )]));
748
749        // Array with null buffer but has no nulls
750        let array_empty_nulls = Arc::new(Int64Array::from_iter_values_with_nulls(
751            vec![1],
752            Some(NullBuffer::from_iter(vec![true])),
753        ));
754        assert!(array_empty_nulls.nulls().is_some());
755        assert!(array_empty_nulls.null_count() == 0);
756
757        let batch = RecordBatch::try_new(schema, vec![array_empty_nulls]).unwrap();
758
759        // Encoding to bytes
760        let mut f = vec![];
761        let mut writer = ArrowWriterBuilder::new(&mut f, batch.schema())
762            .try_build()
763            .unwrap();
764        writer.write(&batch).unwrap();
765        writer.close().unwrap();
766        let mut f = Bytes::from(f);
767        let builder = ArrowReaderBuilder::try_new(f.clone()).unwrap();
768
769        // Ensure the ORC file we wrote indeed has a present stream
770        let stripe = Stripe::new(
771            &mut f,
772            &builder.file_metadata,
773            builder.file_metadata().root_data_type(),
774            &builder.file_metadata().stripe_metadatas()[0],
775        )
776        .unwrap();
777        assert_eq!(stripe.columns().len(), 1);
778        // Make sure we're getting the right column
779        assert_eq!(stripe.columns()[0].name(), "int64");
780        // Then check present stream
781        let present_stream = stripe
782            .stream_map()
783            .get_opt(&stripe.columns()[0], proto::stream::Kind::Present);
784        assert!(present_stream.is_some());
785
786        // Decoding from bytes
787        let reader = builder.build();
788        let rows = reader.collect::<Result<Vec<_>, _>>().unwrap();
789
790        assert_eq!(rows.len(), 1);
791        assert_eq!(rows[0].num_columns(), 1);
792        // Ensure read array has no null buffer
793        assert!(rows[0].column(0).nulls().is_none());
794    }
795
796    fn write_file(
797        schema: SchemaRef,
798        batch: Option<&RecordBatch>,
799        compression: Option<CompressionType>,
800        block_size: Option<usize>,
801        stripe_byte_size: usize,
802    ) -> Bytes {
803        let mut file = Vec::new();
804        let mut builder =
805            ArrowWriterBuilder::new(&mut file, schema).with_stripe_byte_size(stripe_byte_size);
806        if let Some(compression) = compression {
807            builder = builder.with_compression(compression);
808        }
809        if let Some(block_size) = block_size {
810            builder = builder.with_compression_block_size(block_size);
811        }
812        let mut writer = builder.try_build().unwrap();
813        if let Some(batch) = batch {
814            writer.write(batch).unwrap();
815        }
816        writer.close().unwrap();
817        Bytes::from(file)
818    }
819
820    fn decode_postscript(file: &Bytes) -> proto::PostScript {
821        let postscript_len = file[file.len() - 1] as usize;
822        let postscript_start = file.len() - 1 - postscript_len;
823        proto::PostScript::decode(&file[postscript_start..file.len() - 1]).unwrap()
824    }
825
826    #[test]
827    fn test_writer_compression_modes_and_postscript() {
828        let values = (0..4096)
829            .map(|index| format!("compressible-value-{}", index % 8))
830            .collect::<Vec<_>>();
831        let array = Arc::new(StringArray::from(values));
832        let schema = Arc::new(Schema::new(vec![Field::new(
833            "value",
834            ArrowDataType::Utf8,
835            false,
836        )]));
837        let batch = RecordBatch::try_new(schema.clone(), vec![array]).unwrap();
838
839        for (compression, expected_kind, expected_block_size) in [
840            (None, proto::CompressionKind::None, None),
841            (
842                Some(CompressionType::Zlib),
843                proto::CompressionKind::Zlib,
844                Some(64),
845            ),
846            (
847                Some(CompressionType::Snappy),
848                proto::CompressionKind::Snappy,
849                Some(64),
850            ),
851            (
852                Some(CompressionType::Lz4),
853                proto::CompressionKind::Lz4,
854                Some(64),
855            ),
856            (
857                Some(CompressionType::Zstd),
858                proto::CompressionKind::Zstd,
859                Some(64),
860            ),
861        ] {
862            let block_size = compression.map(|_| 64);
863            let file = write_file(
864                schema.clone(),
865                Some(&batch),
866                compression,
867                block_size,
868                usize::MAX,
869            );
870            let postscript = decode_postscript(&file);
871            assert_eq!(postscript.compression(), expected_kind);
872            assert_eq!(postscript.compression_block_size, expected_block_size);
873            assert_eq!(postscript.metadata_length, Some(0));
874            assert!(postscript.footer_length.unwrap() > 0);
875
876            let reader = ArrowReaderBuilder::try_new(file).unwrap().build();
877            let rows = reader.collect::<Result<Vec<_>, _>>().unwrap();
878            assert_eq!(rows, vec![batch.clone()]);
879        }
880
881        let file = write_file(
882            schema,
883            None,
884            Some(CompressionType::Zstd),
885            Some(64),
886            usize::MAX,
887        );
888        let postscript = decode_postscript(&file);
889        assert_eq!(postscript.compression(), proto::CompressionKind::Zstd);
890        assert_eq!(postscript.compression_block_size, Some(64));
891        assert_eq!(postscript.metadata_length, Some(0));
892
893        let reader = ArrowReaderBuilder::try_new(file).unwrap();
894        assert_eq!(reader.file_metadata().number_of_rows(), 0);
895        assert!(reader.file_metadata().stripe_metadatas().is_empty());
896        assert!(reader
897            .build()
898            .collect::<Result<Vec<_>, _>>()
899            .unwrap()
900            .is_empty());
901    }
902
903    #[test]
904    fn test_writer_rejects_invalid_compression_configuration() {
905        let schema = Arc::new(Schema::new(vec![Field::new(
906            "value",
907            ArrowDataType::Int64,
908            false,
909        )]));
910
911        for compression in [None, Some(CompressionType::Zstd)] {
912            for block_size in [0, 1 << 23] {
913                let mut file = Vec::new();
914                let mut builder = ArrowWriterBuilder::new(&mut file, schema.clone())
915                    .with_compression_block_size(block_size);
916                if let Some(compression) = compression {
917                    builder = builder.with_compression(compression);
918                }
919                assert!(builder.try_build().is_err());
920                assert!(file.is_empty());
921            }
922        }
923
924        let mut file = Vec::new();
925        let result = ArrowWriterBuilder::new(&mut file, schema)
926            .with_compression(CompressionType::Lzo)
927            .try_build();
928        assert!(result.is_err());
929        assert!(file.is_empty());
930    }
931
932    #[test]
933    fn test_compressed_multi_stripe_physical_offsets() {
934        let data = (0..100_000).collect::<Vec<i64>>();
935        let schema = Arc::new(Schema::new(vec![Field::new(
936            "value",
937            ArrowDataType::Int64,
938            false,
939        )]));
940        let batch = RecordBatch::try_new(schema, vec![Arc::new(Int64Array::from(data))]).unwrap();
941        let file = write_file(
942            batch.schema(),
943            Some(&batch),
944            Some(CompressionType::Zstd),
945            Some(64),
946            256,
947        );
948
949        let builder = ArrowReaderBuilder::try_new(file.clone()).unwrap();
950        let stripes = builder.file_metadata().stripe_metadatas();
951        assert!(stripes.len() > 1);
952        assert_eq!(stripes[0].offset(), 3);
953        for pair in stripes.windows(2) {
954            let previous = &pair[0];
955            let next = &pair[1];
956            assert_eq!(
957                next.offset(),
958                previous.offset()
959                    + previous.index_length()
960                    + previous.data_length()
961                    + previous.footer_length()
962            );
963        }
964
965        for stripe in stripes {
966            let footer_start = stripe.footer_offset() as usize;
967            let footer_end = footer_start + stripe.footer_length() as usize;
968            let mut decoded_footer = Vec::new();
969            Decompressor::new(
970                file.slice(footer_start..footer_end),
971                builder.file_metadata().compression(),
972                Vec::new(),
973            )
974            .read_to_end(&mut decoded_footer)
975            .unwrap();
976            let footer = proto::StripeFooter::decode(decoded_footer.as_slice()).unwrap();
977            let stream_bytes = footer.streams.iter().map(|s| s.length()).sum::<u64>();
978            assert_eq!(stream_bytes, stripe.data_length());
979            assert_eq!(stripe.index_length(), 0);
980        }
981
982        let reader = builder.build();
983        let rows = reader.collect::<Result<Vec<_>, _>>().unwrap();
984        let actual = concat_batches(&batch.schema(), rows.iter()).unwrap();
985        assert_eq!(actual, batch);
986    }
987}