1use crate::compression::CompressionCodec;
24use crate::reader::Decoder;
25use crate::reader::block::{BlockDecoder, BlockDecoderState};
26use arrow_array::RecordBatch;
27use arrow_schema::{ArrowError, SchemaRef};
28use bytes::Bytes;
29use futures::future::BoxFuture;
30use futures::{FutureExt, Stream};
31use std::mem;
32use std::ops::Range;
33use std::pin::Pin;
34use std::task::{Context, Poll};
35
36mod async_file_reader;
37mod builder;
38mod spawn;
39
40pub use async_file_reader::AsyncFileReader;
41pub use builder::{ReaderBuilder, read_header_info};
42pub use spawn::SpawnedReader;
43
44#[cfg(feature = "object_store")]
45mod store;
46
47use crate::errors::AvroError;
48#[allow(deprecated)]
49#[cfg(feature = "object_store")]
50pub use store::AvroObjectReader;
51
52enum FetchNextBehaviour {
53 ReadSyncMarker,
55 DecodeVLQHeader,
57 ContinueDecoding,
59}
60
61enum ReaderState<R> {
62 InvalidState,
64 Idle { reader: R },
66 FetchingData {
68 future: BoxFuture<'static, Result<(R, Bytes), AvroError>>,
69 next_behaviour: FetchNextBehaviour,
70 },
71 DecodingBlock { data: Bytes, reader: R },
73 ReadingBatches {
75 data: Bytes,
76 block_data: Bytes,
77 remaining_in_block: usize,
78 reader: R,
79 },
80 Flushing,
83 Finished,
85}
86
87pub struct AsyncAvroFileReader<R> {
141 range: Range<u64>,
143 file_size: u64,
144
145 decoder: Decoder,
147 block_decoder: BlockDecoder,
148 codec: Option<CompressionCodec>,
149 sync_marker: [u8; 16],
150
151 reader_state: ReaderState<R>,
153 finishing_partial_block: bool,
154}
155
156impl<R> AsyncAvroFileReader<R> {
157 pub fn builder(reader: R, file_size: u64, batch_size: usize) -> ReaderBuilder<R> {
159 ReaderBuilder::new(reader, file_size, batch_size)
160 }
161
162 fn new(
163 range: Range<u64>,
164 file_size: u64,
165 decoder: Decoder,
166 codec: Option<CompressionCodec>,
167 sync_marker: [u8; 16],
168 reader_state: ReaderState<R>,
169 ) -> Self {
170 Self {
171 range,
172 file_size,
173
174 decoder,
175 block_decoder: Default::default(),
176 codec,
177 sync_marker,
178
179 reader_state,
180 finishing_partial_block: false,
181 }
182 }
183
184 pub fn schema(&self) -> SchemaRef {
188 self.decoder.schema()
189 }
190
191 fn remaining_block_range(&self) -> Result<Range<u64>, AvroError> {
195 let remaining = self.block_decoder.bytes_remaining() as u64
196 + match self.block_decoder.state() {
197 BlockDecoderState::Data => 16, BlockDecoderState::Sync => 0,
199 state => {
200 return Err(AvroError::General(format!(
201 "remaining_block_range called in unexpected state: {state:?}"
202 )));
203 }
204 };
205
206 let fetch_end = self.range.end + remaining;
207 if fetch_end > self.file_size {
208 return Err(AvroError::EOF(
209 "Avro block requires more bytes than what exists in the file".into(),
210 ));
211 }
212
213 Ok(self.range.end..fetch_end)
214 }
215
216 #[inline]
218 fn finish_with_error(
219 &mut self,
220 error: AvroError,
221 ) -> Poll<Option<Result<RecordBatch, AvroError>>> {
222 self.reader_state = ReaderState::Finished;
223 Poll::Ready(Some(Err(error)))
224 }
225
226 #[inline]
227 fn start_flushing(&mut self) {
228 self.reader_state = ReaderState::Flushing;
229 }
230
231 #[inline]
233 fn poll_flush(&mut self) -> Poll<Option<Result<RecordBatch, AvroError>>> {
234 match self.decoder.flush_block() {
235 Ok(Some(batch)) => {
236 self.reader_state = ReaderState::Flushing;
237 Poll::Ready(Some(Ok(batch)))
238 }
239 Ok(None) => {
240 self.reader_state = ReaderState::Finished;
241 Poll::Ready(None)
242 }
243 Err(e) => self.finish_with_error(e),
244 }
245 }
246}
247
248impl<R: AsyncFileReader + Unpin + 'static> AsyncAvroFileReader<R> {
249 async fn fetch_bytes(mut reader: R, range: Range<u64>) -> Result<(R, Bytes), AvroError> {
252 let data = reader.get_bytes(range).await?;
253 Ok((reader, data))
254 }
255
256 #[forbid(clippy::question_mark_used)]
257 fn read_next(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<RecordBatch, AvroError>>> {
258 loop {
259 match mem::replace(&mut self.reader_state, ReaderState::InvalidState) {
260 ReaderState::Idle { reader } => {
261 let range = self.range.clone();
262 if range.start >= range.end {
263 return self.finish_with_error(AvroError::InvalidArgument(format!(
264 "Invalid range specified for Avro file: start {} >= end {}, file_size: {}",
265 range.start, range.end, self.file_size
266 )));
267 }
268
269 let future = Self::fetch_bytes(reader, range).boxed();
270 self.reader_state = ReaderState::FetchingData {
271 future,
272 next_behaviour: FetchNextBehaviour::ReadSyncMarker,
273 };
274 }
275 ReaderState::FetchingData {
276 mut future,
277 next_behaviour,
278 } => {
279 let (reader, data_chunk) = match future.poll_unpin(cx) {
280 Poll::Ready(Ok(data)) => data,
281 Poll::Ready(Err(e)) => return self.finish_with_error(e),
282 Poll::Pending => {
283 self.reader_state = ReaderState::FetchingData {
284 future,
285 next_behaviour,
286 };
287 return Poll::Pending;
288 }
289 };
290
291 match next_behaviour {
292 FetchNextBehaviour::ReadSyncMarker => {
293 let sync_marker_pos = data_chunk
294 .windows(16)
295 .position(|slice| slice == self.sync_marker);
296 let block_start = match sync_marker_pos {
297 Some(pos) => pos + 16, None => {
299 self.reader_state = ReaderState::Finished;
301 return Poll::Ready(None);
302 }
303 };
304
305 self.reader_state = ReaderState::DecodingBlock {
306 reader,
307 data: data_chunk.slice(block_start..),
308 };
309 }
310 FetchNextBehaviour::DecodeVLQHeader => {
311 let mut data = data_chunk;
312
313 while !matches!(self.block_decoder.state(), BlockDecoderState::Data) {
315 if data.is_empty() {
316 return self.finish_with_error(AvroError::EOF(
317 "Unexpected EOF while reading Avro block header".into(),
318 ));
319 }
320 let consumed = match self.block_decoder.decode(&data[..1]) {
321 Ok(consumed) => consumed,
322 Err(e) => return self.finish_with_error(e),
323 };
324 if consumed == 0 {
325 return self.finish_with_error(AvroError::General(
326 "BlockDecoder failed to consume byte during VLQ header parsing"
327 .into(),
328 ));
329 }
330 data = data.slice(consumed..);
331 }
332
333 let bytes_remaining = self.block_decoder.bytes_remaining();
335 let data_to_use = data.slice(..data.len().min(bytes_remaining));
336 let consumed = match self.block_decoder.decode(&data_to_use) {
337 Ok(consumed) => consumed,
338 Err(e) => return self.finish_with_error(e),
339 };
340 if consumed != data_to_use.len() {
341 return self.finish_with_error(AvroError::General(
342 "BlockDecoder failed to consume all bytes after VLQ header parsing"
343 .into(),
344 ));
345 }
346
347 let range_to_fetch = match self.remaining_block_range() {
349 Ok(range) if range.is_empty() => {
350 self.reader_state = ReaderState::DecodingBlock {
352 reader,
353 data: Bytes::new(),
354 };
355 continue;
356 }
357 Ok(range) => range,
358 Err(e) => return self.finish_with_error(e),
359 };
360
361 let future = Self::fetch_bytes(reader, range_to_fetch).boxed();
362 self.reader_state = ReaderState::FetchingData {
363 future,
364 next_behaviour: FetchNextBehaviour::ContinueDecoding,
365 };
366 continue;
367 }
368 FetchNextBehaviour::ContinueDecoding => {
369 self.reader_state = ReaderState::DecodingBlock {
370 reader,
371 data: data_chunk,
372 };
373 }
374 }
375 }
376 ReaderState::InvalidState => {
377 return self.finish_with_error(AvroError::General(
378 "AsyncAvroFileReader in invalid state".into(),
379 ));
380 }
381 ReaderState::DecodingBlock { reader, mut data } => {
382 let consumed = match self.block_decoder.decode(&data) {
384 Ok(consumed) => consumed,
385 Err(e) => return self.finish_with_error(e),
386 };
387 data = data.slice(consumed..);
388
389 if let Some(block) = self.block_decoder.flush() {
391 if block.sync != self.sync_marker {
393 return self.finish_with_error(AvroError::ParseError(
394 "Avro block sync marker does not match file header".to_string(),
395 ));
396 }
397 let block_count = block.count;
398 let block_data = Bytes::from_owner(if let Some(ref codec) = self.codec {
399 match codec.decompress(&block.data) {
400 Ok(decompressed) => decompressed,
401 Err(e) => return self.finish_with_error(e),
402 }
403 } else {
404 block.data
405 });
406
407 self.reader_state = ReaderState::ReadingBatches {
409 reader,
410 data,
411 block_data,
412 remaining_in_block: block_count,
413 };
414 continue;
415 }
416
417 if !data.is_empty() {
419 return self.finish_with_error(AvroError::General(
420 "BlockDecoder failed to make progress decoding Avro block".into(),
421 ));
422 }
423
424 if matches!(self.block_decoder.state(), BlockDecoderState::Finished) {
425 self.finishing_partial_block = false;
427 self.start_flushing();
428 continue;
429 }
430
431 if self.finishing_partial_block {
434 return self.finish_with_error(AvroError::EOF(
435 "Unexpected EOF while reading last Avro block".into(),
436 ));
437 }
438
439 self.finishing_partial_block = true;
445
446 if matches!(
448 self.block_decoder.state(),
449 BlockDecoderState::Count | BlockDecoderState::Size
450 ) {
451 const MAX_VLQ_HEADER_SIZE: u64 = 20;
454 let fetch_end = (self.range.end + MAX_VLQ_HEADER_SIZE).min(self.file_size);
455
456 if fetch_end == self.range.end {
458 return self.finish_with_error(AvroError::EOF(
459 "Unexpected EOF while reading Avro block header".into(),
460 ));
461 }
462
463 let range_to_fetch = self.range.end..fetch_end;
464 self.range.end = fetch_end; let future = Self::fetch_bytes(reader, range_to_fetch).boxed();
467 self.reader_state = ReaderState::FetchingData {
468 future,
469 next_behaviour: FetchNextBehaviour::DecodeVLQHeader,
470 };
471 continue;
472 }
473
474 let range_to_fetch = match self.remaining_block_range() {
476 Ok(range) => range,
477 Err(e) => return self.finish_with_error(e),
478 };
479
480 let future = Self::fetch_bytes(reader, range_to_fetch).boxed();
481 self.reader_state = ReaderState::FetchingData {
482 future,
483 next_behaviour: FetchNextBehaviour::ContinueDecoding,
484 };
485 continue;
486 }
487 ReaderState::ReadingBatches {
488 reader,
489 data,
490 mut block_data,
491 mut remaining_in_block,
492 } => {
493 let (consumed, records_decoded) =
494 match self.decoder.decode_block(&block_data, remaining_in_block) {
495 Ok((consumed, records_decoded)) => (consumed, records_decoded),
496 Err(e) => return self.finish_with_error(e),
497 };
498
499 remaining_in_block -= records_decoded;
500
501 if remaining_in_block == 0 {
502 if data.is_empty() {
503 self.start_flushing();
505 } else {
506 self.reader_state = ReaderState::DecodingBlock { reader, data };
508 }
509 } else {
510 block_data = block_data.slice(consumed..);
512 self.reader_state = ReaderState::ReadingBatches {
513 reader,
514 data,
515 block_data,
516 remaining_in_block,
517 };
518 }
519
520 if self.decoder.batch_is_full() {
523 return match self.decoder.flush_block() {
524 Ok(Some(batch)) => Poll::Ready(Some(Ok(batch))),
525 Ok(None) => self.finish_with_error(AvroError::General(
526 "Decoder reported a full batch, but flush returned None".into(),
527 )),
528 Err(e) => self.finish_with_error(e),
529 };
530 }
531 }
532 ReaderState::Flushing => {
533 return self.poll_flush();
534 }
535 ReaderState::Finished => {
536 self.reader_state = ReaderState::Finished;
538 return Poll::Ready(None);
539 }
540 }
541 }
542 }
543}
544
545impl<R: AsyncFileReader + Unpin + 'static> Stream for AsyncAvroFileReader<R> {
547 type Item = Result<RecordBatch, ArrowError>;
548
549 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
550 self.read_next(cx).map_err(Into::into)
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use crate::codec::Tz;
558 use crate::schema::{
559 AVRO_NAME_METADATA_KEY, AVRO_NAMESPACE_METADATA_KEY, AvroSchema, SCHEMA_METADATA_KEY,
560 };
561 use arrow_array::cast::AsArray;
562 use arrow_array::types::{Int32Type, Int64Type};
563 use arrow_array::*;
564 use arrow_schema::{DataType, Field, Metadata, Schema, SchemaRef, TimeUnit};
565 use futures::{StreamExt, TryStreamExt};
566 use object_store::local::LocalFileSystem;
567 use object_store::path::Path;
568 use object_store::{ObjectStore, ObjectStoreExt};
569 use std::collections::HashMap;
570 use std::sync::Arc;
571
572 #[derive(Clone, Debug)]
575 struct ObjectStoreReader {
576 store: Arc<dyn ObjectStore>,
577 path: Path,
578 }
579
580 impl ObjectStoreReader {
581 fn new(store: Arc<dyn ObjectStore>, path: Path) -> Self {
582 Self { store, path }
583 }
584 }
585
586 impl AsyncFileReader for ObjectStoreReader {
587 fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes, AvroError>> {
588 async move {
589 self.store
590 .get_range(&self.path, range)
591 .await
592 .map_err(|e| AvroError::General(e.to_string()))
593 }
594 .boxed()
595 }
596
597 fn get_byte_ranges(
598 &mut self,
599 ranges: Vec<Range<u64>>,
600 ) -> BoxFuture<'_, Result<Vec<Bytes>, AvroError>> {
601 async move {
602 self.store
603 .get_ranges(&self.path, &ranges)
604 .await
605 .map_err(|e| AvroError::General(e.to_string()))
606 }
607 .boxed()
608 }
609 }
610
611 fn arrow_test_data(file: &str) -> String {
612 let base =
613 std::env::var("ARROW_TEST_DATA").unwrap_or_else(|_| "../testing/data".to_string());
614 format!("{}/{}", base, file)
615 }
616
617 fn get_alltypes_schema() -> SchemaRef {
618 get_alltypes_schema_with_tz("+00:00")
619 }
620
621 fn get_alltypes_schema_with_tz(tz_id: &str) -> SchemaRef {
622 let schema = Schema::new(vec![
623 Field::new("id", DataType::Int32, true),
624 Field::new("bool_col", DataType::Boolean, true),
625 Field::new("tinyint_col", DataType::Int32, true),
626 Field::new("smallint_col", DataType::Int32, true),
627 Field::new("int_col", DataType::Int32, true),
628 Field::new("bigint_col", DataType::Int64, true),
629 Field::new("float_col", DataType::Float32, true),
630 Field::new("double_col", DataType::Float64, true),
631 Field::new("date_string_col", DataType::Binary, true),
632 Field::new("string_col", DataType::Binary, true),
633 Field::new(
634 "timestamp_col",
635 DataType::Timestamp(TimeUnit::Microsecond, Some(tz_id.into())),
636 true,
637 ),
638 ])
639 .with_metadata(HashMap::from([(
640 SCHEMA_METADATA_KEY.into(),
641 r#"{
642 "type": "record",
643 "name": "topLevelRecord",
644 "fields": [
645 {
646 "name": "id",
647 "type": [
648 "int",
649 "null"
650 ]
651 },
652 {
653 "name": "bool_col",
654 "type": [
655 "boolean",
656 "null"
657 ]
658 },
659 {
660 "name": "tinyint_col",
661 "type": [
662 "int",
663 "null"
664 ]
665 },
666 {
667 "name": "smallint_col",
668 "type": [
669 "int",
670 "null"
671 ]
672 },
673 {
674 "name": "int_col",
675 "type": [
676 "int",
677 "null"
678 ]
679 },
680 {
681 "name": "bigint_col",
682 "type": [
683 "long",
684 "null"
685 ]
686 },
687 {
688 "name": "float_col",
689 "type": [
690 "float",
691 "null"
692 ]
693 },
694 {
695 "name": "double_col",
696 "type": [
697 "double",
698 "null"
699 ]
700 },
701 {
702 "name": "date_string_col",
703 "type": [
704 "bytes",
705 "null"
706 ]
707 },
708 {
709 "name": "string_col",
710 "type": [
711 "bytes",
712 "null"
713 ]
714 },
715 {
716 "name": "timestamp_col",
717 "type": [
718 {
719 "type": "long",
720 "logicalType": "timestamp-micros"
721 },
722 "null"
723 ]
724 }
725 ]
726}
727"#
728 .into(),
729 )]));
730 Arc::new(schema)
731 }
732
733 fn get_alltypes_with_nulls_schema() -> SchemaRef {
734 let schema = Schema::new(vec![
735 Field::new("string_col", DataType::Binary, true),
736 Field::new("int_col", DataType::Int32, true),
737 Field::new("bool_col", DataType::Boolean, true),
738 Field::new("bigint_col", DataType::Int64, true),
739 Field::new("float_col", DataType::Float32, true),
740 Field::new("double_col", DataType::Float64, true),
741 Field::new("bytes_col", DataType::Binary, true),
742 ])
743 .with_metadata(HashMap::from([(
744 SCHEMA_METADATA_KEY.into(),
745 r#"{
746 "type": "record",
747 "name": "topLevelRecord",
748 "fields": [
749 {
750 "name": "string_col",
751 "type": [
752 "null",
753 "string"
754 ],
755 "default": null
756 },
757 {
758 "name": "int_col",
759 "type": [
760 "null",
761 "int"
762 ],
763 "default": null
764 },
765 {
766 "name": "bool_col",
767 "type": [
768 "null",
769 "boolean"
770 ],
771 "default": null
772 },
773 {
774 "name": "bigint_col",
775 "type": [
776 "null",
777 "long"
778 ],
779 "default": null
780 },
781 {
782 "name": "float_col",
783 "type": [
784 "null",
785 "float"
786 ],
787 "default": null
788 },
789 {
790 "name": "double_col",
791 "type": [
792 "null",
793 "double"
794 ],
795 "default": null
796 },
797 {
798 "name": "bytes_col",
799 "type": [
800 "null",
801 "bytes"
802 ],
803 "default": null
804 }
805 ]
806}"#
807 .into(),
808 )]));
809
810 Arc::new(schema)
811 }
812
813 fn get_nested_records_schema() -> SchemaRef {
814 let schema = Schema::new(vec![
815 Field::new(
816 "f1",
817 DataType::Struct(
818 vec![
819 Field::new("f1_1", DataType::Utf8, false),
820 Field::new("f1_2", DataType::Int32, false),
821 Field::new(
822 "f1_3",
823 DataType::Struct(
824 vec![Field::new("f1_3_1", DataType::Float64, false)].into(),
825 ),
826 false,
827 )
828 .with_metadata(HashMap::from([
829 (AVRO_NAMESPACE_METADATA_KEY.to_owned(), "ns3".to_owned()),
830 (AVRO_NAME_METADATA_KEY.to_owned(), "record3".to_owned()),
831 ])),
832 ]
833 .into(),
834 ),
835 false,
836 )
837 .with_metadata(HashMap::from([
838 (AVRO_NAMESPACE_METADATA_KEY.to_owned(), "ns2".to_owned()),
839 (AVRO_NAME_METADATA_KEY.to_owned(), "record2".to_owned()),
840 ])),
841 Field::new(
842 "f2",
843 DataType::List(Arc::new(
844 Field::new(
845 "item",
846 DataType::Struct(
847 vec![
848 Field::new("f2_1", DataType::Boolean, false),
849 Field::new("f2_2", DataType::Float32, false),
850 ]
851 .into(),
852 ),
853 false,
854 )
855 .with_metadata(HashMap::from([
856 (AVRO_NAMESPACE_METADATA_KEY.to_owned(), "ns4".to_owned()),
857 (AVRO_NAME_METADATA_KEY.to_owned(), "record4".to_owned()),
858 ])),
859 )),
860 false,
861 ),
862 Field::new(
863 "f3",
864 DataType::Struct(vec![Field::new("f3_1", DataType::Utf8, false)].into()),
865 true,
866 )
867 .with_metadata(HashMap::from([
868 (AVRO_NAMESPACE_METADATA_KEY.to_owned(), "ns5".to_owned()),
869 (AVRO_NAME_METADATA_KEY.to_owned(), "record5".to_owned()),
870 ])),
871 Field::new(
872 "f4",
873 DataType::List(Arc::new(
874 Field::new(
875 "item",
876 DataType::Struct(vec![Field::new("f4_1", DataType::Int64, false)].into()),
877 true,
878 )
879 .with_metadata(HashMap::from([
880 (AVRO_NAMESPACE_METADATA_KEY.to_owned(), "ns6".to_owned()),
881 (AVRO_NAME_METADATA_KEY.to_owned(), "record6".to_owned()),
882 ])),
883 )),
884 false,
885 ),
886 ])
887 .with_metadata(HashMap::from([(
888 SCHEMA_METADATA_KEY.into(),
889 r#"{
890 "type": "record",
891 "namespace": "ns1",
892 "name": "record1",
893 "fields": [
894 {
895 "name": "f1",
896 "type": {
897 "type": "record",
898 "namespace": "ns2",
899 "name": "record2",
900 "fields": [
901 {
902 "name": "f1_1",
903 "type": "string"
904 },
905 {
906 "name": "f1_2",
907 "type": "int"
908 },
909 {
910 "name": "f1_3",
911 "type": {
912 "type": "record",
913 "namespace": "ns3",
914 "name": "record3",
915 "fields": [
916 {
917 "name": "f1_3_1",
918 "type": "double"
919 }
920 ]
921 }
922 }
923 ]
924 }
925 },
926 {
927 "name": "f2",
928 "type": {
929 "type": "array",
930 "items": {
931 "type": "record",
932 "namespace": "ns4",
933 "name": "record4",
934 "fields": [
935 {
936 "name": "f2_1",
937 "type": "boolean"
938 },
939 {
940 "name": "f2_2",
941 "type": "float"
942 }
943 ]
944 }
945 }
946 },
947 {
948 "name": "f3",
949 "type": [
950 "null",
951 {
952 "type": "record",
953 "namespace": "ns5",
954 "name": "record5",
955 "fields": [
956 {
957 "name": "f3_1",
958 "type": "string"
959 }
960 ]
961 }
962 ],
963 "default": null
964 },
965 {
966 "name": "f4",
967 "type": {
968 "type": "array",
969 "items": [
970 "null",
971 {
972 "type": "record",
973 "namespace": "ns6",
974 "name": "record6",
975 "fields": [
976 {
977 "name": "f4_1",
978 "type": "long"
979 }
980 ]
981 }
982 ]
983 }
984 }
985 ]
986}
987"#
988 .into(),
989 )]));
990
991 Arc::new(schema)
992 }
993
994 async fn read_async_file(
995 path: &str,
996 batch_size: usize,
997 range: Option<Range<u64>>,
998 schema: Option<SchemaRef>,
999 projection: Option<Vec<usize>>,
1000 ) -> Result<Vec<RecordBatch>, ArrowError> {
1001 let store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
1002 let location = Path::from_filesystem_path(path).unwrap();
1003
1004 let file_size = store.head(&location).await.unwrap().size;
1005
1006 let file_reader = ObjectStoreReader::new(store, location);
1007 let mut builder = AsyncAvroFileReader::builder(file_reader, file_size, batch_size);
1008
1009 if let Some(s) = schema {
1010 let reader_schema = AvroSchema::try_from(s.as_ref())?;
1011 builder = builder.with_reader_schema(reader_schema);
1012 }
1013
1014 if let Some(proj) = projection {
1015 builder = builder.with_projection(proj);
1016 }
1017
1018 if let Some(range) = range {
1019 builder = builder.with_range(range);
1020 }
1021
1022 let reader = builder.try_build().await?;
1023 reader.try_collect().await
1024 }
1025
1026 #[tokio::test]
1027 async fn test_full_file_read() {
1028 let file = arrow_test_data("avro/alltypes_plain.avro");
1029 let schema = get_alltypes_schema();
1030 let batches = read_async_file(&file, 1024, None, Some(schema), None)
1031 .await
1032 .unwrap();
1033 let batch = &batches[0];
1034
1035 assert_eq!(batch.num_rows(), 8);
1036 assert_eq!(batch.num_columns(), 11);
1037
1038 let id_array = batch
1039 .column(0)
1040 .as_any()
1041 .downcast_ref::<Int32Array>()
1042 .unwrap();
1043 assert_eq!(id_array.value(0), 4);
1044 assert_eq!(id_array.value(7), 1);
1045 }
1046
1047 #[tokio::test]
1048 async fn test_small_batch_size() {
1049 let file = arrow_test_data("avro/alltypes_plain.avro");
1050 let schema = get_alltypes_schema();
1051 let batches = read_async_file(&file, 2, None, Some(schema), None)
1052 .await
1053 .unwrap();
1054 assert_eq!(batches.len(), 4);
1055
1056 let batch = &batches[0];
1057
1058 assert_eq!(batch.num_rows(), 2);
1059 assert_eq!(batch.num_columns(), 11);
1060 }
1061
1062 #[tokio::test]
1063 async fn test_batch_size_one() {
1064 let file = arrow_test_data("avro/alltypes_plain.avro");
1065 let schema = get_alltypes_schema();
1066 let batches = read_async_file(&file, 1, None, Some(schema), None)
1067 .await
1068 .unwrap();
1069 let batch = &batches[0];
1070
1071 assert_eq!(batches.len(), 8);
1072 assert_eq!(batch.num_rows(), 1);
1073 }
1074
1075 #[tokio::test]
1076 async fn test_batch_size_larger_than_file() {
1077 let file = arrow_test_data("avro/alltypes_plain.avro");
1078 let schema = get_alltypes_schema();
1079 let batches = read_async_file(&file, 10000, None, Some(schema), None)
1080 .await
1081 .unwrap();
1082 let batch = &batches[0];
1083
1084 assert_eq!(batch.num_rows(), 8);
1085 }
1086
1087 #[tokio::test]
1088 async fn test_empty_range() {
1089 let file = arrow_test_data("avro/alltypes_plain.avro");
1090 let range = 100..100;
1091 let schema = get_alltypes_schema();
1092 let batches = read_async_file(&file, 1024, Some(range), Some(schema), None)
1093 .await
1094 .unwrap();
1095 assert_eq!(batches.len(), 0);
1096 }
1097
1098 #[tokio::test]
1099 async fn test_range_starting_at_zero() {
1100 let file = arrow_test_data("avro/alltypes_plain.avro");
1102 let store = Arc::new(LocalFileSystem::new());
1103 let location = Path::from_filesystem_path(&file).unwrap();
1104 let meta = store.head(&location).await.unwrap();
1105
1106 let range = 0..meta.size;
1107 let schema = get_alltypes_schema();
1108 let batches = read_async_file(&file, 1024, Some(range), Some(schema), None)
1109 .await
1110 .unwrap();
1111 let batch = &batches[0];
1112
1113 assert_eq!(batch.num_rows(), 8);
1114 }
1115
1116 #[tokio::test]
1117 async fn test_range_after_header() {
1118 let file = arrow_test_data("avro/alltypes_plain.avro");
1119 let store = Arc::new(LocalFileSystem::new());
1120 let location = Path::from_filesystem_path(&file).unwrap();
1121 let meta = store.head(&location).await.unwrap();
1122
1123 let range = 100..meta.size;
1124 let schema = get_alltypes_schema();
1125 let batches = read_async_file(&file, 1024, Some(range), Some(schema), None)
1126 .await
1127 .unwrap();
1128 let batch = &batches[0];
1129
1130 assert!(batch.num_rows() > 0);
1131 }
1132
1133 #[tokio::test]
1134 async fn test_block_sync_marker_mismatch_errors() {
1135 use tempfile::tempdir;
1136 let file = arrow_test_data("avro/alltypes_plain.avro");
1137 let mut bytes = std::fs::read(&file).unwrap();
1138 let last = bytes.len() - 1;
1140 bytes[last] ^= 0xFF;
1141 let dir = tempdir().unwrap();
1142 let path = dir.path().join("corrupt_sync.avro");
1143 std::fs::write(&path, bytes).unwrap();
1144 let schema = get_alltypes_schema();
1145 let err = read_async_file(path.to_str().unwrap(), 1024, None, Some(schema), None)
1146 .await
1147 .expect_err("corrupted block sync marker should fail the read");
1148 assert!(err.to_string().contains("sync marker"), "{err}");
1149 }
1150
1151 #[tokio::test]
1152 async fn test_range_no_sync_marker() {
1153 let file = arrow_test_data("avro/alltypes_plain.avro");
1155 let range = 50..150;
1156 let schema = get_alltypes_schema();
1157 let batches = read_async_file(&file, 1024, Some(range), Some(schema), None)
1158 .await
1159 .unwrap();
1160 assert_eq!(batches.len(), 0);
1161 }
1162
1163 #[tokio::test]
1164 async fn test_range_starting_mid_file() {
1165 let file = arrow_test_data("avro/alltypes_plain.avro");
1166
1167 let range = 700..768; let schema = get_alltypes_schema();
1169 let batches = read_async_file(&file, 1024, Some(range), Some(schema), None)
1170 .await
1171 .unwrap();
1172 assert_eq!(batches.len(), 0);
1173 }
1174
1175 #[tokio::test]
1176 async fn test_range_ending_at_file_size() {
1177 let file = arrow_test_data("avro/alltypes_plain.avro");
1178 let store = Arc::new(LocalFileSystem::new());
1179 let location = Path::from_filesystem_path(&file).unwrap();
1180 let meta = store.head(&location).await.unwrap();
1181
1182 let range = 200..meta.size;
1183 let schema = get_alltypes_schema();
1184 let batches = read_async_file(&file, 1024, Some(range), Some(schema), None)
1185 .await
1186 .unwrap();
1187 let batch = &batches[0];
1188
1189 assert_eq!(batch.num_rows(), 8);
1190 }
1191
1192 #[tokio::test]
1193 async fn test_incomplete_block_requires_fetch() {
1194 let file = arrow_test_data("avro/alltypes_plain.avro");
1196 let range = 0..1200;
1197 let schema = get_alltypes_schema();
1198 let batches = read_async_file(&file, 1024, Some(range), Some(schema), None)
1199 .await
1200 .unwrap();
1201 let batch = &batches[0];
1202
1203 assert_eq!(batch.num_rows(), 8)
1204 }
1205
1206 #[tokio::test]
1207 async fn test_partial_vlq_header_requires_fetch() {
1208 let file = arrow_test_data("avro/alltypes_plain.avro");
1210 let range = 16..676; let schema = get_alltypes_schema();
1212 let batches = read_async_file(&file, 1024, Some(range), Some(schema), None)
1213 .await
1214 .unwrap();
1215 let batch = &batches[0];
1216
1217 assert_eq!(batch.num_rows(), 8)
1218 }
1219
1220 #[cfg(feature = "snappy")]
1221 #[tokio::test]
1222 async fn test_snappy_compressed_with_range() {
1223 {
1224 let file = arrow_test_data("avro/alltypes_plain.snappy.avro");
1225 let store = Arc::new(LocalFileSystem::new());
1226 let location = Path::from_filesystem_path(&file).unwrap();
1227 let meta = store.head(&location).await.unwrap();
1228
1229 let range = 200..meta.size;
1230 let schema = get_alltypes_schema();
1231 let batches = read_async_file(&file, 1024, Some(range), Some(schema), None)
1232 .await
1233 .unwrap();
1234 let batch = &batches[0];
1235
1236 assert!(batch.num_rows() > 0);
1237 }
1238 }
1239
1240 #[tokio::test]
1241 async fn test_nulls() {
1242 let file = arrow_test_data("avro/alltypes_nulls_plain.avro");
1243 let schema = get_alltypes_with_nulls_schema();
1244 let batches = read_async_file(&file, 1024, None, Some(schema), None)
1245 .await
1246 .unwrap();
1247 let batch = &batches[0];
1248
1249 assert_eq!(batch.num_rows(), 1);
1250 for col in batch.columns() {
1251 assert!(col.is_null(0));
1252 }
1253 }
1254
1255 #[tokio::test]
1256 async fn test_nested_records() {
1257 let file = arrow_test_data("avro/nested_records.avro");
1258 let schema = get_nested_records_schema();
1259 let batches = read_async_file(&file, 1024, None, Some(schema), None)
1260 .await
1261 .unwrap();
1262 let batch = &batches[0];
1263
1264 assert_eq!(batch.num_rows(), 2);
1265 assert!(batch.num_columns() > 0);
1266 }
1267
1268 #[tokio::test]
1269 async fn test_stream_produces_multiple_batches() {
1270 let file = arrow_test_data("avro/alltypes_plain.avro");
1271 let store = Arc::new(LocalFileSystem::new());
1272 let location = Path::from_filesystem_path(&file).unwrap();
1273
1274 let file_size = store.head(&location).await.unwrap().size;
1275
1276 let file_reader = ObjectStoreReader::new(store, location);
1277 let schema = get_alltypes_schema();
1278 let reader_schema = AvroSchema::try_from(schema.as_ref()).unwrap();
1279 let reader = AsyncAvroFileReader::builder(
1280 file_reader,
1281 file_size,
1282 2, )
1284 .with_reader_schema(reader_schema)
1285 .try_build()
1286 .await
1287 .unwrap();
1288
1289 let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
1290
1291 assert!(batches.len() > 1);
1292 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
1293 assert_eq!(total_rows, 8);
1294 }
1295
1296 #[tokio::test]
1297 async fn test_stream_early_termination() {
1298 let file = arrow_test_data("avro/alltypes_plain.avro");
1299 let store = Arc::new(LocalFileSystem::new());
1300 let location = Path::from_filesystem_path(&file).unwrap();
1301
1302 let file_size = store.head(&location).await.unwrap().size;
1303
1304 let file_reader = ObjectStoreReader::new(store, location);
1305 let schema = get_alltypes_schema();
1306 let reader_schema = AvroSchema::try_from(schema.as_ref()).unwrap();
1307 let reader = AsyncAvroFileReader::builder(file_reader, file_size, 1)
1308 .with_reader_schema(reader_schema)
1309 .try_build()
1310 .await
1311 .unwrap();
1312
1313 let first_batch = reader.take(1).try_collect::<Vec<_>>().await.unwrap();
1314
1315 assert_eq!(first_batch.len(), 1);
1316 assert!(first_batch[0].num_rows() > 0);
1317 }
1318
1319 #[tokio::test]
1320 async fn test_various_batch_sizes() {
1321 let file = arrow_test_data("avro/alltypes_plain.avro");
1322
1323 for batch_size in [1, 2, 3, 5, 7, 11, 100] {
1324 let schema = get_alltypes_schema();
1325 let batches = read_async_file(&file, batch_size, None, Some(schema), None)
1326 .await
1327 .unwrap();
1328 let batch = &batches[0];
1329
1330 assert_eq!(
1332 batch.num_rows(),
1333 batch_size.min(8),
1334 "Failed with batch_size={}",
1335 batch_size
1336 );
1337 }
1338 }
1339
1340 #[tokio::test]
1341 async fn test_range_larger_than_file() {
1342 let file = arrow_test_data("avro/alltypes_plain.avro");
1343 let store = Arc::new(LocalFileSystem::new());
1344 let location = Path::from_filesystem_path(&file).unwrap();
1345 let meta = store.head(&location).await.unwrap();
1346
1347 let range = 100..(meta.size + 1000);
1349 let schema = get_alltypes_schema();
1350 let batches = read_async_file(&file, 1024, Some(range), Some(schema), None)
1351 .await
1352 .unwrap();
1353 let batch = &batches[0];
1354
1355 assert_eq!(batch.num_rows(), 8);
1357 }
1358
1359 #[tokio::test]
1360 async fn test_builder_with_header_info() {
1361 let file = arrow_test_data("avro/alltypes_plain.avro");
1362 let store = Arc::new(LocalFileSystem::new());
1363 let location = Path::from_filesystem_path(&file).unwrap();
1364
1365 let file_size = store.head(&location).await.unwrap().size;
1366
1367 let mut file_reader = ObjectStoreReader::new(store, location);
1368
1369 let header_info = read_header_info(&mut file_reader, file_size, None)
1370 .await
1371 .unwrap();
1372
1373 assert_eq!(header_info.header_len(), 675);
1374
1375 let writer_schema = header_info.writer_schema().unwrap();
1376 let expected_avro_json: serde_json::Value = serde_json::from_str(
1377 get_alltypes_schema()
1378 .metadata()
1379 .get(SCHEMA_METADATA_KEY)
1380 .unwrap(),
1381 )
1382 .unwrap();
1383 let actual_avro_json: serde_json::Value =
1384 serde_json::from_str(&writer_schema.json_string).unwrap();
1385 assert_eq!(actual_avro_json, expected_avro_json);
1386
1387 let reader = AsyncAvroFileReader::builder(file_reader, file_size, 1024)
1388 .build_with_header(header_info)
1389 .unwrap();
1390
1391 let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
1392
1393 let batch = &batches[0];
1394 assert_eq!(batch.num_rows(), 8)
1395 }
1396
1397 #[tokio::test]
1398 async fn test_roundtrip_write_then_async_read() {
1399 use crate::writer::AvroWriter;
1400 use arrow_array::{Float64Array, StringArray};
1401 use std::fs::File;
1402 use std::io::BufWriter;
1403 use tempfile::tempdir;
1404
1405 let schema = Arc::new(Schema::new(vec![
1407 Field::new("id", DataType::Int32, false),
1408 Field::new("name", DataType::Utf8, true),
1409 Field::new("score", DataType::Float64, true),
1410 Field::new("count", DataType::Int64, false),
1411 ]));
1412
1413 let dir = tempdir().unwrap();
1414 let file_path = dir.path().join("roundtrip_test.avro");
1415
1416 {
1418 let file = File::create(&file_path).unwrap();
1419 let writer = BufWriter::new(file);
1420 let mut avro_writer = AvroWriter::new(writer, schema.as_ref().clone()).unwrap();
1421
1422 let batch1 = RecordBatch::try_new(
1424 schema.clone(),
1425 vec![
1426 Arc::new(Int32Array::from(vec![1, 2, 3])),
1427 Arc::new(StringArray::from(vec![
1428 Some("alice"),
1429 None,
1430 Some("charlie"),
1431 ])),
1432 Arc::new(Float64Array::from(vec![Some(95.5), Some(87.3), None])),
1433 Arc::new(Int64Array::from(vec![10, 20, 30])),
1434 ],
1435 )
1436 .unwrap();
1437 avro_writer.write(&batch1).unwrap();
1438
1439 let batch2 = RecordBatch::try_new(
1441 schema.clone(),
1442 vec![
1443 Arc::new(Int32Array::from(vec![4, 5])),
1444 Arc::new(StringArray::from(vec![Some("diana"), Some("eve")])),
1445 Arc::new(Float64Array::from(vec![None, Some(88.0)])),
1446 Arc::new(Int64Array::from(vec![40, 50])),
1447 ],
1448 )
1449 .unwrap();
1450 avro_writer.write(&batch2).unwrap();
1451
1452 avro_writer.finish().unwrap();
1453 }
1454
1455 let store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
1457 let location = Path::from_filesystem_path(&file_path).unwrap();
1458 let file_size = store.head(&location).await.unwrap().size;
1459
1460 let file_reader = ObjectStoreReader::new(store, location);
1461 let reader = AsyncAvroFileReader::builder(file_reader, file_size, 2)
1462 .try_build()
1463 .await
1464 .unwrap();
1465
1466 let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
1467
1468 assert!(
1470 batches.len() > 1,
1471 "Expected multiple batches with batch_size=2"
1472 );
1473
1474 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
1476 assert_eq!(total_rows, 5);
1477
1478 let combined = arrow::compute::concat_batches(&batches[0].schema(), &batches).unwrap();
1480 assert_eq!(combined.num_rows(), 5);
1481 assert_eq!(combined.num_columns(), 4);
1482
1483 let id_array = combined
1485 .column(0)
1486 .as_any()
1487 .downcast_ref::<Int32Array>()
1488 .unwrap();
1489 assert_eq!(id_array.values(), &[1, 2, 3, 4, 5]);
1490
1491 let name_col = combined.column(1);
1494 let name_array = name_col.as_string::<i32>();
1495 assert_eq!(name_array.value(0), "alice");
1496 assert!(name_col.is_null(1)); assert_eq!(name_array.value(2), "charlie");
1498
1499 let score_array = combined
1501 .column(2)
1502 .as_any()
1503 .downcast_ref::<Float64Array>()
1504 .unwrap();
1505 assert!(!score_array.is_null(0));
1506 assert!((score_array.value(0) - 95.5).abs() < f64::EPSILON);
1507 assert!(score_array.is_null(2)); assert!(score_array.is_null(3)); assert!(!score_array.is_null(4));
1510 assert!((score_array.value(4) - 88.0).abs() < f64::EPSILON);
1511
1512 let count_array = combined
1514 .column(3)
1515 .as_any()
1516 .downcast_ref::<Int64Array>()
1517 .unwrap();
1518 assert_eq!(count_array.values(), &[10, 20, 30, 40, 50]);
1519 }
1520
1521 #[tokio::test]
1522 async fn test_alltypes_no_schema_no_projection() {
1523 let file = arrow_test_data("avro/alltypes_plain.avro");
1525 let batches = read_async_file(&file, 1024, None, None, None)
1526 .await
1527 .unwrap();
1528 let batch = &batches[0];
1529
1530 assert_eq!(batch.num_rows(), 8);
1531 assert_eq!(batch.num_columns(), 11);
1532 assert_eq!(batch.schema().field(0).name(), "id");
1533 }
1534
1535 #[tokio::test]
1536 async fn test_alltypes_no_schema_with_projection() {
1537 let file = arrow_test_data("avro/alltypes_plain.avro");
1539 let batches = read_async_file(&file, 1024, None, None, Some(vec![2, 0, 5]))
1541 .await
1542 .unwrap();
1543 let batch = &batches[0];
1544
1545 assert_eq!(batch.num_rows(), 8);
1546 assert_eq!(batch.num_columns(), 3);
1547 assert_eq!(batch.schema().field(0).name(), "tinyint_col");
1548 assert_eq!(batch.schema().field(1).name(), "id");
1549 assert_eq!(batch.schema().field(2).name(), "bigint_col");
1550
1551 let tinyint_col = batch.column(0).as_primitive::<Int32Type>();
1553 assert_eq!(tinyint_col.values(), &[0, 1, 0, 1, 0, 1, 0, 1]);
1554
1555 let id = batch.column(1).as_primitive::<Int32Type>();
1556 assert_eq!(id.values(), &[4, 5, 6, 7, 2, 3, 0, 1]);
1557
1558 let bigint_col = batch.column(2).as_primitive::<Int64Type>();
1559 assert_eq!(bigint_col.values(), &[0, 10, 0, 10, 0, 10, 0, 10]);
1560 }
1561
1562 #[tokio::test]
1563 async fn test_alltypes_with_schema_no_projection() {
1564 let file = arrow_test_data("avro/alltypes_plain.avro");
1566 let schema = get_alltypes_schema();
1567 let batches = read_async_file(&file, 1024, None, Some(schema), None)
1568 .await
1569 .unwrap();
1570 let batch = &batches[0];
1571
1572 assert_eq!(batch.num_rows(), 8);
1573 assert_eq!(batch.num_columns(), 11);
1574 }
1575
1576 #[tokio::test]
1577 async fn test_alltypes_with_schema_with_projection() {
1578 let file = arrow_test_data("avro/alltypes_plain.avro");
1580 let schema = get_alltypes_schema();
1581 let batches = read_async_file(&file, 1024, None, Some(schema), Some(vec![1, 0]))
1583 .await
1584 .unwrap();
1585 let batch = &batches[0];
1586
1587 assert_eq!(batch.num_rows(), 8);
1588 assert_eq!(batch.num_columns(), 2);
1589 assert_eq!(batch.schema().field(0).name(), "bool_col");
1590 assert_eq!(batch.schema().field(1).name(), "id");
1591
1592 let bool_col = batch.column(0).as_boolean();
1593 assert!(bool_col.value(0));
1594 assert!(!bool_col.value(1));
1595
1596 let id = batch.column(1).as_primitive::<Int32Type>();
1597 assert_eq!(id.values(), &[4, 5, 6, 7, 2, 3, 0, 1]);
1598 }
1599
1600 #[tokio::test]
1601 async fn test_alltypes_with_empty_schema_large_batch() {
1602 let file = arrow_test_data("avro/alltypes_plain.avro");
1604 let schema = Arc::new(Schema::new(Vec::<Field>::new()));
1605 let batches = read_async_file(&file, 1024, None, Some(schema), None)
1606 .await
1607 .unwrap();
1608 assert_eq!(batches.len(), 1);
1609 let batch = &batches[0];
1610
1611 assert_eq!(batch.num_rows(), 8);
1612 assert_eq!(batch.num_columns(), 0);
1613 }
1614
1615 #[tokio::test]
1616 async fn test_alltypes_with_empty_schema_small_batch() {
1617 let file = arrow_test_data("avro/alltypes_plain.avro");
1619 let schema = Arc::new(Schema::new(Vec::<Field>::new()));
1620 let batches = read_async_file(&file, 5, None, Some(schema), None)
1621 .await
1622 .unwrap();
1623
1624 assert_eq!(batches.len(), 2);
1625
1626 assert_eq!(batches[0].num_rows(), 5);
1627 assert_eq!(batches[0].num_columns(), 0);
1628 assert_eq!(batches[1].num_rows(), 3);
1629 assert_eq!(batches[1].num_columns(), 0);
1630 }
1631
1632 #[tokio::test]
1633 async fn test_nested_no_schema_no_projection() {
1634 let file = arrow_test_data("avro/nested_records.avro");
1636 let batches = read_async_file(&file, 1024, None, None, None)
1637 .await
1638 .unwrap();
1639 let batch = &batches[0];
1640
1641 assert_eq!(batch.num_rows(), 2);
1642 assert_eq!(batch.num_columns(), 4);
1643 assert_eq!(batch.schema().field(0).name(), "f1");
1644 assert_eq!(batch.schema().field(1).name(), "f2");
1645 assert_eq!(batch.schema().field(2).name(), "f3");
1646 assert_eq!(batch.schema().field(3).name(), "f4");
1647 }
1648
1649 #[tokio::test]
1650 async fn test_nested_no_schema_with_projection() {
1651 let file = arrow_test_data("avro/nested_records.avro");
1653 let batches = read_async_file(&file, 1024, None, None, Some(vec![2, 0]))
1655 .await
1656 .unwrap();
1657 let batch = &batches[0];
1658
1659 assert_eq!(batch.num_rows(), 2);
1660 assert_eq!(batch.num_columns(), 2);
1661 assert_eq!(batch.schema().field(0).name(), "f3");
1662 assert_eq!(batch.schema().field(1).name(), "f1");
1663 }
1664
1665 #[tokio::test]
1666 async fn test_nested_with_schema_no_projection() {
1667 let file = arrow_test_data("avro/nested_records.avro");
1669 let schema = get_nested_records_schema();
1670 let batches = read_async_file(&file, 1024, None, Some(schema), None)
1671 .await
1672 .unwrap();
1673 let batch = &batches[0];
1674
1675 assert_eq!(batch.num_rows(), 2);
1676 assert_eq!(batch.num_columns(), 4);
1677 }
1678
1679 #[tokio::test]
1680 async fn test_nested_with_schema_with_projection() {
1681 let file = arrow_test_data("avro/nested_records.avro");
1683 let schema = get_nested_records_schema();
1684 let batches = read_async_file(&file, 1024, None, Some(schema), Some(vec![3, 1, 0]))
1686 .await
1687 .unwrap();
1688 let batch = &batches[0];
1689
1690 assert_eq!(batch.num_rows(), 2);
1691 assert_eq!(batch.num_columns(), 3);
1692 assert_eq!(batch.schema().field(0).name(), "f4");
1693 assert_eq!(batch.schema().field(1).name(), "f2");
1694 assert_eq!(batch.schema().field(2).name(), "f1");
1695 }
1696
1697 #[tokio::test]
1698 async fn test_nested_with_empty_schema() {
1699 let file = arrow_test_data("avro/nested_records.avro");
1701 let schema = Arc::new(
1702 Schema::new(Vec::<Field>::new()).with_metadata(HashMap::from([(
1703 SCHEMA_METADATA_KEY.into(),
1704 r#"{
1705 "type": "record",
1706 "namespace": "ns1",
1707 "name": "record1",
1708 "fields": []
1709 }"#
1710 .to_owned(),
1711 )])),
1712 );
1713 let batches = read_async_file(&file, 1024, None, Some(schema), None)
1714 .await
1715 .unwrap();
1716 let batch = &batches[0];
1717
1718 assert_eq!(batch.num_rows(), 2);
1719 assert_eq!(batch.num_columns(), 0);
1720 }
1721
1722 #[tokio::test]
1723 async fn test_projection_error_out_of_bounds() {
1724 let file = arrow_test_data("avro/alltypes_plain.avro");
1725 let err = read_async_file(&file, 1024, None, None, Some(vec![100]))
1727 .await
1728 .unwrap_err();
1729 assert!(matches!(err, ArrowError::AvroError(_)));
1730 assert!(err.to_string().contains("out of bounds"));
1731 }
1732
1733 #[tokio::test]
1734 async fn test_projection_error_duplicate_index() {
1735 let file = arrow_test_data("avro/alltypes_plain.avro");
1736 let err = read_async_file(&file, 1024, None, None, Some(vec![0, 0]))
1738 .await
1739 .unwrap_err();
1740 assert!(matches!(err, ArrowError::AvroError(_)));
1741 assert!(err.to_string().contains("Duplicate projection index"));
1742 }
1743
1744 #[tokio::test]
1745 async fn test_arrow_schema_from_reader_no_reader_schema() {
1746 let file = arrow_test_data("avro/alltypes_plain.avro");
1747 let store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
1748 let location = Path::from_filesystem_path(&file).unwrap();
1749 let file_size = store.head(&location).await.unwrap().size;
1750
1751 let file_reader = ObjectStoreReader::new(store, location);
1752 let expected_schema = get_alltypes_schema()
1753 .as_ref()
1754 .clone()
1755 .with_metadata(Metadata::default());
1756
1757 let reader = AsyncAvroFileReader::builder(file_reader, file_size, 1024)
1759 .try_build()
1760 .await
1761 .unwrap();
1762
1763 assert_eq!(reader.schema().as_ref(), &expected_schema);
1764
1765 let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
1766 let batch = &batches[0];
1767
1768 assert_eq!(batch.schema().as_ref(), &expected_schema);
1769 }
1770
1771 #[tokio::test]
1772 async fn test_arrow_schema_from_reader_with_reader_schema() {
1773 let file = arrow_test_data("avro/alltypes_plain.avro");
1774 let store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
1775 let location = Path::from_filesystem_path(&file).unwrap();
1776 let file_size = store.head(&location).await.unwrap().size;
1777
1778 let file_reader = ObjectStoreReader::new(store, location);
1779 let schema = get_alltypes_schema()
1780 .project(&[0, 1, 7])
1781 .unwrap()
1782 .with_metadata(Metadata::default());
1783 let reader_schema = AvroSchema::try_from(&schema).unwrap();
1784 let expected_schema = schema.clone();
1785
1786 let reader = AsyncAvroFileReader::builder(file_reader, file_size, 1024)
1788 .with_reader_schema(reader_schema)
1789 .try_build()
1790 .await
1791 .unwrap();
1792
1793 assert_eq!(reader.schema().as_ref(), &expected_schema);
1794
1795 let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
1796 let batch = &batches[0];
1797
1798 assert_eq!(batch.schema().as_ref(), &expected_schema);
1799 }
1800
1801 #[tokio::test]
1802 async fn test_arrow_schema_from_reader_nested_records() {
1803 let file = arrow_test_data("avro/nested_records.avro");
1804 let store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
1805 let location = Path::from_filesystem_path(&file).unwrap();
1806 let file_size = store.head(&location).await.unwrap().size;
1807
1808 let file_reader = ObjectStoreReader::new(store, location);
1809
1810 let expected_schema = get_nested_records_schema()
1813 .as_ref()
1814 .clone()
1815 .with_metadata(Metadata::default());
1816
1817 let reader = AsyncAvroFileReader::builder(file_reader, file_size, 1024)
1818 .try_build()
1819 .await
1820 .unwrap();
1821
1822 assert_eq!(reader.schema().as_ref(), &expected_schema);
1823
1824 let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
1825 let batch = &batches[0];
1826
1827 assert_eq!(batch.schema().as_ref(), &expected_schema);
1828 }
1829
1830 #[tokio::test]
1831 async fn test_with_header_size_hint_small() {
1832 let file = arrow_test_data("avro/alltypes_plain.avro");
1834 let store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
1835 let location = Path::from_filesystem_path(&file).unwrap();
1836 let file_size = store.head(&location).await.unwrap().size;
1837
1838 let file_reader = ObjectStoreReader::new(store, location);
1839 let schema = get_alltypes_schema();
1840 let reader_schema = AvroSchema::try_from(schema.as_ref()).unwrap();
1841
1842 let reader = AsyncAvroFileReader::builder(file_reader, file_size, 1024)
1844 .with_reader_schema(reader_schema)
1845 .with_header_size_hint(64)
1846 .try_build()
1847 .await
1848 .unwrap();
1849
1850 let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
1851 let batch = &batches[0];
1852
1853 assert_eq!(batch.num_rows(), 8);
1854 assert_eq!(batch.num_columns(), 11);
1855 }
1856
1857 #[tokio::test]
1858 async fn test_with_header_size_hint_large() {
1859 let file = arrow_test_data("avro/alltypes_plain.avro");
1861 let store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
1862 let location = Path::from_filesystem_path(&file).unwrap();
1863 let file_size = store.head(&location).await.unwrap().size;
1864
1865 let file_reader = ObjectStoreReader::new(store, location);
1866 let schema = get_alltypes_schema();
1867 let reader_schema = AvroSchema::try_from(schema.as_ref()).unwrap();
1868
1869 let reader = AsyncAvroFileReader::builder(file_reader, file_size, 1024)
1871 .with_reader_schema(reader_schema)
1872 .with_header_size_hint(64 * 1024)
1873 .try_build()
1874 .await
1875 .unwrap();
1876
1877 let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
1878 let batch = &batches[0];
1879
1880 assert_eq!(batch.num_rows(), 8);
1881 assert_eq!(batch.num_columns(), 11);
1882 }
1883
1884 #[tokio::test]
1885 async fn test_with_tz_utc() {
1886 let file = arrow_test_data("avro/alltypes_plain.avro");
1887 let store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
1888 let location = Path::from_filesystem_path(&file).unwrap();
1889 let file_size = store.head(&location).await.unwrap().size;
1890
1891 let file_reader = ObjectStoreReader::new(store, location);
1892 let schema = get_alltypes_schema_with_tz("UTC");
1893 let reader_schema = AvroSchema::try_from(schema.as_ref()).unwrap();
1894
1895 let reader = AsyncAvroFileReader::builder(file_reader, file_size, 1024)
1897 .with_reader_schema(reader_schema)
1898 .with_tz(Tz::Utc)
1899 .try_build()
1900 .await
1901 .unwrap();
1902
1903 let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
1904 let batch = &batches[0];
1905
1906 assert_eq!(batch.num_columns(), 11);
1907
1908 let schema = batch.schema();
1909 let ts_field = schema.field_with_name("timestamp_col").unwrap();
1910 assert!(
1911 matches!(
1912 ts_field.data_type(),
1913 DataType::Timestamp(TimeUnit::Microsecond, Some(tz)) if tz.as_ref() == "UTC"
1914 ),
1915 "expected Timestamp(Microsecond, Some(\"UTC\")), got {:?}",
1916 ts_field.data_type()
1917 );
1918 }
1919
1920 #[tokio::test]
1921 async fn test_with_utf8_view_enabled() {
1922 let file = arrow_test_data("avro/nested_records.avro");
1924 let store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
1925 let location = Path::from_filesystem_path(&file).unwrap();
1926 let file_size = store.head(&location).await.unwrap().size;
1927
1928 let file_reader = ObjectStoreReader::new(store, location);
1929
1930 let reader = AsyncAvroFileReader::builder(file_reader, file_size, 1024)
1931 .with_utf8_view(true)
1932 .try_build()
1933 .await
1934 .unwrap();
1935
1936 let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
1937 let batch = &batches[0];
1938
1939 assert_eq!(batch.num_rows(), 2);
1940
1941 let f1_col = batch.column(0);
1944 let f1_struct = f1_col.as_struct();
1945 let f1_1_field = f1_struct.column_by_name("f1_1").unwrap();
1946
1947 assert_eq!(f1_1_field.data_type(), &DataType::Utf8View);
1949 }
1950
1951 #[tokio::test]
1952 async fn test_with_utf8_view_disabled() {
1953 let file = arrow_test_data("avro/nested_records.avro");
1955 let store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
1956 let location = Path::from_filesystem_path(&file).unwrap();
1957 let file_size = store.head(&location).await.unwrap().size;
1958
1959 let file_reader = ObjectStoreReader::new(store, location);
1960
1961 let reader = AsyncAvroFileReader::builder(file_reader, file_size, 1024)
1962 .with_utf8_view(false)
1963 .try_build()
1964 .await
1965 .unwrap();
1966
1967 let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
1968 let batch = &batches[0];
1969
1970 assert_eq!(batch.num_rows(), 2);
1971
1972 let f1_col = batch.column(0);
1975 let f1_struct = f1_col.as_struct();
1976 let f1_1_field = f1_struct.column_by_name("f1_1").unwrap();
1977
1978 assert_eq!(f1_1_field.data_type(), &DataType::Utf8);
1979 }
1980
1981 #[tokio::test]
1982 async fn test_with_strict_mode_disabled_allows_null_second() {
1983 let file = arrow_test_data("avro/alltypes_nulls_plain.avro");
1986 let store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
1987 let location = Path::from_filesystem_path(&file).unwrap();
1988 let file_size = store.head(&location).await.unwrap().size;
1989
1990 let file_reader = ObjectStoreReader::new(store, location);
1991
1992 let reader = AsyncAvroFileReader::builder(file_reader, file_size, 1024)
1994 .with_strict_mode(false)
1995 .try_build()
1996 .await
1997 .unwrap();
1998
1999 let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
2000 assert_eq!(batches.len(), 1);
2001 assert_eq!(batches[0].num_rows(), 1);
2002 }
2003
2004 #[tokio::test]
2005 async fn test_with_strict_mode_enabled_rejects_null_second() {
2006 let file = arrow_test_data("avro/alltypes_plain.avro");
2009 let store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
2010 let location = Path::from_filesystem_path(&file).unwrap();
2011 let file_size = store.head(&location).await.unwrap().size;
2012
2013 let file_reader = ObjectStoreReader::new(store, location);
2014
2015 let result = AsyncAvroFileReader::builder(file_reader, file_size, 1024)
2017 .with_strict_mode(true)
2018 .try_build()
2019 .await;
2020
2021 match result {
2022 Ok(_) => panic!("Expected error for strict_mode with ['T', 'null'] union"),
2023 Err(err) => {
2024 assert!(
2025 err.to_string().contains("disallowed in strict_mode"),
2026 "Expected strict_mode error, got: {}",
2027 err
2028 );
2029 }
2030 }
2031 }
2032
2033 #[tokio::test]
2034 async fn test_with_strict_mode_enabled_valid_schema() {
2035 let file = arrow_test_data("avro/nested_records.avro");
2038 let store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
2039 let location = Path::from_filesystem_path(&file).unwrap();
2040 let file_size = store.head(&location).await.unwrap().size;
2041
2042 let file_reader = ObjectStoreReader::new(store, location);
2043
2044 let reader = AsyncAvroFileReader::builder(file_reader, file_size, 1024)
2046 .with_strict_mode(true)
2047 .try_build()
2048 .await
2049 .unwrap();
2050
2051 let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
2052 assert_eq!(batches.len(), 1);
2053 assert_eq!(batches[0].num_rows(), 2);
2054 }
2055
2056 #[tokio::test]
2057 async fn test_builder_options_combined() {
2058 let file = arrow_test_data("avro/nested_records.avro");
2060 let store: Arc<dyn ObjectStore> = Arc::new(LocalFileSystem::new());
2061 let location = Path::from_filesystem_path(&file).unwrap();
2062 let file_size = store.head(&location).await.unwrap().size;
2063
2064 let file_reader = ObjectStoreReader::new(store, location);
2065
2066 let reader = AsyncAvroFileReader::builder(file_reader, file_size, 2)
2067 .with_header_size_hint(128)
2068 .with_utf8_view(true)
2069 .with_strict_mode(true)
2070 .with_projection(vec![0, 2]) .try_build()
2072 .await
2073 .unwrap();
2074
2075 let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
2076 let batch = &batches[0];
2077
2078 assert_eq!(batch.num_columns(), 2);
2080 assert_eq!(batch.schema().field(0).name(), "f1");
2081 assert_eq!(batch.schema().field(1).name(), "f3");
2082
2083 let f1_col = batch.column(0);
2085 let f1_struct = f1_col.as_struct();
2086 let f1_1_field = f1_struct.column_by_name("f1_1").unwrap();
2087 assert_eq!(f1_1_field.data_type(), &DataType::Utf8View);
2088 }
2089}