Skip to main content

arrow_avro/reader/async_reader/
mod.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Asynchronous implementation of Avro file reader.
19//!
20//! This module provides [`AsyncAvroFileReader`], which supports reading and decoding
21//! the Avro OCF format from any source that implements [`AsyncFileReader`].
22
23use 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    /// Initial read: scan for sync marker, then move to decoding blocks
54    ReadSyncMarker,
55    /// Parse VLQ header bytes one at a time until Data state, then continue decoding
56    DecodeVLQHeader,
57    /// Continue decoding the current block with the fetched data
58    ContinueDecoding,
59}
60
61enum ReaderState<R> {
62    /// Intermediate state to fix ownership issues
63    InvalidState,
64    /// Initial state, fetch initial range
65    Idle { reader: R },
66    /// Fetching data from the reader
67    FetchingData {
68        future: BoxFuture<'static, Result<(R, Bytes), AvroError>>,
69        next_behaviour: FetchNextBehaviour,
70    },
71    /// Decode a block in a loop until completion
72    DecodingBlock { data: Bytes, reader: R },
73    /// Output batches from a decoded block
74    ReadingBatches {
75        data: Bytes,
76        block_data: Bytes,
77        remaining_in_block: usize,
78        reader: R,
79    },
80    /// Successfully finished reading file contents; drain any remaining buffered records
81    /// from the decoder into (possibly partial) output batches.
82    Flushing,
83    /// Done, flush decoder and return
84    Finished,
85}
86
87/// An asynchronous Avro file reader that implements `Stream<Item = Result<RecordBatch, ArrowError>>`.
88/// This uses an [`AsyncFileReader`] to fetch data ranges as needed, starting with fetching the header,
89/// then reading all the blocks in the provided range where:
90/// 1. Reads and decodes data until the header is fully decoded.
91/// 2. Searching from `range.start` for the first sync marker, and starting with the following block.
92///    (If `range.start` is less than the header length, we start at the header length minus the sync marker bytes)
93/// 3. Reading blocks sequentially, decoding them into RecordBatches.
94/// 4. If a block is incomplete (due to range ending mid-block), fetching the remaining bytes from the [`AsyncFileReader`].
95/// 5. If no range was originally provided, reads the full file.
96/// 6. If the range is 0, file_size is 0, or `range.end` is less than the header length, finish immediately.
97///
98/// # Example
99///
100/// ```
101/// #[tokio::main(flavor = "current_thread")]
102/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
103/// use std::io::Cursor;
104/// use std::sync::Arc;
105/// use arrow_array::{ArrayRef, Int32Array, RecordBatch};
106/// use arrow_schema::{DataType, Field, Schema};
107/// use arrow_avro::reader::AsyncAvroFileReader;
108/// use arrow_avro::writer::AvroWriter;
109/// use futures::TryStreamExt;
110///
111/// // Build a minimal Arrow schema and batch
112/// let schema = Schema::new(vec![Field::new("id", DataType::Int32, false)]);
113/// let batch = RecordBatch::try_new(
114///     Arc::new(schema.clone()),
115///     vec![Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef],
116/// )?;
117///
118/// // Write an Avro OCF to memory
119/// let buffer: Vec<u8> = Vec::new();
120/// let mut writer = AvroWriter::new(buffer, schema)?;
121/// writer.write(&batch)?;
122/// writer.finish()?;
123/// let bytes = writer.into_inner();
124///
125/// // Create an async reader from the in-memory bytes
126/// // `tokio::fs::File` also implements `AsyncFileReader` for reading from disk
127/// let file_size = bytes.len();
128/// let cursor = Cursor::new(bytes);
129/// let reader = AsyncAvroFileReader::builder(cursor, file_size as u64, 1024)
130///     .try_build()
131///     .await?;
132///
133/// // Consume the stream of RecordBatches
134/// let batches: Vec<RecordBatch> = reader.try_collect().await?;
135/// assert_eq!(batches.len(), 1);
136/// assert_eq!(batches[0].num_rows(), 3);
137/// Ok(())
138/// }
139/// ```
140pub struct AsyncAvroFileReader<R> {
141    // Members required to fetch data
142    range: Range<u64>,
143    file_size: u64,
144
145    // Members required to actually decode and read data
146    decoder: Decoder,
147    block_decoder: BlockDecoder,
148    codec: Option<CompressionCodec>,
149    sync_marker: [u8; 16],
150
151    // Members keeping the current state of the reader
152    reader_state: ReaderState<R>,
153    finishing_partial_block: bool,
154}
155
156impl<R> AsyncAvroFileReader<R> {
157    /// Returns a builder for a new [`Self`], allowing some optional parameters.
158    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    /// Returns the Arrow schema for batches produced by this reader.
185    ///
186    /// The schema is determined by the writer schema in the file and the reader schema provided to the builder.
187    pub fn schema(&self) -> SchemaRef {
188        self.decoder.schema()
189    }
190
191    /// Calculate the byte range needed to complete the current block.
192    /// Only valid when block_decoder is in Data or Sync state.
193    /// Returns the range to fetch, or an error if EOF would be reached.
194    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, // Include sync marker
198                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    /// Terminate the stream after returning this error once.
217    #[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    /// Drain any remaining buffered records from the decoder.
232    #[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    // The forbid question mark thing shouldn't apply here, as it is within the future,
250    // so exported this to a separate function.
251    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, // Move past the sync marker
298                                None => {
299                                    // Sync marker not found, valid if we arbitrarily split the file at its end.
300                                    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                            // Feed bytes one at a time until we reach Data state (VLQ header complete)
314                            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                            // Now we know the block size. Slice remaining data to what we need.
334                            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                            // May need more data to finish the block.
348                            let range_to_fetch = match self.remaining_block_range() {
349                                Ok(range) if range.is_empty() => {
350                                    // All bytes fetched, move to decoding block directly
351                                    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                    // Try to decode another block from the buffered reader.
382                    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 we reached the end of the block, flush it, and move to read batches.
389                    if let Some(block) = self.block_decoder.flush() {
390                        // Successfully decoded a block.
391                        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                        // Since we have an active block, move to reading batches
407                        self.reader_state = ReaderState::ReadingBatches {
408                            reader,
409                            data,
410                            block_data,
411                            remaining_in_block: block_count,
412                        };
413                        continue;
414                    }
415
416                    // data should always be consumed unless Finished, if it wasn't, something went wrong
417                    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                        // We've already flushed, so if no batch was produced, we are simply done.
425                        self.finishing_partial_block = false;
426                        self.start_flushing();
427                        continue;
428                    }
429
430                    // If we've tried the following stage before, and still can't decode,
431                    // this means the file is truncated or corrupted.
432                    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                    // Avro splitting case: block is incomplete, we need to:
439                    // 1. Parse the length so we know how much to read
440                    // 2. Fetch more data from the reader
441                    // 3. Create a new block data from the remaining slice and the newly fetched data
442                    // 4. Continue decoding until end of block
443                    self.finishing_partial_block = true;
444
445                    // Mid-block, but we don't know how many bytes are missing yet
446                    if matches!(
447                        self.block_decoder.state(),
448                        BlockDecoderState::Count | BlockDecoderState::Size
449                    ) {
450                        // Max VLQ header is 20 bytes (10 bytes each for count and size).
451                        // Fetch just enough to complete it.
452                        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 there is nothing more to fetch, error out
456                        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; // Track that we've fetched these bytes
464
465                        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                    // Otherwise, we're mid-block but know how many bytes are remaining to fetch.
474                    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                            // No more data to read, drain remaining buffered records
502                            self.start_flushing();
503                        } else {
504                            // Finished this block, move to decode next block in the next iteration
505                            self.reader_state = ReaderState::DecodingBlock { reader, data };
506                        }
507                    } else {
508                        // Still more records to decode in this block, slice the already-read data and stay in this state
509                        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                    // We have a full batch ready, emit it
519                    // (This is not mutually exclusive with the block being finished, so the state change is valid)
520                    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                    // Terminal: once finished (including after an error), always yield None
535                    self.reader_state = ReaderState::Finished;
536                    return Poll::Ready(None);
537                }
538            }
539        }
540    }
541}
542
543// To maintain compatibility with the expected stream results in the ecosystem, this returns ArrowError.
544impl<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    /// An [`AsyncFileReader`] reading via an [`ObjectStore`], mirroring the
571    /// example on the [`AsyncFileReader`] trait documentation
572    #[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        // Tests that range starting at 0 correctly skips header
1099        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        // The file ends with the final block's 16-byte sync marker.
1137        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        // Small range unlikely to contain sync marker
1152        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; // Header ends at 675, so this should be mid-block
1166        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        // Range ends mid-block, should trigger fetching_rem_block logic
1193        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        // Range ends mid-VLQ header, triggering the Count|Size partial fetch logic.
1207        let file = arrow_test_data("avro/alltypes_plain.avro");
1208        let range = 16..676; // Header should end at 675
1209        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, // Small batch size to force multiple batches
1281        )
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            // Size should be what was provided, to the limit of the batch in the file
1329            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        // Range extends beyond file size
1345        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        // Should clamp to file size
1353        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        // Schema with nullable and non-nullable fields of various types
1403        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        // Write multiple batches with nulls
1414        {
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            // First batch: 3 rows with some nulls
1420            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            // Second batch: 2 rows
1437            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        // Read back with small batch size to produce multiple output batches
1453        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        // Verify we got multiple output batches due to small batch_size
1466        assert!(
1467            batches.len() > 1,
1468            "Expected multiple batches with batch_size=2"
1469        );
1470
1471        // Verify total row count
1472        let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
1473        assert_eq!(total_rows, 5);
1474
1475        // Concatenate all batches to verify data
1476        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        // Check id column (non-nullable)
1481        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        // Check name column (nullable) - verify nulls are preserved
1489        // Avro strings are read as Binary by default
1490        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)); // second row has null name
1494        assert_eq!(name_array.value(2), "charlie");
1495
1496        // Check score column (nullable) - verify nulls are preserved
1497        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)); // third row has null score
1505        assert!(score_array.is_null(3)); // fourth row has null score
1506        assert!(!score_array.is_null(4));
1507        assert!((score_array.value(4) - 88.0).abs() < f64::EPSILON);
1508
1509        // Check count column (non-nullable)
1510        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        // No reader schema, no projection - uses writer schema from file
1521        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        // No reader schema, with projection - project writer schema
1535        let file = arrow_test_data("avro/alltypes_plain.avro");
1536        // Project [tinyint_col, id, bigint_col] = indices [2, 0, 5]
1537        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        // Verify data values
1549        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        // With reader schema, no projection
1562        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        // With reader schema, with projection
1576        let file = arrow_test_data("avro/alltypes_plain.avro");
1577        let schema = get_alltypes_schema();
1578        // Project [bool_col, id] = indices [1, 0]
1579        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        // With an empty reader schema -- should count rows but produce no columns
1600        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        // With an empty reader schema -- should count rows but produce no columns
1615        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        // No reader schema, no projection
1632        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        // No reader schema, with projection - reorder nested fields
1649        let file = arrow_test_data("avro/nested_records.avro");
1650        // Project [f3, f1] = indices [2, 0]
1651        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        // With reader schema, no projection
1665        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        // With reader schema, with projection
1679        let file = arrow_test_data("avro/nested_records.avro");
1680        let schema = get_nested_records_schema();
1681        // Project [f4, f2, f1] = indices [3, 1, 0]
1682        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        // With an empty reader schema -- should count rows but produce no columns
1697        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        // Index 100 is out of bounds for the 11-field schema
1723        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        // Duplicate index 0
1734        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        // Build reader without providing reader schema - should use writer schema from file
1755        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        // Build reader with provided reader schema - must apply the projection
1784        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        // The schema produced by the reader should match the expected schema,
1808        // attaching Avro type name metadata to fields of record and list types.
1809        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        // Use a very small header size hint to force multiple fetches
1830        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        // Use a tiny header hint (64 bytes) - header is much larger
1840        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        // Use a larger header size hint than needed
1857        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        // Use a large header hint (64KB)
1867        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        // Specify the time zone ID of "UTC" for timestamp fields with time zone.
1893        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        // Test that utf8_view produces StringViewArray instead of StringArray
1920        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        // The f1 struct contains f1_1 which is a string field
1939        // With utf8_view enabled, it should be Utf8View type
1940        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        // Check that the data type is Utf8View
1945        assert_eq!(f1_1_field.data_type(), &DataType::Utf8View);
1946    }
1947
1948    #[tokio::test]
1949    async fn test_with_utf8_view_disabled() {
1950        // Test that without utf8_view, we get regular Utf8
1951        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        // The f1 struct contains f1_1 which is a string field
1970        // Without utf8_view, it should be regular Utf8
1971        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        // Test that with strict_mode disabled, unions of ['T', 'null'] are allowed
1981        // The alltypes_nulls_plain.avro file has unions with null second
1982        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        // Without strict mode, this should succeed
1990        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        // Test that with strict_mode enabled, unions of ['T', 'null'] are rejected
2004        // The alltypes_plain.avro file has unions like ["int", "null"] (null second)
2005        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        // With strict mode, this should fail because of ['T', 'null'] unions
2013        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        // Test that strict_mode works with schemas that have proper ['null', 'T'] unions
2032        // The nested_records.avro file has properly ordered unions
2033        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        // With strict mode, properly ordered unions should still work
2041        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        // Test combining multiple builder options
2055        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]) // f1 and f3
2067            .try_build()
2068            .await
2069            .unwrap();
2070
2071        let batches: Vec<RecordBatch> = reader.try_collect().await.unwrap();
2072        let batch = &batches[0];
2073
2074        // Should have 2 columns (f1 and f3) due to projection
2075        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        // Verify utf8_view is applied
2080        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}