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