Skip to main content

arrow_avro/writer/
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//! Avro writer implementation for the `arrow-avro` crate.
19//!
20//! # Overview
21//!
22//! Use this module to serialize Arrow [`arrow_array::RecordBatch`] values into Avro. Three output
23//! modes are supported:
24//!
25//! * **[`crate::writer::AvroWriter`]** — writes an **Object Container File (OCF)**: a self‑describing
26//!   file with header (schema JSON and metadata), optional compression, data blocks, and
27//!   sync markers. See Avro 1.11.1 "Object Container Files."
28//!   <https://avro.apache.org/docs/1.11.1/specification/#object-container-files>
29//!
30//! * **[`crate::writer::AvroStreamWriter`]** — writes a **Single Object Encoding (SOE) Stream** without
31//!   any container framing. This is useful when the schema is known out‑of‑band (i.e.,
32//!   via a registry) and you want minimal overhead.
33//!
34//! * **[`crate::writer::Encoder`]** — a row-by-row encoder that buffers encoded records into a single
35//!   contiguous byte buffer and returns per-row [`bytes::Bytes`] slices.
36//!   Ideal for publishing individual messages to Kafka, Pulsar, or other message queues
37//!   where each message must be a self-contained Avro payload.
38//!
39//! ## Which writer should you use?
40//!
41//! | Use Case | Recommended Type |
42//! |----------|------------------|
43//! | Write an OCF file to disk | [`crate::writer::AvroWriter`] |
44//! | Stream records continuously to a file/socket | [`crate::writer::AvroStreamWriter`] |
45//! | Publish individual records to Kafka/Pulsar | [`crate::writer::Encoder`] |
46//! | Need per-row byte slices for custom framing | [`crate::writer::Encoder`] |
47//!
48//! ## Per-Record Prefix Formats
49//!
50//! For [`crate::writer::AvroStreamWriter`] and [`crate::writer::Encoder`], each record is automatically prefixed
51//! based on the fingerprint strategy:
52//!
53//! | Strategy | Prefix | Use Case |
54//! |----------|--------|----------|
55//! | `FingerprintStrategy::Rabin` (default) | `0xC3 0x01` + 8-byte LE Rabin fingerprint | Standard Avro SOE |
56//! | `FingerprintStrategy::Id(id)` | `0x00` + 4-byte BE schema ID | [Confluent Schema Registry] |
57//! | `FingerprintStrategy::Id64(id)` | `0x00` + 8-byte BE schema ID | [Apicurio Registry] |
58//!
59//! [Confluent Schema Registry]: https://docs.confluent.io/platform/current/schema-registry/fundamentals/serdes-develop/index.html#wire-format
60//! [Apicurio Registry]: https://www.apicur.io/registry/docs/apicurio-registry/1.3.3.Final/getting-started/assembly-using-kafka-client-serdes.html#registry-serdes-types-avro-registry
61//!
62//! ## Choosing the Avro Schema
63//!
64//! By default, the writer converts your Arrow schema to Avro (including a top‑level record
65//! name). If you already have an Avro schema JSON you want to use verbatim, put it into the
66//! Arrow schema metadata under the [`SCHEMA_METADATA_KEY`]
67//! key before constructing the writer. The builder will use that schema instead of generating
68//! a new one.
69//!
70//! ## Compression
71//!
72//! For OCF ([`crate::writer::AvroWriter`]), you may enable a compression codec via
73//! [`crate::writer::WriterBuilder::with_compression`]. The chosen codec is written into the file header
74//! and used for subsequent blocks. SOE stream writing ([`crate::writer::AvroStreamWriter`], [`crate::writer::Encoder`])
75//! does not apply container‑level compression.
76//!
77//! # Examples
78//!
79//! ## Writing an OCF File
80//!
81//! ```
82//! use std::sync::Arc;
83//! use arrow_array::{ArrayRef, Int64Array, StringArray, RecordBatch};
84//! use arrow_schema::{DataType, Field, Schema};
85//! use arrow_avro::writer::AvroWriter;
86//!
87//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
88//! let schema = Schema::new(vec![
89//!     Field::new("id", DataType::Int64, false),
90//!     Field::new("name", DataType::Utf8, false),
91//! ]);
92//!
93//! let batch = RecordBatch::try_new(
94//!     Arc::new(schema.clone()),
95//!     vec![
96//!         Arc::new(Int64Array::from(vec![1, 2])) as ArrayRef,
97//!         Arc::new(StringArray::from(vec!["alice", "bob"])) as ArrayRef,
98//!     ],
99//! )?;
100//!
101//! let mut writer = AvroWriter::new(Vec::<u8>::new(), schema)?;
102//! writer.write(&batch)?;
103//! writer.finish()?;
104//! let bytes = writer.into_inner();
105//! assert!(!bytes.is_empty());
106//! # Ok(())
107//! # }
108//! ```
109//!
110//! ## Using the Row-by-Row Encoder for Message Queues
111//!
112//! ```
113//! use std::sync::Arc;
114//! use arrow_array::{ArrayRef, Int32Array, RecordBatch};
115//! use arrow_schema::{DataType, Field, Schema};
116//! use arrow_avro::writer::{WriterBuilder, format::AvroSoeFormat};
117//! use arrow_avro::schema::FingerprintStrategy;
118//!
119//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
120//! let schema = Schema::new(vec![Field::new("x", DataType::Int32, false)]);
121//! let batch = RecordBatch::try_new(
122//!     Arc::new(schema.clone()),
123//!     vec![Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef],
124//! )?;
125//!
126//! // Build an Encoder with Confluent wire format (schema ID = 42)
127//! let mut encoder = WriterBuilder::new(schema)
128//!     .with_fingerprint_strategy(FingerprintStrategy::Id(42))
129//!     .build_encoder::<AvroSoeFormat>()?;
130//!
131//! encoder.encode(&batch)?;
132//!
133//! // Get the buffered rows (zero-copy views into a single backing buffer)
134//! let rows = encoder.flush();
135//! assert_eq!(rows.len(), 3);
136//!
137//! // Each row has Confluent wire format: magic byte + 4-byte schema ID + body
138//! for row in rows.iter() {
139//!     assert_eq!(row[0], 0x00); // Confluent magic byte
140//! }
141//! # Ok(())
142//! # }
143//! ```
144//!
145//! ---
146use crate::codec::AvroFieldBuilder;
147use crate::compression::CompressionCodec;
148use crate::errors::AvroError;
149use crate::schema::{
150    AvroSchema, Fingerprint, FingerprintAlgorithm, FingerprintStrategy, SCHEMA_METADATA_KEY,
151};
152use crate::writer::encoder::{RecordEncoder, RecordEncoderBuilder, write_long};
153use crate::writer::format::{AvroFormat, AvroOcfFormat, AvroSoeFormat};
154use arrow_array::RecordBatch;
155use arrow_schema::{Schema, SchemaRef};
156use bytes::{Bytes, BytesMut};
157use std::io::Write;
158use std::sync::Arc;
159
160/// Encodes `RecordBatch` into the Avro binary format.
161mod encoder;
162/// Logic for different Avro container file formats.
163pub mod format;
164
165/// A contiguous set of Avro encoded rows.
166///
167/// `EncodedRows` stores:
168/// - a single backing byte buffer (`bytes::Bytes`)
169/// - a `Vec<usize>` of row boundary offsets (length = `rows + 1`)
170///
171/// This lets callers get per-row payloads as zero-copy `Bytes` slices.
172///
173/// For compatibility with APIs that require owned `Vec<u8>`, use:
174/// `let vecs: Vec<Vec<u8>> = rows.iter().map(|b| b.to_vec()).collect();`
175#[derive(Debug, Clone)]
176pub struct EncodedRows {
177    data: Bytes,
178    offsets: Vec<usize>,
179}
180
181impl EncodedRows {
182    /// Create a new `EncodedRows` from a backing buffer and row boundary offsets.
183    ///
184    /// `offsets` must have length `rows + 1`, and be monotonically non-decreasing.
185    /// The last offset should equal `data.len()`.
186    pub fn new(data: Bytes, offsets: Vec<usize>) -> Self {
187        Self { data, offsets }
188    }
189
190    /// Returns the number of encoded rows stored in this container.
191    #[inline]
192    pub fn len(&self) -> usize {
193        self.offsets.len().saturating_sub(1)
194    }
195
196    /// Returns `true` if this container holds no encoded rows.
197    #[inline]
198    pub fn is_empty(&self) -> bool {
199        self.len() == 0
200    }
201
202    /// Returns a reference to the single contiguous backing buffer.
203    ///
204    /// This buffer contains the payloads of all rows concatenated together.
205    ///
206    /// # Note
207    ///
208    /// To access individual row payloads, prefer using [`Self::row`] or [`Self::iter`]
209    /// rather than slicing this buffer manually.
210    #[inline]
211    pub fn bytes(&self) -> &Bytes {
212        &self.data
213    }
214
215    /// Returns the row boundary offsets.
216    ///
217    /// The returned slice always has the length `self.len() + 1`. The `n`th row payload
218    /// corresponds to `bytes[offsets[n] ... offsets[n+1]]`.
219    #[inline]
220    pub fn offsets(&self) -> &[usize] {
221        &self.offsets
222    }
223
224    /// Return the `n`th row as a zero-copy `Bytes` slice.
225    ///
226    /// # Errors
227    ///
228    /// Returns an error if `n` is out of bounds or if the internal offsets are invalid
229    /// (e.g., offsets are not within the backing buffer).
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use std::sync::Arc;
235    /// use arrow_array::{ArrayRef, Int32Array, RecordBatch};
236    /// use arrow_schema::{DataType, Field, Schema};
237    /// use arrow_avro::writer::WriterBuilder;
238    /// use arrow_avro::writer::format::AvroSoeFormat;
239    ///
240    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
241    /// let schema = Schema::new(vec![Field::new("x", DataType::Int32, false)]);
242    /// let batch = RecordBatch::try_new(
243    ///     Arc::new(schema.clone()),
244    ///     vec![Arc::new(Int32Array::from(vec![1, 2])) as ArrayRef],
245    /// )?;
246    ///
247    /// let mut encoder = WriterBuilder::new(schema).build_encoder::<AvroSoeFormat>()?;
248    /// encoder.encode(&batch)?;
249    /// let rows = encoder.flush();
250    ///
251    /// assert_eq!(rows.iter().count(), 2);
252    /// # Ok(())
253    /// # }
254    /// ```
255    pub fn row(&self, n: usize) -> Result<Bytes, AvroError> {
256        if n >= self.len() {
257            return Err(AvroError::General(format!(
258                "Row index {n} out of bounds for len {}",
259                self.len()
260            )));
261        }
262        // SAFETY:
263        // self.len() is defined as self.offsets.len().saturating_sub(1).
264        // The check `n >= self.len()` above ensures that `n < self.offsets.len() - 1`.
265        // Therefore, both `n` and `n + 1` are strictly within the bounds of `self.offsets`.
266        let (start, end) = unsafe {
267            (
268                *self.offsets.get_unchecked(n),
269                *self.offsets.get_unchecked(n + 1),
270            )
271        };
272        if start > end || end > self.data.len() {
273            return Err(AvroError::General(format!(
274                "Invalid row offsets for row {n}: start={start}, end={end}, data_len={}",
275                self.data.len()
276            )));
277        }
278        Ok(self.data.slice(start..end))
279    }
280
281    /// Iterate over rows as zero-copy `Bytes` slices.
282    ///
283    /// This iterator is infallible and is intended for the common case where
284    /// `EncodedRows` is produced by [`Encoder::flush`], which guarantees valid offsets.
285    ///
286    /// # Examples
287    ///
288    /// ```
289    /// use std::sync::Arc;
290    /// use arrow_array::{ArrayRef, Int32Array, RecordBatch};
291    /// use arrow_schema::{DataType, Field, Schema};
292    /// use arrow_avro::writer::WriterBuilder;
293    /// use arrow_avro::writer::format::AvroSoeFormat;
294    ///
295    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
296    /// let schema = Schema::new(vec![Field::new("x", DataType::Int32, false)]);
297    /// let batch = RecordBatch::try_new(
298    ///     Arc::new(schema.clone()),
299    ///     vec![Arc::new(Int32Array::from(vec![10, 20])) as ArrayRef],
300    /// )?;
301    ///
302    /// let mut encoder = WriterBuilder::new(schema).build_encoder::<AvroSoeFormat>()?;
303    /// encoder.encode(&batch)?;
304    /// let rows = encoder.flush();
305    ///
306    /// assert_eq!(rows.iter().count(), 2);
307    /// # Ok(())
308    /// # }
309    /// ```
310    #[inline]
311    pub fn iter(&self) -> impl ExactSizeIterator<Item = Bytes> + '_ {
312        self.offsets.windows(2).map(|w| self.data.slice(w[0]..w[1]))
313    }
314}
315
316/// Builder to configure and create a `Writer`.
317#[derive(Debug, Clone)]
318pub struct WriterBuilder {
319    schema: Schema,
320    codec: Option<CompressionCodec>,
321    row_capacity: Option<usize>,
322    capacity: usize,
323    fingerprint_strategy: Option<FingerprintStrategy>,
324}
325
326impl WriterBuilder {
327    /// Create a new builder with default settings.
328    ///
329    /// The Avro schema used for writing is determined as follows:
330    /// 1) If the Arrow schema metadata contains `avro::schema` (see `SCHEMA_METADATA_KEY`),
331    ///    that JSON is used verbatim.
332    /// 2) Otherwise, the Arrow schema is converted to an Avro record schema.
333    pub fn new(schema: Schema) -> Self {
334        Self {
335            schema,
336            codec: None,
337            row_capacity: None,
338            capacity: 1024,
339            fingerprint_strategy: None,
340        }
341    }
342
343    /// Set the fingerprinting strategy for the stream writer.
344    /// This determines the per-record prefix format.
345    pub fn with_fingerprint_strategy(mut self, strategy: FingerprintStrategy) -> Self {
346        self.fingerprint_strategy = Some(strategy);
347        self
348    }
349
350    /// Change the compression codec.
351    pub fn with_compression(mut self, codec: Option<CompressionCodec>) -> Self {
352        self.codec = codec;
353        self
354    }
355
356    /// Sets the expected capacity (in bytes) for internal buffers.
357    ///
358    /// This is used as a hint to pre-allocate staging buffers for writing.
359    pub fn with_capacity(mut self, capacity: usize) -> Self {
360        self.capacity = capacity;
361        self
362    }
363
364    /// Sets the expected byte size for each encoded row.
365    ///
366    /// This setting affects [`Encoder`] created via [`build_encoder`](Self::build_encoder).
367    /// It is used as a hint to reduce reallocations when the typical encoded row size is known.
368    pub fn with_row_capacity(mut self, capacity: usize) -> Self {
369        self.row_capacity = Some(capacity);
370        self
371    }
372
373    fn prepare_encoder<F: AvroFormat>(&self) -> Result<(Arc<Schema>, RecordEncoder), AvroError> {
374        let avro_schema = match self.schema.metadata.get(SCHEMA_METADATA_KEY) {
375            Some(json) => AvroSchema::new(json.clone()),
376            None => AvroSchema::try_from(&self.schema)?,
377        };
378        let maybe_fingerprint = if F::NEEDS_PREFIX {
379            match &self.fingerprint_strategy {
380                Some(FingerprintStrategy::Id(id)) => Some(Fingerprint::Id(*id)),
381                Some(FingerprintStrategy::Id64(id)) => Some(Fingerprint::Id64(*id)),
382                Some(strategy) => {
383                    Some(avro_schema.fingerprint(FingerprintAlgorithm::from(*strategy))?)
384                }
385                None => Some(
386                    avro_schema
387                        .fingerprint(FingerprintAlgorithm::from(FingerprintStrategy::Rabin))?,
388                ),
389            }
390        } else {
391            None
392        };
393        let mut md = self.schema.metadata().clone();
394        md.insert(
395            SCHEMA_METADATA_KEY.to_string(),
396            avro_schema.clone().json_string,
397        );
398        let schema = Arc::new(Schema::new_with_metadata(self.schema.fields().clone(), md));
399        let avro_root = AvroFieldBuilder::new(&avro_schema.schema()?).build()?;
400        let encoder = RecordEncoderBuilder::new(&avro_root, schema.as_ref())
401            .with_fingerprint(maybe_fingerprint)
402            .build()?;
403        Ok((schema, encoder))
404    }
405
406    /// Build a new [`Encoder`] for the given [`AvroFormat`].
407    ///
408    /// `Encoder` only supports stream formats (no OCF sync markers). Attempting to build an
409    /// encoder with an OCF format (e.g. [`AvroOcfFormat`]) will return an error.
410    pub fn build_encoder<F: AvroFormat>(self) -> Result<Encoder, AvroError> {
411        if F::default().sync_marker().is_some() {
412            return Err(AvroError::InvalidArgument(
413                "Encoder only supports stream formats (no OCF header/sync marker)".to_string(),
414            ));
415        }
416        let (schema, encoder) = self.prepare_encoder::<F>()?;
417        Ok(Encoder {
418            schema,
419            encoder,
420            row_capacity: self.row_capacity,
421            buffer: BytesMut::with_capacity(self.capacity),
422            offsets: vec![0],
423        })
424    }
425
426    /// Build a new [`Writer`] with the specified [`AvroFormat`] and builder options.
427    pub fn build<W, F>(self, mut writer: W) -> Result<Writer<W, F>, AvroError>
428    where
429        W: Write,
430        F: AvroFormat,
431    {
432        let mut format = F::default();
433        if format.sync_marker().is_none() && !F::NEEDS_PREFIX {
434            return Err(AvroError::InvalidArgument(
435                "AvroBinaryFormat is only supported with Encoder, use build_encoder instead"
436                    .to_string(),
437            ));
438        }
439        let (schema, encoder) = self.prepare_encoder::<F>()?;
440        format.start_stream(&mut writer, &schema, self.codec)?;
441        Ok(Writer {
442            writer,
443            schema,
444            format,
445            compression: self.codec,
446            capacity: self.capacity,
447            encoder,
448        })
449    }
450}
451
452/// A row-by-row encoder for Avro *stream/message* formats (SOE / registry wire formats / raw binary).
453///
454/// Unlike [`Writer`], which emits a single continuous byte stream to a [`std::io::Write`] sink,
455/// `Encoder` tracks row boundaries during encoding and returns an [`EncodedRows`] containing:
456/// - one backing buffer (`Bytes`)
457/// - row boundary offsets
458///
459/// This enables zero-copy per-row payloads (for instance, one Kafka message per Arrow row) without
460/// re-encoding or decoding the byte stream to recover record boundaries.
461///
462/// ### Example
463///
464/// ```
465/// use std::sync::Arc;
466/// use arrow_array::{ArrayRef, Int32Array, RecordBatch};
467/// use arrow_schema::{DataType, Field, Schema};
468/// use arrow_avro::writer::{WriterBuilder, format::AvroSoeFormat};
469/// use arrow_avro::schema::FingerprintStrategy;
470///
471/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
472/// let schema = Schema::new(vec![Field::new("value", DataType::Int32, false)]);
473/// let batch = RecordBatch::try_new(
474///     Arc::new(schema.clone()),
475///     vec![Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef],
476/// )?;
477///
478/// // Configure the encoder (here: Confluent Wire Format with schema ID 100)
479/// let mut encoder = WriterBuilder::new(schema)
480///     .with_fingerprint_strategy(FingerprintStrategy::Id(100))
481///     .build_encoder::<AvroSoeFormat>()?;
482///
483/// // Encode the batch
484/// encoder.encode(&batch)?;
485///
486/// // Get the encoded rows
487/// let rows = encoder.flush();
488///
489/// // Convert to owned Vec<u8> payloads (e.g., for a Kafka producer)
490/// let payloads: Vec<Vec<u8>> = rows.iter().map(|row| row.to_vec()).collect();
491///
492/// assert_eq!(payloads.len(), 3);
493/// assert_eq!(payloads[0][0], 0x00); // Magic byte
494/// # Ok(())
495/// # }
496/// ```
497#[derive(Debug)]
498pub struct Encoder {
499    schema: SchemaRef,
500    encoder: RecordEncoder,
501    row_capacity: Option<usize>,
502    buffer: BytesMut,
503    offsets: Vec<usize>,
504}
505
506impl Encoder {
507    /// Serialize one [`RecordBatch`] into the internal buffer.
508    pub fn encode(&mut self, batch: &RecordBatch) -> Result<(), AvroError> {
509        if batch.schema().fields() != self.schema.fields() {
510            return Err(AvroError::SchemaError(
511                "Schema of RecordBatch differs from Writer schema".to_string(),
512            ));
513        }
514        self.encoder.encode_rows(
515            batch,
516            self.row_capacity.unwrap_or(0),
517            &mut self.buffer,
518            &mut self.offsets,
519        )?;
520        Ok(())
521    }
522
523    /// A convenience method to write a slice of [`RecordBatch`] values.
524    pub fn encode_batches(&mut self, batches: &[RecordBatch]) -> Result<(), AvroError> {
525        for b in batches {
526            self.encode(b)?;
527        }
528        Ok(())
529    }
530
531    /// Drain and return all currently buffered encoded rows.
532    ///
533    /// The returned [`EncodedRows`] provides per-row payloads as `Bytes` slices.
534    pub fn flush(&mut self) -> EncodedRows {
535        let data = self.buffer.split().freeze();
536        let mut offsets = Vec::with_capacity(self.offsets.len());
537        offsets.append(&mut self.offsets);
538        self.offsets.push(0);
539        EncodedRows::new(data, offsets)
540    }
541
542    /// Returns the Arrow schema used by this encoder.
543    ///
544    /// The returned schema includes metadata with the Avro schema JSON under
545    /// the `avro.schema` key.
546    pub fn schema(&self) -> SchemaRef {
547        self.schema.clone()
548    }
549
550    /// Returns the number of encoded rows currently buffered.
551    pub fn buffered_len(&self) -> usize {
552        self.offsets.len().saturating_sub(1)
553    }
554}
555
556/// Generic Avro writer.
557///
558/// This type is generic over the output Write sink (`W`) and the Avro format (`F`).
559/// You’ll usually use the concrete aliases:
560///
561/// * **[`AvroWriter`]** for **OCF** (self‑describing container file)
562/// * **[`AvroStreamWriter`]** for **SOE** Avro streams
563#[derive(Debug)]
564pub struct Writer<W: Write, F: AvroFormat> {
565    writer: W,
566    schema: SchemaRef,
567    format: F,
568    compression: Option<CompressionCodec>,
569    capacity: usize,
570    encoder: RecordEncoder,
571}
572
573/// Alias for an Avro **Object Container File** writer.
574///
575/// ### Quickstart (runnable)
576///
577/// ```
578/// use std::io::Cursor;
579/// use std::sync::Arc;
580/// use arrow_array::{ArrayRef, Int64Array, StringArray, RecordBatch};
581/// use arrow_schema::{DataType, Field, Schema};
582/// use arrow_avro::writer::AvroWriter;
583/// use arrow_avro::reader::ReaderBuilder;
584///
585/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
586/// // Writer schema: { id: long, name: string }
587/// let writer_schema = Schema::new(vec![
588///     Field::new("id", DataType::Int64, false),
589///     Field::new("name", DataType::Utf8, false),
590/// ]);
591///
592/// // Build a RecordBatch with two rows
593/// let batch = RecordBatch::try_new(
594///     Arc::new(writer_schema.clone()),
595///     vec![
596///         Arc::new(Int64Array::from(vec![1, 2])) as ArrayRef,
597///         Arc::new(StringArray::from(vec!["a", "b"])) as ArrayRef,
598///     ],
599/// )?;
600///
601/// // Write an Avro **Object Container File** (OCF) to memory
602/// let mut w = AvroWriter::new(Vec::<u8>::new(), writer_schema.clone())?;
603/// w.write(&batch)?;
604/// w.finish()?;
605/// let bytes = w.into_inner();
606///
607/// // Build a Reader and decode the batch back
608/// let mut r = ReaderBuilder::new().build(Cursor::new(bytes))?;
609/// let out = r.next().unwrap()?;
610/// assert_eq!(out.num_rows(), 2);
611/// # Ok(()) }
612/// ```
613pub type AvroWriter<W> = Writer<W, AvroOcfFormat>;
614
615/// Alias for an Avro **Single Object Encoding** stream writer.
616///
617/// ### Example
618///
619/// This writer automatically adds the appropriate per-record prefix (based on the
620/// fingerprint strategy) before the Avro body of each record. The default is Single
621/// Object Encoding (SOE) with a Rabin fingerprint.
622///
623/// ```
624/// use std::sync::Arc;
625/// use arrow_array::{ArrayRef, Int64Array, RecordBatch};
626/// use arrow_schema::{DataType, Field, Schema};
627/// use arrow_avro::writer::AvroStreamWriter;
628///
629/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
630/// // One‑column Arrow batch
631/// let schema = Schema::new(vec![Field::new("x", DataType::Int64, false)]);
632/// let batch = RecordBatch::try_new(
633///     Arc::new(schema.clone()),
634///     vec![Arc::new(Int64Array::from(vec![10, 20])) as ArrayRef],
635/// )?;
636///
637/// // Write an Avro Single Object Encoding stream to a Vec<u8>
638/// let sink: Vec<u8> = Vec::new();
639/// let mut w = AvroStreamWriter::new(sink, schema)?;
640/// w.write(&batch)?;
641/// w.finish()?;
642/// let bytes = w.into_inner();
643/// assert!(!bytes.is_empty());
644/// # Ok(()) }
645/// ```
646pub type AvroStreamWriter<W> = Writer<W, AvroSoeFormat>;
647
648impl<W: Write> Writer<W, AvroOcfFormat> {
649    /// Convenience constructor – same as [`WriterBuilder::build`] with `AvroOcfFormat`.
650    ///
651    /// ### Example
652    ///
653    /// ```
654    /// use std::sync::Arc;
655    /// use arrow_array::{ArrayRef, Int32Array, RecordBatch};
656    /// use arrow_schema::{DataType, Field, Schema};
657    /// use arrow_avro::writer::AvroWriter;
658    ///
659    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
660    /// let schema = Schema::new(vec![Field::new("id", DataType::Int32, false)]);
661    /// let batch = RecordBatch::try_new(
662    ///     Arc::new(schema.clone()),
663    ///     vec![Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef],
664    /// )?;
665    ///
666    /// let buf: Vec<u8> = Vec::new();
667    /// let mut w = AvroWriter::new(buf, schema)?;
668    /// w.write(&batch)?;
669    /// w.finish()?;
670    /// let bytes = w.into_inner();
671    /// assert!(!bytes.is_empty());
672    /// # Ok(()) }
673    /// ```
674    pub fn new(writer: W, schema: Schema) -> Result<Self, AvroError> {
675        WriterBuilder::new(schema).build::<W, AvroOcfFormat>(writer)
676    }
677
678    /// Return a reference to the 16‑byte sync marker generated for this file.
679    pub fn sync_marker(&self) -> Option<&[u8; 16]> {
680        self.format.sync_marker()
681    }
682}
683
684impl<W: Write> Writer<W, AvroSoeFormat> {
685    /// Convenience constructor to create a new [`AvroStreamWriter`].
686    ///
687    /// The resulting stream contains **Single Object Encodings** (no OCF header/sync).
688    ///
689    /// ### Example
690    ///
691    /// ```
692    /// use std::sync::Arc;
693    /// use arrow_array::{ArrayRef, Int64Array, RecordBatch};
694    /// use arrow_schema::{DataType, Field, Schema};
695    /// use arrow_avro::writer::AvroStreamWriter;
696    ///
697    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
698    /// let schema = Schema::new(vec![Field::new("x", DataType::Int64, false)]);
699    /// let batch = RecordBatch::try_new(
700    ///     Arc::new(schema.clone()),
701    ///     vec![Arc::new(Int64Array::from(vec![10, 20])) as ArrayRef],
702    /// )?;
703    ///
704    /// let sink: Vec<u8> = Vec::new();
705    /// let mut w = AvroStreamWriter::new(sink, schema)?;
706    /// w.write(&batch)?;
707    /// w.finish()?;
708    /// let bytes = w.into_inner();
709    /// assert!(!bytes.is_empty());
710    /// # Ok(()) }
711    /// ```
712    pub fn new(writer: W, schema: Schema) -> Result<Self, AvroError> {
713        WriterBuilder::new(schema).build::<W, AvroSoeFormat>(writer)
714    }
715}
716
717impl<W: Write, F: AvroFormat> Writer<W, F> {
718    /// Serialize one [`RecordBatch`] to the output.
719    pub fn write(&mut self, batch: &RecordBatch) -> Result<(), AvroError> {
720        if batch.schema().fields() != self.schema.fields() {
721            return Err(AvroError::SchemaError(
722                "Schema of RecordBatch differs from Writer schema".to_string(),
723            ));
724        }
725        match self.format.sync_marker() {
726            Some(&sync) => self.write_ocf_block(batch, &sync),
727            None => self.write_stream(batch),
728        }
729    }
730
731    /// A convenience method to write a slice of [`RecordBatch`].
732    ///
733    /// This is equivalent to calling `write` for each batch in the slice.
734    pub fn write_batches(&mut self, batches: &[&RecordBatch]) -> Result<(), AvroError> {
735        for b in batches {
736            self.write(b)?;
737        }
738        Ok(())
739    }
740
741    /// Flush remaining buffered data and (for OCF) ensure the header is present.
742    pub fn finish(&mut self) -> Result<(), AvroError> {
743        self.writer
744            .flush()
745            .map_err(|e| AvroError::IoError(format!("Error flushing writer: {e}"), e))
746    }
747
748    /// Consume the writer, returning the underlying output object.
749    pub fn into_inner(self) -> W {
750        self.writer
751    }
752
753    fn write_ocf_block(&mut self, batch: &RecordBatch, sync: &[u8; 16]) -> Result<(), AvroError> {
754        let mut buf = Vec::<u8>::with_capacity(self.capacity);
755        self.encoder.encode(&mut buf, batch)?;
756        let encoded = match self.compression {
757            Some(codec) => codec.compress(&buf)?,
758            None => buf,
759        };
760        write_long(&mut self.writer, batch.num_rows() as i64)?;
761        write_long(&mut self.writer, encoded.len() as i64)?;
762        self.writer
763            .write_all(&encoded)
764            .map_err(|e| AvroError::IoError(format!("Error writing Avro block: {e}"), e))?;
765        self.writer
766            .write_all(sync)
767            .map_err(|e| AvroError::IoError(format!("Error writing Avro sync: {e}"), e))?;
768        Ok(())
769    }
770
771    fn write_stream(&mut self, batch: &RecordBatch) -> Result<(), AvroError> {
772        self.encoder.encode(&mut self.writer, batch)?;
773        Ok(())
774    }
775}
776
777#[cfg(test)]
778mod tests {
779    use super::*;
780    use crate::compression::CompressionCodec;
781    use crate::reader::ReaderBuilder;
782    use crate::schema::AVRO_NAME_METADATA_KEY;
783    use crate::schema::{AvroSchema, SchemaStore};
784    use crate::test_util::arrow_test_data;
785    use arrow::datatypes::TimeUnit;
786    use arrow::util::pretty::pretty_format_batches;
787    #[cfg(not(feature = "avro_custom_types"))]
788    use arrow_array::Float32Array;
789    #[cfg(feature = "avro_custom_types")]
790    use arrow_array::RunArray;
791    use arrow_array::builder::{Int32Builder, ListBuilder};
792    use arrow_array::cast::AsArray;
793    #[cfg(feature = "avro_custom_types")]
794    use arrow_array::types::{Int16Type, Int64Type};
795    use arrow_array::types::{
796        Int32Type, Time32MillisecondType, Time64MicrosecondType, TimestampMicrosecondType,
797        TimestampMillisecondType, TimestampNanosecondType,
798    };
799    use arrow_array::{
800        Array, ArrayRef, BinaryArray, BooleanArray, Date32Array, Date64Array, Float16Array,
801        Int8Array, Int16Array, Int32Array, Int64Array, IntervalDayTimeArray,
802        IntervalMonthDayNanoArray, IntervalYearMonthArray, PrimitiveArray, RecordBatch,
803        StringArray, StructArray, Time32MillisecondArray, Time32SecondArray,
804        Time64MicrosecondArray, Time64NanosecondArray, TimestampMillisecondArray,
805        TimestampSecondArray, UInt8Array, UInt16Array, UInt32Array, UInt64Array, UnionArray,
806    };
807    use arrow_buffer::{IntervalDayTime, IntervalMonthDayNano};
808    #[cfg(not(feature = "avro_custom_types"))]
809    use arrow_schema::{DataType, Field, Schema};
810    #[cfg(feature = "avro_custom_types")]
811    use arrow_schema::{DataType, Field, Schema};
812    use arrow_schema::{IntervalUnit, UnionMode};
813    use bytes::BytesMut;
814    use half::f16;
815    use serde_json::{Value, json};
816    use std::collections::HashMap;
817    use std::collections::HashSet;
818    use std::fs::File;
819    use std::io::{BufReader, Cursor};
820    use std::path::PathBuf;
821    use std::sync::Arc;
822    use tempfile::NamedTempFile;
823
824    fn files() -> impl Iterator<Item = &'static str> {
825        [
826            // TODO: avoid requiring snappy for this file
827            #[cfg(feature = "snappy")]
828            "avro/alltypes_plain.avro",
829            // Compression codecs are unsupported by Miri
830            #[cfg(all(feature = "snappy", not(miri)))]
831            "avro/alltypes_plain.snappy.avro",
832            #[cfg(all(feature = "zstd", not(miri)))]
833            "avro/alltypes_plain.zstandard.avro",
834            #[cfg(all(feature = "bzip2", not(miri)))]
835            "avro/alltypes_plain.bzip2.avro",
836            #[cfg(all(feature = "xz", not(miri)))]
837            "avro/alltypes_plain.xz.avro",
838        ]
839        .into_iter()
840    }
841
842    fn make_schema() -> Schema {
843        Schema::new(vec![
844            Field::new("id", DataType::Int32, false),
845            Field::new("name", DataType::Binary, false),
846        ])
847    }
848
849    fn make_batch() -> RecordBatch {
850        let ids = Int32Array::from(vec![1, 2, 3]);
851        let names = BinaryArray::from_vec(vec![b"a".as_ref(), b"b".as_ref(), b"c".as_ref()]);
852        RecordBatch::try_new(
853            Arc::new(make_schema()),
854            vec![Arc::new(ids) as ArrayRef, Arc::new(names) as ArrayRef],
855        )
856        .expect("failed to build test RecordBatch")
857    }
858
859    #[test]
860    fn test_stream_writer_writes_prefix_per_row_rt() -> Result<(), AvroError> {
861        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
862        let batch = RecordBatch::try_new(
863            Arc::new(schema.clone()),
864            vec![Arc::new(Int32Array::from(vec![10, 20])) as ArrayRef],
865        )?;
866        let buf: Vec<u8> = Vec::new();
867        let mut writer = AvroStreamWriter::new(buf, schema.clone())?;
868        writer.write(&batch)?;
869        let encoded = writer.into_inner();
870        let mut store = SchemaStore::new(); // Rabin by default
871        let avro_schema = AvroSchema::try_from(&schema)?;
872        let _fp = store.register(avro_schema)?;
873        let mut decoder = ReaderBuilder::new()
874            .with_writer_schema_store(store)
875            .build_decoder()?;
876        let _consumed = decoder.decode(&encoded)?;
877        let decoded = decoder
878            .flush()?
879            .expect("expected at least one batch from decoder");
880        assert_eq!(decoded.num_columns(), 1);
881        assert_eq!(decoded.num_rows(), 2);
882        let col = decoded.column(0).as_primitive::<Int32Type>();
883        assert_eq!(col, &Int32Array::from(vec![10, 20]));
884        Ok(())
885    }
886
887    #[test]
888    fn test_nullable_struct_with_nonnullable_field_sliced_encoding() {
889        use arrow_array::{ArrayRef, Int32Array, StringArray, StructArray};
890        use arrow_buffer::NullBuffer;
891        use arrow_schema::{DataType, Field, Fields, Schema};
892        use std::sync::Arc;
893        let inner_fields = Fields::from(vec![
894            Field::new("id", DataType::Int32, false), // non-nullable
895            Field::new("name", DataType::Utf8, true), // nullable
896        ]);
897        let inner_struct_type = DataType::Struct(inner_fields.clone());
898        let schema = Schema::new(vec![
899            Field::new("before", inner_struct_type.clone(), true), // nullable struct
900            Field::new("after", inner_struct_type.clone(), true),  // nullable struct
901            Field::new("op", DataType::Utf8, false),               // non-nullable
902        ]);
903        let before_ids = Int32Array::from(vec![None, None]);
904        let before_names = StringArray::from(vec![None::<&str>, None]);
905        let before_struct = StructArray::new(
906            inner_fields.clone(),
907            vec![
908                Arc::new(before_ids) as ArrayRef,
909                Arc::new(before_names) as ArrayRef,
910            ],
911            Some(NullBuffer::from(vec![false, false])),
912        );
913        let after_ids = Int32Array::from(vec![1, 2]); // non-nullable, no nulls
914        let after_names = StringArray::from(vec![Some("Alice"), Some("Bob")]);
915        let after_struct = StructArray::new(
916            inner_fields.clone(),
917            vec![
918                Arc::new(after_ids) as ArrayRef,
919                Arc::new(after_names) as ArrayRef,
920            ],
921            Some(NullBuffer::from(vec![true, true])),
922        );
923        let op_col = StringArray::from(vec!["r", "r"]);
924        let batch = RecordBatch::try_new(
925            Arc::new(schema.clone()),
926            vec![
927                Arc::new(before_struct) as ArrayRef,
928                Arc::new(after_struct) as ArrayRef,
929                Arc::new(op_col) as ArrayRef,
930            ],
931        )
932        .expect("failed to create test batch");
933        let mut sink = Vec::new();
934        let mut writer = WriterBuilder::new(schema)
935            .with_fingerprint_strategy(FingerprintStrategy::Id(1))
936            .build::<_, AvroSoeFormat>(&mut sink)
937            .expect("failed to create writer");
938        for row_idx in 0..batch.num_rows() {
939            let single_row = batch.slice(row_idx, 1);
940            let after_col = single_row.column(1);
941            assert_eq!(
942                after_col.null_count(),
943                0,
944                "after column should have no nulls in sliced row"
945            );
946            writer
947                .write(&single_row)
948                .unwrap_or_else(|e| panic!("Failed to encode row {row_idx}: {e}"));
949        }
950        writer.finish().expect("failed to finish writer");
951        assert!(!sink.is_empty(), "encoded output should not be empty");
952    }
953
954    #[test]
955    fn test_nullable_struct_with_decimal_and_timestamp_sliced() {
956        use arrow_array::{
957            ArrayRef, Decimal128Array, Int32Array, StringArray, StructArray,
958            TimestampMicrosecondArray,
959        };
960        use arrow_buffer::NullBuffer;
961        use arrow_schema::{DataType, Field, Fields, Schema};
962        use std::sync::Arc;
963        let row_fields = Fields::from(vec![
964            Field::new("id", DataType::Int32, false),
965            Field::new("name", DataType::Utf8, true),
966            Field::new("category", DataType::Utf8, true),
967            Field::new("price", DataType::Decimal128(10, 2), true),
968            Field::new("stock_quantity", DataType::Int32, true),
969            Field::new(
970                "created_at",
971                DataType::Timestamp(TimeUnit::Microsecond, None),
972                true,
973            ),
974        ]);
975        let row_struct_type = DataType::Struct(row_fields.clone());
976        let schema = Schema::new(vec![
977            Field::new("before", row_struct_type.clone(), true),
978            Field::new("after", row_struct_type.clone(), true),
979            Field::new("op", DataType::Utf8, false),
980        ]);
981        let before_struct = StructArray::new_null(row_fields.clone(), 2);
982        let ids = Int32Array::from(vec![1, 2]);
983        let names = StringArray::from(vec![Some("Widget"), Some("Gadget")]);
984        let categories = StringArray::from(vec![Some("Electronics"), Some("Electronics")]);
985        let prices = Decimal128Array::from(vec![Some(1999), Some(2999)])
986            .with_precision_and_scale(10, 2)
987            .unwrap();
988        let quantities = Int32Array::from(vec![Some(100), Some(50)]);
989        let timestamps = TimestampMicrosecondArray::from(vec![
990            Some(1700000000000000i64),
991            Some(1700000001000000i64),
992        ]);
993        let after_struct = StructArray::new(
994            row_fields.clone(),
995            vec![
996                Arc::new(ids) as ArrayRef,
997                Arc::new(names) as ArrayRef,
998                Arc::new(categories) as ArrayRef,
999                Arc::new(prices) as ArrayRef,
1000                Arc::new(quantities) as ArrayRef,
1001                Arc::new(timestamps) as ArrayRef,
1002            ],
1003            Some(NullBuffer::from(vec![true, true])),
1004        );
1005        let op_col = StringArray::from(vec!["r", "r"]);
1006        let batch = RecordBatch::try_new(
1007            Arc::new(schema.clone()),
1008            vec![
1009                Arc::new(before_struct) as ArrayRef,
1010                Arc::new(after_struct) as ArrayRef,
1011                Arc::new(op_col) as ArrayRef,
1012            ],
1013        )
1014        .expect("failed to create products batch");
1015        let mut sink = Vec::new();
1016        let mut writer = WriterBuilder::new(schema)
1017            .with_fingerprint_strategy(FingerprintStrategy::Id(1))
1018            .build::<_, AvroSoeFormat>(&mut sink)
1019            .expect("failed to create writer");
1020        // Encode row by row
1021        for row_idx in 0..batch.num_rows() {
1022            let single_row = batch.slice(row_idx, 1);
1023            writer
1024                .write(&single_row)
1025                .unwrap_or_else(|e| panic!("Failed to encode product row {row_idx}: {e}"));
1026        }
1027        writer.finish().expect("failed to finish writer");
1028        assert!(!sink.is_empty());
1029    }
1030
1031    #[test]
1032    fn non_nullable_child_in_nullable_struct_should_encode_per_row() {
1033        use arrow_array::{
1034            ArrayRef, Int32Array, Int64Array, RecordBatch, StringArray, StructArray,
1035        };
1036        use arrow_schema::{DataType, Field, Fields, Schema};
1037        use std::sync::Arc;
1038        let row_fields = Fields::from(vec![
1039            Field::new("id", DataType::Int32, false),
1040            Field::new("name", DataType::Utf8, true),
1041        ]);
1042        let row_struct_dt = DataType::Struct(row_fields.clone());
1043        let before: ArrayRef = Arc::new(StructArray::new_null(row_fields.clone(), 1));
1044        let id_col: ArrayRef = Arc::new(Int32Array::from(vec![1]));
1045        let name_col: ArrayRef = Arc::new(StringArray::from(vec![None::<&str>]));
1046        let after: ArrayRef = Arc::new(StructArray::new(
1047            row_fields.clone(),
1048            vec![id_col, name_col],
1049            None,
1050        ));
1051        let schema = Arc::new(Schema::new(vec![
1052            Field::new("before", row_struct_dt.clone(), true),
1053            Field::new("after", row_struct_dt, true),
1054            Field::new("op", DataType::Utf8, false),
1055            Field::new("ts_ms", DataType::Int64, false),
1056        ]));
1057        let op = Arc::new(StringArray::from(vec!["r"])) as ArrayRef;
1058        let ts_ms = Arc::new(Int64Array::from(vec![1732900000000_i64])) as ArrayRef;
1059        let batch = RecordBatch::try_new(schema.clone(), vec![before, after, op, ts_ms]).unwrap();
1060        let mut buf = Vec::new();
1061        let mut writer = WriterBuilder::new(schema.as_ref().clone())
1062            .build::<_, AvroSoeFormat>(&mut buf)
1063            .unwrap();
1064        let single = batch.slice(0, 1);
1065        let res = writer.write(&single);
1066        assert!(
1067            res.is_ok(),
1068            "expected to encode successfully, got: {:?}",
1069            res.err()
1070        );
1071    }
1072
1073    #[test]
1074    fn test_union_nonzero_type_ids() -> Result<(), AvroError> {
1075        use arrow_array::UnionArray;
1076        use arrow_buffer::Buffer;
1077        use arrow_schema::UnionFields;
1078        let union_fields = UnionFields::try_new(
1079            vec![2, 5],
1080            vec![
1081                Field::new("v_str", DataType::Utf8, true),
1082                Field::new("v_int", DataType::Int32, true),
1083            ],
1084        )
1085        .unwrap();
1086        let strings = StringArray::from(vec!["hello", "world"]);
1087        let ints = Int32Array::from(vec![10, 20, 30]);
1088        let type_ids = Buffer::from_slice_ref([2_i8, 5, 5, 2, 5]);
1089        let offsets = Buffer::from_slice_ref([0_i32, 0, 1, 1, 2]);
1090        let union_array = UnionArray::try_new(
1091            union_fields.clone(),
1092            type_ids.into(),
1093            Some(offsets.into()),
1094            vec![Arc::new(strings) as ArrayRef, Arc::new(ints) as ArrayRef],
1095        )?;
1096        let schema = Schema::new(vec![Field::new(
1097            "union_col",
1098            DataType::Union(union_fields, UnionMode::Dense),
1099            false,
1100        )]);
1101        let batch = RecordBatch::try_new(
1102            Arc::new(schema.clone()),
1103            vec![Arc::new(union_array) as ArrayRef],
1104        )?;
1105        let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
1106        assert!(
1107            writer.write(&batch).is_ok(),
1108            "Expected no error from writing"
1109        );
1110        writer.finish()?;
1111        assert!(
1112            writer.finish().is_ok(),
1113            "Expected no error from finishing writer"
1114        );
1115        Ok(())
1116    }
1117
1118    #[test]
1119    fn test_stream_writer_with_id_fingerprint_rt() -> Result<(), AvroError> {
1120        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
1121        let batch = RecordBatch::try_new(
1122            Arc::new(schema.clone()),
1123            vec![Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef],
1124        )?;
1125        let schema_id: u32 = 42;
1126        let mut writer = WriterBuilder::new(schema.clone())
1127            .with_fingerprint_strategy(FingerprintStrategy::Id(schema_id))
1128            .build::<_, AvroSoeFormat>(Vec::new())?;
1129        writer.write(&batch)?;
1130        let encoded = writer.into_inner();
1131        let mut store = SchemaStore::new_with_type(FingerprintAlgorithm::Id);
1132        let avro_schema = AvroSchema::try_from(&schema)?;
1133        let _ = store.set(Fingerprint::Id(schema_id), avro_schema)?;
1134        let mut decoder = ReaderBuilder::new()
1135            .with_writer_schema_store(store)
1136            .build_decoder()?;
1137        let _ = decoder.decode(&encoded)?;
1138        let decoded = decoder
1139            .flush()?
1140            .expect("expected at least one batch from decoder");
1141        assert_eq!(decoded.num_columns(), 1);
1142        assert_eq!(decoded.num_rows(), 3);
1143        let col = decoded.column(0).as_primitive::<Int32Type>();
1144        assert_eq!(col, &Int32Array::from(vec![1, 2, 3]));
1145        Ok(())
1146    }
1147
1148    #[test]
1149    fn test_stream_writer_with_id64_fingerprint_rt() -> Result<(), AvroError> {
1150        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
1151        let batch = RecordBatch::try_new(
1152            Arc::new(schema.clone()),
1153            vec![Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef],
1154        )?;
1155        let schema_id: u64 = 42;
1156        let mut writer = WriterBuilder::new(schema.clone())
1157            .with_fingerprint_strategy(FingerprintStrategy::Id64(schema_id))
1158            .build::<_, AvroSoeFormat>(Vec::new())?;
1159        writer.write(&batch)?;
1160        let encoded = writer.into_inner();
1161        let mut store = SchemaStore::new_with_type(FingerprintAlgorithm::Id64);
1162        let avro_schema = AvroSchema::try_from(&schema)?;
1163        let _ = store.set(Fingerprint::Id64(schema_id), avro_schema)?;
1164        let mut decoder = ReaderBuilder::new()
1165            .with_writer_schema_store(store)
1166            .build_decoder()?;
1167        let _ = decoder.decode(&encoded)?;
1168        let decoded = decoder
1169            .flush()?
1170            .expect("expected at least one batch from decoder");
1171        assert_eq!(decoded.num_columns(), 1);
1172        assert_eq!(decoded.num_rows(), 3);
1173        let col = decoded.column(0).as_primitive::<Int32Type>();
1174        assert_eq!(col, &Int32Array::from(vec![1, 2, 3]));
1175        Ok(())
1176    }
1177
1178    #[test]
1179    fn test_ocf_writer_generates_header_and_sync() -> Result<(), AvroError> {
1180        let batch = make_batch();
1181        let buffer: Vec<u8> = Vec::new();
1182        let mut writer = AvroWriter::new(buffer, make_schema())?;
1183        writer.write(&batch)?;
1184        writer.finish()?;
1185        let out = writer.into_inner();
1186        assert_eq!(&out[..4], b"Obj\x01", "OCF magic bytes missing/incorrect");
1187        let trailer = &out[out.len() - 16..];
1188        assert_eq!(trailer.len(), 16, "expected 16‑byte sync marker");
1189        Ok(())
1190    }
1191
1192    #[test]
1193    fn test_schema_mismatch_yields_error() {
1194        let batch = make_batch();
1195        let alt_schema = Schema::new(vec![Field::new("x", DataType::Int32, false)]);
1196        let buffer = Vec::<u8>::new();
1197        let mut writer = AvroWriter::new(buffer, alt_schema).unwrap();
1198        let err = writer.write(&batch).unwrap_err();
1199        assert!(matches!(err, AvroError::SchemaError(_)));
1200    }
1201
1202    #[test]
1203    fn test_write_batches_accumulates_multiple() -> Result<(), AvroError> {
1204        let batch1 = make_batch();
1205        let batch2 = make_batch();
1206        let buffer = Vec::<u8>::new();
1207        let mut writer = AvroWriter::new(buffer, make_schema())?;
1208        writer.write_batches(&[&batch1, &batch2])?;
1209        writer.finish()?;
1210        let out = writer.into_inner();
1211        assert!(out.len() > 4, "combined batches produced tiny file");
1212        Ok(())
1213    }
1214
1215    #[test]
1216    fn test_finish_without_write_adds_header() -> Result<(), AvroError> {
1217        let buffer = Vec::<u8>::new();
1218        let mut writer = AvroWriter::new(buffer, make_schema())?;
1219        writer.finish()?;
1220        let out = writer.into_inner();
1221        assert_eq!(&out[..4], b"Obj\x01", "finish() should emit OCF header");
1222        Ok(())
1223    }
1224
1225    #[test]
1226    fn test_write_long_encodes_zigzag_varint() -> Result<(), AvroError> {
1227        let mut buf = Vec::new();
1228        write_long(&mut buf, 0)?;
1229        write_long(&mut buf, -1)?;
1230        write_long(&mut buf, 1)?;
1231        write_long(&mut buf, -2)?;
1232        write_long(&mut buf, 2147483647)?;
1233        assert!(
1234            buf.starts_with(&[0x00, 0x01, 0x02, 0x03]),
1235            "zig‑zag varint encodings incorrect: {buf:?}"
1236        );
1237        Ok(())
1238    }
1239
1240    #[test]
1241    fn test_roundtrip_alltypes_roundtrip_writer() -> Result<(), AvroError> {
1242        for rel in files() {
1243            let path = arrow_test_data(rel);
1244            let rdr_file = File::open(&path).expect("open input avro");
1245            let reader = ReaderBuilder::new()
1246                .build(BufReader::new(rdr_file))
1247                .expect("build reader");
1248            let schema = reader.schema();
1249            let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1250            let original =
1251                arrow::compute::concat_batches(&schema, &input_batches).expect("concat input");
1252            let tmp = NamedTempFile::new().expect("create temp file");
1253            let out_path = tmp.into_temp_path();
1254            let out_file = File::create(&out_path).expect("create temp avro");
1255            let codec = if rel.contains(".snappy.") {
1256                Some(CompressionCodec::Snappy)
1257            } else if rel.contains(".zstandard.") {
1258                Some(CompressionCodec::ZStandard)
1259            } else if rel.contains(".bzip2.") {
1260                Some(CompressionCodec::Bzip2)
1261            } else if rel.contains(".xz.") {
1262                Some(CompressionCodec::Xz)
1263            } else {
1264                None
1265            };
1266            let mut writer = WriterBuilder::new(original.schema().as_ref().clone())
1267                .with_compression(codec)
1268                .build::<_, AvroOcfFormat>(out_file)?;
1269            writer.write(&original)?;
1270            writer.finish()?;
1271            drop(writer);
1272            let rt_file = File::open(&out_path).expect("open roundtrip avro");
1273            let rt_reader = ReaderBuilder::new()
1274                .build(BufReader::new(rt_file))
1275                .expect("build roundtrip reader");
1276            let rt_schema = rt_reader.schema();
1277            let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1278            let roundtrip =
1279                arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
1280            assert_eq!(
1281                roundtrip, original,
1282                "Round-trip batch mismatch for file: {rel}"
1283            );
1284        }
1285        Ok(())
1286    }
1287
1288    #[test]
1289    fn test_roundtrip_nested_records_writer() -> Result<(), AvroError> {
1290        let path = arrow_test_data("avro/nested_records.avro");
1291        let rdr_file = File::open(&path).expect("open nested_records.avro");
1292        let reader = ReaderBuilder::new()
1293            .build(BufReader::new(rdr_file))
1294            .expect("build reader for nested_records.avro");
1295        let schema = reader.schema();
1296        let batches = reader.collect::<Result<Vec<_>, _>>()?;
1297        let original = arrow::compute::concat_batches(&schema, &batches).expect("concat original");
1298        let tmp = NamedTempFile::new().expect("create temp file");
1299        let out_path = tmp.into_temp_path();
1300        {
1301            let out_file = File::create(&out_path).expect("create output avro");
1302            let mut writer = AvroWriter::new(out_file, original.schema().as_ref().clone())?;
1303            writer.write(&original)?;
1304            writer.finish()?;
1305        }
1306        let rt_file = File::open(&out_path).expect("open round_trip avro");
1307        let rt_reader = ReaderBuilder::new()
1308            .build(BufReader::new(rt_file))
1309            .expect("build round_trip reader");
1310        let rt_schema = rt_reader.schema();
1311        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1312        let round_trip =
1313            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
1314        assert_eq!(
1315            round_trip, original,
1316            "Round-trip batch mismatch for nested_records.avro"
1317        );
1318        Ok(())
1319    }
1320
1321    #[test]
1322    #[cfg(feature = "snappy")]
1323    fn test_roundtrip_nested_lists_writer() -> Result<(), AvroError> {
1324        let path = arrow_test_data("avro/nested_lists.snappy.avro");
1325        let rdr_file = File::open(&path).expect("open nested_lists.snappy.avro");
1326        let reader = ReaderBuilder::new()
1327            .build(BufReader::new(rdr_file))
1328            .expect("build reader for nested_lists.snappy.avro");
1329        let schema = reader.schema();
1330        let batches = reader.collect::<Result<Vec<_>, _>>()?;
1331        let original = arrow::compute::concat_batches(&schema, &batches).expect("concat original");
1332        let tmp = NamedTempFile::new().expect("create temp file");
1333        let out_path = tmp.into_temp_path();
1334        {
1335            let out_file = File::create(&out_path).expect("create output avro");
1336            let mut writer = WriterBuilder::new(original.schema().as_ref().clone())
1337                .with_compression(Some(CompressionCodec::Snappy))
1338                .build::<_, AvroOcfFormat>(out_file)?;
1339            writer.write(&original)?;
1340            writer.finish()?;
1341        }
1342        let rt_file = File::open(&out_path).expect("open round_trip avro");
1343        let rt_reader = ReaderBuilder::new()
1344            .build(BufReader::new(rt_file))
1345            .expect("build round_trip reader");
1346        let rt_schema = rt_reader.schema();
1347        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1348        let round_trip =
1349            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
1350        assert_eq!(
1351            round_trip, original,
1352            "Round-trip batch mismatch for nested_lists.snappy.avro"
1353        );
1354        Ok(())
1355    }
1356
1357    #[test]
1358    fn test_round_trip_simple_fixed_ocf() -> Result<(), AvroError> {
1359        let path = arrow_test_data("avro/simple_fixed.avro");
1360        let rdr_file = File::open(&path).expect("open avro/simple_fixed.avro");
1361        let reader = ReaderBuilder::new()
1362            .build(BufReader::new(rdr_file))
1363            .expect("build avro reader");
1364        let schema = reader.schema();
1365        let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1366        let original =
1367            arrow::compute::concat_batches(&schema, &input_batches).expect("concat input");
1368        let tmp = NamedTempFile::new().expect("create temp file");
1369        let out_file = File::create(tmp.path()).expect("create temp avro");
1370        let mut writer = AvroWriter::new(out_file, original.schema().as_ref().clone())?;
1371        writer.write(&original)?;
1372        writer.finish()?;
1373        drop(writer);
1374        let rt_file = File::open(tmp.path()).expect("open round_trip avro");
1375        let rt_reader = ReaderBuilder::new()
1376            .build(BufReader::new(rt_file))
1377            .expect("build round_trip reader");
1378        let rt_schema = rt_reader.schema();
1379        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1380        let round_trip =
1381            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
1382        assert_eq!(round_trip, original);
1383        Ok(())
1384    }
1385
1386    // Strict equality (schema + values) only when canonical extension types are enabled
1387    #[test]
1388    #[cfg(feature = "canonical_extension_types")]
1389    fn test_round_trip_duration_and_uuid_ocf() -> Result<(), AvroError> {
1390        use arrow_schema::{DataType, IntervalUnit};
1391        let in_file =
1392            File::open("test/data/duration_uuid.avro").expect("open test/data/duration_uuid.avro");
1393        let reader = ReaderBuilder::new()
1394            .build(BufReader::new(in_file))
1395            .expect("build reader for duration_uuid.avro");
1396        let in_schema = reader.schema();
1397        let has_mdn = in_schema.fields().iter().any(|f| {
1398            matches!(
1399                f.data_type(),
1400                DataType::Interval(IntervalUnit::MonthDayNano)
1401            )
1402        });
1403        assert!(
1404            has_mdn,
1405            "expected at least one Interval(MonthDayNano) field in duration_uuid.avro"
1406        );
1407        let has_uuid_fixed = in_schema
1408            .fields()
1409            .iter()
1410            .any(|f| matches!(f.data_type(), DataType::FixedSizeBinary(16)));
1411        assert!(
1412            has_uuid_fixed,
1413            "expected at least one FixedSizeBinary(16) (uuid) field in duration_uuid.avro"
1414        );
1415        let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1416        let input =
1417            arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
1418        // Write to an in‑memory OCF and read back
1419        let mut writer = AvroWriter::new(Vec::<u8>::new(), in_schema.as_ref().clone())?;
1420        writer.write(&input)?;
1421        writer.finish()?;
1422        let bytes = writer.into_inner();
1423        let rt_reader = ReaderBuilder::new()
1424            .build(Cursor::new(bytes))
1425            .expect("build round_trip reader");
1426        let rt_schema = rt_reader.schema();
1427        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1428        let round_trip =
1429            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
1430        assert_eq!(round_trip, input);
1431        Ok(())
1432    }
1433
1434    // Feature OFF: only values are asserted equal; schema may legitimately differ (uuid as fixed(16))
1435    #[test]
1436    #[cfg(not(feature = "canonical_extension_types"))]
1437    fn test_duration_and_uuid_ocf_without_extensions_round_trips_values() -> Result<(), AvroError> {
1438        use arrow::datatypes::{DataType, IntervalUnit};
1439        use std::io::BufReader;
1440
1441        // Read input Avro (duration + uuid)
1442        let in_file =
1443            File::open("test/data/duration_uuid.avro").expect("open test/data/duration_uuid.avro");
1444        let reader = ReaderBuilder::new()
1445            .build(BufReader::new(in_file))
1446            .expect("build reader for duration_uuid.avro");
1447        let in_schema = reader.schema();
1448
1449        // Sanity checks: has MonthDayNano and a FixedSizeBinary(16)
1450        assert!(
1451            in_schema.fields().iter().any(|f| {
1452                matches!(
1453                    f.data_type(),
1454                    DataType::Interval(IntervalUnit::MonthDayNano)
1455                )
1456            }),
1457            "expected at least one Interval(MonthDayNano) field"
1458        );
1459        assert!(
1460            in_schema
1461                .fields()
1462                .iter()
1463                .any(|f| matches!(f.data_type(), DataType::FixedSizeBinary(16))),
1464            "expected a FixedSizeBinary(16) field (uuid)"
1465        );
1466
1467        let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1468        let input =
1469            arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
1470
1471        // Write to a temp OCF and read back
1472        let mut writer = AvroWriter::new(Vec::<u8>::new(), in_schema.as_ref().clone())?;
1473        writer.write(&input)?;
1474        writer.finish()?;
1475        let bytes = writer.into_inner();
1476        let rt_reader = ReaderBuilder::new()
1477            .build(Cursor::new(bytes))
1478            .expect("build round_trip reader");
1479        let rt_schema = rt_reader.schema();
1480        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1481        let round_trip =
1482            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
1483
1484        // 1) Values must round-trip for both columns
1485        assert_eq!(
1486            round_trip.column(0),
1487            input.column(0),
1488            "duration column values differ"
1489        );
1490        assert_eq!(round_trip.column(1), input.column(1), "uuid bytes differ");
1491
1492        // 2) Schema expectation without extensions:
1493        //    uuid is written as named fixed(16), so reader attaches avro.name
1494        let uuid_rt = rt_schema.field_with_name("uuid_field")?;
1495        assert_eq!(uuid_rt.data_type(), &DataType::FixedSizeBinary(16));
1496        assert_eq!(
1497            uuid_rt.metadata().get("logicalType").map(|s| s.as_str()),
1498            Some("uuid"),
1499            "expected `logicalType = \"uuid\"` on round-tripped field metadata"
1500        );
1501
1502        // 3) Duration remains Interval(MonthDayNano)
1503        let dur_rt = rt_schema.field_with_name("duration_field")?;
1504        assert!(matches!(
1505            dur_rt.data_type(),
1506            DataType::Interval(IntervalUnit::MonthDayNano)
1507        ));
1508
1509        Ok(())
1510    }
1511
1512    // This test reads the same 'nonnullable.impala.avro' used by the reader tests,
1513    // writes it back out with the writer (hitting Map encoding paths), then reads it
1514    // again and asserts exact Arrow equivalence.
1515    #[test]
1516    // TODO: avoid requiring snappy for this file
1517    #[cfg(feature = "snappy")]
1518    fn test_nonnullable_impala_roundtrip_writer() -> Result<(), AvroError> {
1519        // Load source Avro with Map fields
1520        let path = arrow_test_data("avro/nonnullable.impala.avro");
1521        let rdr_file = File::open(&path).expect("open avro/nonnullable.impala.avro");
1522        let reader = ReaderBuilder::new()
1523            .build(BufReader::new(rdr_file))
1524            .expect("build reader for nonnullable.impala.avro");
1525        // Collect all input batches and concatenate to a single RecordBatch
1526        let in_schema = reader.schema();
1527        // Sanity: ensure the file actually contains at least one Map field
1528        let has_map = in_schema
1529            .fields()
1530            .iter()
1531            .any(|f| matches!(f.data_type(), DataType::Map(_, _)));
1532        assert!(
1533            has_map,
1534            "expected at least one Map field in avro/nonnullable.impala.avro"
1535        );
1536
1537        let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1538        let original =
1539            arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
1540        // Write out using the OCF writer into an in-memory Vec<u8>
1541        let buffer = Vec::<u8>::new();
1542        let mut writer = AvroWriter::new(buffer, in_schema.as_ref().clone())?;
1543        writer.write(&original)?;
1544        writer.finish()?;
1545        let out_bytes = writer.into_inner();
1546        // Read the produced bytes back with the Reader
1547        let rt_reader = ReaderBuilder::new()
1548            .build(Cursor::new(out_bytes))
1549            .expect("build reader for round-tripped in-memory OCF");
1550        let rt_schema = rt_reader.schema();
1551        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1552        let roundtrip =
1553            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
1554        // Exact value fidelity (schema + data)
1555        assert_eq!(
1556            roundtrip, original,
1557            "Round-trip Avro map data mismatch for nonnullable.impala.avro"
1558        );
1559        Ok(())
1560    }
1561
1562    #[test]
1563    // TODO: avoid requiring snappy for these files
1564    #[cfg(feature = "snappy")]
1565    fn test_roundtrip_decimals_via_writer() -> Result<(), AvroError> {
1566        // (file, resolve via ARROW_TEST_DATA?)
1567        let files: [(&str, bool); 8] = [
1568            ("avro/fixed_length_decimal.avro", true), // fixed-backed -> Decimal128(25,2)
1569            ("avro/fixed_length_decimal_legacy.avro", true), // legacy fixed[8] -> Decimal64(13,2)
1570            ("avro/int32_decimal.avro", true),        // bytes-backed -> Decimal32(4,2)
1571            ("avro/int64_decimal.avro", true),        // bytes-backed -> Decimal64(10,2)
1572            ("test/data/int256_decimal.avro", false), // bytes-backed -> Decimal256(76,2)
1573            ("test/data/fixed256_decimal.avro", false), // fixed[32]-backed -> Decimal256(76,10)
1574            ("test/data/fixed_length_decimal_legacy_32.avro", false), // legacy fixed[4] -> Decimal32(9,2)
1575            ("test/data/int128_decimal.avro", false), // bytes-backed -> Decimal128(38,2)
1576        ];
1577        for (rel, in_test_data_dir) in files {
1578            // Resolve path the same way as reader::test_decimal
1579            let path: String = if in_test_data_dir {
1580                arrow_test_data(rel)
1581            } else {
1582                PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1583                    .join(rel)
1584                    .to_string_lossy()
1585                    .into_owned()
1586            };
1587            // Read original file into a single RecordBatch for comparison
1588            let f_in = File::open(&path).expect("open input avro");
1589            let rdr = ReaderBuilder::new().build(BufReader::new(f_in))?;
1590            let in_schema = rdr.schema();
1591            let in_batches = rdr.collect::<Result<Vec<_>, _>>()?;
1592            let original =
1593                arrow::compute::concat_batches(&in_schema, &in_batches).expect("concat input");
1594            // Write it out with the OCF writer (no special compression)
1595            let tmp = NamedTempFile::new().expect("create temp file");
1596            let out_path = tmp.into_temp_path();
1597            let out_file = File::create(&out_path).expect("create temp avro");
1598            let mut writer = AvroWriter::new(out_file, original.schema().as_ref().clone())?;
1599            writer.write(&original)?;
1600            writer.finish()?;
1601            // Read back the file we just wrote and compare equality (schema + data)
1602            let f_rt = File::open(&out_path).expect("open roundtrip avro");
1603            let rt_rdr = ReaderBuilder::new().build(BufReader::new(f_rt))?;
1604            let rt_schema = rt_rdr.schema();
1605            let rt_batches = rt_rdr.collect::<Result<Vec<_>, _>>()?;
1606            let roundtrip =
1607                arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat rt");
1608            assert_eq!(roundtrip, original, "decimal round-trip mismatch for {rel}");
1609        }
1610        Ok(())
1611    }
1612
1613    #[test]
1614    fn test_named_types_complex_roundtrip() -> Result<(), AvroError> {
1615        // 1. Read the new, more complex named references file.
1616        let path =
1617            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test/data/named_types_complex.avro");
1618        let rdr_file = File::open(&path).expect("open avro/named_types_complex.avro");
1619
1620        let reader = ReaderBuilder::new()
1621            .build(BufReader::new(rdr_file))
1622            .expect("build reader for named_types_complex.avro");
1623
1624        // 2. Concatenate all batches to one RecordBatch.
1625        let in_schema = reader.schema();
1626        let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1627        let original =
1628            arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
1629
1630        // 3. Sanity Checks: Validate that all named types were reused correctly.
1631        {
1632            let arrow_schema = original.schema();
1633
1634            // --- A. Validate 'User' record reuse ---
1635            let author_field = arrow_schema.field_with_name("author")?;
1636            let author_type = author_field.data_type();
1637            let editors_field = arrow_schema.field_with_name("editors")?;
1638            let editors_item_type = match editors_field.data_type() {
1639                DataType::List(item_field) => item_field.data_type(),
1640                other => panic!("Editors field should be a List, but was {other:?}"),
1641            };
1642            assert_eq!(
1643                author_type, editors_item_type,
1644                "The DataType for the 'author' struct and the 'editors' list items must be identical"
1645            );
1646
1647            // --- B. Validate 'PostStatus' enum reuse ---
1648            let status_field = arrow_schema.field_with_name("status")?;
1649            let status_type = status_field.data_type();
1650            assert!(
1651                matches!(status_type, DataType::Dictionary(_, _)),
1652                "Status field should be a Dictionary (Enum)"
1653            );
1654
1655            let prev_status_field = arrow_schema.field_with_name("previous_status")?;
1656            let prev_status_type = prev_status_field.data_type();
1657            assert_eq!(
1658                status_type, prev_status_type,
1659                "The DataType for 'status' and 'previous_status' enums must be identical"
1660            );
1661
1662            // --- C. Validate 'MD5' fixed reuse ---
1663            let content_hash_field = arrow_schema.field_with_name("content_hash")?;
1664            let content_hash_type = content_hash_field.data_type();
1665            assert!(
1666                matches!(content_hash_type, DataType::FixedSizeBinary(16)),
1667                "Content hash should be FixedSizeBinary(16)"
1668            );
1669
1670            let thumb_hash_field = arrow_schema.field_with_name("thumbnail_hash")?;
1671            let thumb_hash_type = thumb_hash_field.data_type();
1672            assert_eq!(
1673                content_hash_type, thumb_hash_type,
1674                "The DataType for 'content_hash' and 'thumbnail_hash' fixed types must be identical"
1675            );
1676        }
1677
1678        // 4. Write the data to an in-memory buffer.
1679        let buffer: Vec<u8> = Vec::new();
1680        let mut writer = AvroWriter::new(buffer, original.schema().as_ref().clone())?;
1681        writer.write(&original)?;
1682        writer.finish()?;
1683        let bytes = writer.into_inner();
1684
1685        // 5. Read the data back and compare for exact equality.
1686        let rt_reader = ReaderBuilder::new()
1687            .build(Cursor::new(bytes))
1688            .expect("build reader for round-trip");
1689        let rt_schema = rt_reader.schema();
1690        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1691        let roundtrip =
1692            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
1693
1694        assert_eq!(
1695            roundtrip, original,
1696            "Avro complex named types round-trip mismatch"
1697        );
1698
1699        Ok(())
1700    }
1701
1702    // Union Roundtrip Test Helpers
1703
1704    // Asserts that the `actual` schema is a semantically equivalent superset of the `expected` one.
1705    // This allows the `actual` schema to contain additional metadata keys
1706    // (`arrowUnionMode`, `arrowUnionTypeIds`, `avro.name`) that are added during an Arrow-to-Avro-to-Arrow
1707    // roundtrip, while ensuring no other information was lost or changed.
1708    fn assert_schema_is_semantically_equivalent(expected: &Schema, actual: &Schema) {
1709        // Compare top-level schema metadata using the same superset logic.
1710        assert_metadata_is_superset(expected.metadata(), actual.metadata(), "Schema");
1711
1712        // Compare fields.
1713        assert_eq!(
1714            expected.fields().len(),
1715            actual.fields().len(),
1716            "Schema must have the same number of fields"
1717        );
1718
1719        for (expected_field, actual_field) in expected.fields().iter().zip(actual.fields().iter()) {
1720            assert_field_is_semantically_equivalent(expected_field, actual_field);
1721        }
1722    }
1723
1724    fn assert_field_is_semantically_equivalent(expected: &Field, actual: &Field) {
1725        let context = format!("Field '{}'", expected.name());
1726
1727        assert_eq!(
1728            expected.name(),
1729            actual.name(),
1730            "{context}: names must match"
1731        );
1732        assert_eq!(
1733            expected.is_nullable(),
1734            actual.is_nullable(),
1735            "{context}: nullability must match"
1736        );
1737
1738        // Recursively check the data types.
1739        assert_datatype_is_semantically_equivalent(
1740            expected.data_type(),
1741            actual.data_type(),
1742            &context,
1743        );
1744
1745        // Check that metadata is a valid superset.
1746        assert_metadata_is_superset(expected.metadata(), actual.metadata(), &context);
1747    }
1748
1749    fn assert_datatype_is_semantically_equivalent(
1750        expected: &DataType,
1751        actual: &DataType,
1752        context: &str,
1753    ) {
1754        match (expected, actual) {
1755            (DataType::List(expected_field), DataType::List(actual_field))
1756            | (DataType::LargeList(expected_field), DataType::LargeList(actual_field))
1757            | (DataType::Map(expected_field, _), DataType::Map(actual_field, _)) => {
1758                assert_field_is_semantically_equivalent(expected_field, actual_field);
1759            }
1760            (DataType::Struct(expected_fields), DataType::Struct(actual_fields)) => {
1761                assert_eq!(
1762                    expected_fields.len(),
1763                    actual_fields.len(),
1764                    "{context}: struct must have same number of fields"
1765                );
1766                for (ef, af) in expected_fields.iter().zip(actual_fields.iter()) {
1767                    assert_field_is_semantically_equivalent(ef, af);
1768                }
1769            }
1770            (
1771                DataType::Union(expected_fields, expected_mode),
1772                DataType::Union(actual_fields, actual_mode),
1773            ) => {
1774                assert_eq!(
1775                    expected_mode, actual_mode,
1776                    "{context}: union mode must match"
1777                );
1778                assert_eq!(
1779                    expected_fields.len(),
1780                    actual_fields.len(),
1781                    "{context}: union must have same number of variants"
1782                );
1783                for ((exp_id, exp_field), (act_id, act_field)) in
1784                    expected_fields.iter().zip(actual_fields.iter())
1785                {
1786                    assert_eq!(exp_id, act_id, "{context}: union type ids must match");
1787                    assert_field_is_semantically_equivalent(exp_field, act_field);
1788                }
1789            }
1790            _ => {
1791                assert_eq!(expected, actual, "{context}: data types must be identical");
1792            }
1793        }
1794    }
1795
1796    fn assert_batch_data_is_identical(expected: &RecordBatch, actual: &RecordBatch) {
1797        assert_eq!(
1798            expected.num_columns(),
1799            actual.num_columns(),
1800            "RecordBatches must have the same number of columns"
1801        );
1802        assert_eq!(
1803            expected.num_rows(),
1804            actual.num_rows(),
1805            "RecordBatches must have the same number of rows"
1806        );
1807
1808        for i in 0..expected.num_columns() {
1809            let context = format!("Column {i}");
1810            let expected_col = expected.column(i);
1811            let actual_col = actual.column(i);
1812            assert_array_data_is_identical(expected_col, actual_col, &context);
1813        }
1814    }
1815
1816    /// Recursively asserts that the data content of two Arrays is identical.
1817    fn assert_array_data_is_identical(expected: &dyn Array, actual: &dyn Array, context: &str) {
1818        assert_eq!(
1819            expected.nulls(),
1820            actual.nulls(),
1821            "{context}: null buffers must match"
1822        );
1823        assert_eq!(
1824            expected.len(),
1825            actual.len(),
1826            "{context}: array lengths must match"
1827        );
1828
1829        match (expected.data_type(), actual.data_type()) {
1830            (DataType::Union(expected_fields, _), DataType::Union(..)) => {
1831                let expected_union = expected.as_any().downcast_ref::<UnionArray>().unwrap();
1832                let actual_union = actual.as_any().downcast_ref::<UnionArray>().unwrap();
1833
1834                // Compare the type_ids buffer (always the first buffer).
1835                assert_eq!(
1836                    &expected.to_data().buffers()[0],
1837                    &actual.to_data().buffers()[0],
1838                    "{context}: union type_ids buffer mismatch"
1839                );
1840
1841                // For dense unions, compare the value_offsets buffer (the second buffer).
1842                if expected.to_data().buffers().len() > 1 {
1843                    assert_eq!(
1844                        &expected.to_data().buffers()[1],
1845                        &actual.to_data().buffers()[1],
1846                        "{context}: union value_offsets buffer mismatch"
1847                    );
1848                }
1849
1850                // Recursively compare children based on the fields in the DataType.
1851                for (type_id, _) in expected_fields.iter() {
1852                    let child_context = format!("{context} -> child variant {type_id}");
1853                    assert_array_data_is_identical(
1854                        expected_union.child(type_id),
1855                        actual_union.child(type_id),
1856                        &child_context,
1857                    );
1858                }
1859            }
1860            (DataType::Struct(_), DataType::Struct(_)) => {
1861                let expected_struct = expected.as_any().downcast_ref::<StructArray>().unwrap();
1862                let actual_struct = actual.as_any().downcast_ref::<StructArray>().unwrap();
1863                for i in 0..expected_struct.num_columns() {
1864                    let child_context = format!("{context} -> struct child {i}");
1865                    assert_array_data_is_identical(
1866                        expected_struct.column(i),
1867                        actual_struct.column(i),
1868                        &child_context,
1869                    );
1870                }
1871            }
1872            // Fallback for primitive types and other types where buffer comparison is sufficient.
1873            _ => {
1874                assert_eq!(
1875                    expected.to_data().buffers(),
1876                    actual.to_data().buffers(),
1877                    "{context}: data buffers must match"
1878                );
1879            }
1880        }
1881    }
1882
1883    /// Checks that `actual_meta` contains all of `expected_meta`, and any additional
1884    /// keys in `actual_meta` are from a permitted set.
1885    fn assert_metadata_is_superset(
1886        expected_meta: &arrow_schema::Metadata,
1887        actual_meta: &arrow_schema::Metadata,
1888        context: &str,
1889    ) {
1890        let allowed_additions: HashSet<&str> =
1891            vec!["arrowUnionMode", "arrowUnionTypeIds", "avro.name"]
1892                .into_iter()
1893                .collect();
1894        for (key, expected_value) in expected_meta {
1895            match actual_meta.get(key) {
1896                Some(actual_value) => assert_eq!(
1897                    expected_value, actual_value,
1898                    "{context}: preserved metadata for key '{key}' must have the same value"
1899                ),
1900                None => panic!("{context}: metadata key '{key}' was lost during roundtrip"),
1901            }
1902        }
1903        for key in actual_meta.keys() {
1904            if !expected_meta.contains_key(key) && !allowed_additions.contains(key.as_str()) {
1905                panic!("{context}: unexpected metadata key '{key}' was added during roundtrip");
1906            }
1907        }
1908    }
1909
1910    #[test]
1911    fn test_union_roundtrip() -> Result<(), AvroError> {
1912        let file_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1913            .join("test/data/union_fields.avro")
1914            .to_string_lossy()
1915            .into_owned();
1916        let rdr_file = File::open(&file_path).expect("open avro/union_fields.avro");
1917        let reader = ReaderBuilder::new()
1918            .build(BufReader::new(rdr_file))
1919            .expect("build reader for union_fields.avro");
1920        let schema = reader.schema();
1921        let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1922        let original =
1923            arrow::compute::concat_batches(&schema, &input_batches).expect("concat input");
1924        let mut writer = AvroWriter::new(Vec::<u8>::new(), original.schema().as_ref().clone())?;
1925        writer.write(&original)?;
1926        writer.finish()?;
1927        let bytes = writer.into_inner();
1928        let rt_reader = ReaderBuilder::new()
1929            .build(Cursor::new(bytes))
1930            .expect("build round_trip reader");
1931        let rt_schema = rt_reader.schema();
1932        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1933        let round_trip =
1934            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
1935
1936        // The nature of the crate is such that metadata gets appended during the roundtrip,
1937        // so we can't compare the schemas directly. Instead, we semantically compare the schemas and data.
1938        assert_schema_is_semantically_equivalent(&original.schema(), &round_trip.schema());
1939
1940        assert_batch_data_is_identical(&original, &round_trip);
1941        Ok(())
1942    }
1943
1944    #[test]
1945    fn test_enum_roundtrip_uses_reader_fixture() -> Result<(), AvroError> {
1946        // Read the known-good enum file (same as reader::test_simple)
1947        let path = arrow_test_data("avro/simple_enum.avro");
1948        let rdr_file = File::open(&path).expect("open avro/simple_enum.avro");
1949        let reader = ReaderBuilder::new()
1950            .build(BufReader::new(rdr_file))
1951            .expect("build reader for simple_enum.avro");
1952        // Concatenate all batches to one RecordBatch for a clean equality check
1953        let in_schema = reader.schema();
1954        let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1955        let original =
1956            arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
1957        // Sanity: expect at least one Dictionary(Int32, Utf8) column (enum)
1958        let has_enum_dict = in_schema.fields().iter().any(|f| {
1959            matches!(
1960                f.data_type(),
1961                DataType::Dictionary(k, v) if **k == DataType::Int32 && **v == DataType::Utf8
1962            )
1963        });
1964        assert!(
1965            has_enum_dict,
1966            "Expected at least one enum-mapped Dictionary<Int32, Utf8> field"
1967        );
1968        // Write with OCF writer into memory using the reader-provided Arrow schema.
1969        // The writer will embed the Avro JSON from `avro.schema` metadata if present.
1970        let buffer: Vec<u8> = Vec::new();
1971        let mut writer = AvroWriter::new(buffer, in_schema.as_ref().clone())?;
1972        writer.write(&original)?;
1973        writer.finish()?;
1974        let bytes = writer.into_inner();
1975        // Read back and compare for exact equality (schema + data)
1976        let rt_reader = ReaderBuilder::new()
1977            .build(Cursor::new(bytes))
1978            .expect("reader for round-trip");
1979        let rt_schema = rt_reader.schema();
1980        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1981        let roundtrip =
1982            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
1983        assert_eq!(roundtrip, original, "Avro enum round-trip mismatch");
1984        Ok(())
1985    }
1986
1987    #[test]
1988    fn test_builder_propagates_capacity_to_writer() -> Result<(), AvroError> {
1989        let cap = 64 * 1024;
1990        let buffer = Vec::<u8>::new();
1991        let mut writer = WriterBuilder::new(make_schema())
1992            .with_capacity(cap)
1993            .build::<_, AvroOcfFormat>(buffer)?;
1994        assert_eq!(writer.capacity, cap, "builder capacity not propagated");
1995        let batch = make_batch();
1996        writer.write(&batch)?;
1997        writer.finish()?;
1998        let out = writer.into_inner();
1999        assert_eq!(&out[..4], b"Obj\x01", "OCF magic missing/incorrect");
2000        Ok(())
2001    }
2002
2003    #[test]
2004    fn test_stream_writer_stores_capacity_direct_writes() -> Result<(), AvroError> {
2005        use arrow_array::{ArrayRef, Int32Array};
2006        use arrow_schema::{DataType, Field, Schema};
2007        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
2008        let batch = RecordBatch::try_new(
2009            Arc::new(schema.clone()),
2010            vec![Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef],
2011        )?;
2012        let cap = 8192;
2013        let mut writer = WriterBuilder::new(schema)
2014            .with_capacity(cap)
2015            .build::<_, AvroSoeFormat>(Vec::new())?;
2016        assert_eq!(writer.capacity, cap);
2017        writer.write(&batch)?;
2018        let _bytes = writer.into_inner();
2019        Ok(())
2020    }
2021
2022    #[cfg(feature = "avro_custom_types")]
2023    #[test]
2024    fn test_roundtrip_duration_logical_types_ocf() -> Result<(), AvroError> {
2025        let file_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2026            .join("test/data/duration_logical_types.avro")
2027            .to_string_lossy()
2028            .into_owned();
2029
2030        let in_file = File::open(&file_path)
2031            .unwrap_or_else(|_| panic!("Failed to open test file: {file_path}"));
2032
2033        let reader = ReaderBuilder::new()
2034            .build(BufReader::new(in_file))
2035            .expect("build reader for duration_logical_types.avro");
2036        let in_schema = reader.schema();
2037
2038        let expected_units: HashSet<TimeUnit> = [
2039            TimeUnit::Nanosecond,
2040            TimeUnit::Microsecond,
2041            TimeUnit::Millisecond,
2042            TimeUnit::Second,
2043        ]
2044        .into_iter()
2045        .collect();
2046
2047        let found_units: HashSet<TimeUnit> = in_schema
2048            .fields()
2049            .iter()
2050            .filter_map(|f| match f.data_type() {
2051                DataType::Duration(unit) => Some(*unit),
2052                _ => None,
2053            })
2054            .collect();
2055
2056        assert_eq!(
2057            found_units, expected_units,
2058            "Expected to find all four Duration TimeUnits in the schema from the initial read"
2059        );
2060
2061        let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
2062        let input =
2063            arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
2064
2065        let tmp = NamedTempFile::new().expect("create temp file");
2066        {
2067            let out_file = File::create(tmp.path()).expect("create temp avro");
2068            let mut writer = AvroWriter::new(out_file, in_schema.as_ref().clone())?;
2069            writer.write(&input)?;
2070            writer.finish()?;
2071        }
2072
2073        let rt_file = File::open(tmp.path()).expect("open round_trip avro");
2074        let rt_reader = ReaderBuilder::new()
2075            .build(BufReader::new(rt_file))
2076            .expect("build round_trip reader");
2077        let rt_schema = rt_reader.schema();
2078        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2079        let round_trip =
2080            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
2081
2082        assert_eq!(round_trip, input);
2083        Ok(())
2084    }
2085
2086    #[cfg(feature = "avro_custom_types")]
2087    #[test]
2088    fn test_run_end_encoded_roundtrip_writer() -> Result<(), AvroError> {
2089        let run_ends = Int32Array::from(vec![3, 5, 7, 8]);
2090        let run_values = Int32Array::from(vec![Some(1), Some(2), None, Some(3)]);
2091        let ree = RunArray::<Int32Type>::try_new(&run_ends, &run_values)?;
2092        let field = Field::new("x", ree.data_type().clone(), true);
2093        let schema = Schema::new(vec![field]);
2094        let batch = RecordBatch::try_new(
2095            Arc::new(schema.clone()),
2096            vec![Arc::new(ree.clone()) as ArrayRef],
2097        )?;
2098        let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2099        writer.write(&batch)?;
2100        writer.finish()?;
2101        let bytes = writer.into_inner();
2102        let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2103        let out_schema = reader.schema();
2104        let batches = reader.collect::<Result<Vec<_>, _>>()?;
2105        let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2106        assert_eq!(out.num_columns(), 1);
2107        assert_eq!(out.num_rows(), 8);
2108        match out.schema().field(0).data_type() {
2109            DataType::RunEndEncoded(run_ends_field, values_field) => {
2110                assert_eq!(
2111                    run_ends_field.name(),
2112                    Field::REE_RUN_ENDS_FIELD_DEFAULT_NAME
2113                );
2114                assert_eq!(run_ends_field.data_type(), &DataType::Int32);
2115                assert_eq!(values_field.name(), Field::REE_VALUES_FIELD_DEFAULT_NAME);
2116                assert_eq!(values_field.data_type(), &DataType::Int32);
2117                assert!(values_field.is_nullable());
2118                let got_ree = out
2119                    .column(0)
2120                    .as_any()
2121                    .downcast_ref::<RunArray<Int32Type>>()
2122                    .expect("RunArray<Int32Type>");
2123                assert_eq!(got_ree, &ree);
2124            }
2125            other => {
2126                panic!("Unexpected DataType for round-tripped RunEndEncoded column: {other:?}")
2127            }
2128        }
2129        Ok(())
2130    }
2131
2132    #[cfg(feature = "avro_custom_types")]
2133    #[test]
2134    fn test_run_end_encoded_string_values_int16_run_ends_roundtrip_writer() -> Result<(), AvroError>
2135    {
2136        let run_ends = Int16Array::from(vec![2, 5, 7]); // end indices
2137        let run_values = StringArray::from(vec![Some("a"), None, Some("c")]);
2138        let ree = RunArray::<Int16Type>::try_new(&run_ends, &run_values)?;
2139        let field = Field::new("s", ree.data_type().clone(), true);
2140        let schema = Schema::new(vec![field]);
2141        let batch = RecordBatch::try_new(
2142            Arc::new(schema.clone()),
2143            vec![Arc::new(ree.clone()) as ArrayRef],
2144        )?;
2145        let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2146        writer.write(&batch)?;
2147        writer.finish()?;
2148        let bytes = writer.into_inner();
2149        let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2150        let out_schema = reader.schema();
2151        let batches = reader.collect::<Result<Vec<_>, _>>()?;
2152        let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2153        assert_eq!(out.num_columns(), 1);
2154        assert_eq!(out.num_rows(), 7);
2155        match out.schema().field(0).data_type() {
2156            DataType::RunEndEncoded(run_ends_field, values_field) => {
2157                assert_eq!(run_ends_field.data_type(), &DataType::Int16);
2158                assert_eq!(values_field.data_type(), &DataType::Utf8);
2159                assert!(
2160                    values_field.is_nullable(),
2161                    "REE 'values' child should be nullable"
2162                );
2163                let got = out
2164                    .column(0)
2165                    .as_any()
2166                    .downcast_ref::<RunArray<Int16Type>>()
2167                    .expect("RunArray<Int16Type>");
2168                assert_eq!(got, &ree);
2169            }
2170            other => panic!("Unexpected DataType: {other:?}"),
2171        }
2172        Ok(())
2173    }
2174
2175    #[cfg(feature = "avro_custom_types")]
2176    #[test]
2177    fn test_run_end_encoded_int64_run_ends_numeric_values_roundtrip_writer() -> Result<(), AvroError>
2178    {
2179        let run_ends = Int64Array::from(vec![4_i64, 8_i64]);
2180        let run_values = Int32Array::from(vec![Some(999), Some(-5)]);
2181        let ree = RunArray::<Int64Type>::try_new(&run_ends, &run_values)?;
2182        let field = Field::new("y", ree.data_type().clone(), true);
2183        let schema = Schema::new(vec![field]);
2184        let batch = RecordBatch::try_new(
2185            Arc::new(schema.clone()),
2186            vec![Arc::new(ree.clone()) as ArrayRef],
2187        )?;
2188        let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2189        writer.write(&batch)?;
2190        writer.finish()?;
2191        let bytes = writer.into_inner();
2192        let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2193        let out_schema = reader.schema();
2194        let batches = reader.collect::<Result<Vec<_>, _>>()?;
2195        let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2196        assert_eq!(out.num_columns(), 1);
2197        assert_eq!(out.num_rows(), 8);
2198        match out.schema().field(0).data_type() {
2199            DataType::RunEndEncoded(run_ends_field, values_field) => {
2200                assert_eq!(run_ends_field.data_type(), &DataType::Int64);
2201                assert_eq!(values_field.data_type(), &DataType::Int32);
2202                assert!(values_field.is_nullable());
2203                let got = out
2204                    .column(0)
2205                    .as_any()
2206                    .downcast_ref::<RunArray<Int64Type>>()
2207                    .expect("RunArray<Int64Type>");
2208                assert_eq!(got, &ree);
2209            }
2210            other => panic!("Unexpected DataType for REE column: {other:?}"),
2211        }
2212        Ok(())
2213    }
2214
2215    #[cfg(feature = "avro_custom_types")]
2216    #[test]
2217    fn test_run_end_encoded_sliced_roundtrip_writer() -> Result<(), AvroError> {
2218        let run_ends = Int32Array::from(vec![3, 5, 7, 8]);
2219        let run_values = Int32Array::from(vec![Some(1), Some(2), None, Some(3)]);
2220        let base = RunArray::<Int32Type>::try_new(&run_ends, &run_values)?;
2221        let offset = 1usize;
2222        let length = 6usize;
2223        let base_values = base.values().as_primitive::<Int32Type>();
2224        let mut logical_window: Vec<Option<i32>> = Vec::with_capacity(length);
2225        for i in offset..offset + length {
2226            let phys = base.get_physical_index(i);
2227            let v = if base_values.is_null(phys) {
2228                None
2229            } else {
2230                Some(base_values.value(phys))
2231            };
2232            logical_window.push(v);
2233        }
2234
2235        fn compress_run_ends_i32(vals: &[Option<i32>]) -> (Int32Array, Int32Array) {
2236            if vals.is_empty() {
2237                return (Int32Array::new_null(0), Int32Array::new_null(0));
2238            }
2239            let mut run_ends_out: Vec<i32> = Vec::new();
2240            let mut run_vals_out: Vec<Option<i32>> = Vec::new();
2241            let mut cur = vals[0];
2242            let mut len = 1i32;
2243            for v in &vals[1..] {
2244                if *v == cur {
2245                    len += 1;
2246                } else {
2247                    let last_end = run_ends_out.last().copied().unwrap_or(0);
2248                    run_ends_out.push(last_end + len);
2249                    run_vals_out.push(cur);
2250                    cur = *v;
2251                    len = 1;
2252                }
2253            }
2254            let last_end = run_ends_out.last().copied().unwrap_or(0);
2255            run_ends_out.push(last_end + len);
2256            run_vals_out.push(cur);
2257            (
2258                Int32Array::from(run_ends_out),
2259                Int32Array::from(run_vals_out),
2260            )
2261        }
2262        let (owned_run_ends, owned_run_values) = compress_run_ends_i32(&logical_window);
2263        let owned_slice = RunArray::<Int32Type>::try_new(&owned_run_ends, &owned_run_values)?;
2264        let field = Field::new("x", owned_slice.data_type().clone(), true);
2265        let schema = Schema::new(vec![field]);
2266        let batch = RecordBatch::try_new(
2267            Arc::new(schema.clone()),
2268            vec![Arc::new(owned_slice.clone()) as ArrayRef],
2269        )?;
2270        let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2271        writer.write(&batch)?;
2272        writer.finish()?;
2273        let bytes = writer.into_inner();
2274        let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2275        let out_schema = reader.schema();
2276        let batches = reader.collect::<Result<Vec<_>, _>>()?;
2277        let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2278        assert_eq!(out.num_columns(), 1);
2279        assert_eq!(out.num_rows(), length);
2280        match out.schema().field(0).data_type() {
2281            DataType::RunEndEncoded(run_ends_field, values_field) => {
2282                assert_eq!(run_ends_field.data_type(), &DataType::Int32);
2283                assert_eq!(values_field.data_type(), &DataType::Int32);
2284                assert!(values_field.is_nullable());
2285                let got = out
2286                    .column(0)
2287                    .as_any()
2288                    .downcast_ref::<RunArray<Int32Type>>()
2289                    .expect("RunArray<Int32Type>");
2290                fn expand_ree_to_int32(a: &RunArray<Int32Type>) -> Int32Array {
2291                    let vals = a.values().as_primitive::<Int32Type>();
2292                    let mut out: Vec<Option<i32>> = Vec::with_capacity(a.len());
2293                    for i in 0..a.len() {
2294                        let phys = a.get_physical_index(i);
2295                        out.push(if vals.is_null(phys) {
2296                            None
2297                        } else {
2298                            Some(vals.value(phys))
2299                        });
2300                    }
2301                    Int32Array::from(out)
2302                }
2303                let got_logical = expand_ree_to_int32(got);
2304                let expected_logical = Int32Array::from(logical_window);
2305                assert_eq!(
2306                    got_logical, expected_logical,
2307                    "Logical values differ after REE slice round-trip"
2308                );
2309            }
2310            other => panic!("Unexpected DataType for REE column: {other:?}"),
2311        }
2312        Ok(())
2313    }
2314
2315    #[cfg(not(feature = "avro_custom_types"))]
2316    #[test]
2317    fn test_run_end_encoded_roundtrip_writer_feature_off() -> Result<(), AvroError> {
2318        use arrow_schema::{DataType, Field, Schema};
2319        let run_ends = arrow_array::Int32Array::from(vec![3, 5, 7, 8]);
2320        let run_values = arrow_array::Int32Array::from(vec![Some(1), Some(2), None, Some(3)]);
2321        let ree = arrow_array::RunArray::<arrow_array::types::Int32Type>::try_new(
2322            &run_ends,
2323            &run_values,
2324        )?;
2325        let field = Field::new("x", ree.data_type().clone(), true);
2326        let schema = Schema::new(vec![field]);
2327        let batch =
2328            RecordBatch::try_new(Arc::new(schema.clone()), vec![Arc::new(ree) as ArrayRef])?;
2329        let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2330        writer.write(&batch)?;
2331        writer.finish()?;
2332        let bytes = writer.into_inner();
2333        let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2334        let out_schema = reader.schema();
2335        let batches = reader.collect::<Result<Vec<_>, _>>()?;
2336        let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2337        assert_eq!(out.num_columns(), 1);
2338        assert_eq!(out.num_rows(), 8);
2339        assert_eq!(out.schema().field(0).data_type(), &DataType::Int32);
2340        let got = out.column(0).as_primitive::<Int32Type>();
2341        let expected = Int32Array::from(vec![
2342            Some(1),
2343            Some(1),
2344            Some(1),
2345            Some(2),
2346            Some(2),
2347            None,
2348            None,
2349            Some(3),
2350        ]);
2351        assert_eq!(got, &expected);
2352        Ok(())
2353    }
2354
2355    #[cfg(not(feature = "avro_custom_types"))]
2356    #[test]
2357    fn test_run_end_encoded_string_values_int16_run_ends_roundtrip_writer_feature_off()
2358    -> Result<(), AvroError> {
2359        use arrow_schema::{DataType, Field, Schema};
2360        let run_ends = arrow_array::Int16Array::from(vec![2, 5, 7]);
2361        let run_values = arrow_array::StringArray::from(vec![Some("a"), None, Some("c")]);
2362        let ree = arrow_array::RunArray::<arrow_array::types::Int16Type>::try_new(
2363            &run_ends,
2364            &run_values,
2365        )?;
2366        let field = Field::new("s", ree.data_type().clone(), true);
2367        let schema = Schema::new(vec![field]);
2368        let batch =
2369            RecordBatch::try_new(Arc::new(schema.clone()), vec![Arc::new(ree) as ArrayRef])?;
2370        let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2371        writer.write(&batch)?;
2372        writer.finish()?;
2373        let bytes = writer.into_inner();
2374        let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2375        let out_schema = reader.schema();
2376        let batches = reader.collect::<Result<Vec<_>, _>>()?;
2377        let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2378        assert_eq!(out.num_columns(), 1);
2379        assert_eq!(out.num_rows(), 7);
2380        assert_eq!(out.schema().field(0).data_type(), &DataType::Utf8);
2381        let got = out
2382            .column(0)
2383            .as_any()
2384            .downcast_ref::<arrow_array::StringArray>()
2385            .expect("StringArray");
2386        let expected = arrow_array::StringArray::from(vec![
2387            Some("a"),
2388            Some("a"),
2389            None,
2390            None,
2391            None,
2392            Some("c"),
2393            Some("c"),
2394        ]);
2395        assert_eq!(got, &expected);
2396        Ok(())
2397    }
2398
2399    #[cfg(not(feature = "avro_custom_types"))]
2400    #[test]
2401    fn test_run_end_encoded_int64_run_ends_numeric_values_roundtrip_writer_feature_off()
2402    -> Result<(), AvroError> {
2403        use arrow_schema::{DataType, Field, Schema};
2404        let run_ends = arrow_array::Int64Array::from(vec![4_i64, 8_i64]);
2405        let run_values = Int32Array::from(vec![Some(999), Some(-5)]);
2406        let ree = arrow_array::RunArray::<arrow_array::types::Int64Type>::try_new(
2407            &run_ends,
2408            &run_values,
2409        )?;
2410        let field = Field::new("y", ree.data_type().clone(), true);
2411        let schema = Schema::new(vec![field]);
2412        let batch =
2413            RecordBatch::try_new(Arc::new(schema.clone()), vec![Arc::new(ree) as ArrayRef])?;
2414        let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2415        writer.write(&batch)?;
2416        writer.finish()?;
2417        let bytes = writer.into_inner();
2418        let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2419        let out_schema = reader.schema();
2420        let batches = reader.collect::<Result<Vec<_>, _>>()?;
2421        let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2422        assert_eq!(out.num_columns(), 1);
2423        assert_eq!(out.num_rows(), 8);
2424        assert_eq!(out.schema().field(0).data_type(), &DataType::Int32);
2425        let got = out.column(0).as_primitive::<Int32Type>();
2426        let expected = Int32Array::from(vec![
2427            Some(999),
2428            Some(999),
2429            Some(999),
2430            Some(999),
2431            Some(-5),
2432            Some(-5),
2433            Some(-5),
2434            Some(-5),
2435        ]);
2436        assert_eq!(got, &expected);
2437        Ok(())
2438    }
2439
2440    #[cfg(not(feature = "avro_custom_types"))]
2441    #[test]
2442    fn test_run_end_encoded_sliced_roundtrip_writer_feature_off() -> Result<(), AvroError> {
2443        use arrow_schema::{DataType, Field, Schema};
2444        let run_ends = Int32Array::from(vec![2, 4, 6]);
2445        let run_values = Int32Array::from(vec![Some(1), Some(2), None]);
2446        let ree = arrow_array::RunArray::<arrow_array::types::Int32Type>::try_new(
2447            &run_ends,
2448            &run_values,
2449        )?;
2450        let field = Field::new("x", ree.data_type().clone(), true);
2451        let schema = Schema::new(vec![field]);
2452        let batch =
2453            RecordBatch::try_new(Arc::new(schema.clone()), vec![Arc::new(ree) as ArrayRef])?;
2454        let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2455        writer.write(&batch)?;
2456        writer.finish()?;
2457        let bytes = writer.into_inner();
2458        let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2459        let out_schema = reader.schema();
2460        let batches = reader.collect::<Result<Vec<_>, _>>()?;
2461        let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2462        assert_eq!(out.num_columns(), 1);
2463        assert_eq!(out.num_rows(), 6);
2464        assert_eq!(out.schema().field(0).data_type(), &DataType::Int32);
2465        let got = out.column(0).as_primitive::<Int32Type>();
2466        let expected = Int32Array::from(vec![Some(1), Some(1), Some(2), Some(2), None, None]);
2467        assert_eq!(got, &expected);
2468        Ok(())
2469    }
2470
2471    #[test]
2472    // TODO: avoid requiring snappy for this file
2473    #[cfg(feature = "snappy")]
2474    fn test_nullable_impala_roundtrip() -> Result<(), AvroError> {
2475        let path = arrow_test_data("avro/nullable.impala.avro");
2476        let rdr_file = File::open(&path).expect("open avro/nullable.impala.avro");
2477        let reader = ReaderBuilder::new()
2478            .build(BufReader::new(rdr_file))
2479            .expect("build reader for nullable.impala.avro");
2480        let in_schema = reader.schema();
2481        assert!(
2482            in_schema.fields().iter().any(|f| f.is_nullable()),
2483            "expected at least one nullable field in avro/nullable.impala.avro"
2484        );
2485        let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
2486        let original =
2487            arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
2488        let buffer: Vec<u8> = Vec::new();
2489        let mut writer = AvroWriter::new(buffer, in_schema.as_ref().clone())?;
2490        writer.write(&original)?;
2491        writer.finish()?;
2492        let out_bytes = writer.into_inner();
2493        let rt_reader = ReaderBuilder::new()
2494            .build(Cursor::new(out_bytes))
2495            .expect("build reader for round-tripped in-memory OCF");
2496        let rt_schema = rt_reader.schema();
2497        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2498        let roundtrip =
2499            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
2500        assert_eq!(
2501            roundtrip, original,
2502            "Round-trip Avro data mismatch for nullable.impala.avro"
2503        );
2504        Ok(())
2505    }
2506
2507    #[test]
2508    #[cfg(feature = "snappy")]
2509    fn test_datapage_v2_roundtrip() -> Result<(), AvroError> {
2510        let path = arrow_test_data("avro/datapage_v2.snappy.avro");
2511        let rdr_file = File::open(&path).expect("open avro/datapage_v2.snappy.avro");
2512        let reader = ReaderBuilder::new()
2513            .build(BufReader::new(rdr_file))
2514            .expect("build reader for datapage_v2.snappy.avro");
2515        let in_schema = reader.schema();
2516        let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
2517        let original =
2518            arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
2519        let mut writer = AvroWriter::new(Vec::<u8>::new(), in_schema.as_ref().clone())?;
2520        writer.write(&original)?;
2521        writer.finish()?;
2522        let bytes = writer.into_inner();
2523        let rt_reader = ReaderBuilder::new()
2524            .build(Cursor::new(bytes))
2525            .expect("build round-trip reader");
2526        let rt_schema = rt_reader.schema();
2527        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2528        let round_trip =
2529            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
2530        assert_eq!(
2531            round_trip, original,
2532            "Round-trip batch mismatch for datapage_v2.snappy.avro"
2533        );
2534        Ok(())
2535    }
2536
2537    #[test]
2538    #[cfg(feature = "snappy")]
2539    fn test_single_nan_roundtrip() -> Result<(), AvroError> {
2540        let path = arrow_test_data("avro/single_nan.avro");
2541        let in_file = File::open(&path).expect("open avro/single_nan.avro");
2542        let reader = ReaderBuilder::new()
2543            .build(BufReader::new(in_file))
2544            .expect("build reader for single_nan.avro");
2545        let in_schema = reader.schema();
2546        let in_batches = reader.collect::<Result<Vec<_>, _>>()?;
2547        let original =
2548            arrow::compute::concat_batches(&in_schema, &in_batches).expect("concat input");
2549        let mut writer = AvroWriter::new(Vec::<u8>::new(), original.schema().as_ref().clone())?;
2550        writer.write(&original)?;
2551        writer.finish()?;
2552        let bytes = writer.into_inner();
2553        let rt_reader = ReaderBuilder::new()
2554            .build(Cursor::new(bytes))
2555            .expect("build round_trip reader");
2556        let rt_schema = rt_reader.schema();
2557        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2558        let round_trip =
2559            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
2560        assert_eq!(
2561            round_trip, original,
2562            "Round-trip batch mismatch for avro/single_nan.avro"
2563        );
2564        Ok(())
2565    }
2566    #[test]
2567    // TODO: avoid requiring snappy for this file
2568    #[cfg(feature = "snappy")]
2569    fn test_dict_pages_offset_zero_roundtrip() -> Result<(), AvroError> {
2570        let path = arrow_test_data("avro/dict-page-offset-zero.avro");
2571        let rdr_file = File::open(&path).expect("open avro/dict-page-offset-zero.avro");
2572        let reader = ReaderBuilder::new()
2573            .build(BufReader::new(rdr_file))
2574            .expect("build reader for dict-page-offset-zero.avro");
2575        let in_schema = reader.schema();
2576        let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
2577        let original =
2578            arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
2579        let buffer: Vec<u8> = Vec::new();
2580        let mut writer = AvroWriter::new(buffer, original.schema().as_ref().clone())?;
2581        writer.write(&original)?;
2582        writer.finish()?;
2583        let bytes = writer.into_inner();
2584        let rt_reader = ReaderBuilder::new()
2585            .build(Cursor::new(bytes))
2586            .expect("build reader for round-trip");
2587        let rt_schema = rt_reader.schema();
2588        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2589        let roundtrip =
2590            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
2591        assert_eq!(
2592            roundtrip, original,
2593            "Round-trip batch mismatch for avro/dict-page-offset-zero.avro"
2594        );
2595        Ok(())
2596    }
2597
2598    #[test]
2599    #[cfg(feature = "snappy")]
2600    fn test_repeated_no_annotation_roundtrip() -> Result<(), AvroError> {
2601        let path = arrow_test_data("avro/repeated_no_annotation.avro");
2602        let in_file = File::open(&path).expect("open avro/repeated_no_annotation.avro");
2603        let reader = ReaderBuilder::new()
2604            .build(BufReader::new(in_file))
2605            .expect("build reader for repeated_no_annotation.avro");
2606        let in_schema = reader.schema();
2607        let in_batches = reader.collect::<Result<Vec<_>, _>>()?;
2608        let original =
2609            arrow::compute::concat_batches(&in_schema, &in_batches).expect("concat input");
2610        let mut writer = AvroWriter::new(Vec::<u8>::new(), original.schema().as_ref().clone())?;
2611        writer.write(&original)?;
2612        writer.finish()?;
2613        let bytes = writer.into_inner();
2614        let rt_reader = ReaderBuilder::new()
2615            .build(Cursor::new(bytes))
2616            .expect("build reader for round-trip buffer");
2617        let rt_schema = rt_reader.schema();
2618        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2619        let round_trip =
2620            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round-trip");
2621        assert_eq!(
2622            round_trip, original,
2623            "Round-trip batch mismatch for avro/repeated_no_annotation.avro"
2624        );
2625        Ok(())
2626    }
2627
2628    #[test]
2629    fn test_nested_record_type_reuse_roundtrip() -> Result<(), AvroError> {
2630        let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2631            .join("test/data/nested_record_reuse.avro")
2632            .to_string_lossy()
2633            .into_owned();
2634        let in_file = File::open(&path).expect("open avro/nested_record_reuse.avro");
2635        let reader = ReaderBuilder::new()
2636            .build(BufReader::new(in_file))
2637            .expect("build reader for nested_record_reuse.avro");
2638        let in_schema = reader.schema();
2639        let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
2640        let input =
2641            arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
2642        let mut writer = AvroWriter::new(Vec::<u8>::new(), in_schema.as_ref().clone())?;
2643        writer.write(&input)?;
2644        writer.finish()?;
2645        let bytes = writer.into_inner();
2646        let rt_reader = ReaderBuilder::new()
2647            .build(Cursor::new(bytes))
2648            .expect("build round_trip reader");
2649        let rt_schema = rt_reader.schema();
2650        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2651        let round_trip =
2652            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
2653        assert_eq!(
2654            round_trip, input,
2655            "Round-trip batch mismatch for nested_record_reuse.avro"
2656        );
2657        Ok(())
2658    }
2659
2660    #[test]
2661    fn test_enum_type_reuse_roundtrip() -> Result<(), AvroError> {
2662        let path =
2663            std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test/data/enum_reuse.avro");
2664        let rdr_file = std::fs::File::open(&path).expect("open test/data/enum_reuse.avro");
2665        let reader = ReaderBuilder::new()
2666            .build(std::io::BufReader::new(rdr_file))
2667            .expect("build reader for enum_reuse.avro");
2668        let in_schema = reader.schema();
2669        let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
2670        let original =
2671            arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
2672        let mut writer = AvroWriter::new(Vec::<u8>::new(), original.schema().as_ref().clone())?;
2673        writer.write(&original)?;
2674        writer.finish()?;
2675        let bytes = writer.into_inner();
2676        let rt_reader = ReaderBuilder::new()
2677            .build(std::io::Cursor::new(bytes))
2678            .expect("build round_trip reader");
2679        let rt_schema = rt_reader.schema();
2680        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2681        let round_trip =
2682            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
2683        assert_eq!(
2684            round_trip, original,
2685            "Avro enum type reuse round-trip mismatch"
2686        );
2687        Ok(())
2688    }
2689
2690    #[test]
2691    #[cfg_attr(miri, ignore)] // Takes too long
2692    fn comprehensive_e2e_test_roundtrip() -> Result<(), AvroError> {
2693        let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2694            .join("test/data/comprehensive_e2e.avro");
2695        let rdr_file = File::open(&path).expect("open test/data/comprehensive_e2e.avro");
2696        let reader = ReaderBuilder::new()
2697            .build(BufReader::new(rdr_file))
2698            .expect("build reader for comprehensive_e2e.avro");
2699        let in_schema = reader.schema();
2700        let in_batches = reader.collect::<Result<Vec<_>, _>>()?;
2701        let original =
2702            arrow::compute::concat_batches(&in_schema, &in_batches).expect("concat input");
2703        let sink: Vec<u8> = Vec::new();
2704        let mut writer = AvroWriter::new(sink, original.schema().as_ref().clone())?;
2705        writer.write(&original)?;
2706        writer.finish()?;
2707        let bytes = writer.into_inner();
2708        let rt_reader = ReaderBuilder::new()
2709            .build(Cursor::new(bytes))
2710            .expect("build round-trip reader");
2711        let rt_schema = rt_reader.schema();
2712        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2713        let roundtrip =
2714            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
2715        assert_eq!(
2716            roundtrip, original,
2717            "Round-trip batch mismatch for comprehensive_e2e.avro"
2718        );
2719        Ok(())
2720    }
2721
2722    #[test]
2723    fn test_roundtrip_new_time_encoders_writer() -> Result<(), AvroError> {
2724        let schema = Schema::new(vec![
2725            Field::new("d32", DataType::Date32, false),
2726            Field::new("t32_ms", DataType::Time32(TimeUnit::Millisecond), false),
2727            Field::new("t64_us", DataType::Time64(TimeUnit::Microsecond), false),
2728            Field::new(
2729                "ts_ms",
2730                DataType::Timestamp(TimeUnit::Millisecond, None),
2731                false,
2732            ),
2733            Field::new(
2734                "ts_us",
2735                DataType::Timestamp(TimeUnit::Microsecond, None),
2736                false,
2737            ),
2738            Field::new(
2739                "ts_ns",
2740                DataType::Timestamp(TimeUnit::Nanosecond, None),
2741                false,
2742            ),
2743        ]);
2744        let d32 = Date32Array::from(vec![0, 1, -1]);
2745        let t32_ms: PrimitiveArray<Time32MillisecondType> =
2746            vec![0_i32, 12_345_i32, 86_399_999_i32].into();
2747        let t64_us: PrimitiveArray<Time64MicrosecondType> =
2748            vec![0_i64, 1_234_567_i64, 86_399_999_999_i64].into();
2749        let ts_ms: PrimitiveArray<TimestampMillisecondType> =
2750            vec![0_i64, -1_i64, 1_700_000_000_000_i64].into();
2751        let ts_us: PrimitiveArray<TimestampMicrosecondType> = vec![0_i64, 1_i64, -1_i64].into();
2752        let ts_ns: PrimitiveArray<TimestampNanosecondType> = vec![0_i64, 1_i64, -1_i64].into();
2753        let batch = RecordBatch::try_new(
2754            Arc::new(schema.clone()),
2755            vec![
2756                Arc::new(d32) as ArrayRef,
2757                Arc::new(t32_ms) as ArrayRef,
2758                Arc::new(t64_us) as ArrayRef,
2759                Arc::new(ts_ms) as ArrayRef,
2760                Arc::new(ts_us) as ArrayRef,
2761                Arc::new(ts_ns) as ArrayRef,
2762            ],
2763        )?;
2764        let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2765        writer.write(&batch)?;
2766        writer.finish()?;
2767        let bytes = writer.into_inner();
2768        let rt_reader = ReaderBuilder::new()
2769            .build(std::io::Cursor::new(bytes))
2770            .expect("build reader for round-trip of new time encoders");
2771        let rt_schema = rt_reader.schema();
2772        let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2773        let roundtrip =
2774            arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
2775        assert_eq!(roundtrip, batch);
2776        Ok(())
2777    }
2778
2779    fn make_encoder_schema() -> Schema {
2780        Schema::new(vec![
2781            Field::new("a", DataType::Int32, false),
2782            Field::new("b", DataType::Int32, false),
2783        ])
2784    }
2785
2786    fn make_encoder_batch(schema: &Schema) -> RecordBatch {
2787        let a = Int32Array::from(vec![1, 2, 3]);
2788        let b = Int32Array::from(vec![10, 20, 30]);
2789        RecordBatch::try_new(
2790            Arc::new(schema.clone()),
2791            vec![Arc::new(a) as ArrayRef, Arc::new(b) as ArrayRef],
2792        )
2793        .expect("failed to build test RecordBatch")
2794    }
2795
2796    fn make_real_avro_schema_and_batch() -> Result<(Schema, RecordBatch, AvroSchema), AvroError> {
2797        let avro_json = r#"
2798        {
2799          "type": "record",
2800          "name": "User",
2801          "fields": [
2802            { "name": "id",     "type": "long" },
2803            { "name": "name",   "type": "string" },
2804            { "name": "active", "type": "boolean" },
2805            { "name": "tags",   "type": { "type": "array", "items": "int" } },
2806            { "name": "opt",    "type": ["null", "string"], "default": null }
2807          ]
2808        }"#;
2809        let avro_schema = AvroSchema::new(avro_json.to_string());
2810        let mut md = HashMap::new();
2811        md.insert(
2812            SCHEMA_METADATA_KEY.to_string(),
2813            avro_schema.json_string.clone(),
2814        );
2815        let item_field = Arc::new(Field::new(
2816            Field::LIST_FIELD_DEFAULT_NAME,
2817            DataType::Int32,
2818            false,
2819        ));
2820        let schema = Schema::new_with_metadata(
2821            vec![
2822                Field::new("id", DataType::Int64, false),
2823                Field::new("name", DataType::Utf8, false),
2824                Field::new("active", DataType::Boolean, false),
2825                Field::new("tags", DataType::List(item_field.clone()), false),
2826                Field::new("opt", DataType::Utf8, true),
2827            ],
2828            md,
2829        );
2830        let id = Int64Array::from(vec![1, 2, 3]);
2831        let name = StringArray::from(vec!["alice", "bob", "carol"]);
2832        let active = BooleanArray::from(vec![true, false, true]);
2833        let mut tags_builder = ListBuilder::new(Int32Builder::new()).with_field(item_field);
2834        tags_builder.values().append_value(1);
2835        tags_builder.values().append_value(2);
2836        tags_builder.append(true);
2837        tags_builder.append(true);
2838        tags_builder.values().append_value(3);
2839        tags_builder.append(true);
2840        let tags = tags_builder.finish();
2841        let opt = StringArray::from(vec![Some("x"), None, Some("z")]);
2842        let batch = RecordBatch::try_new(
2843            Arc::new(schema.clone()),
2844            vec![
2845                Arc::new(id) as ArrayRef,
2846                Arc::new(name) as ArrayRef,
2847                Arc::new(active) as ArrayRef,
2848                Arc::new(tags) as ArrayRef,
2849                Arc::new(opt) as ArrayRef,
2850            ],
2851        )?;
2852        Ok((schema, batch, avro_schema))
2853    }
2854
2855    #[test]
2856    fn test_row_writer_matches_stream_writer_soe() -> Result<(), AvroError> {
2857        let schema = make_encoder_schema();
2858        let batch = make_encoder_batch(&schema);
2859        let mut stream = AvroStreamWriter::new(Vec::<u8>::new(), schema.clone())?;
2860        stream.write(&batch)?;
2861        stream.finish()?;
2862        let stream_bytes = stream.into_inner();
2863        let mut row_writer = WriterBuilder::new(schema).build_encoder::<AvroSoeFormat>()?;
2864        row_writer.encode(&batch)?;
2865        let rows = row_writer.flush();
2866        let row_bytes: Vec<u8> = rows.bytes().to_vec();
2867        assert_eq!(stream_bytes, row_bytes);
2868        Ok(())
2869    }
2870
2871    #[test]
2872    fn test_row_writer_flush_clears_buffer() -> Result<(), AvroError> {
2873        let schema = make_encoder_schema();
2874        let batch = make_encoder_batch(&schema);
2875        let mut row_writer = WriterBuilder::new(schema).build_encoder::<AvroSoeFormat>()?;
2876        row_writer.encode(&batch)?;
2877        assert_eq!(row_writer.buffered_len(), batch.num_rows());
2878        let out1 = row_writer.flush();
2879        assert_eq!(out1.len(), batch.num_rows());
2880        assert_eq!(row_writer.buffered_len(), 0);
2881        let out2 = row_writer.flush();
2882        assert_eq!(out2.len(), 0);
2883        Ok(())
2884    }
2885
2886    #[test]
2887    fn test_row_writer_roundtrip_decoder_soe_real_avro_data() -> Result<(), AvroError> {
2888        let (schema, batch, avro_schema) = make_real_avro_schema_and_batch()?;
2889        let mut store = SchemaStore::new();
2890        store.register(avro_schema.clone())?;
2891        let mut row_writer = WriterBuilder::new(schema).build_encoder::<AvroSoeFormat>()?;
2892        row_writer.encode(&batch)?;
2893        let rows = row_writer.flush();
2894        let mut decoder = ReaderBuilder::new()
2895            .with_writer_schema_store(store)
2896            .with_batch_size(1024)
2897            .build_decoder()?;
2898        for row in rows.iter() {
2899            let consumed = decoder.decode(row.as_ref())?;
2900            assert_eq!(
2901                consumed,
2902                row.len(),
2903                "decoder should consume the full row frame"
2904            );
2905        }
2906        let out = decoder.flush()?.expect("decoded batch");
2907        let expected = pretty_format_batches(std::slice::from_ref(&batch))?.to_string();
2908        let actual = pretty_format_batches(&[out])?.to_string();
2909        assert_eq!(expected, actual);
2910        Ok(())
2911    }
2912
2913    #[test]
2914    fn test_row_writer_roundtrip_decoder_soe_streaming_chunks() -> Result<(), AvroError> {
2915        let (schema, batch, avro_schema) = make_real_avro_schema_and_batch()?;
2916        let mut store = SchemaStore::new();
2917        store.register(avro_schema.clone())?;
2918        let mut row_writer = WriterBuilder::new(schema).build_encoder::<AvroSoeFormat>()?;
2919        row_writer.encode(&batch)?;
2920        let rows = row_writer.flush();
2921        // Build a contiguous stream and frame boundaries (prefix sums) from EncodedRows.
2922        let mut stream: Vec<u8> = Vec::new();
2923        let mut boundaries: Vec<usize> = Vec::with_capacity(rows.len() + 1);
2924        boundaries.push(0usize);
2925        for row in rows.iter() {
2926            stream.extend_from_slice(row.as_ref());
2927            boundaries.push(stream.len());
2928        }
2929        let mut decoder = ReaderBuilder::new()
2930            .with_writer_schema_store(store)
2931            .with_batch_size(1024)
2932            .build_decoder()?;
2933        let mut buffered = BytesMut::new();
2934        let chunk_rows = [1usize, 2, 3, 1, 4, 2];
2935        let mut row_idx = 0usize;
2936        let mut i = 0usize;
2937        let n_rows = rows.len();
2938        while row_idx < n_rows {
2939            let take = chunk_rows[i % chunk_rows.len()];
2940            i += 1;
2941            let end_row = (row_idx + take).min(n_rows);
2942            let byte_start = boundaries[row_idx];
2943            let byte_end = boundaries[end_row];
2944            buffered.extend_from_slice(&stream[byte_start..byte_end]);
2945            loop {
2946                let consumed = decoder.decode(&buffered)?;
2947                if consumed == 0 {
2948                    break;
2949                }
2950                let _ = buffered.split_to(consumed);
2951            }
2952            assert!(
2953                buffered.is_empty(),
2954                "expected decoder to consume the entire frame-aligned chunk"
2955            );
2956            row_idx = end_row;
2957        }
2958        let out = decoder.flush()?.expect("decoded batch");
2959        let expected = pretty_format_batches(std::slice::from_ref(&batch))?.to_string();
2960        let actual = pretty_format_batches(&[out])?.to_string();
2961        assert_eq!(expected, actual);
2962        Ok(())
2963    }
2964
2965    #[test]
2966    fn test_row_writer_roundtrip_decoder_confluent_wire_format_id() -> Result<(), AvroError> {
2967        let (schema, batch, avro_schema) = make_real_avro_schema_and_batch()?;
2968        let schema_id: u32 = 42;
2969        let mut store = SchemaStore::new_with_type(FingerprintAlgorithm::Id);
2970        store.set(Fingerprint::Id(schema_id), avro_schema.clone())?;
2971        let mut row_writer = WriterBuilder::new(schema)
2972            .with_fingerprint_strategy(FingerprintStrategy::Id(schema_id))
2973            .build_encoder::<AvroSoeFormat>()?;
2974        row_writer.encode(&batch)?;
2975        let rows = row_writer.flush();
2976        let mut decoder = ReaderBuilder::new()
2977            .with_writer_schema_store(store)
2978            .with_batch_size(1024)
2979            .build_decoder()?;
2980        for row in rows.iter() {
2981            let consumed = decoder.decode(row.as_ref())?;
2982            assert_eq!(consumed, row.len());
2983        }
2984        let out = decoder.flush()?.expect("decoded batch");
2985        let expected = pretty_format_batches(std::slice::from_ref(&batch))?.to_string();
2986        let actual = pretty_format_batches(&[out])?.to_string();
2987        assert_eq!(expected, actual);
2988        Ok(())
2989    }
2990    #[test]
2991    fn test_encoder_encode_batches_flush_and_encoded_rows_methods_with_avro_binary_format()
2992    -> Result<(), AvroError> {
2993        use crate::writer::format::AvroBinaryFormat;
2994        use arrow_array::{ArrayRef, Int32Array, RecordBatch};
2995        use arrow_schema::{DataType, Field, Schema};
2996        use std::sync::Arc;
2997        let schema = Schema::new(vec![
2998            Field::new("a", DataType::Int32, false),
2999            Field::new("b", DataType::Int32, false),
3000        ]);
3001        let schema_ref = Arc::new(schema.clone());
3002        let batch1 = RecordBatch::try_new(
3003            schema_ref.clone(),
3004            vec![
3005                Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef,
3006                Arc::new(Int32Array::from(vec![10, 20, 30])) as ArrayRef,
3007            ],
3008        )?;
3009        let batch2 = RecordBatch::try_new(
3010            schema_ref,
3011            vec![
3012                Arc::new(Int32Array::from(vec![4, 5])) as ArrayRef,
3013                Arc::new(Int32Array::from(vec![40, 50])) as ArrayRef,
3014            ],
3015        )?;
3016        let mut encoder = WriterBuilder::new(schema).build_encoder::<AvroBinaryFormat>()?;
3017        let empty = Encoder::flush(&mut encoder);
3018        assert_eq!(EncodedRows::len(&empty), 0);
3019        assert!(EncodedRows::is_empty(&empty));
3020        assert_eq!(EncodedRows::bytes(&empty).as_ref(), &[] as &[u8]);
3021        assert_eq!(EncodedRows::offsets(&empty), &[0usize]);
3022        assert_eq!(EncodedRows::iter(&empty).count(), 0);
3023        let empty_vecs: Vec<Vec<u8>> = empty.iter().map(|b| b.to_vec()).collect();
3024        assert!(empty_vecs.is_empty());
3025        let batches = vec![batch1, batch2];
3026        Encoder::encode_batches(&mut encoder, &batches)?;
3027        assert_eq!(encoder.buffered_len(), 5);
3028        let rows = Encoder::flush(&mut encoder);
3029        assert_eq!(
3030            encoder.buffered_len(),
3031            0,
3032            "Encoder::flush should reset the internal offsets"
3033        );
3034        assert_eq!(EncodedRows::len(&rows), 5);
3035        assert!(!EncodedRows::is_empty(&rows));
3036        let expected_offsets: &[usize] = &[0, 2, 4, 6, 8, 10];
3037        assert_eq!(EncodedRows::offsets(&rows), expected_offsets);
3038        let expected_rows: Vec<Vec<u8>> = vec![
3039            vec![2, 20],
3040            vec![4, 40],
3041            vec![6, 60],
3042            vec![8, 80],
3043            vec![10, 100],
3044        ];
3045        let expected_stream: Vec<u8> = expected_rows.concat();
3046        assert_eq!(
3047            EncodedRows::bytes(&rows).as_ref(),
3048            expected_stream.as_slice()
3049        );
3050        for (i, expected) in expected_rows.iter().enumerate() {
3051            assert_eq!(EncodedRows::row(&rows, i)?.as_ref(), expected.as_slice());
3052        }
3053        let iter_rows: Vec<Vec<u8>> = EncodedRows::iter(&rows).map(|b| b.to_vec()).collect();
3054        assert_eq!(iter_rows, expected_rows);
3055        let recreated = EncodedRows::new(
3056            EncodedRows::bytes(&rows).clone(),
3057            EncodedRows::offsets(&rows).to_vec(),
3058        );
3059        assert_eq!(EncodedRows::len(&recreated), EncodedRows::len(&rows));
3060        assert_eq!(EncodedRows::bytes(&recreated), EncodedRows::bytes(&rows));
3061        assert_eq!(
3062            EncodedRows::offsets(&recreated),
3063            EncodedRows::offsets(&rows)
3064        );
3065        let rec_vecs: Vec<Vec<u8>> = recreated.iter().map(|b| b.to_vec()).collect();
3066        assert_eq!(rec_vecs, iter_rows);
3067        let empty_again = Encoder::flush(&mut encoder);
3068        assert!(EncodedRows::is_empty(&empty_again));
3069        Ok(())
3070    }
3071
3072    #[test]
3073    fn test_writer_builder_build_rejects_avro_binary_format() {
3074        use crate::writer::format::AvroBinaryFormat;
3075        use arrow_schema::{DataType, Field, Schema};
3076        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
3077        let err = WriterBuilder::new(schema)
3078            .build::<_, AvroBinaryFormat>(Vec::<u8>::new())
3079            .unwrap_err();
3080        match err {
3081            AvroError::InvalidArgument(msg) => assert_eq!(
3082                msg,
3083                "AvroBinaryFormat is only supported with Encoder, use build_encoder instead"
3084            ),
3085            other => panic!("expected InvalidArgumentError, got {other:?}"),
3086        }
3087    }
3088    #[test]
3089    fn test_row_encoder_avro_binary_format_roundtrip_decoder_with_soe_framing()
3090    -> Result<(), AvroError> {
3091        use crate::writer::format::AvroBinaryFormat;
3092        let (schema, batch, avro_schema) = make_real_avro_schema_and_batch()?;
3093        let batches: Vec<RecordBatch> = vec![batch.clone(), batch.slice(1, 2)];
3094        let expected = arrow::compute::concat_batches(&batch.schema(), &batches)?;
3095        let mut binary_encoder =
3096            WriterBuilder::new(schema.clone()).build_encoder::<AvroBinaryFormat>()?;
3097        binary_encoder.encode_batches(&batches)?;
3098        let binary_rows = binary_encoder.flush();
3099        assert_eq!(
3100            binary_rows.len(),
3101            expected.num_rows(),
3102            "binary encoder row count mismatch"
3103        );
3104        let mut soe_encoder = WriterBuilder::new(schema).build_encoder::<AvroSoeFormat>()?;
3105        soe_encoder.encode_batches(&batches)?;
3106        let soe_rows = soe_encoder.flush();
3107        assert_eq!(
3108            soe_rows.len(),
3109            binary_rows.len(),
3110            "SOE vs binary row count mismatch"
3111        );
3112        let mut store = SchemaStore::new(); // Rabin by default
3113        let fp = store.register(avro_schema)?;
3114        let fp_le_bytes = match fp {
3115            Fingerprint::Rabin(v) => v.to_le_bytes(),
3116            other => panic!("expected Rabin fingerprint from SchemaStore::new(), got {other:?}"),
3117        };
3118        const SOE_MAGIC: [u8; 2] = [0xC3, 0x01];
3119        const SOE_PREFIX_LEN: usize = 2 + 8;
3120        for i in 0..binary_rows.len() {
3121            let body = binary_rows.row(i)?;
3122            let soe = soe_rows.row(i)?;
3123            assert!(
3124                soe.len() >= SOE_PREFIX_LEN,
3125                "expected SOE row to include prefix"
3126            );
3127            assert_eq!(&soe.as_ref()[..2], &SOE_MAGIC);
3128            assert_eq!(&soe.as_ref()[2..SOE_PREFIX_LEN], &fp_le_bytes);
3129            assert_eq!(
3130                &soe.as_ref()[SOE_PREFIX_LEN..],
3131                body.as_ref(),
3132                "SOE body bytes differ from AvroBinaryFormat body bytes (row {i})"
3133            );
3134        }
3135        let mut decoder = ReaderBuilder::new()
3136            .with_writer_schema_store(store)
3137            .with_batch_size(1024)
3138            .build_decoder()?;
3139        for body in binary_rows.iter() {
3140            let mut framed = Vec::with_capacity(SOE_PREFIX_LEN + body.len());
3141            framed.extend_from_slice(&SOE_MAGIC);
3142            framed.extend_from_slice(&fp_le_bytes);
3143            framed.extend_from_slice(body.as_ref());
3144            let consumed = decoder.decode(&framed)?;
3145            assert_eq!(
3146                consumed,
3147                framed.len(),
3148                "decoder should consume the full SOE-framed message"
3149            );
3150        }
3151        let out = decoder.flush()?.expect("expected a decoded RecordBatch");
3152        let expected_str = pretty_format_batches(&[expected])?.to_string();
3153        let actual_str = pretty_format_batches(&[out])?.to_string();
3154        assert_eq!(expected_str, actual_str);
3155        Ok(())
3156    }
3157
3158    #[test]
3159    fn test_row_encoder_avro_binary_format_roundtrip_decoder_streaming_chunks()
3160    -> Result<(), AvroError> {
3161        use crate::writer::format::AvroBinaryFormat;
3162        let (schema, batch, avro_schema) = make_real_avro_schema_and_batch()?;
3163        let mut encoder = WriterBuilder::new(schema).build_encoder::<AvroBinaryFormat>()?;
3164        encoder.encode(&batch)?;
3165        let rows = encoder.flush();
3166        let mut store = SchemaStore::new();
3167        let fp = store.register(avro_schema)?;
3168        let fp_le_bytes = match fp {
3169            Fingerprint::Rabin(v) => v.to_le_bytes(),
3170            other => panic!("expected Rabin fingerprint from SchemaStore::new(), got {other:?}"),
3171        };
3172        const SOE_MAGIC: [u8; 2] = [0xC3, 0x01];
3173        const SOE_PREFIX_LEN: usize = 2 + 8;
3174        let mut stream: Vec<u8> = Vec::new();
3175        for body in rows.iter() {
3176            let msg_len: u32 = (SOE_PREFIX_LEN + body.len())
3177                .try_into()
3178                .expect("message length must fit in u32");
3179            stream.extend_from_slice(&msg_len.to_le_bytes());
3180            stream.extend_from_slice(&SOE_MAGIC);
3181            stream.extend_from_slice(&fp_le_bytes);
3182            stream.extend_from_slice(body.as_ref());
3183        }
3184        let mut decoder = ReaderBuilder::new()
3185            .with_writer_schema_store(store)
3186            .with_batch_size(1024)
3187            .build_decoder()?;
3188        let chunk_sizes = [1usize, 2, 3, 5, 8, 13, 21, 34];
3189        let mut pos = 0usize;
3190        let mut i = 0usize;
3191        let mut buffered = BytesMut::new();
3192        let mut decoded_frames = 0usize;
3193        while pos < stream.len() {
3194            let take = chunk_sizes[i % chunk_sizes.len()];
3195            i += 1;
3196            let end = (pos + take).min(stream.len());
3197            buffered.extend_from_slice(&stream[pos..end]);
3198            pos = end;
3199            loop {
3200                if buffered.len() < 4 {
3201                    break;
3202                }
3203                let msg_len =
3204                    u32::from_le_bytes([buffered[0], buffered[1], buffered[2], buffered[3]])
3205                        as usize;
3206                if buffered.len() < 4 + msg_len {
3207                    break;
3208                }
3209                let frame = buffered.split_to(4 + msg_len);
3210                let payload = &frame[4..];
3211                let consumed = decoder.decode(payload)?;
3212                assert_eq!(
3213                    consumed,
3214                    payload.len(),
3215                    "decoder should consume the full SOE-framed message"
3216                );
3217
3218                decoded_frames += 1;
3219            }
3220        }
3221        assert!(
3222            buffered.is_empty(),
3223            "expected transport framer to consume all bytes; leftover = {}",
3224            buffered.len()
3225        );
3226        assert_eq!(
3227            decoded_frames,
3228            rows.len(),
3229            "expected to decode exactly one frame per encoded row"
3230        );
3231        let out = decoder.flush()?.expect("expected decoded RecordBatch");
3232        let expected_str = pretty_format_batches(std::slice::from_ref(&batch))?.to_string();
3233        let actual_str = pretty_format_batches(&[out])?.to_string();
3234        assert_eq!(expected_str, actual_str);
3235        Ok(())
3236    }
3237
3238    /// Helper to roundtrip a RecordBatch through OCF writer/reader
3239    fn roundtrip_ocf(batch: &RecordBatch) -> Result<RecordBatch, AvroError> {
3240        let schema = batch.schema();
3241        let mut buffer = Vec::<u8>::new();
3242        let mut writer = AvroWriter::new(&mut buffer, schema.as_ref().clone())?;
3243        writer.write(batch)?;
3244        writer.finish()?;
3245        drop(writer);
3246        let reader = ReaderBuilder::new()
3247            .build(Cursor::new(buffer))
3248            .expect("build reader for roundtrip OCF");
3249        // Get the Avro schema JSON from the OCF header
3250        let avro_schema_json = reader
3251            .avro_header()
3252            .get(SCHEMA_METADATA_KEY)
3253            .map(|raw| std::str::from_utf8(raw).expect("valid UTF-8").to_string());
3254        // Get the Arrow schema and add the Avro schema metadata
3255        let arrow_schema = reader.schema();
3256        let rt_schema = if let Some(json) = avro_schema_json {
3257            let mut metadata = arrow_schema.metadata().clone();
3258            metadata.insert(SCHEMA_METADATA_KEY.to_string(), json);
3259            Arc::new(Schema::new_with_metadata(
3260                arrow_schema.fields().clone(),
3261                metadata,
3262            ))
3263        } else {
3264            arrow_schema
3265        };
3266        let rt_batches: Vec<RecordBatch> = reader.collect::<Result<Vec<_>, _>>()?;
3267        Ok(arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip"))
3268    }
3269
3270    /// Assert that an array roundtrips through Avro OCF and comes back identical.
3271    #[cfg(feature = "avro_custom_types")]
3272    fn assert_round_trip(array: ArrayRef) {
3273        assert_round_trip_widened(array.clone(), array);
3274    }
3275
3276    /// Assert that an input array roundtrips through Avro OCF and produces the expected output.
3277    fn assert_round_trip_widened(input: ArrayRef, expected: ArrayRef) {
3278        let schema = Schema::new(vec![Field::new("val", input.data_type().clone(), true)]);
3279        let batch =
3280            RecordBatch::try_new(Arc::new(schema), vec![input]).expect("failed to create batch");
3281        let roundtrip = roundtrip_ocf(&batch).expect("roundtrip failed");
3282        assert_eq!(
3283            roundtrip.column(0).data_type(),
3284            expected.data_type(),
3285            "output data type mismatch"
3286        );
3287        assert_eq!(
3288            roundtrip.column(0).to_data(),
3289            expected.to_data(),
3290            "output data mismatch"
3291        );
3292    }
3293
3294    #[cfg(feature = "avro_custom_types")]
3295    #[test]
3296    fn test_roundtrip_int8_custom_types() {
3297        assert_round_trip(Arc::new(Int8Array::from(vec![
3298            Some(i8::MIN),
3299            Some(-1),
3300            Some(0),
3301            None,
3302            Some(1),
3303            Some(i8::MAX),
3304        ])));
3305    }
3306
3307    #[cfg(not(feature = "avro_custom_types"))]
3308    #[test]
3309    fn test_roundtrip_int8_no_custom_widens_to_int32() {
3310        assert_round_trip_widened(
3311            Arc::new(Int8Array::from(vec![
3312                Some(i8::MIN),
3313                Some(-1),
3314                Some(0),
3315                None,
3316                Some(1),
3317                Some(i8::MAX),
3318            ])),
3319            Arc::new(Int32Array::from(vec![
3320                Some(i8::MIN as i32),
3321                Some(-1),
3322                Some(0),
3323                None,
3324                Some(1),
3325                Some(i8::MAX as i32),
3326            ])),
3327        );
3328    }
3329
3330    #[cfg(feature = "avro_custom_types")]
3331    #[test]
3332    fn test_roundtrip_int16_custom_types() {
3333        assert_round_trip(Arc::new(Int16Array::from(vec![
3334            Some(i16::MIN),
3335            Some(-1),
3336            Some(0),
3337            None,
3338            Some(1),
3339            Some(i16::MAX),
3340        ])));
3341    }
3342
3343    #[cfg(not(feature = "avro_custom_types"))]
3344    #[test]
3345    fn test_roundtrip_int16_no_custom_widens_to_int32() {
3346        assert_round_trip_widened(
3347            Arc::new(Int16Array::from(vec![
3348                Some(i16::MIN),
3349                Some(-1),
3350                Some(0),
3351                None,
3352                Some(1),
3353                Some(i16::MAX),
3354            ])),
3355            Arc::new(Int32Array::from(vec![
3356                Some(i16::MIN as i32),
3357                Some(-1),
3358                Some(0),
3359                None,
3360                Some(1),
3361                Some(i16::MAX as i32),
3362            ])),
3363        );
3364    }
3365
3366    #[cfg(feature = "avro_custom_types")]
3367    #[test]
3368    fn test_roundtrip_uint8_custom_types() {
3369        assert_round_trip(Arc::new(UInt8Array::from(vec![
3370            Some(0u8),
3371            Some(1),
3372            None,
3373            Some(127),
3374            Some(u8::MAX),
3375        ])));
3376    }
3377
3378    #[cfg(not(feature = "avro_custom_types"))]
3379    #[test]
3380    fn test_roundtrip_uint8_no_custom_widens_to_int32() {
3381        assert_round_trip_widened(
3382            Arc::new(UInt8Array::from(vec![
3383                Some(0u8),
3384                Some(1),
3385                None,
3386                Some(127),
3387                Some(u8::MAX),
3388            ])),
3389            Arc::new(Int32Array::from(vec![
3390                Some(0i32),
3391                Some(1),
3392                None,
3393                Some(127),
3394                Some(u8::MAX as i32),
3395            ])),
3396        );
3397    }
3398
3399    #[cfg(feature = "avro_custom_types")]
3400    #[test]
3401    fn test_roundtrip_uint16_custom_types() {
3402        assert_round_trip(Arc::new(UInt16Array::from(vec![
3403            Some(0u16),
3404            Some(1),
3405            None,
3406            Some(32767),
3407            Some(u16::MAX),
3408        ])));
3409    }
3410
3411    #[cfg(not(feature = "avro_custom_types"))]
3412    #[test]
3413    fn test_roundtrip_uint16_no_custom_widens_to_int32() {
3414        assert_round_trip_widened(
3415            Arc::new(UInt16Array::from(vec![
3416                Some(0u16),
3417                Some(1),
3418                None,
3419                Some(32767),
3420                Some(u16::MAX),
3421            ])),
3422            Arc::new(Int32Array::from(vec![
3423                Some(0i32),
3424                Some(1),
3425                None,
3426                Some(32767),
3427                Some(u16::MAX as i32),
3428            ])),
3429        );
3430    }
3431
3432    #[cfg(feature = "avro_custom_types")]
3433    #[test]
3434    fn test_roundtrip_uint32_custom_types() {
3435        assert_round_trip(Arc::new(UInt32Array::from(vec![
3436            Some(0u32),
3437            Some(1),
3438            None,
3439            Some(i32::MAX as u32),
3440            Some(u32::MAX),
3441        ])));
3442    }
3443
3444    #[cfg(not(feature = "avro_custom_types"))]
3445    #[test]
3446    fn test_roundtrip_uint32_no_custom_widens_to_int64() {
3447        assert_round_trip_widened(
3448            Arc::new(UInt32Array::from(vec![
3449                Some(0u32),
3450                Some(1),
3451                None,
3452                Some(i32::MAX as u32),
3453                Some(u32::MAX),
3454            ])),
3455            Arc::new(Int64Array::from(vec![
3456                Some(0i64),
3457                Some(1),
3458                None,
3459                Some(i32::MAX as i64),
3460                Some(u32::MAX as i64),
3461            ])),
3462        );
3463    }
3464
3465    #[cfg(feature = "avro_custom_types")]
3466    #[test]
3467    fn test_roundtrip_uint64_custom_types() {
3468        assert_round_trip(Arc::new(UInt64Array::from(vec![
3469            Some(0u64),
3470            Some(1),
3471            None,
3472            Some(i64::MAX as u64),
3473            Some(u64::MAX),
3474        ])));
3475    }
3476
3477    #[cfg(not(feature = "avro_custom_types"))]
3478    #[test]
3479    fn test_roundtrip_uint64_no_custom_widens_to_int64() {
3480        assert_round_trip_widened(
3481            Arc::new(UInt64Array::from(vec![
3482                Some(0u64),
3483                Some(1),
3484                None,
3485                Some(i64::MAX as u64),
3486            ])),
3487            Arc::new(Int64Array::from(vec![
3488                Some(0i64),
3489                Some(1),
3490                None,
3491                Some(i64::MAX),
3492            ])),
3493        );
3494    }
3495
3496    #[cfg(not(feature = "avro_custom_types"))]
3497    #[test]
3498    fn test_roundtrip_uint64_overflow_errors_without_custom() {
3499        use arrow_array::UInt64Array;
3500        let schema = Schema::new(vec![Field::new("val", DataType::UInt64, false)]);
3501        let values: Vec<u64> = vec![u64::MAX];
3502        let array = UInt64Array::from(values);
3503        let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(array) as ArrayRef])
3504            .expect("create batch");
3505        let result = roundtrip_ocf(&batch);
3506        assert!(
3507            result.is_err(),
3508            "Expected error when encoding UInt64 > i64::MAX without avro_custom_types"
3509        );
3510    }
3511
3512    #[cfg(feature = "avro_custom_types")]
3513    #[test]
3514    fn test_roundtrip_float16_custom_types() {
3515        assert_round_trip(Arc::new(Float16Array::from(vec![
3516            Some(f16::ZERO),
3517            Some(f16::ONE),
3518            None,
3519            Some(f16::NEG_ONE),
3520            Some(f16::MAX),
3521            Some(f16::MIN),
3522        ])));
3523    }
3524
3525    #[cfg(not(feature = "avro_custom_types"))]
3526    #[test]
3527    #[cfg_attr(miri, ignore)] // Unsupported inline assembly
3528    fn test_roundtrip_float16_no_custom_widens_to_float32() {
3529        assert_round_trip_widened(
3530            Arc::new(Float16Array::from(vec![
3531                Some(f16::ZERO),
3532                Some(f16::ONE),
3533                None,
3534                Some(f16::NEG_ONE),
3535            ])),
3536            Arc::new(Float32Array::from(vec![
3537                Some(0.0f32),
3538                Some(1.0),
3539                None,
3540                Some(-1.0),
3541            ])),
3542        );
3543    }
3544
3545    #[cfg(feature = "avro_custom_types")]
3546    #[test]
3547    fn test_roundtrip_date64_custom_types() {
3548        assert_round_trip(Arc::new(Date64Array::from(vec![
3549            Some(0i64),
3550            Some(86_400_000),
3551            None,
3552            Some(1_609_459_200_000),
3553        ])));
3554    }
3555
3556    #[cfg(not(feature = "avro_custom_types"))]
3557    #[test]
3558    fn test_roundtrip_date64_no_custom_as_timestamp_millis() {
3559        assert_round_trip_widened(
3560            Arc::new(Date64Array::from(vec![
3561                Some(0i64),
3562                Some(86_400_000),
3563                None,
3564                Some(1_609_459_200_000),
3565            ])),
3566            Arc::new(TimestampMillisecondArray::from(vec![
3567                Some(0i64),
3568                Some(86_400_000),
3569                None,
3570                Some(1_609_459_200_000),
3571            ])),
3572        );
3573    }
3574
3575    #[cfg(feature = "avro_custom_types")]
3576    #[test]
3577    fn test_roundtrip_time64_nanosecond_custom_types() {
3578        assert_round_trip(Arc::new(Time64NanosecondArray::from(vec![
3579            Some(0i64),
3580            Some(1_000_000_000),
3581            None,
3582            Some(86_399_999_999_999),
3583        ])));
3584    }
3585
3586    #[cfg(not(feature = "avro_custom_types"))]
3587    #[test]
3588    fn test_roundtrip_time64_nanos_no_custom_truncates_to_micros() {
3589        // Use values evenly divisible by 1000 to avoid truncation issues
3590        assert_round_trip_widened(
3591            Arc::new(Time64NanosecondArray::from(vec![
3592                Some(0i64),
3593                Some(1_000_000_000),
3594                None,
3595                Some(86_399_999_000_000),
3596            ])),
3597            Arc::new(Time64MicrosecondArray::from(vec![
3598                Some(0i64),
3599                Some(1_000_000),
3600                None,
3601                Some(86_399_999_000),
3602            ])),
3603        );
3604    }
3605
3606    #[cfg(feature = "avro_custom_types")]
3607    #[test]
3608    fn test_roundtrip_time32_second_custom_types() {
3609        assert_round_trip(Arc::new(Time32SecondArray::from(vec![
3610            Some(0i32),
3611            Some(3600),
3612            None,
3613            Some(86399),
3614        ])));
3615    }
3616
3617    #[cfg(not(feature = "avro_custom_types"))]
3618    #[test]
3619    fn test_roundtrip_time32_second_no_custom_scales_to_millis() {
3620        assert_round_trip_widened(
3621            Arc::new(Time32SecondArray::from(vec![
3622                Some(0i32),
3623                Some(3600),
3624                None,
3625                Some(86399),
3626            ])),
3627            Arc::new(Time32MillisecondArray::from(vec![
3628                Some(0i32),
3629                Some(3_600_000),
3630                None,
3631                Some(86_399_000),
3632            ])),
3633        );
3634    }
3635
3636    #[cfg(feature = "avro_custom_types")]
3637    #[test]
3638    fn test_roundtrip_timestamp_second_custom_types() {
3639        assert_round_trip(Arc::new(
3640            TimestampSecondArray::from(vec![Some(0i64), Some(1609459200), None, Some(1735689600)])
3641                .with_timezone("+00:00"),
3642        ));
3643    }
3644
3645    #[cfg(not(feature = "avro_custom_types"))]
3646    #[test]
3647    fn test_roundtrip_timestamp_second_no_custom_scales_to_millis() {
3648        assert_round_trip_widened(
3649            Arc::new(
3650                TimestampSecondArray::from(vec![
3651                    Some(0i64),
3652                    Some(1609459200),
3653                    None,
3654                    Some(1735689600),
3655                ])
3656                .with_timezone("+00:00"),
3657            ),
3658            Arc::new(
3659                TimestampMillisecondArray::from(vec![
3660                    Some(0i64),
3661                    Some(1_609_459_200_000),
3662                    None,
3663                    Some(1_735_689_600_000),
3664                ])
3665                .with_timezone("+00:00"),
3666            ),
3667        );
3668    }
3669
3670    #[cfg(feature = "avro_custom_types")]
3671    #[test]
3672    fn test_roundtrip_interval_year_month_custom_types() {
3673        assert_round_trip(Arc::new(IntervalYearMonthArray::from(vec![
3674            Some(0i32),
3675            Some(12),
3676            None,
3677            Some(-6),
3678            Some(25),
3679        ])));
3680    }
3681
3682    #[cfg(not(feature = "avro_custom_types"))]
3683    #[test]
3684    fn test_roundtrip_interval_year_month_no_custom() {
3685        // Only non-negative values for standard Avro duration
3686        assert_round_trip_widened(
3687            Arc::new(IntervalYearMonthArray::from(vec![
3688                Some(0i32),
3689                Some(12),
3690                None,
3691                Some(25),
3692            ])),
3693            Arc::new(IntervalMonthDayNanoArray::from(vec![
3694                Some(IntervalMonthDayNano::new(0, 0, 0)),
3695                Some(IntervalMonthDayNano::new(12, 0, 0)),
3696                None,
3697                Some(IntervalMonthDayNano::new(25, 0, 0)),
3698            ])),
3699        );
3700    }
3701
3702    #[cfg(feature = "avro_custom_types")]
3703    #[test]
3704    fn test_roundtrip_interval_day_time_custom_types() {
3705        assert_round_trip(Arc::new(IntervalDayTimeArray::from(vec![
3706            Some(IntervalDayTime::new(0, 0)),
3707            Some(IntervalDayTime::new(1, 1000)),
3708            None,
3709            Some(IntervalDayTime::new(30, 3600000)),
3710        ])));
3711    }
3712
3713    #[cfg(not(feature = "avro_custom_types"))]
3714    #[test]
3715    fn test_roundtrip_interval_day_time_no_custom() {
3716        assert_round_trip_widened(
3717            Arc::new(IntervalDayTimeArray::from(vec![
3718                Some(IntervalDayTime::new(0, 0)),
3719                Some(IntervalDayTime::new(1, 1000)),
3720                None,
3721                Some(IntervalDayTime::new(30, 3600000)),
3722            ])),
3723            Arc::new(IntervalMonthDayNanoArray::from(vec![
3724                Some(IntervalMonthDayNano::new(0, 0, 0)),
3725                Some(IntervalMonthDayNano::new(0, 1, 1_000_000_000)),
3726                None,
3727                Some(IntervalMonthDayNano::new(0, 30, 3_600_000_000_000)),
3728            ])),
3729        );
3730    }
3731
3732    #[cfg(feature = "avro_custom_types")]
3733    #[test]
3734    fn test_roundtrip_interval_month_day_nano_custom_types() {
3735        assert_round_trip(Arc::new(IntervalMonthDayNanoArray::from(vec![
3736            Some(IntervalMonthDayNano::new(0, 0, 0)),
3737            Some(IntervalMonthDayNano::new(1, 2, 3)),
3738            None,
3739            Some(IntervalMonthDayNano::new(-4, -5, -6)),
3740        ])));
3741    }
3742
3743    #[cfg(not(feature = "avro_custom_types"))]
3744    #[test]
3745    fn test_roundtrip_interval_month_day_nano_no_custom() {
3746        // Only representable values for Avro duration: non-negative and whole milliseconds
3747        assert_round_trip_widened(
3748            Arc::new(IntervalMonthDayNanoArray::from(vec![
3749                Some(IntervalMonthDayNano::new(0, 0, 0)),
3750                Some(IntervalMonthDayNano::new(1, 2, 3_000_000)),
3751                None,
3752                Some(IntervalMonthDayNano::new(4, 5, 6_000_000)),
3753            ])),
3754            Arc::new(IntervalMonthDayNanoArray::from(vec![
3755                Some(IntervalMonthDayNano::new(0, 0, 0)),
3756                Some(IntervalMonthDayNano::new(1, 2, 3_000_000)),
3757                None,
3758                Some(IntervalMonthDayNano::new(4, 5, 6_000_000)),
3759            ])),
3760        );
3761    }
3762
3763    fn schemas_equal_ignoring_metadata(left: &Schema, right: &Schema) -> bool {
3764        if left.fields().len() != right.fields().len() {
3765            return false;
3766        }
3767        for (l, r) in left.fields().iter().zip(right.fields().iter()) {
3768            if l.name() != r.name()
3769                || l.data_type() != r.data_type()
3770                || l.is_nullable() != r.is_nullable()
3771            {
3772                return false;
3773            }
3774        }
3775        true
3776    }
3777
3778    fn avro_field_type<'a>(avro_schema: &'a Value, name: &str) -> &'a Value {
3779        let fields = avro_schema
3780            .get("fields")
3781            .and_then(|v| v.as_array())
3782            .expect("avro schema has 'fields' array");
3783        fields
3784            .iter()
3785            .find(|f| f.get("name").and_then(|n| n.as_str()) == Some(name))
3786            .unwrap_or_else(|| panic!("avro schema missing field '{name}'"))
3787            .get("type")
3788            .expect("field has 'type'")
3789    }
3790
3791    #[test]
3792    #[cfg_attr(miri, ignore)] // Unsupported inline assembly
3793    fn e2e_types_and_schema_alignment() -> Result<(), AvroError> {
3794        // Values are chosen to:
3795        // - exercise full UInt64 range when `avro_custom_types` is enabled
3796        // - exercise negative / sub-millisecond intervals when `avro_custom_types` is enabled
3797        // - remain representable under standard Avro logical types when `avro_custom_types` is disabled
3798        let i8_values: Vec<Option<i8>> = vec![Some(i8::MIN), Some(-1), Some(i8::MAX)];
3799        let i16_values: Vec<Option<i16>> = vec![Some(i16::MIN), Some(-1), Some(i16::MAX)];
3800        let u8_values: Vec<Option<u8>> = vec![Some(0), Some(1), Some(u8::MAX)];
3801        let u16_values: Vec<Option<u16>> = vec![Some(0), Some(1), Some(u16::MAX)];
3802        let u32_values: Vec<Option<u32>> = vec![Some(0), Some(1), Some(u32::MAX)];
3803        let u64_values: Vec<Option<u64>> = if cfg!(feature = "avro_custom_types") {
3804            vec![Some(0), Some(i64::MAX as u64), Some((i64::MAX as u64) + 1)]
3805        } else {
3806            // Must remain <= i64::MAX when `avro_custom_types` is disabled
3807            vec![Some(0), Some((i64::MAX as u64) - 1), Some(i64::MAX as u64)]
3808        };
3809        let f16_values: Vec<Option<f16>> = vec![
3810            Some(f16::from_f32(1.5)),
3811            Some(f16::from_f32(-2.0)),
3812            Some(f16::from_f32(0.0)),
3813        ];
3814        let date64_values: Vec<Option<i64>> = vec![Some(-86_400_000), Some(0), Some(86_400_000)];
3815        let time32s_values: Vec<Option<i32>> = vec![Some(0), Some(1), Some(86_399)];
3816        let time64ns_values: Vec<Option<i64>> = vec![
3817            Some(0),
3818            Some(1_234_567_890), // truncation case for no-custom (nanos -> micros)
3819            Some(86_399_000_000_123_i64), // near end-of-day, also truncation
3820        ];
3821        let ts_s_local_values: Vec<Option<i64>> = vec![Some(-1), Some(0), Some(1)];
3822        let ts_s_utc_values: Vec<Option<i64>> = vec![Some(1), Some(2), Some(3)];
3823        let iv_ym_values: Vec<Option<i32>> = if cfg!(feature = "avro_custom_types") {
3824            vec![Some(0), Some(-6), Some(25)]
3825        } else {
3826            // Avro duration cannot represent negative months without custom types
3827            vec![Some(0), Some(12), Some(25)]
3828        };
3829        let iv_dt_values: Vec<Option<IntervalDayTime>> = if cfg!(feature = "avro_custom_types") {
3830            vec![
3831                Some(IntervalDayTime::new(0, 0)),
3832                Some(IntervalDayTime::new(1, 1000)),
3833                Some(IntervalDayTime::new(-1, -1000)),
3834            ]
3835        } else {
3836            // Avro duration cannot represent negative day-time without custom types
3837            vec![
3838                Some(IntervalDayTime::new(0, 0)),
3839                Some(IntervalDayTime::new(1, 1000)),
3840                Some(IntervalDayTime::new(30, 3_600_000)),
3841            ]
3842        };
3843        let iv_mdn_values: Vec<Option<IntervalMonthDayNano>> =
3844            if cfg!(feature = "avro_custom_types") {
3845                vec![
3846                    Some(IntervalMonthDayNano::new(0, 0, 0)),
3847                    Some(IntervalMonthDayNano::new(1, 2, 3)), // sub-millisecond
3848                    Some(IntervalMonthDayNano::new(-1, -2, -3)), // negative
3849                ]
3850            } else {
3851                // Avro duration requires non-negative and whole milliseconds
3852                vec![
3853                    Some(IntervalMonthDayNano::new(0, 0, 0)),
3854                    Some(IntervalMonthDayNano::new(1, 2, 3_000_000)), // 3ms
3855                    Some(IntervalMonthDayNano::new(10, 20, 30_000_000_000)), // 30s
3856                ]
3857            };
3858        // Build a batch containing all impacted types from issue #9290
3859        let schema = Schema::new(vec![
3860            Field::new("i8", DataType::Int8, false),
3861            Field::new("i16", DataType::Int16, false),
3862            Field::new("u8", DataType::UInt8, false),
3863            Field::new("u16", DataType::UInt16, false),
3864            Field::new("u32", DataType::UInt32, false),
3865            Field::new("u64", DataType::UInt64, false),
3866            Field::new("f16", DataType::Float16, false),
3867            Field::new("date64", DataType::Date64, false),
3868            Field::new("time32s", DataType::Time32(TimeUnit::Second), false),
3869            Field::new("time64ns", DataType::Time64(TimeUnit::Nanosecond), false),
3870            Field::new(
3871                "ts_s_local",
3872                DataType::Timestamp(TimeUnit::Second, None),
3873                false,
3874            ),
3875            Field::new(
3876                "ts_s_utc",
3877                DataType::Timestamp(TimeUnit::Second, Some("+00:00".into())),
3878                false,
3879            ),
3880            Field::new("iv_ym", DataType::Interval(IntervalUnit::YearMonth), false),
3881            Field::new("iv_dt", DataType::Interval(IntervalUnit::DayTime), false),
3882            Field::new(
3883                "iv_mdn",
3884                DataType::Interval(IntervalUnit::MonthDayNano),
3885                false,
3886            ),
3887        ]);
3888        let batch = RecordBatch::try_new(
3889            Arc::new(schema.clone()),
3890            vec![
3891                Arc::new(Int8Array::from(i8_values.clone())) as ArrayRef,
3892                Arc::new(Int16Array::from(i16_values.clone())) as ArrayRef,
3893                Arc::new(UInt8Array::from(u8_values.clone())) as ArrayRef,
3894                Arc::new(UInt16Array::from(u16_values.clone())) as ArrayRef,
3895                Arc::new(UInt32Array::from(u32_values.clone())) as ArrayRef,
3896                Arc::new(UInt64Array::from(u64_values.clone())) as ArrayRef,
3897                Arc::new(Float16Array::from(f16_values.clone())) as ArrayRef,
3898                Arc::new(Date64Array::from(date64_values.clone())) as ArrayRef,
3899                Arc::new(Time32SecondArray::from(time32s_values.clone())) as ArrayRef,
3900                Arc::new(Time64NanosecondArray::from(time64ns_values.clone())) as ArrayRef,
3901                Arc::new(TimestampSecondArray::from(ts_s_local_values.clone())) as ArrayRef,
3902                Arc::new(
3903                    TimestampSecondArray::from(ts_s_utc_values.clone()).with_timezone("+00:00"),
3904                ) as ArrayRef,
3905                Arc::new(IntervalYearMonthArray::from(iv_ym_values.clone())) as ArrayRef,
3906                Arc::new(IntervalDayTimeArray::from(iv_dt_values.clone())) as ArrayRef,
3907                Arc::new(IntervalMonthDayNanoArray::from(iv_mdn_values.clone())) as ArrayRef,
3908            ],
3909        )?;
3910        let rt = roundtrip_ocf(&batch)?;
3911        let rt_schema = rt.schema();
3912        let avro_schema_json = rt_schema
3913            .metadata()
3914            .get(SCHEMA_METADATA_KEY)
3915            .expect("avro.schema missing in round-tripped batch metadata");
3916        let avro_schema: Value =
3917            serde_json::from_str(avro_schema_json).expect("valid avro schema json");
3918        let rt_arrow_schema = rt.schema();
3919        if cfg!(feature = "avro_custom_types") {
3920            assert!(
3921                schemas_equal_ignoring_metadata(rt_arrow_schema.as_ref(), &schema),
3922                "Schema fields mismatch.\nExpected: {schema:?}\nGot: {rt_arrow_schema:?}"
3923            );
3924            for field_name in ["u64", "f16", "iv_ym", "iv_dt", "iv_mdn"] {
3925                let field = rt_arrow_schema
3926                    .field_with_name(field_name)
3927                    .expect("field exists");
3928                assert!(
3929                    field.metadata().get(AVRO_NAME_METADATA_KEY).is_some(),
3930                    "Field '{field_name}' should have avro.name metadata"
3931                );
3932            }
3933        } else {
3934            // Without avro_custom_types, Avro's type system is narrower than Arrow's.
3935            // Each field below shows the expected type AFTER round-tripping through Avro,
3936            // which differs from the original `schema` above:
3937            let exp_schema = Schema::new(vec![
3938                Field::new("i8", DataType::Int32, false),
3939                Field::new("i16", DataType::Int32, false),
3940                Field::new("u8", DataType::Int32, false),
3941                Field::new("u16", DataType::Int32, false),
3942                Field::new("u32", DataType::Int64, false),
3943                Field::new("u64", DataType::Int64, false),
3944                Field::new("f16", DataType::Float32, false),
3945                Field::new(
3946                    "date64",
3947                    DataType::Timestamp(TimeUnit::Millisecond, None),
3948                    false,
3949                ),
3950                Field::new("time32s", DataType::Time32(TimeUnit::Millisecond), false),
3951                Field::new("time64ns", DataType::Time64(TimeUnit::Microsecond), false),
3952                Field::new(
3953                    "ts_s_local",
3954                    DataType::Timestamp(TimeUnit::Millisecond, None),
3955                    false,
3956                ),
3957                Field::new(
3958                    "ts_s_utc",
3959                    DataType::Timestamp(TimeUnit::Millisecond, Some("+00:00".into())),
3960                    false,
3961                ),
3962                Field::new(
3963                    "iv_ym",
3964                    DataType::Interval(IntervalUnit::MonthDayNano),
3965                    false,
3966                ),
3967                Field::new(
3968                    "iv_dt",
3969                    DataType::Interval(IntervalUnit::MonthDayNano),
3970                    false,
3971                ),
3972                Field::new(
3973                    "iv_mdn",
3974                    DataType::Interval(IntervalUnit::MonthDayNano),
3975                    false,
3976                ),
3977            ]);
3978            assert!(
3979                schemas_equal_ignoring_metadata(rt_arrow_schema.as_ref(), &exp_schema),
3980                "Schema fields mismatch.\nExpected: {exp_schema:?}\nGot: {rt_arrow_schema:?}"
3981            );
3982            for field_name in ["iv_ym", "iv_dt", "iv_mdn"] {
3983                let field = rt_arrow_schema
3984                    .field_with_name(field_name)
3985                    .expect("field exists");
3986                assert!(
3987                    field.metadata().get(AVRO_NAME_METADATA_KEY).is_some(),
3988                    "Field '{field_name}' should have avro.name metadata"
3989                );
3990            }
3991        }
3992        if cfg!(feature = "avro_custom_types") {
3993            assert_eq!(
3994                avro_field_type(&avro_schema, "i8"),
3995                &json!({"type":"int","logicalType":"arrow.int8"})
3996            );
3997            assert_eq!(
3998                avro_field_type(&avro_schema, "i16"),
3999                &json!({"type":"int","logicalType":"arrow.int16"})
4000            );
4001            assert_eq!(
4002                avro_field_type(&avro_schema, "u8"),
4003                &json!({"type":"int","logicalType":"arrow.uint8"})
4004            );
4005            assert_eq!(
4006                avro_field_type(&avro_schema, "u16"),
4007                &json!({"type":"int","logicalType":"arrow.uint16"})
4008            );
4009            assert_eq!(
4010                avro_field_type(&avro_schema, "u32"),
4011                &json!({"type":"long","logicalType":"arrow.uint32"})
4012            );
4013            assert_eq!(
4014                avro_field_type(&avro_schema, "u64"),
4015                &json!({"type":"fixed","name":"u64","size":8,"logicalType":"arrow.uint64"})
4016            );
4017            assert_eq!(
4018                avro_field_type(&avro_schema, "f16"),
4019                &json!({"type":"fixed","name":"f16","size":2,"logicalType":"arrow.float16"})
4020            );
4021            assert_eq!(
4022                avro_field_type(&avro_schema, "date64"),
4023                &json!({"type":"long","logicalType":"arrow.date64"})
4024            );
4025            assert_eq!(
4026                avro_field_type(&avro_schema, "time32s"),
4027                &json!({"type":"int","logicalType":"arrow.time32-second"})
4028            );
4029            assert_eq!(
4030                avro_field_type(&avro_schema, "time64ns"),
4031                &json!({"type":"long","logicalType":"arrow.time64-nanosecond"})
4032            );
4033            assert_eq!(
4034                avro_field_type(&avro_schema, "ts_s_local"),
4035                &json!({"type":"long","logicalType":"arrow.local-timestamp-second"})
4036            );
4037            assert_eq!(
4038                avro_field_type(&avro_schema, "ts_s_utc"),
4039                &json!({"type":"long","logicalType":"arrow.timestamp-second"})
4040            );
4041            assert_eq!(
4042                avro_field_type(&avro_schema, "iv_ym"),
4043                &json!({"type":"fixed","name":"iv_ym","size":4,"logicalType":"arrow.interval-year-month"})
4044            );
4045            assert_eq!(
4046                avro_field_type(&avro_schema, "iv_dt"),
4047                &json!({"type":"fixed","name":"iv_dt","size":8,"logicalType":"arrow.interval-day-time"})
4048            );
4049            assert_eq!(
4050                avro_field_type(&avro_schema, "iv_mdn"),
4051                &json!({"type":"fixed","name":"iv_mdn","size":16,"logicalType":"arrow.interval-month-day-nano"})
4052            );
4053        } else {
4054            // Without custom types:
4055            // - small ints widen to int
4056            // - UInt32/UInt64 widen to long
4057            // - Float16 widens to float
4058            // - Date64 coerces to local-timestamp-millis
4059            // - Time32(Second) coerces to time-millis and scales seconds->millis
4060            // - Time64(Nanosecond) coerces to time-micros and truncates nanos->micros
4061            // - Timestamp(Second) coerces to timestamp-millis / local-timestamp-millis and scales seconds->millis
4062            // - Intervals YearMonth/DayTime encode as Avro duration (fixed 12) with arrowIntervalUnit annotation
4063            assert_eq!(avro_field_type(&avro_schema, "i8"), &json!("int"));
4064            assert_eq!(avro_field_type(&avro_schema, "i16"), &json!("int"));
4065            assert_eq!(avro_field_type(&avro_schema, "u8"), &json!("int"));
4066            assert_eq!(avro_field_type(&avro_schema, "u16"), &json!("int"));
4067            assert_eq!(avro_field_type(&avro_schema, "u32"), &json!("long"));
4068            assert_eq!(avro_field_type(&avro_schema, "u64"), &json!("long"));
4069            assert_eq!(avro_field_type(&avro_schema, "f16"), &json!("float"));
4070            assert_eq!(
4071                avro_field_type(&avro_schema, "date64"),
4072                &json!({"type":"long","logicalType":"local-timestamp-millis"})
4073            );
4074            assert_eq!(
4075                avro_field_type(&avro_schema, "time32s"),
4076                &json!({"type":"int","logicalType":"time-millis"})
4077            );
4078            assert_eq!(
4079                avro_field_type(&avro_schema, "time64ns"),
4080                &json!({"type":"long","logicalType":"time-micros"})
4081            );
4082            assert_eq!(
4083                avro_field_type(&avro_schema, "ts_s_local"),
4084                &json!({"type":"long","logicalType":"local-timestamp-millis"})
4085            );
4086            assert_eq!(
4087                avro_field_type(&avro_schema, "ts_s_utc"),
4088                &json!({"type":"long","logicalType":"timestamp-millis"})
4089            );
4090            assert_eq!(
4091                avro_field_type(&avro_schema, "iv_ym"),
4092                &json!({"type":"fixed","name":"iv_ym","size":12,"logicalType":"duration"})
4093            );
4094            assert_eq!(
4095                avro_field_type(&avro_schema, "iv_dt"),
4096                &json!({"type":"fixed","name":"iv_dt","size":12,"logicalType":"duration"})
4097            );
4098            assert_eq!(
4099                avro_field_type(&avro_schema, "iv_mdn"),
4100                &json!({"type":"fixed","name":"iv_mdn","size":12,"logicalType":"duration"})
4101            );
4102        }
4103        if cfg!(feature = "avro_custom_types") {
4104            assert_eq!(
4105                rt.column(0).as_ref(),
4106                &Int8Array::from(i8_values) as &dyn Array
4107            );
4108            assert_eq!(
4109                rt.column(1).as_ref(),
4110                &Int16Array::from(i16_values) as &dyn Array
4111            );
4112            assert_eq!(
4113                rt.column(2).as_ref(),
4114                &UInt8Array::from(u8_values) as &dyn Array
4115            );
4116            assert_eq!(
4117                rt.column(3).as_ref(),
4118                &UInt16Array::from(u16_values) as &dyn Array
4119            );
4120            assert_eq!(
4121                rt.column(4).as_ref(),
4122                &UInt32Array::from(u32_values) as &dyn Array
4123            );
4124            assert_eq!(
4125                rt.column(5).as_ref(),
4126                &UInt64Array::from(u64_values) as &dyn Array
4127            );
4128            assert_eq!(
4129                rt.column(6).as_ref(),
4130                &Float16Array::from(f16_values) as &dyn Array
4131            );
4132            assert_eq!(
4133                rt.column(7).as_ref(),
4134                &Date64Array::from(date64_values) as &dyn Array
4135            );
4136            assert_eq!(
4137                rt.column(8).as_ref(),
4138                &Time32SecondArray::from(time32s_values) as &dyn Array
4139            );
4140            assert_eq!(
4141                rt.column(9).as_ref(),
4142                &Time64NanosecondArray::from(time64ns_values) as &dyn Array
4143            );
4144            assert_eq!(
4145                rt.column(10).as_ref(),
4146                &TimestampSecondArray::from(ts_s_local_values) as &dyn Array
4147            );
4148            assert_eq!(
4149                rt.column(11).as_ref(),
4150                &TimestampSecondArray::from(ts_s_utc_values).with_timezone("+00:00") as &dyn Array
4151            );
4152            assert_eq!(
4153                rt.column(12).as_ref(),
4154                &IntervalYearMonthArray::from(iv_ym_values) as &dyn Array
4155            );
4156            assert_eq!(
4157                rt.column(13).as_ref(),
4158                &IntervalDayTimeArray::from(iv_dt_values) as &dyn Array
4159            );
4160            assert_eq!(
4161                rt.column(14).as_ref(),
4162                &IntervalMonthDayNanoArray::from(iv_mdn_values) as &dyn Array
4163            );
4164        } else {
4165            let exp_i8: Vec<Option<i32>> = i8_values.iter().map(|v| v.map(|x| x as i32)).collect();
4166            let exp_i16: Vec<Option<i32>> =
4167                i16_values.iter().map(|v| v.map(|x| x as i32)).collect();
4168            let exp_u8: Vec<Option<i32>> = u8_values.iter().map(|v| v.map(|x| x as i32)).collect();
4169            let exp_u16: Vec<Option<i32>> =
4170                u16_values.iter().map(|v| v.map(|x| x as i32)).collect();
4171            let exp_u32: Vec<Option<i64>> =
4172                u32_values.iter().map(|v| v.map(|x| x as i64)).collect();
4173            let exp_u64: Vec<Option<i64>> =
4174                u64_values.iter().map(|v| v.map(|x| x as i64)).collect();
4175            let exp_f16: Vec<Option<f32>> =
4176                f16_values.iter().map(|v| v.map(|x| x.to_f32())).collect();
4177            let exp_time32_ms: Vec<Option<i32>> = time32s_values
4178                .iter()
4179                .map(|v| v.map(|x| x.saturating_mul(1000)))
4180                .collect();
4181            let exp_time64_us: Vec<Option<i64>> = time64ns_values
4182                .iter()
4183                .map(|v| v.map(|x| x / 1000))
4184                .collect();
4185            let exp_ts_local_ms: Vec<Option<i64>> = ts_s_local_values
4186                .iter()
4187                .map(|v| v.map(|x| x * 1000))
4188                .collect();
4189            let exp_ts_utc_ms: Vec<Option<i64>> = ts_s_utc_values
4190                .iter()
4191                .map(|v| v.map(|x| x * 1000))
4192                .collect();
4193            // Interval conversions to MonthDayNano via Avro duration
4194            let exp_iv_ym: Vec<Option<IntervalMonthDayNano>> = iv_ym_values
4195                .iter()
4196                .map(|v| v.map(|months| IntervalMonthDayNano::new(months, 0, 0)))
4197                .collect();
4198            let exp_iv_dt: Vec<Option<IntervalMonthDayNano>> = iv_dt_values
4199                .iter()
4200                .map(|v| {
4201                    v.map(|dt| {
4202                        IntervalMonthDayNano::new(0, dt.days, (dt.milliseconds as i64) * 1_000_000)
4203                    })
4204                })
4205                .collect();
4206            assert_eq!(
4207                rt.column(0).as_ref(),
4208                &Int32Array::from(exp_i8) as &dyn Array
4209            );
4210            assert_eq!(
4211                rt.column(1).as_ref(),
4212                &Int32Array::from(exp_i16) as &dyn Array
4213            );
4214            assert_eq!(
4215                rt.column(2).as_ref(),
4216                &Int32Array::from(exp_u8) as &dyn Array
4217            );
4218            assert_eq!(
4219                rt.column(3).as_ref(),
4220                &Int32Array::from(exp_u16) as &dyn Array
4221            );
4222            assert_eq!(
4223                rt.column(4).as_ref(),
4224                &arrow_array::Int64Array::from(exp_u32) as &dyn Array
4225            );
4226            assert_eq!(
4227                rt.column(5).as_ref(),
4228                &arrow_array::Int64Array::from(exp_u64) as &dyn Array
4229            );
4230            assert_eq!(
4231                rt.column(6).as_ref(),
4232                &arrow_array::Float32Array::from(exp_f16) as &dyn Array
4233            );
4234            assert_eq!(
4235                rt.column(7).as_ref(),
4236                &TimestampMillisecondArray::from(date64_values) as &dyn Array
4237            );
4238            assert_eq!(
4239                rt.column(8).as_ref(),
4240                &Time32MillisecondArray::from(exp_time32_ms) as &dyn Array
4241            );
4242            assert_eq!(
4243                rt.column(9).as_ref(),
4244                &Time64MicrosecondArray::from(exp_time64_us) as &dyn Array
4245            );
4246            assert_eq!(
4247                rt.column(10).as_ref(),
4248                &TimestampMillisecondArray::from(exp_ts_local_ms) as &dyn Array
4249            );
4250            assert_eq!(
4251                rt.column(11).as_ref(),
4252                &TimestampMillisecondArray::from(exp_ts_utc_ms).with_timezone("+00:00")
4253                    as &dyn Array
4254            );
4255            assert_eq!(
4256                rt.column(12).as_ref(),
4257                &IntervalMonthDayNanoArray::from(exp_iv_ym) as &dyn Array
4258            );
4259            assert_eq!(
4260                rt.column(13).as_ref(),
4261                &IntervalMonthDayNanoArray::from(exp_iv_dt) as &dyn Array
4262            );
4263            assert_eq!(
4264                rt.column(14).as_ref(),
4265                &IntervalMonthDayNanoArray::from(iv_mdn_values) as &dyn Array
4266            );
4267        }
4268        Ok(())
4269    }
4270
4271    #[cfg(not(feature = "avro_custom_types"))]
4272    #[test]
4273    fn non_custom_uint64_overflow_errors() -> Result<(), AvroError> {
4274        let schema = Schema::new(vec![Field::new("u64", DataType::UInt64, false)]);
4275        let values: Vec<Option<u64>> = vec![Some((i64::MAX as u64) + 1)];
4276        let batch = RecordBatch::try_new(
4277            Arc::new(schema.clone()),
4278            vec![Arc::new(UInt64Array::from(values)) as ArrayRef],
4279        )?;
4280        let mut w = AvroWriter::new(Vec::<u8>::new(), schema)?;
4281        let err = w
4282            .write(&batch)
4283            .expect_err("expected UInt64 overflow error when avro_custom_types is disabled");
4284        match err {
4285            AvroError::InvalidArgument(msg) => {
4286                assert_eq!(
4287                    msg,
4288                    "UInt64 value 9223372036854775808 exceeds i64::MAX; enable avro_custom_types feature for full UInt64 support"
4289                );
4290            }
4291            other => panic!("expected InvalidArgument, got {other:?}"),
4292        }
4293        Ok(())
4294    }
4295
4296    #[cfg(not(feature = "avro_custom_types"))]
4297    #[test]
4298    fn non_custom_interval_year_month_negative_errors() -> Result<(), AvroError> {
4299        let schema = Schema::new(vec![Field::new(
4300            "iv_ym",
4301            DataType::Interval(IntervalUnit::YearMonth),
4302            false,
4303        )]);
4304        let values: Vec<Option<i32>> = vec![Some(-1)];
4305        let batch = RecordBatch::try_new(
4306            Arc::new(schema.clone()),
4307            vec![Arc::new(IntervalYearMonthArray::from(values)) as ArrayRef],
4308        )?;
4309
4310        let mut w = AvroWriter::new(Vec::<u8>::new(), schema)?;
4311        let err = w
4312            .write(&batch)
4313            .expect_err("expected negative Interval(YearMonth) error");
4314        match err {
4315            AvroError::InvalidArgument(msg) => {
4316                assert_eq!(
4317                    msg,
4318                    "Avro 'duration' cannot encode negative months; enable `avro_custom_types` to round-trip signed Arrow Interval(YearMonth)"
4319                );
4320            }
4321            other => panic!("expected InvalidArgument, got {other:?}"),
4322        }
4323        Ok(())
4324    }
4325
4326    #[cfg(not(feature = "avro_custom_types"))]
4327    #[test]
4328    fn non_custom_interval_day_time_negative_errors() -> Result<(), AvroError> {
4329        let schema = Schema::new(vec![Field::new(
4330            "iv_dt",
4331            DataType::Interval(IntervalUnit::DayTime),
4332            false,
4333        )]);
4334        let values: Vec<Option<IntervalDayTime>> = vec![Some(IntervalDayTime::new(-1, 0))];
4335        let batch = RecordBatch::try_new(
4336            Arc::new(schema.clone()),
4337            vec![Arc::new(IntervalDayTimeArray::from(values)) as ArrayRef],
4338        )?;
4339        let mut w = AvroWriter::new(Vec::<u8>::new(), schema)?;
4340        let err = w
4341            .write(&batch)
4342            .expect_err("expected negative Interval(DayTime) error");
4343        match err {
4344            AvroError::InvalidArgument(msg) => {
4345                assert_eq!(
4346                    msg,
4347                    "Avro 'duration' cannot encode negative days or milliseconds; enable `avro_custom_types` to round-trip signed Arrow Interval(DayTime)"
4348                );
4349            }
4350            other => panic!("expected InvalidArgument, got {other:?}"),
4351        }
4352        Ok(())
4353    }
4354
4355    #[cfg(not(feature = "avro_custom_types"))]
4356    #[test]
4357    fn non_custom_interval_month_day_nano_negative_errors() -> Result<(), AvroError> {
4358        let schema = Schema::new(vec![Field::new(
4359            "iv_mdn",
4360            DataType::Interval(IntervalUnit::MonthDayNano),
4361            false,
4362        )]);
4363        let values: Vec<Option<IntervalMonthDayNano>> =
4364            vec![Some(IntervalMonthDayNano::new(-1, 0, 0))];
4365        let batch = RecordBatch::try_new(
4366            Arc::new(schema.clone()),
4367            vec![Arc::new(IntervalMonthDayNanoArray::from(values)) as ArrayRef],
4368        )?;
4369        let mut w = AvroWriter::new(Vec::<u8>::new(), schema)?;
4370        let err = w
4371            .write(&batch)
4372            .expect_err("expected negative Interval(MonthDayNano) error");
4373        match err {
4374            AvroError::InvalidArgument(msg) => {
4375                assert_eq!(
4376                    msg,
4377                    "Avro 'duration' cannot encode negative months/days/nanoseconds; enable `avro_custom_types` to round-trip signed Arrow intervals"
4378                );
4379            }
4380            other => panic!("expected InvalidArgument, got {other:?}"),
4381        }
4382        Ok(())
4383    }
4384
4385    #[cfg(not(feature = "avro_custom_types"))]
4386    #[test]
4387    fn non_custom_interval_month_day_nano_sub_millis_errors() -> Result<(), AvroError> {
4388        let schema = Schema::new(vec![Field::new(
4389            "iv_mdn",
4390            DataType::Interval(IntervalUnit::MonthDayNano),
4391            false,
4392        )]);
4393        let values: Vec<Option<IntervalMonthDayNano>> =
4394            vec![Some(IntervalMonthDayNano::new(0, 0, 1))];
4395        let batch = RecordBatch::try_new(
4396            Arc::new(schema.clone()),
4397            vec![Arc::new(IntervalMonthDayNanoArray::from(values)) as ArrayRef],
4398        )?;
4399        let mut w = AvroWriter::new(Vec::<u8>::new(), schema)?;
4400        let err = w
4401            .write(&batch)
4402            .expect_err("expected sub-millisecond Interval(MonthDayNano) error");
4403        match err {
4404            AvroError::InvalidArgument(msg) => {
4405                assert_eq!(
4406                    msg,
4407                    "Avro 'duration' requires whole milliseconds; nanoseconds must be divisible by 1_000_000 (enable `avro_custom_types` to preserve nanosecond intervals)"
4408                );
4409            }
4410            other => panic!("expected InvalidArgument, got {other:?}"),
4411        }
4412        Ok(())
4413    }
4414
4415    #[cfg(not(feature = "avro_custom_types"))]
4416    #[test]
4417    fn non_custom_time32_second_scaling_overflow_errors() -> Result<(), AvroError> {
4418        let schema = Schema::new(vec![Field::new(
4419            "time32s",
4420            DataType::Time32(TimeUnit::Second),
4421            false,
4422        )]);
4423        let values: Vec<Option<i32>> = vec![Some((i32::MAX / 1000) + 1)];
4424        let batch = RecordBatch::try_new(
4425            Arc::new(schema.clone()),
4426            vec![Arc::new(Time32SecondArray::from(values)) as ArrayRef],
4427        )?;
4428        let mut w = AvroWriter::new(Vec::<u8>::new(), schema)?;
4429        let err = w
4430            .write(&batch)
4431            .expect_err("expected time32 seconds->millis overflow error");
4432        match err {
4433            AvroError::InvalidArgument(msg) => {
4434                assert_eq!(msg, "time32(secs) * 1000 overflowed");
4435            }
4436            other => panic!("expected InvalidArgument, got {other:?}"),
4437        }
4438        Ok(())
4439    }
4440
4441    #[cfg(not(feature = "avro_custom_types"))]
4442    #[test]
4443    fn non_custom_timestamp_second_scaling_overflow_errors() -> Result<(), AvroError> {
4444        let schema = Schema::new(vec![Field::new(
4445            "ts_s_local",
4446            DataType::Timestamp(TimeUnit::Second, None),
4447            false,
4448        )]);
4449        // i64::MAX / 1000 + 1 will overflow when multiplied by 1000
4450        let values: Vec<Option<i64>> = vec![Some((i64::MAX / 1000) + 1)];
4451        let batch = RecordBatch::try_new(
4452            Arc::new(schema.clone()),
4453            vec![Arc::new(TimestampSecondArray::from(values)) as ArrayRef],
4454        )?;
4455        let mut w = AvroWriter::new(Vec::<u8>::new(), schema)?;
4456        let err = w
4457            .write(&batch)
4458            .expect_err("expected timestamp seconds->millis overflow error");
4459        match err {
4460            AvroError::InvalidArgument(msg) => {
4461                assert_eq!(msg, "timestamp(secs) * 1000 overflowed");
4462            }
4463            other => panic!("expected InvalidArgument, got {other:?}"),
4464        }
4465        Ok(())
4466    }
4467}