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