Skip to main content

parquet/arrow/arrow_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//! Contains writer which writes arrow data into parquet data.
19
20use crate::column::chunker::ContentDefinedChunker;
21
22use bytes::Bytes;
23use std::io::Write;
24use std::slice::Iter;
25use std::sync::{Arc, Mutex};
26use std::vec::IntoIter;
27
28use arrow_array::cast::AsArray;
29use arrow_array::{ArrayRef, Int32Array, RecordBatch, RecordBatchWriter};
30use arrow_array::{PrimitiveArray, types::*};
31use arrow_schema::{
32    ArrowError, DataType as ArrowDataType, Field, IntervalUnit, SchemaRef, TimeUnit,
33};
34
35use super::schema::{add_encoded_arrow_schema_to_metadata, decimal_length_from_precision};
36
37use crate::arrow::ArrowSchemaConverter;
38use crate::arrow::arrow_writer::byte_array::ByteArrayEncoder;
39use crate::basic::PageType;
40use crate::column::page::{CompressedPage, PageWriteSpec, PageWriter};
41use crate::column::page_encryption::PageEncryptor;
42use crate::column::writer::encoder::ColumnValueEncoder;
43use crate::column::writer::{
44    ColumnCloseResult, ColumnWriter, GenericColumnWriter, get_column_writer,
45};
46use crate::data_type::{ByteArray, FixedLenByteArray};
47use std::collections::HashSet;
48type DistinctValuesSet = HashSet<u64>;
49#[cfg(feature = "encryption")]
50use crate::encryption::encrypt::FileEncryptor;
51use crate::errors::{ParquetError, Result};
52use crate::file::metadata::{KeyValue, ParquetMetaData, RowGroupMetaData};
53use crate::file::properties::{WriterProperties, WriterPropertiesPtr};
54use crate::file::writer::{SerializedFileWriter, SerializedRowGroupWriter};
55use crate::parquet_thrift::{ThriftCompactOutputProtocol, WriteThrift};
56use crate::schema::types::{ColumnDescPtr, SchemaDescPtr, SchemaDescriptor};
57use levels::{ArrayLevels, calculate_array_levels};
58
59mod byte_array;
60mod levels;
61
62#[doc(inline)]
63pub use crate::column::page_store::{
64    InMemoryPageStore, InMemoryPageStoreFactory, PageKey, PageStore, PageStoreArgs,
65    PageStoreFactory,
66};
67
68/// Encodes [`RecordBatch`] to parquet
69///
70/// Writes Arrow `RecordBatch`es to a Parquet writer. Multiple [`RecordBatch`] will be encoded
71/// to the same row group, up to `max_row_group_size` rows. Any remaining rows will be
72/// flushed on close, leading the final row group in the output file to potentially
73/// contain fewer than `max_row_group_size` rows
74///
75/// # Example: Writing `RecordBatch`es
76/// ```
77/// # use std::sync::Arc;
78/// # use bytes::Bytes;
79/// # use arrow_array::{ArrayRef, Int64Array};
80/// # use arrow_array::RecordBatch;
81/// # use parquet::arrow::arrow_writer::ArrowWriter;
82/// # use parquet::arrow::arrow_reader::ParquetRecordBatchReader;
83/// let col = Arc::new(Int64Array::from_iter_values([1, 2, 3])) as ArrayRef;
84/// let to_write = RecordBatch::try_from_iter([("col", col)]).unwrap();
85///
86/// let mut buffer = Vec::new();
87/// let mut writer = ArrowWriter::try_new(&mut buffer, to_write.schema(), None).unwrap();
88/// writer.write(&to_write).unwrap();
89/// writer.close().unwrap();
90///
91/// let mut reader = ParquetRecordBatchReader::try_new(Bytes::from(buffer), 1024).unwrap();
92/// let read = reader.next().unwrap().unwrap();
93///
94/// assert_eq!(to_write, read);
95/// ```
96///
97/// # Memory Usage and Limiting
98///
99/// The nature of Parquet requires buffering of an entire row group before it can
100/// be flushed to the underlying writer. Data is mostly buffered in its encoded
101/// form, reducing memory usage. However, some data such as dictionary keys,
102/// large strings or very nested data may still result in non-trivial memory
103/// usage.
104///
105/// See Also:
106/// * [`ArrowWriter::memory_size`]: the current memory usage of the writer.
107/// * [`ArrowWriter::in_progress_size`]: Estimated size of the buffered row group,
108///
109/// Call [`Self::flush`] to trigger an early flush of a row group based on a
110/// memory threshold and/or global memory pressure. However,  smaller row groups
111/// result in higher metadata overheads, and thus may worsen compression ratios
112/// and query performance.
113///
114/// ```no_run
115/// # use std::io::Write;
116/// # use arrow_array::RecordBatch;
117/// # use parquet::arrow::ArrowWriter;
118/// # let mut writer: ArrowWriter<Vec<u8>> = todo!();
119/// # let batch: RecordBatch = todo!();
120/// writer.write(&batch).unwrap();
121/// // Trigger an early flush if anticipated size exceeds 1_000_000
122/// if writer.in_progress_size() > 1_000_000 {
123///     writer.flush().unwrap();
124/// }
125/// ```
126///
127/// ## Type Support
128///
129/// The writer supports writing all Arrow [`DataType`]s that have a direct mapping to
130/// Parquet types including  [`StructArray`] and [`ListArray`].
131///
132/// The following are not supported:
133///
134/// * [`IntervalMonthDayNanoArray`]: Parquet does not [support nanosecond intervals].
135///
136/// [`DataType`]: https://docs.rs/arrow/latest/arrow/datatypes/enum.DataType.html
137/// [`StructArray`]: https://docs.rs/arrow/latest/arrow/array/struct.StructArray.html
138/// [`ListArray`]: https://docs.rs/arrow/latest/arrow/array/type.ListArray.html
139/// [`IntervalMonthDayNanoArray`]: https://docs.rs/arrow/latest/arrow/array/type.IntervalMonthDayNanoArray.html
140/// [support nanosecond intervals]: https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#interval
141///
142/// ## Type Compatibility
143/// The writer can write Arrow [`RecordBatch`]s that are logically equivalent. This means that for
144/// a  given column, the writer can accept multiple Arrow [`DataType`]s that contain the same
145/// value type.
146///
147/// For example, the following [`DataType`]s are all logically equivalent and can be written
148/// to the same column:
149/// * String, LargeString, StringView
150/// * Binary, LargeBinary, BinaryView
151///
152/// The writer can will also accept both native and dictionary encoded arrays if the dictionaries
153/// contain compatible values.
154/// ```
155/// # use std::sync::Arc;
156/// # use arrow_array::{DictionaryArray, LargeStringArray, RecordBatch, StringArray, UInt8Array};
157/// # use arrow_schema::{DataType, Field, Schema};
158/// # use parquet::arrow::arrow_writer::ArrowWriter;
159/// let record_batch1 = RecordBatch::try_new(
160///    Arc::new(Schema::new(vec![Field::new("col", DataType::LargeUtf8, false)])),
161///    vec![Arc::new(LargeStringArray::from_iter_values(vec!["a", "b"]))]
162///  )
163/// .unwrap();
164///
165/// let mut buffer = Vec::new();
166/// let mut writer = ArrowWriter::try_new(&mut buffer, record_batch1.schema(), None).unwrap();
167/// writer.write(&record_batch1).unwrap();
168///
169/// let record_batch2 = RecordBatch::try_new(
170///     Arc::new(Schema::new(vec![Field::new(
171///         "col",
172///         DataType::Dictionary(Box::new(DataType::UInt8), Box::new(DataType::Utf8)),
173///          false,
174///     )])),
175///     vec![Arc::new(DictionaryArray::new(
176///          UInt8Array::from_iter_values(vec![0, 1]),
177///          Arc::new(StringArray::from_iter_values(vec!["b", "c"])),
178///      ))],
179///  )
180///  .unwrap();
181///  writer.write(&record_batch2).unwrap();
182///  writer.close();
183/// ```
184pub struct ArrowWriter<W: Write> {
185    /// Underlying Parquet writer
186    writer: SerializedFileWriter<W>,
187
188    /// The in-progress row group if any
189    in_progress: Option<ArrowRowGroupWriter>,
190
191    /// A copy of the Arrow schema.
192    ///
193    /// The schema is used to verify that each record batch written has the correct schema
194    arrow_schema: SchemaRef,
195
196    /// Creates new [`ArrowRowGroupWriter`] instances as required
197    row_group_writer_factory: ArrowRowGroupWriterFactory,
198
199    /// The maximum number of rows to write to each row group, or None for unlimited
200    max_row_group_row_count: Option<usize>,
201
202    /// The maximum size in bytes for a row group, or None for unlimited
203    max_row_group_bytes: Option<usize>,
204
205    /// CDC chunkers persisted across row groups (one per leaf column).
206    cdc_chunkers: Option<Vec<ContentDefinedChunker>>,
207}
208
209impl<W: Write + Send> std::fmt::Debug for ArrowWriter<W> {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        let buffered_memory = self.in_progress_size();
212        f.debug_struct("ArrowWriter")
213            .field("writer", &self.writer)
214            .field("in_progress_size", &format_args!("{buffered_memory} bytes"))
215            .field("in_progress_rows", &self.in_progress_rows())
216            .field("arrow_schema", &self.arrow_schema)
217            .field("max_row_group_row_count", &self.max_row_group_row_count)
218            .field("max_row_group_bytes", &self.max_row_group_bytes)
219            .finish()
220    }
221}
222
223impl<W: Write + Send> ArrowWriter<W> {
224    /// Try to create a new Arrow writer
225    ///
226    /// The writer will fail if:
227    ///  * a `SerializedFileWriter` cannot be created from the ParquetWriter
228    ///  * the Arrow schema contains unsupported datatypes such as Unions
229    pub fn try_new(
230        writer: W,
231        arrow_schema: SchemaRef,
232        props: Option<WriterProperties>,
233    ) -> Result<Self> {
234        let options = ArrowWriterOptions::new().with_properties(props.unwrap_or_default());
235        Self::try_new_with_options(writer, arrow_schema, options)
236    }
237
238    /// Try to create a new Arrow writer with [`ArrowWriterOptions`].
239    ///
240    /// The writer will fail if:
241    ///  * a `SerializedFileWriter` cannot be created from the ParquetWriter
242    ///  * the Arrow schema contains unsupported datatypes such as Unions
243    pub fn try_new_with_options(
244        writer: W,
245        arrow_schema: SchemaRef,
246        options: ArrowWriterOptions,
247    ) -> Result<Self> {
248        let mut props = options.properties;
249
250        let schema = if let Some(parquet_schema) = options.schema_descr {
251            parquet_schema.clone()
252        } else {
253            let mut converter = ArrowSchemaConverter::new().with_coerce_types(props.coerce_types());
254            if let Some(schema_root) = &options.schema_root {
255                converter = converter.schema_root(schema_root);
256            }
257
258            converter.convert(&arrow_schema)?
259        };
260
261        if !options.skip_arrow_metadata {
262            // add serialized arrow schema
263            add_encoded_arrow_schema_to_metadata(&arrow_schema, &mut props);
264        }
265
266        let max_row_group_row_count = props.max_row_group_row_count();
267        let max_row_group_bytes = props.max_row_group_bytes();
268
269        let props_ptr = Arc::new(props);
270        let file_writer =
271            SerializedFileWriter::new(writer, schema.root_schema_ptr(), Arc::clone(&props_ptr))?;
272
273        let mut row_group_writer_factory =
274            ArrowRowGroupWriterFactory::new(&file_writer, arrow_schema.clone());
275        if let Some(page_store_factory) = options.page_store_factory {
276            row_group_writer_factory =
277                row_group_writer_factory.with_page_store_factory(page_store_factory);
278        }
279
280        let cdc_chunkers = props_ptr
281            .content_defined_chunking()
282            .map(|opts| {
283                file_writer
284                    .schema_descr()
285                    .columns()
286                    .iter()
287                    .map(|desc| ContentDefinedChunker::new(desc, opts))
288                    .collect::<Result<Vec<_>>>()
289            })
290            .transpose()?;
291
292        Ok(Self {
293            writer: file_writer,
294            in_progress: None,
295            arrow_schema,
296            row_group_writer_factory,
297            max_row_group_row_count,
298            max_row_group_bytes,
299            cdc_chunkers,
300        })
301    }
302
303    /// Returns metadata for any flushed row groups
304    pub fn flushed_row_groups(&self) -> &[RowGroupMetaData] {
305        self.writer.flushed_row_groups()
306    }
307
308    /// Estimated memory usage, in bytes, of this `ArrowWriter`
309    ///
310    /// This estimate is formed bu summing the values of
311    /// [`ArrowColumnWriter::memory_size`] all in progress columns.
312    pub fn memory_size(&self) -> usize {
313        match &self.in_progress {
314            Some(in_progress) => in_progress.writers.iter().map(|x| x.memory_size()).sum(),
315            None => 0,
316        }
317    }
318
319    /// Anticipated encoded size of the in progress row group.
320    ///
321    /// This estimate the row group size after being completely encoded is,
322    /// formed by summing the values of
323    /// [`ArrowColumnWriter::get_estimated_total_bytes`] for all in progress
324    /// columns.
325    pub fn in_progress_size(&self) -> usize {
326        match &self.in_progress {
327            Some(in_progress) => in_progress
328                .writers
329                .iter()
330                .map(|x| x.get_estimated_total_bytes())
331                .sum(),
332            None => 0,
333        }
334    }
335
336    /// Returns the number of rows buffered in the in progress row group
337    pub fn in_progress_rows(&self) -> usize {
338        self.in_progress
339            .as_ref()
340            .map(|x| x.buffered_rows)
341            .unwrap_or_default()
342    }
343
344    /// Returns the number of bytes written by this instance
345    pub fn bytes_written(&self) -> usize {
346        self.writer.bytes_written()
347    }
348
349    /// Encodes the provided [`RecordBatch`]
350    ///
351    /// If this would cause the current row group to exceed [`WriterProperties::max_row_group_row_count`]
352    /// rows or [`WriterProperties::max_row_group_bytes`] bytes, the contents of `batch` will be
353    /// written to one or more row groups such that limits are respected.
354    ///
355    /// If both limits are `None`, all data is written to a single row group.
356    /// If one limit is set, that limit is respected.
357    /// If both limits are set, the lower bound (whichever triggers first) is respected.
358    ///
359    /// This will fail if the `batch`'s schema does not match the writer's schema,
360    /// or if a logical null is reachable at a position the writer schema declares
361    /// non-nullable.
362    pub fn write(&mut self, batch: &RecordBatch) -> Result<()> {
363        if batch.num_rows() == 0 {
364            return Ok(());
365        }
366
367        // Rows not yet handed to a row group writer. Splitting iterates here instead of
368        // recursing, so a small row group limit over a large batch cannot exhaust the stack.
369        let mut remaining = batch.clone();
370
371        loop {
372            let in_progress = match &mut self.in_progress {
373                Some(in_progress) => in_progress,
374                x => x.insert(
375                    self.row_group_writer_factory
376                        .create_row_group_writer(self.writer.flushed_row_groups().len())?,
377                ),
378            };
379            let buffered_rows = in_progress.buffered_rows;
380
381            // Leading rows of `remaining` that still fit in the current row group, when the
382            // rest has to go to a later one.
383            let mut split_at = match self.max_row_group_row_count {
384                Some(max_rows) if buffered_rows + remaining.num_rows() > max_rows => {
385                    Some(max_rows - buffered_rows)
386                }
387                _ => None,
388            };
389
390            // Check byte limit: if we have buffered data, use measured average row size
391            // to split batch proactively before exceeding byte limit. Both limits apply to
392            // the same rows, so measure against whatever the row limit already trimmed
393            // `remaining` down to; otherwise the row limit would always win.
394            let candidate_rows = split_at.unwrap_or_else(|| remaining.num_rows());
395
396            if let Some(max_bytes) = self.max_row_group_bytes
397                && buffered_rows > 0
398            {
399                let current_bytes = in_progress.get_estimated_total_bytes();
400
401                if current_bytes >= max_bytes {
402                    self.flush()?;
403                    continue;
404                }
405
406                let avg_row_bytes = current_bytes / buffered_rows;
407                if let Some(rows_that_fit) = (max_bytes - current_bytes).checked_div(avg_row_bytes)
408                {
409                    // At this point, `current_bytes < max_bytes` (checked above)
410                    if candidate_rows > rows_that_fit {
411                        if rows_that_fit > 0 {
412                            split_at = Some(rows_that_fit);
413                        } else {
414                            self.flush()?;
415                            continue;
416                        }
417                    }
418                }
419            }
420
421            let rest = split_at.map(|to_write| {
422                let rest = remaining.slice(to_write, remaining.num_rows() - to_write);
423                remaining = remaining.slice(0, to_write);
424                rest
425            });
426
427            let in_progress = self.in_progress.as_mut().unwrap();
428            match self.cdc_chunkers.as_mut() {
429                Some(chunkers) => in_progress.write_with_chunkers(&remaining, chunkers)?,
430                None => in_progress.write(&remaining)?,
431            }
432
433            let should_flush = self
434                .max_row_group_row_count
435                .is_some_and(|max| in_progress.buffered_rows >= max)
436                || self
437                    .max_row_group_bytes
438                    .is_some_and(|max| in_progress.get_estimated_total_bytes() >= max);
439
440            if should_flush {
441                self.flush()?
442            }
443
444            match rest {
445                Some(rest) => remaining = rest,
446                None => return Ok(()),
447            }
448        }
449    }
450
451    /// Writes the given buf bytes to the internal buffer.
452    ///
453    /// It's safe to use this method to write data to the underlying writer,
454    /// because it will ensure that the buffering and byte‐counting layers are used.
455    pub fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
456        self.writer.write_all(buf)
457    }
458
459    /// Flushes underlying writer
460    pub fn sync(&mut self) -> std::io::Result<()> {
461        self.writer.flush()
462    }
463
464    /// Flushes all buffered rows into a new row group
465    ///
466    /// Note the underlying writer is not flushed with this call.
467    /// If this is a desired behavior, please call [`ArrowWriter::sync`].
468    pub fn flush(&mut self) -> Result<()> {
469        let Some(in_progress) = self.in_progress.take() else {
470            return Ok(());
471        };
472
473        let mut row_group_writer = self.writer.next_row_group()?;
474        for chunk in in_progress.close()? {
475            chunk.append_to_row_group(&mut row_group_writer)?;
476        }
477        row_group_writer.close()?;
478        Ok(())
479    }
480
481    /// Additional [`KeyValue`] metadata to be written in addition to those from [`WriterProperties`]
482    ///
483    /// This method provide a way to append kv_metadata after write RecordBatch
484    pub fn append_key_value_metadata(&mut self, kv_metadata: KeyValue) {
485        self.writer.append_key_value_metadata(kv_metadata)
486    }
487
488    /// Returns a reference to the underlying writer.
489    pub fn inner(&self) -> &W {
490        self.writer.inner()
491    }
492
493    /// Returns a mutable reference to the underlying writer.
494    ///
495    /// **Warning**: if you write directly to this writer, you will skip
496    /// the `TrackedWrite` buffering and byte‐counting layers. That’ll cause
497    /// the file footer’s recorded offsets and sizes to diverge from reality,
498    /// resulting in an unreadable or corrupted Parquet file.
499    ///
500    /// If you want to write safely to the underlying writer, use [`Self::write_all`].
501    pub fn inner_mut(&mut self) -> &mut W {
502        self.writer.inner_mut()
503    }
504
505    /// Flushes any outstanding data and returns the underlying writer.
506    pub fn into_inner(mut self) -> Result<W> {
507        self.flush()?;
508        self.writer.into_inner()
509    }
510
511    /// Close and finalize the underlying Parquet writer
512    ///
513    /// Unlike [`Self::close`] this does not consume self
514    ///
515    /// Attempting to write after calling finish will result in an error
516    pub fn finish(&mut self) -> Result<ParquetMetaData> {
517        self.flush()?;
518        self.writer.finish()
519    }
520
521    /// Close and finalize the underlying Parquet writer
522    pub fn close(mut self) -> Result<ParquetMetaData> {
523        self.finish()
524    }
525
526    /// Converts this writer into a lower-level [`SerializedFileWriter`] and [`ArrowRowGroupWriterFactory`].
527    ///
528    /// Flushes any outstanding data before returning.
529    ///
530    /// This can be useful to provide more control over how files are written, for example
531    /// to write columns in parallel. See the example on [`ArrowColumnWriter`].
532    pub fn into_serialized_writer(
533        mut self,
534    ) -> Result<(SerializedFileWriter<W>, ArrowRowGroupWriterFactory)> {
535        self.flush()?;
536        Ok((self.writer, self.row_group_writer_factory))
537    }
538}
539
540impl<W: Write + Send> RecordBatchWriter for ArrowWriter<W> {
541    fn write(&mut self, batch: &RecordBatch) -> Result<(), ArrowError> {
542        self.write(batch).map_err(|e| e.into())
543    }
544
545    fn close(self) -> std::result::Result<(), ArrowError> {
546        self.close()?;
547        Ok(())
548    }
549}
550
551/// Arrow-specific configuration settings for writing parquet files.
552///
553/// See [`ArrowWriter`] for how to configure the writer.
554#[derive(Debug, Clone, Default)]
555pub struct ArrowWriterOptions {
556    properties: WriterProperties,
557    skip_arrow_metadata: bool,
558    schema_root: Option<String>,
559    schema_descr: Option<SchemaDescriptor>,
560    page_store_factory: Option<Arc<dyn PageStoreFactory>>,
561}
562
563impl ArrowWriterOptions {
564    /// Creates a new [`ArrowWriterOptions`] with the default settings.
565    pub fn new() -> Self {
566        Self::default()
567    }
568
569    /// Sets the [`WriterProperties`] for writing parquet files.
570    pub fn with_properties(self, properties: WriterProperties) -> Self {
571        Self { properties, ..self }
572    }
573
574    /// Sets the [`PageStoreFactory`] used to buffer completed pages while a row
575    /// group is being written.
576    ///
577    /// The default implementation ([`InMemoryPageStore`]) buffers all completed
578    /// pages on the heap until the row group is flushed, so peak write memory
579    /// grows with the row group size. Using this API, pages can be spilled to a
580    /// file or object storage instead, reducing peak write memory substantially
581    /// at the expense of an extra write to and read from secondary storage.
582    ///
583    /// # Example: spilling pages to a temp file
584    ///
585    /// A simple spilling backend uses one temp file per column chunk; `put`
586    /// appends the page and `take` reads it back.
587    ///
588    /// ```
589    /// # use std::fs::File;
590    /// # use std::io::{Read, Seek, SeekFrom, Write};
591    /// # use std::sync::Arc;
592    /// # use bytes::Bytes;
593    /// # use arrow_array::{ArrayRef, Int64Array, RecordBatch};
594    /// # use parquet::arrow::arrow_writer::{
595    /// #     ArrowWriter, ArrowWriterOptions, PageKey, PageStore, PageStoreArgs, PageStoreFactory,
596    /// # };
597    /// # use parquet::arrow::arrow_reader::ParquetRecordBatchReader;
598    /// # use parquet::errors::Result;
599    /// struct TempFilePageStore {
600    ///     file: File,
601    ///     /// Total size of the file
602    ///     end: u64,
603    ///     /// Location of pages: (offset, len)
604    ///     locs: Vec<(u64, usize)>,
605    /// }
606    ///
607    /// impl PageStore for TempFilePageStore {
608    ///     fn put(&mut self, value: Bytes) -> Result<PageKey> {
609    ///         // Append to the end of the file
610    ///         self.file.seek(SeekFrom::Start(self.end))?;
611    ///         self.file.write_all(&value)?;
612    ///         let key = PageKey::new(self.locs.len() as u64);
613    ///         self.locs.push((self.end, value.len()));
614    ///         self.end += value.len() as u64;
615    ///         Ok(key)
616    ///     }
617    ///
618    ///     fn take(&mut self, key: PageKey) -> Result<Bytes> {
619    ///         let (offset, len) = self.locs[key.get() as usize];
620    ///         let mut buf = vec![0u8; len];
621    ///         self.file.seek(SeekFrom::Start(offset))?;
622    ///         self.file.read_exact(&mut buf)?;
623    ///         Ok(Bytes::from(buf))
624    ///     }
625    /// }
626    ///
627    /// /// Factory for creating [`TempFilePageStore`]
628    /// #[derive(Debug)]
629    /// struct TempFilePageStoreFactory;
630    ///
631    /// impl PageStoreFactory for TempFilePageStoreFactory {
632    ///     fn create(&self, args: &PageStoreArgs<'_>) -> Result<Box<dyn PageStore>> {
633    ///         // `args` exposes the column index and descriptor (physical/logical
634    ///         // type, path), so a real backend might choose to spill only large columns.
635    ///         let _ = (args.column_index(), args.column_descriptor());
636    ///         Ok(Box::new(TempFilePageStore {
637    ///             file: tempfile::tempfile()?, // temp file is cleaned on drop
638    ///             end: 0,
639    ///             locs: Vec::new(),
640    ///         }))
641    ///     }
642    /// }
643    /// // write 1000 integers
644    /// let col = Arc::new(Int64Array::from_iter_values(0..1000)) as ArrayRef;
645    /// let to_write = RecordBatch::try_from_iter([("col", col)]).unwrap();
646    ///
647    /// let options =
648    ///     ArrowWriterOptions::new().with_page_store_factory(Arc::new(TempFilePageStoreFactory));
649    /// let mut buffer = Vec::new();
650    /// let mut writer =
651    ///     ArrowWriter::try_new_with_options(&mut buffer, to_write.schema(), options).unwrap();
652    /// writer.write(&to_write).unwrap();
653    /// writer.close().unwrap();
654    ///
655    /// // buffer now holds valid Parquet data, which can be read as normal:
656    /// let mut reader = ParquetRecordBatchReader::try_new(Bytes::from(buffer), 1024).unwrap();
657    /// assert_eq!(to_write, reader.next().unwrap().unwrap());
658    /// ```
659    pub fn with_page_store_factory(self, page_store_factory: Arc<dyn PageStoreFactory>) -> Self {
660        Self {
661            page_store_factory: Some(page_store_factory),
662            ..self
663        }
664    }
665
666    /// Skip encoding the embedded arrow metadata (defaults to `false`)
667    ///
668    /// Parquet files generated by the [`ArrowWriter`] contain embedded arrow schema
669    /// by default.
670    ///
671    /// Set `skip_arrow_metadata` to true, to skip encoding the embedded metadata.
672    pub fn with_skip_arrow_metadata(self, skip_arrow_metadata: bool) -> Self {
673        Self {
674            skip_arrow_metadata,
675            ..self
676        }
677    }
678
679    /// Set the name of the root parquet schema element (defaults to `"arrow_schema"`)
680    pub fn with_schema_root(self, schema_root: String) -> Self {
681        Self {
682            schema_root: Some(schema_root),
683            ..self
684        }
685    }
686
687    /// Explicitly specify the Parquet schema to be used
688    ///
689    /// If omitted (the default), the [`ArrowSchemaConverter`] is used to compute the
690    /// Parquet [`SchemaDescriptor`]. This may be used When the [`SchemaDescriptor`] is
691    /// already known or must be calculated using custom logic.
692    pub fn with_parquet_schema(self, schema_descr: SchemaDescriptor) -> Self {
693        Self {
694            schema_descr: Some(schema_descr),
695            ..self
696        }
697    }
698}
699
700/// A single column chunk produced by [`ArrowColumnWriter`].
701///
702/// Holds the serialized page blobs (each page's header ‖ compressed data, in
703/// write order) in a [`PageStore`], plus the handles needed to read them back,
704/// in order, when the chunk is spliced into the output file.
705struct ArrowColumnChunkData {
706    length: usize,
707    store: Box<dyn PageStore>,
708    keys: Vec<PageKey>,
709    /// Handles to the dictionary page's blobs (header then data) in the store.
710    ///
711    /// A dictionary page is produced at most once and bounded by
712    /// `dict_page_size_limit`, but it must be written *first* in the chunk even
713    /// though the data pages reach the writer before it (see
714    /// [`PageWriter::defers_dictionary_ordering`]). Its header and data are `put`
715    /// into the store like any other page — which keeps the store uniform, and
716    /// lets an oversized dictionary page spill — and their handles are held apart
717    /// so they can be emitted ahead of the data pages at splice.
718    /// Empty for non-dictionary columns.
719    dictionary_keys: Vec<PageKey>,
720    /// Serialized length of the dictionary page (0 if there is none), recorded
721    /// so the data pages can be shifted past it when offsets are rewritten to a
722    /// dictionary-first layout at splice.
723    dictionary_len: usize,
724}
725
726impl ArrowColumnChunkData {
727    fn new(store: Box<dyn PageStore>) -> Self {
728        Self {
729            length: 0,
730            store,
731            keys: Vec::new(),
732            dictionary_keys: Vec::new(),
733            dictionary_len: 0,
734        }
735    }
736
737    /// Append a data-page blob to the store, recording its handle in write
738    /// order.
739    fn push(&mut self, value: Bytes) -> Result<()> {
740        let key = self.store.put(value)?;
741        self.keys.push(key);
742        Ok(())
743    }
744
745    /// Store a dictionary-page blob (header or data) in the page store,
746    /// recording its handle (emitted first at splice) and accumulating its
747    /// serialized length.
748    fn push_dictionary(&mut self, value: Bytes) -> Result<()> {
749        self.dictionary_len += value.len();
750        let key = self.store.put(value)?;
751        self.dictionary_keys.push(key);
752        Ok(())
753    }
754
755    /// Bytes this chunk currently holds on the heap: whatever the store keeps
756    /// resident (zero for a spilling backend).
757    fn memory_size(&self) -> usize {
758        self.store.memory_size()
759    }
760}
761
762/// A streaming iterator over one column chunk's buffered page blobs, in final
763/// file order: the dictionary page (if any) first, then the data pages.
764///
765/// Each blob is taken back out of the [`PageStore`] *as it is
766/// consumed* and released immediately afterwards, so splicing a chunk into the
767/// output file never materializes more than a single page in memory at a time.
768/// This is what keeps the splice phase within the memory bound for a spilling
769/// backend (an in-memory store already holds the bytes, so it is unaffected).
770struct StreamingColumnChunkPages {
771    store: Box<dyn PageStore>,
772    /// Page handles in final file order: the dictionary page first (if any),
773    /// then the data pages.
774    keys: IntoIter<PageKey>,
775}
776
777impl StreamingColumnChunkPages {
778    fn new(data: ArrowColumnChunkData) -> Self {
779        // The dictionary page must be emitted first, ahead of the data pages,
780        // even though it was the last page produced.
781        let keys = if data.dictionary_keys.is_empty() {
782            data.keys
783        } else {
784            let mut keys = Vec::with_capacity(data.dictionary_keys.len() + data.keys.len());
785            keys.extend(data.dictionary_keys);
786            keys.extend(data.keys);
787            keys
788        };
789        Self {
790            store: data.store,
791            keys: keys.into_iter(),
792        }
793    }
794}
795
796impl Iterator for StreamingColumnChunkPages {
797    type Item = Result<Bytes>;
798
799    fn next(&mut self) -> Option<Self::Item> {
800        let key = self.keys.next()?;
801        Some(self.store.take(key))
802    }
803}
804
805/// A shared [`ArrowColumnChunkData`]
806///
807/// This allows it to be owned by [`ArrowPageWriter`] whilst allowing access via
808/// [`ArrowRowGroupWriter`] on flush, without requiring self-referential borrows
809type SharedColumnChunk = Arc<Mutex<ArrowColumnChunkData>>;
810
811struct ArrowPageWriter {
812    buffer: SharedColumnChunk,
813    #[cfg(feature = "encryption")]
814    page_encryptor: Option<PageEncryptor>,
815}
816
817impl ArrowPageWriter {
818    /// Create a page writer that buffers completed pages in `store`.
819    fn new(store: Box<dyn PageStore>) -> Self {
820        Self {
821            buffer: Arc::new(Mutex::new(ArrowColumnChunkData::new(store))),
822            #[cfg(feature = "encryption")]
823            page_encryptor: None,
824        }
825    }
826
827    #[cfg(feature = "encryption")]
828    pub fn with_encryptor(mut self, page_encryptor: Option<PageEncryptor>) -> Self {
829        self.page_encryptor = page_encryptor;
830        self
831    }
832
833    #[cfg(feature = "encryption")]
834    fn page_encryptor_mut(&mut self) -> Option<&mut PageEncryptor> {
835        self.page_encryptor.as_mut()
836    }
837
838    // Mirrors the signature of the encryption-enabled version above, so that the
839    // callers do not need a `cfg` of their own.
840    #[cfg(not(feature = "encryption"))]
841    #[expect(
842        clippy::needless_pass_by_ref_mut,
843        reason = "mirrors the encryption-enabled signature"
844    )]
845    fn page_encryptor_mut(&mut self) -> Option<&mut PageEncryptor> {
846        None
847    }
848}
849
850impl PageWriter for ArrowPageWriter {
851    fn write_page(&mut self, page: CompressedPage) -> Result<PageWriteSpec> {
852        let page = match self.page_encryptor_mut() {
853            Some(page_encryptor) => page_encryptor.encrypt_compressed_page(page)?,
854            None => page,
855        };
856
857        let page_header = page.to_thrift_header()?;
858        let header = {
859            let mut header = Vec::with_capacity(1024);
860
861            match self.page_encryptor_mut() {
862                Some(page_encryptor) => {
863                    page_encryptor.encrypt_page_header(&page_header, &mut header)?;
864                    if page.compressed_page().is_data_page() {
865                        page_encryptor.increment_page();
866                    }
867                }
868                None => {
869                    let mut protocol = ThriftCompactOutputProtocol::new(&mut header);
870                    page_header.write_thrift(&mut protocol)?;
871                }
872            }
873
874            Bytes::from(header)
875        };
876
877        let mut buf = self.buffer.try_lock().unwrap();
878
879        let data = page.compressed_page().buffer().clone();
880        let compressed_size = data.len() + header.len();
881
882        let mut spec = PageWriteSpec::new();
883        spec.page_type = page.page_type();
884        spec.num_values = page.num_values();
885        spec.uncompressed_size = page.uncompressed_size() + header.len();
886        spec.offset = buf.length as u64;
887        spec.compressed_size = compressed_size;
888        spec.bytes_written = compressed_size as u64;
889
890        buf.length += compressed_size;
891        if spec.page_type == PageType::DICTIONARY_PAGE {
892            // Recorded apart from the data pages so it is emitted first at
893            // splice — see `ArrowColumnChunkData::dictionary_keys`.
894            buf.push_dictionary(header)?;
895            buf.push_dictionary(data)?;
896        } else {
897            buf.push(header)?;
898            buf.push(data)?;
899        }
900
901        Ok(spec)
902    }
903
904    fn defers_dictionary_ordering(&self) -> bool {
905        // The Arrow chunk is buffered in full and spliced at row-group flush, so
906        // data pages may be accepted before the dictionary page and reordered
907        // then. This lets `GenericColumnWriter` stream dictionary-column data
908        // pages straight through instead of buffering them in memory.
909        true
910    }
911
912    fn buffered_memory_size(&self) -> usize {
913        // Only what is actually resident: a spilling store reports ~0 here even
914        // though the chunk's bytes have all passed through it.
915        self.buffer.try_lock().unwrap().memory_size()
916    }
917
918    fn close(&mut self) -> Result<()> {
919        Ok(())
920    }
921}
922
923/// A leaf column that can be encoded by [`ArrowColumnWriter`]
924#[derive(Debug)]
925pub struct ArrowLeafColumn(ArrayLevels);
926
927/// Computes the [`ArrowLeafColumn`] for a potentially nested [`ArrayRef`]
928///
929/// This function can be used to encode individual columns in parallel.
930/// See example on [`ArrowColumnWriter`]. A reachable logical null in a
931/// non-nullable `field` returns an error when the leaf is written.
932pub fn compute_leaves(field: &Field, array: &ArrayRef) -> Result<Vec<ArrowLeafColumn>> {
933    let levels = calculate_array_levels(array, field)?;
934    Ok(levels.into_iter().map(ArrowLeafColumn).collect())
935}
936
937/// The data for a single column chunk, see [`ArrowColumnWriter`]
938pub struct ArrowColumnChunk {
939    data: ArrowColumnChunkData,
940    close: ColumnCloseResult,
941}
942
943impl std::fmt::Debug for ArrowColumnChunk {
944    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
945        f.debug_struct("ArrowColumnChunk")
946            .field("length", &self.data.length)
947            .finish_non_exhaustive()
948    }
949}
950
951impl ArrowColumnChunk {
952    /// Returns the [`ColumnCloseResult`] produced when the chunk was closed.
953    ///
954    /// Exposes encoding information, collected statistics, and the optional
955    /// [`ColumnIndexMetaData`](crate::file::page_index::column_index::ColumnIndexMetaData)
956    /// / [`OffsetIndexMetaData`](crate::file::page_index::offset_index::OffsetIndexMetaData)
957    /// gathered for the column chunk.
958    pub fn close(&self) -> &ColumnCloseResult {
959        &self.close
960    }
961
962    /// Returns a mutable reference to the [`ColumnCloseResult`].
963    ///
964    /// This allows callers to mutate the close result before the chunk is
965    /// appended to a row group — for example, clearing `column_index` or
966    /// `bloom_filter` based on a dynamic rule that inspects the encodings and
967    /// collected page statistics.
968    pub fn close_mut(&mut self) -> &mut ColumnCloseResult {
969        &mut self.close
970    }
971
972    /// Splices this column's buffered pages into the row group, streaming them
973    /// back out of the [`PageStore`] one page at a time.
974    pub fn append_to_row_group<W: Write + Send>(
975        self,
976        writer: &mut SerializedRowGroupWriter<'_, W>,
977    ) -> Result<()> {
978        let ArrowColumnChunk { data, close } = self;
979
980        // The dictionary page is produced *after* the data pages on this path (so
981        // they can stream straight through) but must be written *first*, so move
982        // it ahead of the data pages in the recorded offsets before the splice.
983        let close = close.update_dictionary_location(data.dictionary_len)?;
984
985        let pages = StreamingColumnChunkPages::new(data);
986        writer.append_column_from_pages(pages, close)
987    }
988}
989
990/// Encodes [`ArrowLeafColumn`] to [`ArrowColumnChunk`]
991///
992/// `ArrowColumnWriter` instances can be created using an [`ArrowRowGroupWriterFactory`];
993///
994/// Note: This is a low-level interface for applications that require
995/// fine-grained control of encoding (e.g. encoding using multiple threads),
996/// see [`ArrowWriter`] for a higher-level interface
997///
998/// # Example: Encoding two Arrow Array's in Parallel
999/// ```
1000/// // The arrow schema
1001/// # use std::sync::Arc;
1002/// # use arrow_array::*;
1003/// # use arrow_schema::*;
1004/// # use parquet::arrow::ArrowSchemaConverter;
1005/// # use parquet::arrow::arrow_writer::{compute_leaves, ArrowColumnChunk, ArrowLeafColumn, ArrowRowGroupWriterFactory};
1006/// # use parquet::file::properties::WriterProperties;
1007/// # use parquet::file::writer::{SerializedFileWriter, SerializedRowGroupWriter};
1008/// #
1009/// let schema = Arc::new(Schema::new(vec![
1010///     Field::new("i32", DataType::Int32, false),
1011///     Field::new("f32", DataType::Float32, false),
1012/// ]));
1013///
1014/// // Compute the parquet schema
1015/// let props = Arc::new(WriterProperties::default());
1016/// let parquet_schema = ArrowSchemaConverter::new()
1017///   .with_coerce_types(props.coerce_types())
1018///   .convert(&schema)
1019///   .unwrap();
1020///
1021/// // Create parquet writer
1022/// let root_schema = parquet_schema.root_schema_ptr();
1023/// // write to memory in the example, but this could be a File
1024/// let mut out = Vec::with_capacity(1024);
1025/// let mut writer = SerializedFileWriter::new(&mut out, root_schema, props.clone())
1026///   .unwrap();
1027///
1028/// // Create a factory for building Arrow column writers
1029/// let row_group_factory = ArrowRowGroupWriterFactory::new(&writer, Arc::clone(&schema));
1030/// // Create column writers for the 0th row group
1031/// let col_writers = row_group_factory.create_column_writers(0).unwrap();
1032///
1033/// // Spawn a worker thread for each column
1034/// //
1035/// // Note: This is for demonstration purposes, a thread-pool e.g. rayon or tokio, would be better.
1036/// // The `map` produces an iterator of type `tuple of (thread handle, send channel)`.
1037/// let mut workers: Vec<_> = col_writers
1038///     .into_iter()
1039///     .map(|mut col_writer| {
1040///         let (send, recv) = std::sync::mpsc::channel::<ArrowLeafColumn>();
1041///         let handle = std::thread::spawn(move || {
1042///             // receive Arrays to encode via the channel
1043///             for col in recv {
1044///                 col_writer.write(&col)?;
1045///             }
1046///             // once the input is complete, close the writer
1047///             // to return the newly created ArrowColumnChunk
1048///             col_writer.close()
1049///         });
1050///         (handle, send)
1051///     })
1052///     .collect();
1053///
1054/// // Start row group
1055/// let mut row_group_writer: SerializedRowGroupWriter<'_, _> = writer
1056///   .next_row_group()
1057///   .unwrap();
1058///
1059/// // Create some example input columns to encode
1060/// let to_write = vec![
1061///     Arc::new(Int32Array::from_iter_values([1, 2, 3])) as _,
1062///     Arc::new(Float32Array::from_iter_values([1., 45., -1.])) as _,
1063/// ];
1064///
1065/// // Send the input columns to the workers
1066/// let mut worker_iter = workers.iter_mut();
1067/// for (arr, field) in to_write.iter().zip(&schema.fields) {
1068///     for leaves in compute_leaves(field, arr).unwrap() {
1069///         worker_iter.next().unwrap().1.send(leaves).unwrap();
1070///     }
1071/// }
1072///
1073/// // Wait for the workers to complete encoding, and append
1074/// // the resulting column chunks to the row group (and the file)
1075/// for (handle, send) in workers {
1076///     drop(send); // Drop send side to signal termination
1077///     // wait for the worker to send the completed chunk
1078///     let chunk: ArrowColumnChunk = handle.join().unwrap().unwrap();
1079///     chunk.append_to_row_group(&mut row_group_writer).unwrap();
1080/// }
1081/// // Close the row group which writes to the underlying file
1082/// row_group_writer.close().unwrap();
1083///
1084/// let metadata = writer.close().unwrap();
1085/// assert_eq!(metadata.file_metadata().num_rows(), 3);
1086/// ```
1087pub struct ArrowColumnWriter {
1088    writer: ArrowColumnWriterImpl,
1089    chunk: SharedColumnChunk,
1090    /// Non-null value hashes accumulated across all writes for this column's row group.
1091    /// `None` when tracking is disabled via [`WriterProperties::write_row_group_number_distinct_values`].
1092    distinct_values_seen: Option<DistinctValuesSet>,
1093}
1094
1095impl std::fmt::Debug for ArrowColumnWriter {
1096    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1097        f.debug_struct("ArrowColumnWriter").finish_non_exhaustive()
1098    }
1099}
1100
1101enum ArrowColumnWriterImpl {
1102    ByteArray(GenericColumnWriter<'static, ByteArrayEncoder>),
1103    Column(ColumnWriter<'static>),
1104}
1105
1106impl ArrowColumnWriter {
1107    /// Write an [`ArrowLeafColumn`]
1108    pub fn write(&mut self, col: &ArrowLeafColumn) -> Result<()> {
1109        col.0.validate()?;
1110        self.write_internal(&col.0)
1111    }
1112
1113    /// Write with content-defined chunking, inserting page flushes at chunk boundaries.
1114    fn write_with_chunker(
1115        &mut self,
1116        col: &ArrowLeafColumn,
1117        chunker: &mut ContentDefinedChunker,
1118    ) -> Result<()> {
1119        let levels = &col.0;
1120        levels.validate()?;
1121        let chunks = chunker.get_arrow_chunks(
1122            levels.def_level_data().as_ref(),
1123            levels.rep_level_data().as_ref(),
1124            levels.array(),
1125        )?;
1126
1127        let num_chunks = chunks.len();
1128        for (i, chunk) in chunks.iter().enumerate() {
1129            let chunk_levels = levels.slice_for_chunk(chunk);
1130            self.write_internal(&chunk_levels)?;
1131
1132            // Add a page break after each chunk except the last
1133            if i + 1 < num_chunks {
1134                match &mut self.writer {
1135                    ArrowColumnWriterImpl::Column(c) => c.add_data_page()?,
1136                    ArrowColumnWriterImpl::ByteArray(c) => c.add_data_page()?,
1137                }
1138            }
1139        }
1140        Ok(())
1141    }
1142
1143    fn write_internal(&mut self, levels: &ArrayLevels) -> Result<()> {
1144        if let Some(seen) = &mut self.distinct_values_seen {
1145            let array = levels.array();
1146            let non_null = levels.non_null_indices();
1147            match array.as_any_dictionary_opt() {
1148                Some(dict) => {
1149                    // Hash referenced values, not key indices: keys can map to different
1150                    // values across batches, and unreferenced values must not count toward NDV.
1151                    let values = dict.values();
1152                    let keys = dict.normalized_keys();
1153                    let referenced_value_indices: Vec<usize> = non_null
1154                        .iter()
1155                        .map(|&pos| keys[pos])
1156                        .filter(|&val_idx| values.is_valid(val_idx))
1157                        .collect();
1158                    update_distinct_values_seen(values.as_ref(), &referenced_value_indices, seen);
1159                }
1160                // For plain arrays, hash the actual values directly.
1161                None => update_distinct_values_seen(array.as_ref(), non_null, seen),
1162            }
1163        }
1164
1165        match &mut self.writer {
1166            ArrowColumnWriterImpl::Column(c) => {
1167                let leaf = levels.array();
1168                match leaf.as_any_dictionary_opt() {
1169                    Some(dictionary) => {
1170                        let materialized =
1171                            arrow_select::take::take(dictionary.values(), dictionary.keys(), None)?;
1172                        write_leaf(c, &materialized, levels)?
1173                    }
1174                    None => write_leaf(c, leaf, levels)?,
1175                };
1176            }
1177            ArrowColumnWriterImpl::ByteArray(c) => {
1178                write_primitive(c, levels.array().as_ref(), levels)?;
1179            }
1180        }
1181        Ok(())
1182    }
1183
1184    /// Close this column returning the written [`ArrowColumnChunk`]
1185    ///
1186    /// # Errors
1187    ///
1188    /// Returns an error if the column could not be finalised, or if another thread
1189    /// panicked while holding the column chunk. The caller cannot cause either.
1190    pub fn close(self) -> Result<ArrowColumnChunk> {
1191        let distinct_count = self
1192            .distinct_values_seen
1193            .as_ref()
1194            .filter(|s| !s.is_empty())
1195            .map(|s| s.len() as u64);
1196        let close = match self.writer {
1197            ArrowColumnWriterImpl::ByteArray(mut c) => {
1198                if let Some(count) = distinct_count {
1199                    c.set_distinct_count_override(count);
1200                }
1201                c.close()?
1202            }
1203            ArrowColumnWriterImpl::Column(mut c) => {
1204                if let Some(count) = distinct_count {
1205                    c.set_distinct_count_override(count);
1206                }
1207                c.close()?
1208            }
1209        };
1210        // Closing the writer above dropped the only other handle on the chunk.
1211        let chunk = Arc::try_unwrap(self.chunk)
1212            .map_err(|_| general_err!("Internal Error: the column chunk is still shared"))?;
1213        let data = chunk
1214            .into_inner()
1215            .map_err(|_| general_err!("The column chunk lock is poisoned"))?;
1216        Ok(ArrowColumnChunk { data, close })
1217    }
1218
1219    /// Returns the estimated total memory usage by the writer.
1220    ///
1221    /// This  [`Self::get_estimated_total_bytes`] this is an estimate
1222    /// of the current memory usage and not it's anticipated encoded size.
1223    ///
1224    /// This includes:
1225    /// 1. Data buffered in encoded form
1226    /// 2. Data buffered in un-encoded form (e.g. `usize` dictionary keys)
1227    ///
1228    /// This value should be greater than or equal to [`Self::get_estimated_total_bytes`]
1229    pub fn memory_size(&self) -> usize {
1230        match &self.writer {
1231            ArrowColumnWriterImpl::ByteArray(c) => c.memory_size(),
1232            ArrowColumnWriterImpl::Column(c) => c.memory_size(),
1233        }
1234    }
1235
1236    /// Returns the estimated total encoded bytes for this column writer.
1237    ///
1238    /// This includes:
1239    /// 1. Data buffered in encoded form
1240    /// 2. An estimate of how large the data buffered in un-encoded form would be once encoded
1241    ///
1242    /// This value should be less than or equal to [`Self::memory_size`]
1243    pub fn get_estimated_total_bytes(&self) -> usize {
1244        match &self.writer {
1245            ArrowColumnWriterImpl::ByteArray(c) => c.get_estimated_total_bytes() as _,
1246            ArrowColumnWriterImpl::Column(c) => c.get_estimated_total_bytes() as _,
1247        }
1248    }
1249}
1250
1251/// Encodes [`RecordBatch`] to a parquet row group
1252///
1253/// Note: this structure is created by [`ArrowRowGroupWriterFactory`] internally used to
1254/// create [`ArrowRowGroupWriter`]s, but it is not exposed publicly.
1255///
1256/// See the example on [`ArrowColumnWriter`] for how to encode columns in parallel
1257#[derive(Debug)]
1258struct ArrowRowGroupWriter {
1259    writers: Vec<ArrowColumnWriter>,
1260    schema: SchemaRef,
1261    buffered_rows: usize,
1262}
1263
1264impl ArrowRowGroupWriter {
1265    fn new(writers: Vec<ArrowColumnWriter>, arrow: &SchemaRef) -> Self {
1266        Self {
1267            writers,
1268            schema: arrow.clone(),
1269            buffered_rows: 0,
1270        }
1271    }
1272
1273    fn write(&mut self, batch: &RecordBatch) -> Result<()> {
1274        self.buffered_rows += batch.num_rows();
1275        let mut writers = self.writers.iter_mut();
1276        for (field, column) in self.schema.fields().iter().zip(batch.columns()) {
1277            for leaf in compute_leaves(field.as_ref(), column)? {
1278                writers.next().unwrap().write(&leaf)?;
1279            }
1280        }
1281        Ok(())
1282    }
1283
1284    fn write_with_chunkers(
1285        &mut self,
1286        batch: &RecordBatch,
1287        chunkers: &mut [ContentDefinedChunker],
1288    ) -> Result<()> {
1289        self.buffered_rows += batch.num_rows();
1290        let mut writers = self.writers.iter_mut();
1291        let mut chunkers = chunkers.iter_mut();
1292        for (field, column) in self.schema.fields().iter().zip(batch.columns()) {
1293            for leaf in compute_leaves(field.as_ref(), column)? {
1294                writers
1295                    .next()
1296                    .unwrap()
1297                    .write_with_chunker(&leaf, chunkers.next().unwrap())?;
1298            }
1299        }
1300        Ok(())
1301    }
1302
1303    /// Returns the estimated total encoded bytes for this row group
1304    fn get_estimated_total_bytes(&self) -> usize {
1305        self.writers
1306            .iter()
1307            .map(|x| x.get_estimated_total_bytes())
1308            .sum()
1309    }
1310
1311    fn close(self) -> Result<Vec<ArrowColumnChunk>> {
1312        self.writers
1313            .into_iter()
1314            .map(|writer| writer.close())
1315            .collect()
1316    }
1317}
1318
1319/// Factory that creates new column writers for each row group in the Parquet file.
1320///
1321/// You can create this structure via an [`ArrowWriter::into_serialized_writer`].
1322/// See the example on [`ArrowColumnWriter`] for how to encode columns in parallel
1323#[derive(Debug)]
1324pub struct ArrowRowGroupWriterFactory {
1325    schema: SchemaDescPtr,
1326    arrow_schema: SchemaRef,
1327    props: WriterPropertiesPtr,
1328    page_store_factory: Arc<dyn PageStoreFactory>,
1329    #[cfg(feature = "encryption")]
1330    file_encryptor: Option<Arc<FileEncryptor>>,
1331}
1332
1333impl ArrowRowGroupWriterFactory {
1334    /// Create a new [`ArrowRowGroupWriterFactory`] for the provided file writer and Arrow schema
1335    pub fn new<W: Write + Send>(
1336        file_writer: &SerializedFileWriter<W>,
1337        arrow_schema: SchemaRef,
1338    ) -> Self {
1339        let schema = Arc::clone(file_writer.schema_descr_ptr());
1340        let props = Arc::clone(file_writer.properties());
1341        Self {
1342            schema,
1343            arrow_schema,
1344            props,
1345            page_store_factory: Arc::new(InMemoryPageStoreFactory),
1346            #[cfg(feature = "encryption")]
1347            file_encryptor: file_writer.file_encryptor(),
1348        }
1349    }
1350
1351    /// Set the [`PageStoreFactory`] used to allocate the buffer for each column
1352    /// chunk, e.g. to spill completed pages to a temp file or object storage
1353    /// instead of the heap. Defaults to [`InMemoryPageStoreFactory`].
1354    pub fn with_page_store_factory(
1355        mut self,
1356        page_store_factory: Arc<dyn PageStoreFactory>,
1357    ) -> Self {
1358        self.page_store_factory = page_store_factory;
1359        self
1360    }
1361
1362    fn create_row_group_writer(&self, row_group_index: usize) -> Result<ArrowRowGroupWriter> {
1363        let writers = self.create_column_writers(row_group_index)?;
1364        Ok(ArrowRowGroupWriter::new(writers, &self.arrow_schema))
1365    }
1366
1367    /// Create column writers for a new row group, with the given row group index
1368    pub fn create_column_writers(&self, row_group_index: usize) -> Result<Vec<ArrowColumnWriter>> {
1369        let mut writers = Vec::with_capacity(self.arrow_schema.fields.len());
1370        let mut leaves = self.schema.columns().iter();
1371        let column_factory = self.column_writer_factory(row_group_index);
1372        for field in &self.arrow_schema.fields {
1373            column_factory.get_arrow_column_writer(
1374                field.data_type(),
1375                &self.props,
1376                &mut leaves,
1377                &mut writers,
1378            )?;
1379        }
1380        Ok(writers)
1381    }
1382
1383    #[cfg(feature = "encryption")]
1384    fn column_writer_factory(&self, row_group_idx: usize) -> ArrowColumnWriterFactory {
1385        ArrowColumnWriterFactory::new()
1386            .with_page_store_factory(self.page_store_factory.clone())
1387            .with_file_encryptor(row_group_idx, self.file_encryptor.clone())
1388    }
1389
1390    #[cfg(not(feature = "encryption"))]
1391    fn column_writer_factory(&self, _row_group_idx: usize) -> ArrowColumnWriterFactory {
1392        ArrowColumnWriterFactory::new().with_page_store_factory(self.page_store_factory.clone())
1393    }
1394}
1395
1396/// Creates [`ArrowColumnWriter`] instances
1397struct ArrowColumnWriterFactory {
1398    /// Allocates the per-column-chunk [`PageStore`] backing each page writer.
1399    page_store_factory: Arc<dyn PageStoreFactory>,
1400    #[cfg(feature = "encryption")]
1401    row_group_index: usize,
1402    #[cfg(feature = "encryption")]
1403    file_encryptor: Option<Arc<FileEncryptor>>,
1404}
1405
1406impl ArrowColumnWriterFactory {
1407    pub fn new() -> Self {
1408        Self {
1409            page_store_factory: Arc::new(InMemoryPageStoreFactory),
1410            #[cfg(feature = "encryption")]
1411            row_group_index: 0,
1412            #[cfg(feature = "encryption")]
1413            file_encryptor: None,
1414        }
1415    }
1416
1417    /// Use `page_store_factory` to allocate the buffer for each column chunk.
1418    pub fn with_page_store_factory(
1419        mut self,
1420        page_store_factory: Arc<dyn PageStoreFactory>,
1421    ) -> Self {
1422        self.page_store_factory = page_store_factory;
1423        self
1424    }
1425
1426    #[cfg(feature = "encryption")]
1427    pub fn with_file_encryptor(
1428        mut self,
1429        row_group_index: usize,
1430        file_encryptor: Option<Arc<FileEncryptor>>,
1431    ) -> Self {
1432        self.row_group_index = row_group_index;
1433        self.file_encryptor = file_encryptor;
1434        self
1435    }
1436
1437    #[cfg(feature = "encryption")]
1438    fn create_page_writer(
1439        &self,
1440        column_descriptor: &ColumnDescPtr,
1441        column_index: usize,
1442    ) -> Result<Box<ArrowPageWriter>> {
1443        let column_path = column_descriptor.path().string();
1444        let page_encryptor = PageEncryptor::create_if_column_encrypted(
1445            self.file_encryptor.as_ref(),
1446            self.row_group_index,
1447            column_index,
1448            &column_path,
1449        )?;
1450        let args = PageStoreArgs::new(column_index, column_descriptor);
1451        let store = self.page_store_factory.create(&args)?;
1452        Ok(Box::new(
1453            ArrowPageWriter::new(store).with_encryptor(page_encryptor),
1454        ))
1455    }
1456
1457    #[cfg(not(feature = "encryption"))]
1458    fn create_page_writer(
1459        &self,
1460        column_descriptor: &ColumnDescPtr,
1461        column_index: usize,
1462    ) -> Result<Box<ArrowPageWriter>> {
1463        let args = PageStoreArgs::new(column_index, column_descriptor);
1464        let store = self.page_store_factory.create(&args)?;
1465        Ok(Box::new(ArrowPageWriter::new(store)))
1466    }
1467
1468    /// Gets an [`ArrowColumnWriter`] for the given `data_type`, appending the
1469    /// output ColumnDesc to `leaves` and the column writers to `out`
1470    fn get_arrow_column_writer(
1471        &self,
1472        data_type: &ArrowDataType,
1473        props: &WriterPropertiesPtr,
1474        leaves: &mut Iter<'_, ColumnDescPtr>,
1475        out: &mut Vec<ArrowColumnWriter>,
1476    ) -> Result<()> {
1477        let write_distinct_values = props.write_row_group_number_distinct_values();
1478
1479        // Instantiate writers for normal columns
1480        let col = |desc: &ColumnDescPtr| -> Result<ArrowColumnWriter> {
1481            let page_writer = self.create_page_writer(desc, out.len())?;
1482            let chunk = page_writer.buffer.clone();
1483            let writer = get_column_writer(desc.clone(), props.clone(), page_writer);
1484            Ok(ArrowColumnWriter {
1485                chunk,
1486                writer: ArrowColumnWriterImpl::Column(writer),
1487                distinct_values_seen: write_distinct_values.then(HashSet::new),
1488            })
1489        };
1490
1491        // Instantiate writers for byte arrays (e.g. Utf8,  Binary, etc)
1492        let bytes = |desc: &ColumnDescPtr| -> Result<ArrowColumnWriter> {
1493            let page_writer = self.create_page_writer(desc, out.len())?;
1494            let chunk = page_writer.buffer.clone();
1495            let writer = GenericColumnWriter::new(desc.clone(), props.clone(), page_writer);
1496            Ok(ArrowColumnWriter {
1497                chunk,
1498                writer: ArrowColumnWriterImpl::ByteArray(writer),
1499                distinct_values_seen: write_distinct_values.then(HashSet::new),
1500            })
1501        };
1502
1503        match data_type {
1504            _ if data_type.is_primitive() => out.push(col(leaves.next().unwrap())?),
1505            ArrowDataType::FixedSizeBinary(_) | ArrowDataType::Boolean | ArrowDataType::Null => {
1506                out.push(col(leaves.next().unwrap())?)
1507            }
1508            ArrowDataType::LargeBinary
1509            | ArrowDataType::Binary
1510            | ArrowDataType::Utf8
1511            | ArrowDataType::LargeUtf8
1512            | ArrowDataType::BinaryView
1513            | ArrowDataType::Utf8View => out.push(bytes(leaves.next().unwrap())?),
1514            ArrowDataType::List(f)
1515            | ArrowDataType::LargeList(f)
1516            | ArrowDataType::FixedSizeList(f, _)
1517            | ArrowDataType::ListView(f)
1518            | ArrowDataType::LargeListView(f) => {
1519                self.get_arrow_column_writer(f.data_type(), props, leaves, out)?
1520            }
1521            ArrowDataType::Struct(fields) => {
1522                for field in fields {
1523                    self.get_arrow_column_writer(field.data_type(), props, leaves, out)?
1524                }
1525            }
1526            ArrowDataType::Map(f, _) => match f.data_type() {
1527                ArrowDataType::Struct(f) => {
1528                    self.get_arrow_column_writer(f[0].data_type(), props, leaves, out)?;
1529                    self.get_arrow_column_writer(f[1].data_type(), props, leaves, out)?
1530                }
1531                _ => unreachable!("invalid map type"),
1532            },
1533            ArrowDataType::Dictionary(_, value_type) => match value_type.as_ref() {
1534                ArrowDataType::Utf8
1535                | ArrowDataType::LargeUtf8
1536                | ArrowDataType::Binary
1537                | ArrowDataType::LargeBinary => out.push(bytes(leaves.next().unwrap())?),
1538                ArrowDataType::Utf8View | ArrowDataType::BinaryView => {
1539                    out.push(bytes(leaves.next().unwrap())?)
1540                }
1541                _ => self.get_arrow_column_writer(value_type, props, leaves, out)?,
1542            },
1543            ArrowDataType::RunEndEncoded(_, value_field) => {
1544                self.get_arrow_column_writer(value_field.data_type(), props, leaves, out)?
1545            }
1546            _ => {
1547                return Err(ParquetError::NYI(format!(
1548                    "Attempting to write an Arrow type {data_type} to parquet that is not yet implemented"
1549                )));
1550            }
1551        }
1552        Ok(())
1553    }
1554}
1555
1556fn write_leaf(
1557    writer: &mut ColumnWriter<'_>,
1558    column: &dyn arrow_array::Array,
1559    levels: &ArrayLevels,
1560) -> Result<usize> {
1561    let indices = levels.non_null_indices();
1562
1563    match writer {
1564        // Note: this should match the contents of arrow_to_parquet_type
1565        ColumnWriter::Int32ColumnWriter(typed) => {
1566            match column.data_type() {
1567                ArrowDataType::Null => {
1568                    let array = Int32Array::new_null(column.len());
1569                    write_primitive(typed, array.values(), levels)
1570                }
1571                ArrowDataType::Int8 => {
1572                    let array: Int32Array = column.as_primitive::<Int8Type>().unary(|x| x as i32);
1573                    write_primitive(typed, array.values(), levels)
1574                }
1575                ArrowDataType::Int16 => {
1576                    let array: Int32Array = column.as_primitive::<Int16Type>().unary(|x| x as i32);
1577                    write_primitive(typed, array.values(), levels)
1578                }
1579                ArrowDataType::Int32 => {
1580                    write_primitive(typed, column.as_primitive::<Int32Type>().values(), levels)
1581                }
1582                ArrowDataType::UInt8 => {
1583                    let array: Int32Array = column.as_primitive::<UInt8Type>().unary(|x| x as i32);
1584                    write_primitive(typed, array.values(), levels)
1585                }
1586                ArrowDataType::UInt16 => {
1587                    let array: Int32Array = column.as_primitive::<UInt16Type>().unary(|x| x as i32);
1588                    write_primitive(typed, array.values(), levels)
1589                }
1590                ArrowDataType::UInt32 => {
1591                    // follow C++ implementation and use overflow/reinterpret cast from  u32 to i32 which will map
1592                    // `(i32::MAX as u32)..u32::MAX` to `i32::MIN..0`
1593                    let array = column.as_primitive::<UInt32Type>();
1594                    write_primitive(typed, array.values().inner().typed_data(), levels)
1595                }
1596                ArrowDataType::Date32 => {
1597                    let array = column.as_primitive::<Date32Type>();
1598                    write_primitive(typed, array.values(), levels)
1599                }
1600                ArrowDataType::Time32(TimeUnit::Second) => {
1601                    let array = column.as_primitive::<Time32SecondType>();
1602                    write_primitive(typed, array.values(), levels)
1603                }
1604                ArrowDataType::Time32(TimeUnit::Millisecond) => {
1605                    let array = column.as_primitive::<Time32MillisecondType>();
1606                    write_primitive(typed, array.values(), levels)
1607                }
1608                ArrowDataType::Date64 => {
1609                    // If the column is a Date64, we truncate it
1610                    let array: Int32Array = column
1611                        .as_primitive::<Date64Type>()
1612                        .unary(|x| (x / 86_400_000) as _);
1613
1614                    write_primitive(typed, array.values(), levels)
1615                }
1616                ArrowDataType::Decimal32(_, _) => {
1617                    let array = column
1618                        .as_primitive::<Decimal32Type>()
1619                        .unary::<_, Int32Type>(|v| v);
1620                    write_primitive(typed, array.values(), levels)
1621                }
1622                ArrowDataType::Decimal64(_, _) => {
1623                    // use the int32 to represent the decimal with low precision
1624                    let array = column
1625                        .as_primitive::<Decimal64Type>()
1626                        .unary::<_, Int32Type>(|v| v as i32);
1627                    write_primitive(typed, array.values(), levels)
1628                }
1629                ArrowDataType::Decimal128(_, _) => {
1630                    // use the int32 to represent the decimal with low precision
1631                    let array = column
1632                        .as_primitive::<Decimal128Type>()
1633                        .unary::<_, Int32Type>(|v| v as i32);
1634                    write_primitive(typed, array.values(), levels)
1635                }
1636                ArrowDataType::Decimal256(_, _) => {
1637                    // use the int32 to represent the decimal with low precision
1638                    let array = column
1639                        .as_primitive::<Decimal256Type>()
1640                        .unary::<_, Int32Type>(|v| v.as_i128() as i32);
1641                    write_primitive(typed, array.values(), levels)
1642                }
1643                d => Err(ParquetError::General(format!("Cannot coerce {d} to I32"))),
1644            }
1645        }
1646        ColumnWriter::BoolColumnWriter(typed) => {
1647            let array = column.as_boolean();
1648            let values = get_bool_array_slice(array, indices.iter().copied());
1649            typed.write_batch_internal(
1650                values.as_slice(),
1651                None,
1652                levels.def_level_data().as_ref(),
1653                levels.rep_level_data().as_ref(),
1654                None,
1655                None,
1656                None,
1657            )
1658        }
1659        ColumnWriter::Int64ColumnWriter(typed) => {
1660            match column.data_type() {
1661                ArrowDataType::Date64 => {
1662                    let array = column
1663                        .as_primitive::<Date64Type>()
1664                        .reinterpret_cast::<Int64Type>();
1665
1666                    write_primitive(typed, array.values(), levels)
1667                }
1668                ArrowDataType::Int64 => {
1669                    let array = column.as_primitive::<Int64Type>();
1670                    write_primitive(typed, array.values(), levels)
1671                }
1672                ArrowDataType::UInt64 => {
1673                    let values = column.as_primitive::<UInt64Type>().values();
1674                    // follow C++ implementation and use overflow/reinterpret cast from  u64 to i64 which will map
1675                    // `(i64::MAX as u64)..u64::MAX` to `i64::MIN..0`
1676                    let array = values.inner().typed_data::<i64>();
1677                    write_primitive(typed, array, levels)
1678                }
1679                ArrowDataType::Time64(TimeUnit::Microsecond) => {
1680                    let array = column.as_primitive::<Time64MicrosecondType>();
1681                    write_primitive(typed, array.values(), levels)
1682                }
1683                ArrowDataType::Time64(TimeUnit::Nanosecond) => {
1684                    let array = column.as_primitive::<Time64NanosecondType>();
1685                    write_primitive(typed, array.values(), levels)
1686                }
1687                ArrowDataType::Timestamp(unit, _) => match unit {
1688                    TimeUnit::Second => {
1689                        let array = column.as_primitive::<TimestampSecondType>();
1690                        write_primitive(typed, array.values(), levels)
1691                    }
1692                    TimeUnit::Millisecond => {
1693                        let array = column.as_primitive::<TimestampMillisecondType>();
1694                        write_primitive(typed, array.values(), levels)
1695                    }
1696                    TimeUnit::Microsecond => {
1697                        let array = column.as_primitive::<TimestampMicrosecondType>();
1698                        write_primitive(typed, array.values(), levels)
1699                    }
1700                    TimeUnit::Nanosecond => {
1701                        let array = column.as_primitive::<TimestampNanosecondType>();
1702                        write_primitive(typed, array.values(), levels)
1703                    }
1704                },
1705                ArrowDataType::Duration(unit) => match unit {
1706                    TimeUnit::Second => {
1707                        let array = column.as_primitive::<DurationSecondType>();
1708                        write_primitive(typed, array.values(), levels)
1709                    }
1710                    TimeUnit::Millisecond => {
1711                        let array = column.as_primitive::<DurationMillisecondType>();
1712                        write_primitive(typed, array.values(), levels)
1713                    }
1714                    TimeUnit::Microsecond => {
1715                        let array = column.as_primitive::<DurationMicrosecondType>();
1716                        write_primitive(typed, array.values(), levels)
1717                    }
1718                    TimeUnit::Nanosecond => {
1719                        let array = column.as_primitive::<DurationNanosecondType>();
1720                        write_primitive(typed, array.values(), levels)
1721                    }
1722                },
1723                ArrowDataType::Decimal64(_, _) => {
1724                    let array = column
1725                        .as_primitive::<Decimal64Type>()
1726                        .reinterpret_cast::<Int64Type>();
1727                    write_primitive(typed, array.values(), levels)
1728                }
1729                ArrowDataType::Decimal128(_, _) => {
1730                    // use the int64 to represent the decimal with low precision
1731                    let array = column
1732                        .as_primitive::<Decimal128Type>()
1733                        .unary::<_, Int64Type>(|v| v as i64);
1734                    write_primitive(typed, array.values(), levels)
1735                }
1736                ArrowDataType::Decimal256(_, _) => {
1737                    // use the int64 to represent the decimal with low precision
1738                    let array = column
1739                        .as_primitive::<Decimal256Type>()
1740                        .unary::<_, Int64Type>(|v| v.as_i128() as i64);
1741                    write_primitive(typed, array.values(), levels)
1742                }
1743                d => Err(ParquetError::General(format!("Cannot coerce {d} to I64"))),
1744            }
1745        }
1746        ColumnWriter::Int96ColumnWriter(_typed) => {
1747            unreachable!("Currently unreachable because data type not supported")
1748        }
1749        ColumnWriter::FloatColumnWriter(typed) => {
1750            let array = column.as_primitive::<Float32Type>();
1751            write_primitive(typed, array.values(), levels)
1752        }
1753        ColumnWriter::DoubleColumnWriter(typed) => {
1754            let array = column.as_primitive::<Float64Type>();
1755            write_primitive(typed, array.values(), levels)
1756        }
1757        ColumnWriter::ByteArrayColumnWriter(_) => {
1758            unreachable!("should use ByteArrayWriter")
1759        }
1760        ColumnWriter::FixedLenByteArrayColumnWriter(typed) => {
1761            let bytes = match column.data_type() {
1762                ArrowDataType::Interval(interval_unit) => match interval_unit {
1763                    IntervalUnit::YearMonth => {
1764                        let array = column.as_primitive::<IntervalYearMonthType>();
1765                        get_interval_ym_array_slice(array, indices.iter().copied())
1766                    }
1767                    IntervalUnit::DayTime => {
1768                        let array = column.as_primitive::<IntervalDayTimeType>();
1769                        get_interval_dt_array_slice(array, indices.iter().copied())
1770                    }
1771                    IntervalUnit::MonthDayNano => {
1772                        return Err(ParquetError::NYI(format!(
1773                            "Attempting to write an Arrow interval type {interval_unit:?} to parquet that is not yet implemented"
1774                        )));
1775                    }
1776                },
1777                ArrowDataType::FixedSizeBinary(_) => {
1778                    let array = column.as_fixed_size_binary();
1779                    get_fsb_array_slice(array, indices.iter().copied())
1780                }
1781                ArrowDataType::Decimal32(_, _) => {
1782                    let array = column.as_primitive::<Decimal32Type>();
1783                    get_decimal_array_slice(array, indices.iter().copied())
1784                }
1785                ArrowDataType::Decimal64(_, _) => {
1786                    let array = column.as_primitive::<Decimal64Type>();
1787                    get_decimal_array_slice(array, indices.iter().copied())
1788                }
1789                ArrowDataType::Decimal128(_, _) => {
1790                    let array = column.as_primitive::<Decimal128Type>();
1791                    get_decimal_array_slice(array, indices.iter().copied())
1792                }
1793                ArrowDataType::Decimal256(_, _) => {
1794                    let array = column.as_primitive::<Decimal256Type>();
1795                    get_decimal_array_slice(array, indices.iter().copied())
1796                }
1797                ArrowDataType::Float16 => {
1798                    let array = column.as_primitive::<Float16Type>();
1799                    get_float_16_array_slice(array, indices.iter().copied())
1800                }
1801                _ => {
1802                    return Err(ParquetError::NYI(
1803                        "Attempting to write an Arrow type that is not yet implemented".to_string(),
1804                    ));
1805                }
1806            };
1807            typed.write_batch_internal(
1808                bytes.as_slice(),
1809                None,
1810                levels.def_level_data().as_ref(),
1811                levels.rep_level_data().as_ref(),
1812                None,
1813                None,
1814                None,
1815            )
1816        }
1817    }
1818}
1819
1820fn write_primitive<E: ColumnValueEncoder>(
1821    writer: &mut GenericColumnWriter<E>,
1822    values: &E::Values,
1823    levels: &ArrayLevels,
1824) -> Result<usize> {
1825    writer.write_batch_internal(
1826        values,
1827        Some(levels.non_null_indices()),
1828        levels.def_level_data().as_ref(),
1829        levels.rep_level_data().as_ref(),
1830        None,
1831        None,
1832        None,
1833    )
1834}
1835
1836fn get_bool_array_slice(
1837    array: &arrow_array::BooleanArray,
1838    indices: impl ExactSizeIterator<Item = usize>,
1839) -> Vec<bool> {
1840    let mut values = Vec::with_capacity(indices.len());
1841    for i in indices {
1842        values.push(array.value(i))
1843    }
1844    values
1845}
1846
1847/// Returns 12-byte values representing 3 values of months, days and milliseconds (4-bytes each).
1848/// An Arrow YearMonth interval only stores months, thus only the first 4 bytes are populated.
1849fn get_interval_ym_array_slice(
1850    array: &arrow_array::IntervalYearMonthArray,
1851    indices: impl ExactSizeIterator<Item = usize>,
1852) -> Vec<FixedLenByteArray> {
1853    chunk_array_slice(12, indices, move |i, chunk| {
1854        let value = array.value(i);
1855        chunk[0..4].copy_from_slice(&value.to_le_bytes());
1856    })
1857}
1858
1859/// Returns 12-byte values representing 3 values of months, days and milliseconds (4-bytes each).
1860/// An Arrow DayTime interval only stores days and millis, thus the first 4 bytes are not populated.
1861fn get_interval_dt_array_slice(
1862    array: &arrow_array::IntervalDayTimeArray,
1863    indices: impl ExactSizeIterator<Item = usize>,
1864) -> Vec<FixedLenByteArray> {
1865    chunk_array_slice(12, indices, move |i, chunk| {
1866        let value = array.value(i);
1867        chunk[4..8].copy_from_slice(&value.days.to_le_bytes());
1868        chunk[8..12].copy_from_slice(&value.milliseconds.to_le_bytes());
1869    })
1870}
1871
1872trait NativeDecimalType: DecimalType {
1873    type NativeBytes: AsRef<[u8]>;
1874
1875    fn to_be_bytes(value: Self::Native) -> Self::NativeBytes;
1876}
1877impl NativeDecimalType for Decimal32Type {
1878    type NativeBytes = [u8; Self::BYTE_LENGTH];
1879
1880    fn to_be_bytes(value: Self::Native) -> Self::NativeBytes {
1881        value.to_be_bytes()
1882    }
1883}
1884impl NativeDecimalType for Decimal64Type {
1885    type NativeBytes = [u8; Self::BYTE_LENGTH];
1886
1887    fn to_be_bytes(value: Self::Native) -> Self::NativeBytes {
1888        value.to_be_bytes()
1889    }
1890}
1891impl NativeDecimalType for Decimal128Type {
1892    type NativeBytes = [u8; Self::BYTE_LENGTH];
1893
1894    fn to_be_bytes(value: Self::Native) -> Self::NativeBytes {
1895        value.to_be_bytes()
1896    }
1897}
1898impl NativeDecimalType for Decimal256Type {
1899    type NativeBytes = [u8; Self::BYTE_LENGTH];
1900
1901    fn to_be_bytes(value: Self::Native) -> Self::NativeBytes {
1902        value.to_be_bytes()
1903    }
1904}
1905
1906fn get_decimal_array_slice<T: NativeDecimalType>(
1907    array: &PrimitiveArray<T>,
1908    indices: impl ExactSizeIterator<Item = usize>,
1909) -> Vec<FixedLenByteArray> {
1910    let chunk_size = decimal_length_from_precision(array.precision());
1911    assert!(chunk_size <= T::BYTE_LENGTH);
1912
1913    if chunk_size == T::BYTE_LENGTH {
1914        // Special-case that allows inlining memcpy.
1915        chunk_array_slice(chunk_size, indices, move |i, chunk| {
1916            let as_be_bytes = T::to_be_bytes(array.value(i));
1917            chunk.copy_from_slice(as_be_bytes.as_ref());
1918        })
1919    } else {
1920        chunk_array_slice(chunk_size, indices, move |i, chunk| {
1921            let as_be_bytes = T::to_be_bytes(array.value(i));
1922            let resized_value = &as_be_bytes.as_ref()[(T::BYTE_LENGTH - chunk.len())..];
1923            chunk.copy_from_slice(resized_value);
1924        })
1925    }
1926}
1927
1928fn get_float_16_array_slice(
1929    array: &arrow_array::Float16Array,
1930    indices: impl ExactSizeIterator<Item = usize>,
1931) -> Vec<FixedLenByteArray> {
1932    chunk_array_slice(2, indices, move |i, chunk| {
1933        let value = array.value(i).to_le_bytes();
1934        chunk.copy_from_slice(&value);
1935    })
1936}
1937
1938fn get_fsb_array_slice(
1939    array: &arrow_array::FixedSizeBinaryArray,
1940    indices: impl ExactSizeIterator<Item = usize>,
1941) -> Vec<FixedLenByteArray> {
1942    chunk_array_slice(array.value_size(), indices, move |i, chunk| {
1943        let value = array.value(i);
1944        chunk.copy_from_slice(value);
1945    })
1946}
1947
1948#[inline]
1949fn chunk_array_slice(
1950    chunk_size: usize,
1951    indices: impl ExactSizeIterator<Item = usize>,
1952    writer: impl Fn(usize, &mut [u8]),
1953) -> Vec<FixedLenByteArray> {
1954    let capacity = indices.len() * chunk_size;
1955    // TODO: This could be done with Vec::spare_capacity_mut,
1956    //       but [MaybeUninit]::write_copy_of_slice is gated behind MSRV 1.93
1957    let mut arena = vec![0; capacity];
1958    for (i, chunk) in indices.zip(arena.chunks_exact_mut(chunk_size)) {
1959        writer(i, chunk);
1960    }
1961    chunk_contiguous_vec(arena, chunk_size)
1962}
1963
1964fn chunk_contiguous_vec(arena: Vec<u8>, chunk_size: usize) -> Vec<FixedLenByteArray> {
1965    let mut values = Vec::with_capacity(arena.len() / chunk_size);
1966    let mut arena = Bytes::from(arena);
1967    while arena.len() >= chunk_size {
1968        let slice = arena.split_to(chunk_size);
1969        values.push(FixedLenByteArray::from(ByteArray::from(slice)));
1970    }
1971    values
1972}
1973
1974/// Hash a byte slice to a u64 for NDV tracking.
1975#[inline]
1976fn hash_bytes(bytes: &[u8]) -> u64 {
1977    twox_hash::XxHash64::oneshot(0, bytes)
1978}
1979
1980/// Returns the fixed byte width for primitive Arrow types, or `None` for variable-length types.
1981fn fixed_byte_width(dt: &ArrowDataType) -> Option<usize> {
1982    use ArrowDataType::*;
1983    match dt {
1984        Int8 | UInt8 => Some(1),
1985        Int16 | UInt16 | Float16 => Some(2),
1986        Int32 | UInt32 | Float32 | Date32 | Time32(_) | Decimal32(_, _) => Some(4),
1987        Int64
1988        | UInt64
1989        | Float64
1990        | Date64
1991        | Time64(_)
1992        | Timestamp(_, _)
1993        | Duration(_)
1994        | Decimal64(_, _) => Some(8),
1995        Interval(IntervalUnit::YearMonth) => Some(4),
1996        Interval(IntervalUnit::DayTime) => Some(8),
1997        Interval(IntervalUnit::MonthDayNano) => Some(16),
1998        Decimal128(_, _) => Some(16),
1999        Decimal256(_, _) => Some(32),
2000        _ => None,
2001    }
2002}
2003
2004/// Hash the non-null values in `array` (at `non_null_indices`) into `seen`.
2005///
2006/// Handles primitive, boolean, fixed-size-binary, and variable-length (Utf8/Binary)
2007/// arrays. Unsupported types are silently skipped, leaving `seen` unchanged for
2008/// those values (NDV is best-effort).
2009fn update_distinct_values_seen(
2010    array: &dyn arrow_array::Array,
2011    non_null_indices: &[usize],
2012    seen: &mut DistinctValuesSet,
2013) {
2014    let data = array.to_data();
2015    let offset = data.offset();
2016
2017    match array.data_type() {
2018        ArrowDataType::Boolean => {
2019            let arr = array
2020                .as_any()
2021                .downcast_ref::<arrow_array::BooleanArray>()
2022                .unwrap();
2023            for &row in non_null_indices {
2024                seen.insert(arr.value(row) as u64);
2025            }
2026        }
2027        ArrowDataType::Utf8 | ArrowDataType::Binary => {
2028            let offsets = data.buffers()[0].typed_data::<i32>();
2029            let values = data.buffers()[1].as_slice();
2030            for &row in non_null_indices {
2031                let start = offsets[offset + row] as usize;
2032                let end = offsets[offset + row + 1] as usize;
2033                seen.insert(hash_bytes(&values[start..end]));
2034            }
2035        }
2036        ArrowDataType::LargeUtf8 | ArrowDataType::LargeBinary => {
2037            let offsets = data.buffers()[0].typed_data::<i64>();
2038            let values = data.buffers()[1].as_slice();
2039            for &row in non_null_indices {
2040                let start = offsets[offset + row] as usize;
2041                let end = offsets[offset + row + 1] as usize;
2042                seen.insert(hash_bytes(&values[start..end]));
2043            }
2044        }
2045        ArrowDataType::FixedSizeBinary(byte_width) => {
2046            let byte_width = *byte_width as usize;
2047            let buffer = data.buffers()[0].as_slice();
2048            for &row in non_null_indices {
2049                let start = (offset + row) * byte_width;
2050                seen.insert(hash_bytes(&buffer[start..start + byte_width]));
2051            }
2052        }
2053        ArrowDataType::Utf8View => {
2054            let string_view_array = array.as_string_view();
2055            for &row in non_null_indices {
2056                seen.insert(hash_bytes(string_view_array.value(row).as_bytes()));
2057            }
2058        }
2059        ArrowDataType::BinaryView => {
2060            let binary_view_array = array.as_binary_view();
2061            for &row in non_null_indices {
2062                seen.insert(hash_bytes(binary_view_array.value(row)));
2063            }
2064        }
2065        data_type => {
2066            if let Some(width) = fixed_byte_width(data_type) {
2067                let buffer = data.buffers()[0].as_slice();
2068                for &row in non_null_indices {
2069                    let pos = (offset + row) * width;
2070                    seen.insert(hash_bytes(&buffer[pos..pos + width]));
2071                }
2072            }
2073            // nested types (List, LargeList, etc.) are Parquet groups, not leaf columns: skip
2074        }
2075    }
2076}
2077
2078// Allow the helpers to use the same imports in unit and integration tests.
2079#[cfg(test)]
2080use crate as parquet_crate;
2081
2082#[cfg(test)]
2083#[path = "../../../tests/arrow_writer/roundtrip_helpers.rs"]
2084mod roundtrip_helpers;
2085
2086#[cfg(test)]
2087mod tests {
2088    use super::roundtrip_helpers::{
2089        RoundTripTest, SMALL_SIZE, required_and_optional, roundtrip_opts,
2090        roundtrip_opts_with_array_validation,
2091    };
2092    use super::*;
2093    use std::cmp::Ordering;
2094    use std::collections::HashMap;
2095
2096    use std::fs::File;
2097
2098    use crate::arrow::ARROW_SCHEMA_META_KEY;
2099    use crate::arrow::arrow_reader::{ParquetRecordBatchReader, ParquetRecordBatchReaderBuilder};
2100    use crate::column::page::{Page, PageReader};
2101    use crate::file::metadata::thrift::PageHeader;
2102    use crate::file::page_index::column_index::ColumnIndexMetaData;
2103    use crate::file::reader::SerializedPageReader;
2104    use crate::parquet_thrift::{ReadThrift, ThriftSliceInputProtocol};
2105    use crate::schema::types::ColumnPath;
2106    use arrow::datatypes::{DataType, Schema};
2107    use arrow::error::Result as ArrowResult;
2108    use arrow::util::data_gen::create_random_array;
2109    use arrow::util::pretty::pretty_format_batches;
2110    use arrow::{array::*, buffer::Buffer};
2111    use arrow_buffer::{IntervalMonthDayNano, NullBuffer, OffsetBuffer};
2112    use arrow_schema::Fields;
2113    use arrow_select::concat::concat;
2114
2115    use crate::basic::{Encoding, EncodingMask};
2116    use crate::data_type::AsBytes;
2117    use crate::file::metadata::{ColumnChunkMetaData, ParquetMetaData, ParquetMetaDataReader};
2118    use crate::file::properties::{
2119        BloomFilterPosition, EnabledStatistics, ReaderProperties, WriterVersion,
2120    };
2121    use crate::file::serialized_reader::ReadOptionsBuilder;
2122    use crate::file::{
2123        reader::{FileReader, SerializedFileReader},
2124        statistics::Statistics,
2125    };
2126
2127    /// A [`PageStore`] that allocates *sparse, non-contiguous* handles and keeps
2128    /// blobs in a `HashMap` — nothing like the default `Vec<Bytes>`. Used to
2129    /// prove the writer relies only on the opaque-handle contract and never on
2130    /// handles being dense `Vec` indices. Records how many blobs were stored.
2131    #[derive(Debug, Default)]
2132    struct RecordingPageStore {
2133        next: u64,
2134        blobs: HashMap<u64, Bytes>,
2135        puts: Arc<std::sync::atomic::AtomicUsize>,
2136    }
2137
2138    impl PageStore for RecordingPageStore {
2139        fn put(&mut self, value: Bytes) -> Result<PageKey> {
2140            // Deliberately non-sequential, never-zero handles.
2141            let id = 100 + self.next * 7;
2142            self.next += 1;
2143            self.puts.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2144            self.blobs.insert(id, value);
2145            Ok(PageKey::new(id))
2146        }
2147
2148        fn take(&mut self, key: PageKey) -> Result<Bytes> {
2149            self.blobs
2150                .remove(&key.get())
2151                .ok_or_else(|| ParquetError::General(format!("missing key {}", key.get())))
2152        }
2153    }
2154
2155    #[derive(Debug)]
2156    struct RecordingPageStoreFactory {
2157        puts: Arc<std::sync::atomic::AtomicUsize>,
2158    }
2159
2160    impl PageStoreFactory for RecordingPageStoreFactory {
2161        fn create(&self, _args: &PageStoreArgs<'_>) -> Result<Box<dyn PageStore>> {
2162            Ok(Box::new(RecordingPageStore {
2163                puts: self.puts.clone(),
2164                ..Default::default()
2165            }))
2166        }
2167    }
2168
2169    /// A custom [`PageStore`] must produce byte-identical files to the in-memory
2170    /// default, across dictionary and non-dictionary columns and multiple row
2171    /// groups (so multiple store instances are exercised).
2172    #[test]
2173    fn custom_page_store_is_byte_identical_to_default() {
2174        let schema = Arc::new(Schema::new(vec![
2175            Field::new("i", DataType::Int32, true),
2176            // A low-cardinality string column to exercise the dictionary path.
2177            Field::new("s", DataType::Utf8, true),
2178        ]));
2179        let i = Int32Array::from(vec![Some(1), None, Some(3), Some(4), Some(5), Some(6)]);
2180        let s = StringArray::from(vec![
2181            Some("a"),
2182            Some("bb"),
2183            Some("a"),
2184            None,
2185            Some("bb"),
2186            Some("ccc"),
2187        ]);
2188        let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(i), Arc::new(s)]).unwrap();
2189
2190        // Small row groups so multiple column chunks (hence multiple store
2191        // instances) are produced.
2192        let props = WriterProperties::builder()
2193            .set_max_row_group_row_count(Some(3))
2194            .build();
2195
2196        let write = |factory: Option<Arc<dyn PageStoreFactory>>| {
2197            let mut buffer = Vec::new();
2198            let mut opts = ArrowWriterOptions::new().with_properties(props.clone());
2199            if let Some(factory) = factory {
2200                opts = opts.with_page_store_factory(factory);
2201            }
2202            let mut writer =
2203                ArrowWriter::try_new_with_options(&mut buffer, schema.clone(), opts).unwrap();
2204            writer.write(&batch).unwrap();
2205            writer.close().unwrap();
2206            buffer
2207        };
2208
2209        let default_bytes = write(None);
2210
2211        let puts = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2212        let custom_bytes = write(Some(Arc::new(RecordingPageStoreFactory {
2213            puts: puts.clone(),
2214        })));
2215
2216        assert!(
2217            puts.load(std::sync::atomic::Ordering::Relaxed) > 0,
2218            "custom PageStore was never written to"
2219        );
2220        assert_eq!(
2221            default_bytes, custom_bytes,
2222            "a custom PageStore must produce byte-identical output to the default"
2223        );
2224    }
2225
2226    /// A dictionary-encoded column written through the deferred-ordering Arrow
2227    /// path must round-trip correctly even with the offset index disabled, when
2228    /// only the chunk-level dictionary/data page offsets are rewritten (there is
2229    /// no offset index to rebuild). Spans multiple data pages so the
2230    /// dictionary-first reordering is exercised.
2231    #[test]
2232    #[cfg_attr(miri, ignore)] // Takes too long
2233    fn dictionary_column_round_trips_with_offset_index_disabled() {
2234        let schema = Arc::new(Schema::new(vec![Field::new("k", DataType::Int32, true)]));
2235
2236        // Low cardinality so the column stays dictionary-encoded; enough rows to
2237        // span several data pages within a single row group.
2238        let values: Vec<Option<i32>> = (0..50_000).map(|i| Some(i % 8)).collect();
2239        let array = Int32Array::from(values.clone());
2240        let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(array)]).unwrap();
2241
2242        let props = WriterProperties::builder()
2243            .set_offset_index_disabled(true)
2244            .set_data_page_row_count_limit(4096)
2245            .build();
2246        let opts = ArrowWriterOptions::new().with_properties(props);
2247
2248        let mut buffer = Vec::new();
2249        let mut writer =
2250            ArrowWriter::try_new_with_options(&mut buffer, schema.clone(), opts).unwrap();
2251        writer.write(&batch).unwrap();
2252        writer.close().unwrap();
2253
2254        let reader = ParquetRecordBatchReader::try_new(Bytes::from(buffer), values.len()).unwrap();
2255        let read: Vec<RecordBatch> = reader.collect::<ArrowResult<_>>().unwrap();
2256        let read_values: Vec<Option<i32>> = read
2257            .iter()
2258            .flat_map(|b| b.column(0).as_primitive::<Int32Type>().iter())
2259            .collect();
2260        assert_eq!(read_values, values);
2261    }
2262
2263    /// The dictionary page is routed through the [`PageStore`] like any other
2264    /// page rather than held resident in memory, so a dictionary column chunk's
2265    /// *entire* serialized size — dictionary page included — passes through the
2266    /// store.
2267    #[test]
2268    fn dictionary_page_is_routed_through_the_store() {
2269        /// A store that sums the bytes handed to `put`.
2270        #[derive(Debug, Default)]
2271        struct SizeRecordingPageStore {
2272            blobs: Vec<Bytes>,
2273            bytes_put: Arc<std::sync::atomic::AtomicUsize>,
2274        }
2275        impl PageStore for SizeRecordingPageStore {
2276            fn put(&mut self, value: Bytes) -> Result<PageKey> {
2277                self.bytes_put
2278                    .fetch_add(value.len(), std::sync::atomic::Ordering::Relaxed);
2279                let key = PageKey::new(self.blobs.len() as u64);
2280                self.blobs.push(value);
2281                Ok(key)
2282            }
2283            fn take(&mut self, key: PageKey) -> Result<Bytes> {
2284                Ok(std::mem::take(&mut self.blobs[key.get() as usize]))
2285            }
2286        }
2287        #[derive(Debug)]
2288        struct Factory {
2289            bytes_put: Arc<std::sync::atomic::AtomicUsize>,
2290        }
2291        impl PageStoreFactory for Factory {
2292            fn create(&self, _args: &PageStoreArgs<'_>) -> Result<Box<dyn PageStore>> {
2293                Ok(Box::new(SizeRecordingPageStore {
2294                    bytes_put: self.bytes_put.clone(),
2295                    ..Default::default()
2296                }))
2297            }
2298        }
2299
2300        let schema = Arc::new(Schema::new(vec![Field::new("s", DataType::Utf8, false)]));
2301        // Low cardinality keeps the column dictionary-encoded with a real,
2302        // non-empty dictionary page.
2303        let values: Vec<&str> = (0..2048)
2304            .map(|i| ["alpha", "beta", "gamma", "delta"][i % 4])
2305            .collect();
2306        let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(StringArray::from(values))])
2307            .unwrap();
2308
2309        let bytes_put = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2310        let opts = ArrowWriterOptions::new().with_page_store_factory(Arc::new(Factory {
2311            bytes_put: bytes_put.clone(),
2312        }));
2313
2314        // A single batch / single column means exactly one row group and one
2315        // store instance, so the bytes it saw map to one column chunk.
2316        let mut buffer = Vec::new();
2317        let mut writer =
2318            ArrowWriter::try_new_with_options(&mut buffer, schema.clone(), opts).unwrap();
2319        writer.write(&batch).unwrap();
2320        writer.close().unwrap();
2321
2322        let reader = SerializedFileReader::new(Bytes::from(buffer)).unwrap();
2323        let column = reader.metadata().row_group(0).column(0);
2324        assert!(
2325            column.dictionary_page_offset().is_some(),
2326            "expected the column to be dictionary-encoded"
2327        );
2328
2329        // The bytes the store was handed must account for the whole chunk,
2330        // dictionary page included. Holding the dictionary page apart from the
2331        // store would make this fall short by the dictionary page's size.
2332        assert_eq!(
2333            bytes_put.load(std::sync::atomic::Ordering::Relaxed) as i64,
2334            column.compressed_size(),
2335            "the dictionary page must pass through the store like any other page"
2336        );
2337    }
2338
2339    /// The `page_encoding_stats` list of a dictionary-encoded column chunk must
2340    /// list the dictionary page *before* the data pages, matching the on-disk
2341    /// page layout (the dictionary page is always written first) and the order
2342    /// produced by the column-at-a-time `SerializedFileWriter`.
2343    #[test]
2344    fn dictionary_page_encoding_stats_lists_dictionary_first() {
2345        use crate::basic::PageType;
2346        use crate::file::metadata::ParquetMetaDataOptions;
2347
2348        // A low-cardinality column so it stays dictionary-encoded, with enough
2349        // rows to emit at least one data page in addition to the dictionary page.
2350        let schema = Arc::new(Schema::new(vec![Field::new("k", DataType::Int32, true)]));
2351        let values: Vec<Option<i32>> = (0..4096).map(|i| Some(i % 4)).collect();
2352        let array = Int32Array::from(values);
2353        let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(array)]).unwrap();
2354
2355        let mut buffer = Vec::new();
2356        let mut writer = ArrowWriter::try_new(&mut buffer, schema.clone(), None).unwrap();
2357        writer.write(&batch).unwrap();
2358        writer.close().unwrap();
2359        let buffer = Bytes::from(buffer);
2360
2361        // Read the *full*, ordered page encoding stats. By default this crate
2362        // collapses them to a bitmask, which would discard the ordering this
2363        // test is about, so opt out of that here.
2364        let options = ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
2365        let metadata = ParquetMetaDataReader::new()
2366            .with_metadata_options(Some(options))
2367            .parse_and_finish(&buffer)
2368            .unwrap();
2369
2370        let col = metadata.row_group(0).column(0);
2371        assert!(
2372            col.dictionary_page_offset().is_some(),
2373            "column should be dictionary encoded"
2374        );
2375        let stats = col
2376            .page_encoding_stats()
2377            .expect("full (non-mask) page encoding stats should be present");
2378
2379        // Arrow C++ (`IsColumnChunkFullyDictionaryEncoded`) requires the
2380        // dictionary page entry to be the first one.
2381        assert_eq!(
2382            stats[0].page_type,
2383            PageType::DICTIONARY_PAGE,
2384            "the DICTIONARY_PAGE encoding stat must come first, got {stats:?}"
2385        );
2386        assert!(
2387            stats[1..]
2388                .iter()
2389                .all(|s| s.page_type == PageType::DATA_PAGE)
2390        );
2391    }
2392
2393    fn get_bytes_after_close(schema: SchemaRef, expected_batch: &RecordBatch) -> Vec<u8> {
2394        let mut buffer = vec![];
2395
2396        let mut writer = ArrowWriter::try_new(&mut buffer, schema, None).unwrap();
2397        writer.write(expected_batch).unwrap();
2398        writer.close().unwrap();
2399
2400        buffer
2401    }
2402
2403    fn get_bytes_by_into_inner(schema: SchemaRef, expected_batch: &RecordBatch) -> Vec<u8> {
2404        let mut writer = ArrowWriter::try_new(Vec::new(), schema, None).unwrap();
2405        writer.write(expected_batch).unwrap();
2406        writer.into_inner().unwrap()
2407    }
2408
2409    #[test]
2410    fn roundtrip_bytes() {
2411        // define schema
2412        let schema = Arc::new(Schema::new(vec![
2413            Field::new("a", DataType::Int32, false),
2414            Field::new("b", DataType::Int32, true),
2415        ]));
2416
2417        // create some data
2418        let a = Int32Array::from(vec![1, 2, 3, 4, 5]);
2419        let b = Int32Array::from(vec![Some(1), None, None, Some(4), Some(5)]);
2420
2421        // build a record batch
2422        let expected_batch =
2423            RecordBatch::try_new(schema.clone(), vec![Arc::new(a), Arc::new(b)]).unwrap();
2424
2425        for buffer in [
2426            get_bytes_after_close(schema.clone(), &expected_batch),
2427            get_bytes_by_into_inner(schema, &expected_batch),
2428        ] {
2429            let cursor = Bytes::from(buffer);
2430            let mut record_batch_reader = ParquetRecordBatchReader::try_new(cursor, 1024).unwrap();
2431
2432            let actual_batch = record_batch_reader
2433                .next()
2434                .expect("No batch found")
2435                .expect("Unable to get batch");
2436
2437            assert_eq!(expected_batch.schema(), actual_batch.schema());
2438            assert_eq!(expected_batch.num_columns(), actual_batch.num_columns());
2439            assert_eq!(expected_batch.num_rows(), actual_batch.num_rows());
2440            for i in 0..expected_batch.num_columns() {
2441                let expected_data = expected_batch.column(i).to_data();
2442                let actual_data = actual_batch.column(i).to_data();
2443
2444                assert_eq!(expected_data, actual_data);
2445            }
2446        }
2447    }
2448
2449    fn read_column(file: Vec<u8>) -> ArrayRef {
2450        let reader = ParquetRecordBatchReader::try_new(Bytes::from(file), 4096).unwrap();
2451        let batches: Vec<RecordBatch> = reader.map(|b| b.unwrap()).collect();
2452        let arrays: Vec<&dyn Array> = batches.iter().map(|b| b.column(0).as_ref()).collect();
2453        concat(&arrays).unwrap()
2454    }
2455
2456    fn roundtrip_compatible_column(field: Field, col: ArrayRef) -> ArrayRef {
2457        let writer_schema = Arc::new(Schema::new(vec![field]));
2458        let batch_schema = Arc::new(Schema::new(vec![Field::new(
2459            "c",
2460            col.data_type().clone(),
2461            col.logical_null_count() != 0,
2462        )]));
2463        let batch = RecordBatch::try_new(batch_schema, vec![col]).unwrap();
2464        // Isolate level/value compatibility from Arrow metadata reconstruction,
2465        // which is covered separately by the default-reader regression below.
2466        let options = ArrowWriterOptions::new().with_skip_arrow_metadata(true);
2467        let mut file = vec![];
2468        let mut writer =
2469            ArrowWriter::try_new_with_options(&mut file, writer_schema, options).unwrap();
2470        writer.write(&batch).unwrap();
2471        writer.close().unwrap();
2472        read_column(file)
2473    }
2474
2475    fn assert_compatible_roundtrip(field: Field, actual: ArrayRef, expected: &ArrayRef) {
2476        assert!(!compute_leaves(&field, &actual).unwrap().is_empty());
2477        assert_eq!(
2478            roundtrip_compatible_column(field, actual).as_ref(),
2479            expected.as_ref()
2480        );
2481    }
2482
2483    fn run_end_type(value_type: DataType, nullable_values: bool) -> DataType {
2484        DataType::RunEndEncoded(
2485            Arc::new(Field::new(
2486                Field::REE_RUN_ENDS_FIELD_DEFAULT_NAME,
2487                DataType::Int32,
2488                false,
2489            )),
2490            Arc::new(Field::new(
2491                Field::REE_VALUES_FIELD_DEFAULT_NAME,
2492                value_type,
2493                nullable_values,
2494            )),
2495        )
2496    }
2497
2498    #[test]
2499    fn arrow_writer_required_ree_schema_rejects_nullable_dense_input() {
2500        let writer_schema = Arc::new(Schema::new(vec![Field::new(
2501            "c",
2502            run_end_type(DataType::Int32, false),
2503            false,
2504        )]));
2505        let batch_schema = Arc::new(Schema::new(vec![Field::new("c", DataType::Int32, true)]));
2506        let batch = RecordBatch::try_new(
2507            batch_schema.clone(),
2508            vec![Arc::new(Int32Array::from(vec![Some(7), None, Some(9)]))],
2509        )
2510        .unwrap();
2511
2512        for skip_metadata in [false, true] {
2513            for cdc in [false, true] {
2514                let props = WriterProperties::builder()
2515                    .set_content_defined_chunking(cdc.then(Default::default))
2516                    .build();
2517                let options = ArrowWriterOptions::new()
2518                    .with_properties(props)
2519                    .with_skip_arrow_metadata(skip_metadata);
2520                let mut writer = ArrowWriter::try_new_with_options(
2521                    Vec::new(),
2522                    writer_schema.clone(),
2523                    options.clone(),
2524                )
2525                .unwrap();
2526                let error = writer.write(&batch).unwrap_err().to_string();
2527                assert!(
2528                    error.contains("Found null at index 1 for required field 'c'"),
2529                    "skip_metadata={skip_metadata}, cdc={cdc}: {error}"
2530                );
2531
2532                // A nullable batch schema is fine when the data has no actual nulls.
2533                let values: ArrayRef = Arc::new(Int32Array::from(vec![7, 8, 9]));
2534                let batch =
2535                    RecordBatch::try_new(batch_schema.clone(), vec![values.clone()]).unwrap();
2536                let mut file = vec![];
2537                let mut writer =
2538                    ArrowWriter::try_new_with_options(&mut file, writer_schema.clone(), options)
2539                        .unwrap();
2540                writer.write(&batch).unwrap();
2541                writer.close().unwrap();
2542                assert_eq!(read_column(file).as_ref(), values.as_ref());
2543            }
2544        }
2545    }
2546
2547    #[test]
2548    fn ree_nulls_respect_dense_schema_nullability() {
2549        let run_ends = Int32Array::from(vec![2, 4, 5]);
2550        let values = Int32Array::from(vec![Some(1), None, Some(2)]);
2551        let ree: ArrayRef = Arc::new(Int32RunArray::try_new(&run_ends, &values).unwrap());
2552        let ree_batch = RecordBatch::try_new(
2553            Arc::new(Schema::new(vec![Field::new(
2554                "c",
2555                ree.data_type().clone(),
2556                true,
2557            )])),
2558            vec![ree],
2559        )
2560        .unwrap();
2561
2562        let nullable_schema = Arc::new(Schema::new(vec![Field::new("c", DataType::Int32, true)]));
2563        let mut file = vec![];
2564        let mut writer = ArrowWriter::try_new(&mut file, nullable_schema, None).unwrap();
2565        writer.write(&ree_batch).unwrap();
2566        writer.close().unwrap();
2567        assert_eq!(
2568            read_column(file).as_primitive::<Int32Type>(),
2569            &Int32Array::from(vec![Some(1), Some(1), None, None, Some(2)])
2570        );
2571
2572        let required_schema = Arc::new(Schema::new(vec![Field::new("c", DataType::Int32, false)]));
2573        let mut writer = ArrowWriter::try_new(Vec::new(), required_schema, None).unwrap();
2574        let error = writer.write(&ree_batch).unwrap_err().to_string();
2575        assert!(error.contains("Found null") && error.contains("required field 'c'"));
2576    }
2577
2578    #[test]
2579    fn required_null_validation_respects_reachability_and_write_timing() {
2580        let required = Field::new("c", DataType::Int32, false);
2581        let nullable: ArrayRef = Arc::new(Int32Array::from(vec![Some(1), None]));
2582        let leaves = compute_leaves(&required, &nullable).unwrap();
2583        let schema = Arc::new(Schema::new(vec![required.clone()]));
2584        let writer = ArrowWriter::try_new(Vec::new(), schema, None).unwrap();
2585        let (_file, factory) = writer.into_serialized_writer().unwrap();
2586        let mut columns = factory.create_column_writers(0).unwrap();
2587        assert!(
2588            columns[0]
2589                .write(&leaves[0])
2590                .unwrap_err()
2591                .to_string()
2592                .contains("required field 'c'")
2593        );
2594
2595        // Only a referenced logical dictionary null is invalid, not an unused value.
2596        let values: ArrayRef = Arc::new(Int32Array::from(vec![Some(7), None]));
2597        let dict: ArrayRef = Arc::new(DictionaryArray::new(
2598            Int8Array::from(vec![0, 0]),
2599            values.clone(),
2600        ));
2601        assert_eq!(
2602            roundtrip_compatible_column(required.clone(), dict).as_primitive::<Int32Type>(),
2603            &Int32Array::from(vec![7, 7])
2604        );
2605        let dict: ArrayRef = Arc::new(DictionaryArray::new(Int8Array::from(vec![0, 1]), values));
2606        let leaves = compute_leaves(&required, &dict).unwrap();
2607        assert!(leaves[0].0.validate().is_err());
2608        let dict: ArrayRef = Arc::new(DictionaryArray::new(
2609            Int8Array::from(vec![Some(0), None]),
2610            Arc::new(Int32Array::from(vec![7])),
2611        ));
2612        let leaves = compute_leaves(&required, &dict).unwrap();
2613        assert!(leaves[0].0.validate().is_err());
2614
2615        // A nullable ancestor masks an otherwise required child null, including
2616        // when the target child has a schema-only REE wrapper.
2617        let actual_fields = Fields::from(vec![Field::new("source_child", DataType::Int32, true)]);
2618        let array: ArrayRef = Arc::new(StructArray::new(
2619            actual_fields.clone(),
2620            vec![nullable.clone()],
2621            Some(NullBuffer::from(vec![true, false])),
2622        ));
2623        let unmasked: ArrayRef = Arc::new(StructArray::new(
2624            actual_fields,
2625            vec![nullable.clone()],
2626            None,
2627        ));
2628        for child_type in [DataType::Int32, run_end_type(DataType::Int32, false)] {
2629            let target = Field::new(
2630                "c",
2631                DataType::Struct(Fields::from(vec![Field::new(
2632                    "target_child",
2633                    child_type,
2634                    false,
2635                )])),
2636                true,
2637            );
2638            let leaves = compute_leaves(&target, &unmasked).unwrap();
2639            assert!(leaves[0].0.validate().is_err());
2640            let leaves = compute_leaves(&target, &array).unwrap();
2641            assert!(leaves[0].0.validate().is_ok());
2642            let result = roundtrip_compatible_column(target, array.clone());
2643            assert_eq!(result.as_struct().fields()[0].name(), "target_child");
2644            assert!(result.is_null(1));
2645        }
2646
2647        // Unused list child slots and out-of-slice scalar nulls are unreachable.
2648        let values: ArrayRef = Arc::new(Int32Array::from(vec![None, Some(9), None]));
2649        let list: ArrayRef = Arc::new(ListArray::new(
2650            Arc::new(Field::new_list_field(DataType::Int32, true)),
2651            OffsetBuffer::new(vec![1_i32, 2].into()),
2652            values,
2653            None,
2654        ));
2655        let target = Field::new(
2656            "c",
2657            DataType::List(Arc::new(Field::new_list_field(DataType::Int32, false))),
2658            false,
2659        );
2660        assert!(
2661            compute_leaves(&target, &list).unwrap()[0]
2662                .0
2663                .validate()
2664                .is_ok()
2665        );
2666        assert_eq!(
2667            roundtrip_compatible_column(required, nullable.slice(0, 1)).as_primitive::<Int32Type>(),
2668            &Int32Array::from(vec![1])
2669        );
2670    }
2671
2672    fn check_required_list_validation(list_type: fn(Arc<Field>) -> DataType) {
2673        let make_array = |values: Vec<Option<i32>>,
2674                          offsets: Vec<i32>,
2675                          sizes: Vec<i32>,
2676                          nulls: Option<NullBuffer>| {
2677            let child = Arc::new(Field::new("source_child", DataType::Int32, true));
2678            let values: ArrayRef = Arc::new(Int32Array::from(values));
2679            match list_type(child.clone()) {
2680                DataType::List(_) => Arc::new(ListArray::new(
2681                    child,
2682                    OffsetBuffer::new(offsets.into()),
2683                    values,
2684                    nulls,
2685                )) as ArrayRef,
2686                DataType::LargeList(_) => Arc::new(LargeListArray::new(
2687                    child,
2688                    OffsetBuffer::new(
2689                        offsets
2690                            .into_iter()
2691                            .map(i64::from)
2692                            .collect::<Vec<_>>()
2693                            .into(),
2694                    ),
2695                    values,
2696                    nulls,
2697                )) as ArrayRef,
2698                DataType::ListView(_) => Arc::new(ListViewArray::new(
2699                    child,
2700                    offsets.into(),
2701                    sizes.into(),
2702                    values,
2703                    nulls,
2704                )) as ArrayRef,
2705                DataType::LargeListView(_) => Arc::new(LargeListViewArray::new(
2706                    child,
2707                    offsets
2708                        .into_iter()
2709                        .map(i64::from)
2710                        .collect::<Vec<_>>()
2711                        .into(),
2712                    sizes.into_iter().map(i64::from).collect::<Vec<_>>().into(),
2713                    values,
2714                    nulls,
2715                )) as ArrayRef,
2716                _ => unreachable!(),
2717            }
2718        };
2719        let is_view = matches!(
2720            list_type(Arc::new(Field::new("item", DataType::Int32, true))),
2721            DataType::ListView(_) | DataType::LargeListView(_)
2722        );
2723        let offsets = |starts: Vec<i32>, end: i32| {
2724            let mut result = starts;
2725            if !is_view {
2726                result.push(end);
2727            }
2728            result
2729        };
2730        let target = Field::new(
2731            "c",
2732            list_type(Arc::new(Field::new("target_child", DataType::Int32, false))),
2733            true,
2734        );
2735        let invalid = make_array(vec![None, Some(7)], offsets(vec![0], 2), vec![2], None);
2736        let valid = make_array(vec![Some(7), Some(8)], offsets(vec![0], 2), vec![2], None);
2737        let masked = make_array(
2738            vec![None, Some(7)],
2739            offsets(vec![0, 1], 2),
2740            vec![1, 1],
2741            Some(NullBuffer::from(vec![false, true])),
2742        );
2743        let unused = make_array(
2744            vec![None, Some(7), None],
2745            offsets(vec![1], 2),
2746            vec![1],
2747            None,
2748        );
2749        let empty = make_array(vec![None], offsets(vec![0], 0), vec![0], None);
2750        let sliced = make_array(
2751            vec![None, Some(7)],
2752            offsets(vec![0, 1], 2),
2753            vec![1, 1],
2754            None,
2755        )
2756        .slice(1, 1);
2757        let schema = Arc::new(Schema::new(vec![target.clone()]));
2758
2759        // Binding succeeds even for invalid input; writing the affected leaf rejects it.
2760        for (name, array, invalid) in [
2761            ("reachable null", invalid, true),
2762            ("valid required child", valid.clone(), false),
2763            ("null ancestor", masked, false),
2764            ("unused child slots", unused, false),
2765            ("empty list", empty, false),
2766            ("out-of-slice null", sliced, false),
2767        ] {
2768            let leaves = compute_leaves(&target, &array).unwrap();
2769            assert_eq!(leaves.len(), 1);
2770            for cdc in [false, true] {
2771                let props = WriterProperties::builder()
2772                    .set_content_defined_chunking(cdc.then(Default::default))
2773                    .build();
2774                let mut writer =
2775                    ArrowWriter::try_new(Vec::new(), schema.clone(), Some(props)).unwrap();
2776                let mut chunkers = writer.cdc_chunkers.take();
2777                let (_file, factory) = writer.into_serialized_writer().unwrap();
2778                let mut columns = factory.create_column_writers(0).unwrap();
2779                let before = (
2780                    columns[0].memory_size(),
2781                    columns[0].get_estimated_total_bytes(),
2782                );
2783                let result = match &mut chunkers {
2784                    Some(chunkers) => columns[0].write_with_chunker(&leaves[0], &mut chunkers[0]),
2785                    None => columns[0].write(&leaves[0]),
2786                };
2787                if invalid {
2788                    let error = result.unwrap_err().to_string();
2789                    assert!(
2790                        error.contains("Found null at index 0 for required field 'target_child'"),
2791                        "{name}, cdc={cdc}: {error}"
2792                    );
2793                    assert_eq!(
2794                        before,
2795                        (
2796                            columns[0].memory_size(),
2797                            columns[0].get_estimated_total_bytes()
2798                        ),
2799                        "invalid leaf must be rejected before encoding"
2800                    );
2801                    // A rejected leaf write must leave no encoded values behind.
2802                    let valid_leaves = compute_leaves(&target, &valid).unwrap();
2803                    match &mut chunkers {
2804                        Some(chunkers) => columns[0]
2805                            .write_with_chunker(&valid_leaves[0], &mut chunkers[0])
2806                            .unwrap(),
2807                        None => columns[0].write(&valid_leaves[0]).unwrap(),
2808                    }
2809                    assert_eq!(
2810                        columns
2811                            .pop()
2812                            .unwrap()
2813                            .close()
2814                            .unwrap()
2815                            .close
2816                            .metadata
2817                            .num_values(),
2818                        2
2819                    );
2820                } else {
2821                    result.unwrap();
2822                    columns.pop().unwrap().close().unwrap();
2823                }
2824
2825                // Exercise the ordinary and CDC row-group driving entry points as well.
2826                let batch = RecordBatch::try_new(
2827                    Arc::new(Schema::new(vec![Field::new(
2828                        "c",
2829                        array.data_type().clone(),
2830                        true,
2831                    )])),
2832                    vec![array.clone()],
2833                )
2834                .unwrap();
2835                let props = WriterProperties::builder()
2836                    .set_content_defined_chunking(cdc.then(Default::default))
2837                    .build();
2838                let mut writer =
2839                    ArrowWriter::try_new(Vec::new(), schema.clone(), Some(props)).unwrap();
2840                let result = writer.write(&batch);
2841                if invalid {
2842                    assert!(
2843                        result
2844                            .unwrap_err()
2845                            .to_string()
2846                            .contains("required field 'target_child'")
2847                    );
2848                } else {
2849                    result.unwrap();
2850                    writer.close().unwrap();
2851                }
2852            }
2853        }
2854    }
2855
2856    #[test]
2857    fn required_list_validation_preserves_level_generation() {
2858        check_required_list_validation(DataType::List);
2859    }
2860
2861    #[test]
2862    fn required_large_list_validation_preserves_level_generation() {
2863        check_required_list_validation(DataType::LargeList);
2864    }
2865
2866    #[test]
2867    fn required_list_view_validation_preserves_level_generation() {
2868        check_required_list_validation(DataType::ListView);
2869    }
2870
2871    #[test]
2872    fn required_large_list_view_validation_preserves_level_generation() {
2873        check_required_list_validation(DataType::LargeListView);
2874    }
2875
2876    fn nullable_int32_list() -> ArrayRef {
2877        Arc::new(ListArray::new(
2878            Arc::new(Field::new_list_field(DataType::Int32, true)),
2879            OffsetBuffer::new(vec![0_i32, 2, 3].into()),
2880            Arc::new(Int32Array::from(vec![Some(1), None, Some(3)])),
2881            None,
2882        ))
2883    }
2884
2885    #[test]
2886    fn arrow_writer_dense_list_under_ree_schema() {
2887        let list = nullable_int32_list();
2888        // The schema wrapper is physical, not a second logical list node.
2889        let field = Field::new("c", run_end_type(list.data_type().clone(), false), false);
2890        assert_compatible_roundtrip(field, list.clone(), &list);
2891    }
2892
2893    #[test]
2894    fn arrow_writer_dense_list_under_dictionary_schema() {
2895        let list = nullable_int32_list();
2896        let field = Field::new(
2897            "c",
2898            DataType::Dictionary(Box::new(DataType::Int8), Box::new(list.data_type().clone())),
2899            false,
2900        );
2901        assert_compatible_roundtrip(field, list.clone(), &list);
2902    }
2903
2904    #[test]
2905    fn arrow_writer_dense_values_under_nested_ree_schema() {
2906        let dense: ArrayRef = Arc::new(Int32Array::from(vec![Some(7), None, Some(9)]));
2907        let field = Field::new(
2908            "c",
2909            run_end_type(run_end_type(DataType::Int32, true), false),
2910            false,
2911        );
2912        assert_compatible_roundtrip(field, dense.clone(), &dense);
2913    }
2914
2915    #[test]
2916    fn arrow_writer_dense_values_under_dictionary_ree_schema() {
2917        let dense: ArrayRef = Arc::new(Int32Array::from(vec![Some(7), None, Some(9)]));
2918        let field = Field::new(
2919            "c",
2920            DataType::Dictionary(
2921                Box::new(DataType::Int8),
2922                Box::new(run_end_type(DataType::Int32, true)),
2923            ),
2924            false,
2925        );
2926        assert_compatible_roundtrip(field, dense.clone(), &dense);
2927    }
2928
2929    #[test]
2930    fn arrow_writer_dense_values_under_ree_dictionary_schema() {
2931        let dense: ArrayRef = Arc::new(Int32Array::from(vec![Some(7), None, Some(9)]));
2932        // Hoist REE value nullability before peeling the dictionary; the
2933        // dense batch contains an actual null despite the required outer field.
2934        let field = Field::new(
2935            "c",
2936            run_end_type(
2937                DataType::Dictionary(Box::new(DataType::Int8), Box::new(DataType::Int32)),
2938                true,
2939            ),
2940            false,
2941        );
2942        assert_compatible_roundtrip(field, dense.clone(), &dense);
2943    }
2944
2945    #[test]
2946    fn arrow_writer_dense_struct_under_ree_child_schema() {
2947        let dense: ArrayRef = Arc::new(Int32Array::from(vec![Some(7), None, Some(9)]));
2948        let struct_fields = Fields::from(vec![Field::new("value", DataType::Int32, true)]);
2949        let dense_struct: ArrayRef = Arc::new(StructArray::new(struct_fields, vec![dense], None));
2950        // A wrapper on a child must not make the enclosing struct incompatible.
2951        let field = Field::new(
2952            "c",
2953            DataType::Struct(Fields::from(vec![Field::new(
2954                "value",
2955                run_end_type(DataType::Int32, true),
2956                false,
2957            )])),
2958            false,
2959        );
2960        assert_compatible_roundtrip(field, dense_struct.clone(), &dense_struct);
2961    }
2962
2963    #[test]
2964    fn arrow_writer_dense_list_under_ree_child_schema() {
2965        let list = nullable_int32_list();
2966        let field = Field::new(
2967            "c",
2968            DataType::List(Arc::new(Field::new(
2969                "item",
2970                run_end_type(DataType::Int32, true),
2971                false,
2972            ))),
2973            false,
2974        );
2975        assert_compatible_roundtrip(field, list.clone(), &list);
2976    }
2977
2978    #[test]
2979    fn arrow_writer_dense_list_under_dictionary_child_schema() {
2980        let list = nullable_int32_list();
2981        let field = Field::new(
2982            "c",
2983            DataType::List(Arc::new(Field::new_list_field(
2984                DataType::Dictionary(Box::new(DataType::Int8), Box::new(DataType::Int32)),
2985                true,
2986            ))),
2987            false,
2988        );
2989        assert_compatible_roundtrip(field, list.clone(), &list);
2990    }
2991
2992    #[test]
2993    fn arrow_writer_dictionary_list_under_dense_child_schema() {
2994        let dict: ArrayRef = Arc::new(DictionaryArray::new(
2995            Int8Array::from(vec![Some(0), None, Some(1)]),
2996            Arc::new(Int32Array::from(vec![1, 3])),
2997        ));
2998        let dict_list: ArrayRef = Arc::new(ListArray::new(
2999            Arc::new(Field::new_list_field(dict.data_type().clone(), true)),
3000            OffsetBuffer::new(vec![0_i32, 2, 3].into()),
3001            dict,
3002            None,
3003        ));
3004        let expected = nullable_int32_list();
3005        assert_compatible_roundtrip(
3006            Field::new("c", expected.data_type().clone(), false),
3007            dict_list,
3008            &expected,
3009        );
3010    }
3011
3012    #[test]
3013    fn arrow_writer_dense_map_under_ree_value_schema() {
3014        let key_field = Arc::new(Field::new("keys", DataType::Utf8, false));
3015        let entries = StructArray::new(
3016            Fields::from(vec![
3017                key_field.clone(),
3018                Arc::new(Field::new("values", DataType::Int32, true)),
3019            ]),
3020            vec![
3021                Arc::new(StringArray::from(vec!["a", "b", "c"])) as ArrayRef,
3022                Arc::new(Int32Array::from(vec![Some(7), None, Some(9)])),
3023            ],
3024            None,
3025        );
3026        let map: ArrayRef = Arc::new(MapArray::new(
3027            Arc::new(Field::new("entries", entries.data_type().clone(), false)),
3028            OffsetBuffer::new(vec![0_i32, 2, 3].into()),
3029            entries,
3030            None,
3031            false,
3032        ));
3033        // Resolve the value wrapper after walking through the entries struct.
3034        let field = Field::new(
3035            "c",
3036            DataType::Map(
3037                Arc::new(Field::new(
3038                    "entries",
3039                    DataType::Struct(Fields::from(vec![
3040                        key_field,
3041                        Arc::new(Field::new(
3042                            "values",
3043                            run_end_type(DataType::Int32, true),
3044                            false,
3045                        )),
3046                    ])),
3047                    false,
3048                )),
3049                false,
3050            ),
3051            false,
3052        );
3053        assert_compatible_roundtrip(field, map.clone(), &map);
3054    }
3055
3056    #[test]
3057    fn arrow_writer_nested_dictionary_metadata_roundtrips_with_default_reader() {
3058        let dense: ArrayRef = Arc::new(StructArray::new(
3059            Fields::from(vec![Field::new("item", DataType::Int32, true)]),
3060            vec![Arc::new(Int32Array::from(vec![Some(7), None, Some(9)]))],
3061            None,
3062        ));
3063        let field = Field::new(
3064            "value",
3065            DataType::Dictionary(
3066                Box::new(DataType::Int8),
3067                Box::new(dense.data_type().clone()),
3068            ),
3069            false,
3070        )
3071        .with_metadata(HashMap::from([(
3072            "description".to_string(),
3073            "nested dictionary".to_string(),
3074        )]));
3075        let writer_schema = Arc::new(Schema::new(vec![field.clone()]));
3076        let expected_schema = Arc::new(Schema::new(vec![
3077            field.with_data_type(dense.data_type().clone()),
3078        ]));
3079        let batch = RecordBatch::try_new(expected_schema.clone(), vec![dense]).unwrap();
3080
3081        // Keep Arrow metadata enabled: ignoring it would hide the original
3082        // reader-initialization failure caused by the nested dictionary hint.
3083        let mut file = vec![];
3084        let mut writer = ArrowWriter::try_new(&mut file, writer_schema, None).unwrap();
3085        writer.write(&batch).unwrap();
3086        writer.close().unwrap();
3087
3088        let builder = ParquetRecordBatchReaderBuilder::try_new(Bytes::from(file)).unwrap();
3089        assert!(
3090            builder
3091                .metadata()
3092                .file_metadata()
3093                .key_value_metadata()
3094                .unwrap()
3095                .iter()
3096                .any(|kv| kv.key == ARROW_SCHEMA_META_KEY)
3097        );
3098        assert_eq!(builder.schema(), &expected_schema);
3099        let decoded = builder
3100            .build()
3101            .unwrap()
3102            .collect::<ArrowResult<Vec<_>>>()
3103            .unwrap();
3104        assert_eq!(decoded, vec![batch]);
3105    }
3106
3107    #[test]
3108    fn arrow_writer_page_size() {
3109        let schema = Arc::new(Schema::new(vec![Field::new("col", DataType::Utf8, false)]));
3110
3111        let mut builder = StringBuilder::with_capacity(100, 329 * 10_000);
3112
3113        // Generate an array of 10 unique 10 character string
3114        for i in 0..10 {
3115            let value = i
3116                .to_string()
3117                .repeat(10)
3118                .chars()
3119                .take(10)
3120                .collect::<String>();
3121
3122            builder.append_value(value);
3123        }
3124
3125        let array = Arc::new(builder.finish());
3126
3127        let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
3128
3129        let file = tempfile::tempfile().unwrap();
3130
3131        // Set everything very low so we fallback to PLAIN encoding after the first row
3132        let props = WriterProperties::builder()
3133            .set_data_page_size_limit(1)
3134            .set_dictionary_page_size_limit(1)
3135            .set_write_batch_size(1)
3136            .build();
3137
3138        let mut writer =
3139            ArrowWriter::try_new(file.try_clone().unwrap(), batch.schema(), Some(props))
3140                .expect("Unable to write file");
3141        writer.write(&batch).unwrap();
3142        writer.close().unwrap();
3143
3144        let options = ReadOptionsBuilder::new().with_page_index().build();
3145        let reader =
3146            SerializedFileReader::new_with_options(file.try_clone().unwrap(), options).unwrap();
3147
3148        let column = reader.metadata().row_group(0).columns();
3149
3150        assert_eq!(column.len(), 1);
3151
3152        // We should write one row before falling back to PLAIN encoding so there should still be a
3153        // dictionary page.
3154        assert!(
3155            column[0].dictionary_page_offset().is_some(),
3156            "Expected a dictionary page"
3157        );
3158
3159        let page_index = reader
3160            .metadata()
3161            .page_index()
3162            .expect("page index should be present");
3163        let page_locations = page_index
3164            .page_locations(0, 0)
3165            .expect("page locations should exist");
3166
3167        // We should fallback to PLAIN encoding after the first row and our max page size is 1 bytes
3168        // so we expect one dictionary encoded page and then a page per row thereafter.
3169        assert_eq!(
3170            page_locations.len(),
3171            10,
3172            "Expected 10 pages but got {page_locations:#?}"
3173        );
3174    }
3175
3176    const MEDIUM_SIZE: usize = 63;
3177
3178    fn check_bloom_filter<T: AsBytes>(
3179        files: Vec<Bytes>,
3180        file_column: String,
3181        positive_values: Vec<T>,
3182        negative_values: Vec<T>,
3183    ) {
3184        files.into_iter().take(1).for_each(|file| {
3185            let file_reader = SerializedFileReader::new_with_options(
3186                file,
3187                ReadOptionsBuilder::new()
3188                    .with_reader_properties(
3189                        ReaderProperties::builder()
3190                            .set_read_bloom_filter(true)
3191                            .build(),
3192                    )
3193                    .build(),
3194            )
3195            .expect("Unable to open file as Parquet");
3196            let metadata = file_reader.metadata();
3197
3198            // Gets bloom filters from all row groups.
3199            let mut bloom_filters: Vec<_> = vec![];
3200            for (ri, row_group) in metadata.row_groups().iter().enumerate() {
3201                if let Some((column_index, _)) = row_group
3202                    .columns()
3203                    .iter()
3204                    .enumerate()
3205                    .find(|(_, column)| column.column_path().string() == file_column)
3206                {
3207                    let row_group_reader = file_reader
3208                        .get_row_group(ri)
3209                        .expect("Unable to read row group");
3210                    if let Some(sbbf) = row_group_reader.get_column_bloom_filter(column_index) {
3211                        bloom_filters.push(sbbf.clone());
3212                    } else {
3213                        panic!("No bloom filter for column named {file_column} found");
3214                    }
3215                } else {
3216                    panic!("No column named {file_column} found");
3217                }
3218            }
3219
3220            positive_values.iter().for_each(|value| {
3221                let found = bloom_filters.iter().find(|sbbf| sbbf.check(value));
3222                assert!(
3223                    found.is_some(),
3224                    "{}",
3225                    format!("Value {:?} should be in bloom filter", value.as_bytes())
3226                );
3227            });
3228
3229            negative_values.iter().for_each(|value| {
3230                let found = bloom_filters.iter().find(|sbbf| sbbf.check(value));
3231                assert!(
3232                    found.is_none(),
3233                    "{}",
3234                    format!("Value {:?} should not be in bloom filter", value.as_bytes())
3235                );
3236            });
3237        });
3238    }
3239
3240    #[test]
3241    #[cfg_attr(miri, ignore)] // Takes too long
3242    fn bool_large_single_column() {
3243        let values = Arc::new(
3244            [None, Some(true), Some(false)]
3245                .iter()
3246                .cycle()
3247                .copied()
3248                .take(200_000)
3249                .collect::<BooleanArray>(),
3250        );
3251        let schema = Schema::new(vec![Field::new("col", values.data_type().clone(), true)]);
3252        let expected_batch = RecordBatch::try_new(Arc::new(schema), vec![values]).unwrap();
3253        let file = tempfile::tempfile().unwrap();
3254
3255        let mut writer =
3256            ArrowWriter::try_new(file.try_clone().unwrap(), expected_batch.schema(), None)
3257                .expect("Unable to write file");
3258        writer.write(&expected_batch).unwrap();
3259        writer.close().unwrap();
3260    }
3261
3262    #[test]
3263    fn check_page_offset_index_with_nan() {
3264        let values = Arc::new(Float64Array::from(vec![f64::NAN; 10]));
3265        let schema = Schema::new(vec![Field::new("col", DataType::Float64, true)]);
3266        let batch = RecordBatch::try_new(Arc::new(schema), vec![values]).unwrap();
3267
3268        let mut out = Vec::with_capacity(1024);
3269        let mut writer =
3270            ArrowWriter::try_new(&mut out, batch.schema(), None).expect("Unable to write file");
3271        writer.write(&batch).unwrap();
3272        let file_meta_data = writer.close().unwrap();
3273        for row_group in file_meta_data.row_groups() {
3274            for column in row_group.columns() {
3275                assert!(column.offset_index_offset().is_some());
3276                assert!(column.offset_index_length().is_some());
3277                assert!(column.column_index_offset().is_some());
3278                assert!(column.column_index_length().is_some());
3279            }
3280        }
3281        if let Some(page_index) = file_meta_data.page_index() {
3282            for rg in 0..file_meta_data.num_row_groups() {
3283                for col in 0..file_meta_data.row_group(rg).num_columns() {
3284                    let idx = page_index
3285                        .column_index(rg, col)
3286                        .expect("column index should exist");
3287                    assert!(idx.nan_counts().is_some());
3288                    let ColumnIndexMetaData::DOUBLE(float_idx) = idx else {
3289                        panic!("expected double statistics")
3290                    };
3291                    for i in 0..idx.num_pages() as usize {
3292                        assert_eq!(float_idx.nan_count(i), Some(10));
3293                        assert_eq!(
3294                            f64::NAN.total_cmp(float_idx.min_value(i).unwrap()),
3295                            Ordering::Equal
3296                        );
3297                        assert_eq!(
3298                            f64::NAN.total_cmp(float_idx.max_value(i).unwrap()),
3299                            Ordering::Equal
3300                        );
3301                    }
3302                }
3303            }
3304        } else {
3305            panic!("page index should be present");
3306        }
3307    }
3308
3309    #[test]
3310    fn check_page_offset_index_with_mixed_nan() {
3311        let schema = Arc::new(Schema::new(vec![Field::new(
3312            "col",
3313            DataType::Float64,
3314            true,
3315        )]));
3316
3317        let mut out = Vec::with_capacity(1024);
3318        let props = WriterProperties::builder()
3319            .set_data_page_row_count_limit(10)
3320            .build();
3321        let mut writer = ArrowWriter::try_new(&mut out, schema.clone(), Some(props))
3322            .expect("Unable to write file");
3323
3324        // write a page of all NaN (since batch min and max are NaN, global min/max are NaN)
3325        let values = Arc::new(Float64Array::from(vec![f64::NAN; 10]));
3326        let batch = RecordBatch::try_new(schema.clone(), vec![values]).unwrap();
3327        writer.write(&batch).unwrap();
3328
3329        // write a page of all -NaN (batch min/max is -NaN, should update global min to -NaN)
3330        let values = Arc::new(Float64Array::from(vec![-f64::NAN; 10]));
3331        let batch = RecordBatch::try_new(schema.clone(), vec![values]).unwrap();
3332        writer.write(&batch).unwrap();
3333
3334        // write a page of all 0 (non-NaN should override global min/max, now 0/0)
3335        let values = Arc::new(Float64Array::from(vec![0_f64; 10]));
3336        let batch = RecordBatch::try_new(schema.clone(), vec![values]).unwrap();
3337        writer.write(&batch).unwrap();
3338
3339        // write a mixed page (should now have min -1, max 1)
3340        let values = Arc::new(Float64Array::from(vec![
3341            -1.0,
3342            0.0,
3343            f64::NAN,
3344            -f64::NAN,
3345            1.0,
3346        ]));
3347        let batch = RecordBatch::try_new(schema.clone(), vec![values]).unwrap();
3348        writer.write(&batch).unwrap();
3349
3350        let file_meta_data = writer.close().unwrap();
3351
3352        // check the column chunk stats are correct
3353        let col_stats = file_meta_data
3354            .row_group(0)
3355            .column(0)
3356            .statistics()
3357            .expect("missing column chunk statistics");
3358
3359        assert_eq!(col_stats.nan_count_opt(), Some(22));
3360        assert_eq!(col_stats.min_bytes_opt(), Some((-1.0f64).as_bytes()));
3361        assert_eq!(col_stats.max_bytes_opt(), Some(1.0f64.as_bytes()));
3362
3363        assert!(file_meta_data.page_index().is_some());
3364        let col_idx = &file_meta_data.page_index().unwrap().column_index(0, 0);
3365        assert_eq!(col_idx.as_ref().unwrap().num_pages(), 4);
3366
3367        // test each page
3368        let Some(ColumnIndexMetaData::DOUBLE(float_idx)) = col_idx else {
3369            panic!("expected double statistics")
3370        };
3371
3372        assert_eq!(float_idx.nan_counts, Some(vec![10, 10, 0, 2]));
3373        assert_eq!(
3374            f64::NAN.total_cmp(float_idx.min_value(0).unwrap()),
3375            Ordering::Equal
3376        );
3377        assert_eq!(
3378            f64::NAN.total_cmp(float_idx.max_value(0).unwrap()),
3379            Ordering::Equal
3380        );
3381        assert_eq!(
3382            (-f64::NAN).total_cmp(float_idx.min_value(1).unwrap()),
3383            Ordering::Equal
3384        );
3385        assert_eq!(
3386            (-f64::NAN).total_cmp(float_idx.max_value(1).unwrap()),
3387            Ordering::Equal
3388        );
3389        assert_eq!(float_idx.min_value(2), Some(&0.0));
3390        assert_eq!(float_idx.max_value(2), Some(&0.0));
3391        assert_eq!(float_idx.min_value(3), Some(&-1.0));
3392        assert_eq!(float_idx.max_value(3), Some(&1.0));
3393    }
3394
3395    #[test]
3396    #[should_panic(
3397        expected = "Attempting to write an Arrow interval type MonthDayNano to parquet that is not yet implemented"
3398    )]
3399    fn interval_month_day_nano_single_column() {
3400        required_and_optional::<IntervalMonthDayNanoArray, _>(vec![
3401            IntervalMonthDayNano::new(0, 1, 5),
3402            IntervalMonthDayNano::new(0, 3, 2),
3403            IntervalMonthDayNano::new(3, -2, -5),
3404            IntervalMonthDayNano::new(-200, 4, -1),
3405        ]);
3406    }
3407
3408    #[test]
3409    #[cfg_attr(miri, ignore)] // Takes too long
3410    fn i32_column_bloom_filter_at_end() {
3411        let array = Arc::new(Int32Array::from_iter(0..SMALL_SIZE as i32));
3412        let files = RoundTripTest::new(array)
3413            .with_nullable(false)
3414            .with_bloom_filter(true)
3415            .with_bloom_filter_position(BloomFilterPosition::End)
3416            .run();
3417
3418        check_bloom_filter(
3419            files,
3420            "col".to_string(),
3421            (0..SMALL_SIZE as i32).collect(),
3422            (SMALL_SIZE as i32 + 1..SMALL_SIZE as i32 + 10).collect(),
3423        );
3424    }
3425
3426    #[test]
3427    #[cfg_attr(miri, ignore)] // Takes too long
3428    fn i32_column_bloom_filter() {
3429        let array = Arc::new(Int32Array::from_iter(0..SMALL_SIZE as i32));
3430        let files = RoundTripTest::new(array)
3431            .with_nullable(false)
3432            .with_bloom_filter(true)
3433            .run();
3434
3435        check_bloom_filter(
3436            files,
3437            "col".to_string(),
3438            (0..SMALL_SIZE as i32).collect(),
3439            (SMALL_SIZE as i32 + 1..SMALL_SIZE as i32 + 10).collect(),
3440        );
3441    }
3442
3443    fn write_with_bloom_filter(array: ArrayRef, dictionary_page_size_limit: usize) -> Bytes {
3444        let schema = Arc::new(Schema::new(vec![Field::new(
3445            "col",
3446            array.data_type().clone(),
3447            false,
3448        )]));
3449        let batch = RecordBatch::try_new(schema.clone(), vec![array]).unwrap();
3450        let props = WriterProperties::builder()
3451            .set_dictionary_enabled(true)
3452            .set_dictionary_page_size_limit(dictionary_page_size_limit)
3453            .set_write_batch_size(256)
3454            .set_bloom_filter_enabled(true)
3455            .build();
3456        let mut buf = Vec::new();
3457        let mut writer = ArrowWriter::try_new(&mut buf, schema, Some(props)).unwrap();
3458        writer.write(&batch).unwrap();
3459        writer.close().unwrap();
3460        Bytes::from(buf)
3461    }
3462
3463    fn data_page_encoding_mask(file: &Bytes) -> EncodingMask {
3464        let metadata = ParquetMetaDataReader::new().parse_and_finish(file).unwrap();
3465        *metadata
3466            .row_group(0)
3467            .column(0)
3468            .page_encoding_stats_mask()
3469            .unwrap()
3470    }
3471
3472    /// While a column is dictionary encoded the bloom filter is populated from the dictionary
3473    /// when it is flushed, so a chunk that stays dictionary encoded must still contain every value.
3474    #[test]
3475    fn string_column_bloom_filter_populated_from_dictionary() {
3476        let values: Vec<String> = (0..2000).map(|i| format!("value-{}", i % 10)).collect();
3477        let array = Arc::new(StringArray::from_iter_values(&values));
3478        let file = write_with_bloom_filter(array, 1024 * 1024);
3479        assert!(data_page_encoding_mask(&file).is_only(Encoding::RLE_DICTIONARY));
3480
3481        check_bloom_filter(
3482            vec![file],
3483            "col".to_string(),
3484            (0..10).map(|i| format!("value-{i}").into_bytes()).collect(),
3485            (10..20)
3486                .map(|i| format!("value-{i}").into_bytes())
3487                .collect(),
3488        );
3489    }
3490
3491    /// After falling back from dictionary encoding the filter holds the dictionary's values
3492    /// and every value written plain afterwards.
3493    #[test]
3494    fn string_column_bloom_filter_across_dictionary_fallback() {
3495        let values: Vec<String> = (0..2000).map(|i| format!("value-{i}")).collect();
3496        let array = Arc::new(StringArray::from_iter_values(&values));
3497        let file = write_with_bloom_filter(array, 1024);
3498        let encodings = data_page_encoding_mask(&file);
3499        assert!(
3500            encodings.is_set(Encoding::RLE_DICTIONARY) && encodings.is_set(Encoding::PLAIN),
3501            "expected dictionary and plain data pages, got {encodings:?}"
3502        );
3503
3504        check_bloom_filter(
3505            vec![file],
3506            "col".to_string(),
3507            values.into_iter().map(String::into_bytes).collect(),
3508            (2000..2010)
3509                .map(|i| format!("value-{i}").into_bytes())
3510                .collect(),
3511        );
3512    }
3513
3514    #[test]
3515    fn i64_column_bloom_filter_populated_from_dictionary() {
3516        let array = Arc::new(Int64Array::from_iter_values((0..2000).map(|i| i % 10)));
3517        let file = write_with_bloom_filter(array, 1024 * 1024);
3518        assert!(data_page_encoding_mask(&file).is_only(Encoding::RLE_DICTIONARY));
3519
3520        check_bloom_filter(
3521            vec![file],
3522            "col".to_string(),
3523            (0..10i64).collect(),
3524            (10..20i64).collect(),
3525        );
3526    }
3527
3528    #[test]
3529    fn i64_column_bloom_filter_across_dictionary_fallback() {
3530        let array = Arc::new(Int64Array::from_iter_values(0..2000i64));
3531        let file = write_with_bloom_filter(array, 1024);
3532        let encodings = data_page_encoding_mask(&file);
3533        assert!(
3534            encodings.is_set(Encoding::RLE_DICTIONARY) && encodings.is_set(Encoding::PLAIN),
3535            "expected dictionary and plain data pages, got {encodings:?}"
3536        );
3537
3538        check_bloom_filter(
3539            vec![file],
3540            "col".to_string(),
3541            (0..2000i64).collect(),
3542            (2000..2010i64).collect(),
3543        );
3544    }
3545
3546    /// Test that bloom filter folding produces correct results even when
3547    /// the configured NDV differs significantly from actual NDV.
3548    /// A large NDV means a larger initial filter that gets folded down;
3549    /// a small NDV means a smaller initial filter.
3550    #[test]
3551    #[cfg_attr(miri, ignore)] // Takes too long
3552    fn i32_column_bloom_filter_fixed_ndv() {
3553        let array = Arc::new(Int32Array::from_iter(0..SMALL_SIZE as i32));
3554
3555        // NDV much larger than actual distinct values — tests folding a large filter down
3556        let files = RoundTripTest::new(array.clone())
3557            .with_nullable(false)
3558            .with_bloom_filter(true)
3559            .with_bloom_filter_ndv(1_000_000)
3560            .run();
3561
3562        check_bloom_filter(
3563            files,
3564            "col".to_string(),
3565            (0..SMALL_SIZE as i32).collect(),
3566            (SMALL_SIZE as i32 + 1..SMALL_SIZE as i32 + 10).collect(),
3567        );
3568
3569        // NDV smaller than actual distinct values — tests the underestimate path
3570        let files = RoundTripTest::new(array)
3571            .with_nullable(false)
3572            .with_bloom_filter(true)
3573            .with_bloom_filter_ndv(3)
3574            .run();
3575
3576        check_bloom_filter(
3577            files,
3578            "col".to_string(),
3579            (0..SMALL_SIZE as i32).collect(),
3580            (SMALL_SIZE as i32 + 1..SMALL_SIZE as i32 + 10).collect(),
3581        );
3582    }
3583
3584    #[test]
3585    #[cfg_attr(miri, ignore)] // Takes too long
3586    fn binary_column_bloom_filter() {
3587        let one_vec: Vec<u8> = (0..SMALL_SIZE as u8).collect();
3588        let many_vecs: Vec<_> = std::iter::repeat_n(one_vec, SMALL_SIZE).collect();
3589        let many_vecs_iter = many_vecs.iter().map(|v| v.as_slice());
3590
3591        let array = Arc::new(BinaryArray::from_iter_values(many_vecs_iter));
3592        let files = RoundTripTest::new(array)
3593            .with_nullable(false)
3594            .with_bloom_filter(true)
3595            .run();
3596
3597        check_bloom_filter(
3598            files,
3599            "col".to_string(),
3600            many_vecs,
3601            vec![vec![(SMALL_SIZE + 1) as u8]],
3602        );
3603    }
3604
3605    #[test]
3606    #[cfg_attr(miri, ignore)] // Takes too long
3607    fn empty_string_null_column_bloom_filter() {
3608        let raw_values: Vec<_> = (0..SMALL_SIZE).map(|i| i.to_string()).collect();
3609        let raw_strs = raw_values.iter().map(|s| s.as_str());
3610
3611        let array = Arc::new(StringArray::from_iter_values(raw_strs));
3612        let files = RoundTripTest::new(array)
3613            .with_nullable(false)
3614            .with_bloom_filter(true)
3615            .run();
3616
3617        let optional_raw_values: Vec<_> = raw_values
3618            .iter()
3619            .enumerate()
3620            .filter_map(|(i, v)| if i % 2 == 0 { None } else { Some(v.as_str()) })
3621            .collect();
3622        // For null slots, empty string should not be in bloom filter.
3623        check_bloom_filter(files, "col".to_string(), optional_raw_values, vec![""]);
3624    }
3625
3626    #[test]
3627    fn list_and_map_coerced_names() {
3628        // Create map and list with non-Parquet naming
3629        let list_field =
3630            Field::new_list("my_list", Field::new("item", DataType::Int32, false), false);
3631        let map_field = Field::new_map(
3632            "my_map",
3633            "my_entries",
3634            Field::new("my_keys", DataType::Int32, false),
3635            Field::new("my_values", DataType::Int32, true),
3636            false,
3637            true,
3638        );
3639
3640        let list_array = create_random_array(&list_field, 100, 0.0, 0.0).unwrap();
3641        let map_array = create_random_array(&map_field, 100, 0.0, 0.0).unwrap();
3642
3643        let arrow_schema = Arc::new(Schema::new(vec![list_field, map_field]));
3644
3645        // Write data to Parquet but coerce names to match spec
3646        let props = Some(WriterProperties::builder().set_coerce_types(true).build());
3647        let file = tempfile::tempfile().unwrap();
3648        let mut writer =
3649            ArrowWriter::try_new(file.try_clone().unwrap(), arrow_schema.clone(), props).unwrap();
3650
3651        let batch = RecordBatch::try_new(arrow_schema, vec![list_array, map_array]).unwrap();
3652        writer.write(&batch).unwrap();
3653        let file_metadata = writer.close().unwrap();
3654
3655        let schema = file_metadata.file_metadata().schema();
3656        // Coerced name of "item" should be "element"
3657        let list_field = &schema.get_fields()[0].get_fields()[0];
3658        assert_eq!(list_field.get_fields()[0].name(), "element");
3659
3660        let map_field = &schema.get_fields()[1].get_fields()[0];
3661        // Coerced name of "entries" should be "key_value"
3662        assert_eq!(map_field.name(), "key_value");
3663        // Coerced name of "my_keys" should be "key"
3664        assert_eq!(map_field.get_fields()[0].name(), "key");
3665        // Coerced name of "my_values" should be "value"
3666        assert_eq!(map_field.get_fields()[1].name(), "value");
3667
3668        // Double check schema after reading from the file
3669        let reader = SerializedFileReader::new(file).unwrap();
3670        let file_schema = reader.metadata().file_metadata().schema();
3671        let fields = file_schema.get_fields();
3672        let list_field = &fields[0].get_fields()[0];
3673        assert_eq!(list_field.get_fields()[0].name(), "element");
3674        let map_field = &fields[1].get_fields()[0];
3675        assert_eq!(map_field.name(), "key_value");
3676        assert_eq!(map_field.get_fields()[0].name(), "key");
3677        assert_eq!(map_field.get_fields()[1].name(), "value");
3678    }
3679
3680    #[test]
3681    #[cfg_attr(miri, ignore)] // Takes too long
3682    fn fallback_flush_data_page() {
3683        //tests if the Fallback::flush_data_page clears all buffers correctly
3684        let raw_values: Vec<_> = (0..MEDIUM_SIZE).map(|i| i.to_string()).collect();
3685        let values = Arc::new(StringArray::from(raw_values));
3686        let encodings = vec![
3687            Encoding::DELTA_BYTE_ARRAY,
3688            Encoding::DELTA_LENGTH_BYTE_ARRAY,
3689        ];
3690        let data_type = values.data_type().clone();
3691        let schema = Arc::new(Schema::new(vec![Field::new("col", data_type, false)]));
3692        let expected_batch = RecordBatch::try_new(schema, vec![values]).unwrap();
3693
3694        let row_group_sizes = [1024, SMALL_SIZE, SMALL_SIZE / 2, SMALL_SIZE / 2 + 1, 10];
3695        let data_page_size_limit: usize = 32;
3696        let write_batch_size: usize = 16;
3697
3698        for encoding in &encodings {
3699            for row_group_size in row_group_sizes {
3700                let props = WriterProperties::builder()
3701                    .set_writer_version(WriterVersion::PARQUET_2_0)
3702                    .set_max_row_group_row_count(Some(row_group_size))
3703                    .set_dictionary_enabled(false)
3704                    .set_encoding(*encoding)
3705                    .set_data_page_size_limit(data_page_size_limit)
3706                    .set_write_batch_size(write_batch_size)
3707                    .build();
3708
3709                roundtrip_opts_with_array_validation(&expected_batch, props, |a, b| {
3710                    let string_array_a = StringArray::from(a.clone());
3711                    let string_array_b = StringArray::from(b.clone());
3712                    let vec_a: Vec<&str> = string_array_a.iter().map(|v| v.unwrap()).collect();
3713                    let vec_b: Vec<&str> = string_array_b.iter().map(|v| v.unwrap()).collect();
3714                    assert_eq!(
3715                        vec_a, vec_b,
3716                        "failed for encoder: {encoding:?} and row_group_size: {row_group_size:?}"
3717                    );
3718                });
3719            }
3720        }
3721    }
3722
3723    #[test]
3724    #[cfg_attr(miri, ignore)] // Takes too long
3725    fn u32_min_max() {
3726        // check values roundtrip through parquet
3727        let src = [
3728            u32::MIN,
3729            1,
3730            (i32::MAX as u32) - 1,
3731            i32::MAX as u32,
3732            (i32::MAX as u32) + 1,
3733            u32::MAX - 1,
3734            u32::MAX,
3735        ];
3736        let values = Arc::new(UInt32Array::from_iter_values(src.iter().copied()));
3737        let files = RoundTripTest::new(values).with_nullable(false).run();
3738
3739        for file in files {
3740            // check statistics are valid
3741            let reader = SerializedFileReader::new(file).unwrap();
3742            let metadata = reader.metadata();
3743
3744            let mut row_offset = 0;
3745            for row_group in metadata.row_groups() {
3746                assert_eq!(row_group.num_columns(), 1);
3747                let column = row_group.column(0);
3748
3749                let num_values = column.num_values() as usize;
3750                let src_slice = &src[row_offset..row_offset + num_values];
3751                row_offset += column.num_values() as usize;
3752
3753                let stats = column.statistics().unwrap();
3754                if let Statistics::Int32(stats) = stats {
3755                    assert_eq!(
3756                        *stats.min_opt().unwrap() as u32,
3757                        *src_slice.iter().min().unwrap()
3758                    );
3759                    assert_eq!(
3760                        *stats.max_opt().unwrap() as u32,
3761                        *src_slice.iter().max().unwrap()
3762                    );
3763                } else {
3764                    panic!("Statistics::Int32 missing")
3765                }
3766            }
3767        }
3768    }
3769
3770    #[test]
3771    #[cfg_attr(miri, ignore)] // Takes too long
3772    fn u64_min_max() {
3773        // check values roundtrip through parquet
3774        let src = [
3775            u64::MIN,
3776            1,
3777            (i64::MAX as u64) - 1,
3778            i64::MAX as u64,
3779            (i64::MAX as u64) + 1,
3780            u64::MAX - 1,
3781            u64::MAX,
3782        ];
3783        let values = Arc::new(UInt64Array::from_iter_values(src.iter().copied()));
3784        let files = RoundTripTest::new(values).with_nullable(false).run();
3785
3786        for file in files {
3787            // check statistics are valid
3788            let reader = SerializedFileReader::new(file).unwrap();
3789            let metadata = reader.metadata();
3790
3791            let mut row_offset = 0;
3792            for row_group in metadata.row_groups() {
3793                assert_eq!(row_group.num_columns(), 1);
3794                let column = row_group.column(0);
3795
3796                let num_values = column.num_values() as usize;
3797                let src_slice = &src[row_offset..row_offset + num_values];
3798                row_offset += column.num_values() as usize;
3799
3800                let stats = column.statistics().unwrap();
3801                if let Statistics::Int64(stats) = stats {
3802                    assert_eq!(
3803                        *stats.min_opt().unwrap() as u64,
3804                        *src_slice.iter().min().unwrap()
3805                    );
3806                    assert_eq!(
3807                        *stats.max_opt().unwrap() as u64,
3808                        *src_slice.iter().max().unwrap()
3809                    );
3810                } else {
3811                    panic!("Statistics::Int64 missing")
3812                }
3813            }
3814        }
3815    }
3816
3817    #[test]
3818    #[cfg_attr(miri, ignore)] // Takes too long
3819    fn statistics_null_counts_only_nulls() {
3820        // check that null-count statistics for "only NULL"-columns are correct
3821        let values = Arc::new(UInt64Array::from(vec![None, None]));
3822        let files = RoundTripTest::new(values).run();
3823
3824        for file in files {
3825            // check statistics are valid
3826            let reader = SerializedFileReader::new(file).unwrap();
3827            let metadata = reader.metadata();
3828            assert_eq!(metadata.num_row_groups(), 1);
3829            let row_group = metadata.row_group(0);
3830            assert_eq!(row_group.num_columns(), 1);
3831            let column = row_group.column(0);
3832            let stats = column.statistics().unwrap();
3833            assert_eq!(stats.null_count_opt(), Some(2));
3834        }
3835    }
3836
3837    fn row_group_sizes(metadata: &ParquetMetaData) -> Vec<i64> {
3838        metadata.row_groups().iter().map(|x| x.num_rows()).collect()
3839    }
3840
3841    #[test]
3842    fn test_aggregates_records() {
3843        let arrays = [
3844            Int32Array::from((0..100).collect::<Vec<_>>()),
3845            Int32Array::from((0..50).collect::<Vec<_>>()),
3846            Int32Array::from((200..500).collect::<Vec<_>>()),
3847        ];
3848
3849        let schema = Arc::new(Schema::new(vec![Field::new(
3850            "int",
3851            ArrowDataType::Int32,
3852            false,
3853        )]));
3854
3855        let file = tempfile::tempfile().unwrap();
3856
3857        let props = WriterProperties::builder()
3858            .set_max_row_group_row_count(Some(200))
3859            .build();
3860
3861        let mut writer =
3862            ArrowWriter::try_new(file.try_clone().unwrap(), schema.clone(), Some(props)).unwrap();
3863
3864        for array in arrays {
3865            let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(array)]).unwrap();
3866            writer.write(&batch).unwrap();
3867        }
3868
3869        writer.close().unwrap();
3870
3871        let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
3872        assert_eq!(&row_group_sizes(builder.metadata()), &[200, 200, 50]);
3873
3874        let batches = builder
3875            .with_batch_size(100)
3876            .build()
3877            .unwrap()
3878            .collect::<ArrowResult<Vec<_>>>()
3879            .unwrap();
3880
3881        assert_eq!(batches.len(), 5);
3882        assert!(batches.iter().all(|x| x.num_columns() == 1));
3883
3884        let batch_sizes: Vec<_> = batches.iter().map(|x| x.num_rows()).collect();
3885
3886        assert_eq!(&batch_sizes, &[100, 100, 100, 100, 50]);
3887
3888        let values: Vec<_> = batches
3889            .iter()
3890            .flat_map(|x| {
3891                x.column(0)
3892                    .as_any()
3893                    .downcast_ref::<Int32Array>()
3894                    .unwrap()
3895                    .values()
3896                    .iter()
3897                    .copied()
3898            })
3899            .collect();
3900
3901        let expected_values: Vec<_> = [0..100, 0..50, 200..500].into_iter().flatten().collect();
3902        assert_eq!(&values, &expected_values)
3903    }
3904
3905    #[test]
3906    fn complex_aggregate() {
3907        // Tests aggregating nested data
3908        let field_a = Arc::new(Field::new("leaf_a", DataType::Int32, false));
3909        let field_b = Arc::new(Field::new("leaf_b", DataType::Int32, true));
3910        let struct_a = Arc::new(Field::new(
3911            "struct_a",
3912            DataType::Struct(vec![field_a.clone(), field_b.clone()].into()),
3913            true,
3914        ));
3915
3916        let list_a = Arc::new(Field::new("list", DataType::List(struct_a), true));
3917        let struct_b = Arc::new(Field::new(
3918            "struct_b",
3919            DataType::Struct(vec![list_a.clone()].into()),
3920            false,
3921        ));
3922
3923        let schema = Arc::new(Schema::new(vec![struct_b]));
3924
3925        // create nested data
3926        let field_a_array = Int32Array::from(vec![1, 2, 3, 4, 5, 6]);
3927        let field_b_array =
3928            Int32Array::from_iter(vec![Some(1), None, Some(2), None, None, Some(6)]);
3929
3930        let struct_a_array = StructArray::from(vec![
3931            (field_a.clone(), Arc::new(field_a_array) as ArrayRef),
3932            (field_b.clone(), Arc::new(field_b_array) as ArrayRef),
3933        ]);
3934
3935        let list_data = ArrayDataBuilder::new(list_a.data_type().clone())
3936            .len(5)
3937            .add_buffer(Buffer::from_iter(vec![
3938                0_i32, 1_i32, 1_i32, 3_i32, 3_i32, 5_i32,
3939            ]))
3940            .null_bit_buffer(Some(Buffer::from_iter(vec![
3941                true, false, true, false, true,
3942            ])))
3943            .child_data(vec![struct_a_array.into_data()])
3944            .build()
3945            .unwrap();
3946
3947        let list_a_array = Arc::new(ListArray::from(list_data)) as ArrayRef;
3948        let struct_b_array = StructArray::from(vec![(list_a.clone(), list_a_array)]);
3949
3950        let batch1 =
3951            RecordBatch::try_from_iter(vec![("struct_b", Arc::new(struct_b_array) as ArrayRef)])
3952                .unwrap();
3953
3954        let field_a_array = Int32Array::from(vec![6, 7, 8, 9, 10]);
3955        let field_b_array = Int32Array::from_iter(vec![None, None, None, Some(1), None]);
3956
3957        let struct_a_array = StructArray::from(vec![
3958            (field_a, Arc::new(field_a_array) as ArrayRef),
3959            (field_b, Arc::new(field_b_array) as ArrayRef),
3960        ]);
3961
3962        let list_data = ArrayDataBuilder::new(list_a.data_type().clone())
3963            .len(2)
3964            .add_buffer(Buffer::from_iter(vec![0_i32, 4_i32, 5_i32]))
3965            .child_data(vec![struct_a_array.into_data()])
3966            .build()
3967            .unwrap();
3968
3969        let list_a_array = Arc::new(ListArray::from(list_data)) as ArrayRef;
3970        let struct_b_array = StructArray::from(vec![(list_a, list_a_array)]);
3971
3972        let batch2 =
3973            RecordBatch::try_from_iter(vec![("struct_b", Arc::new(struct_b_array) as ArrayRef)])
3974                .unwrap();
3975
3976        let batches = &[batch1, batch2];
3977
3978        // Verify data is as expected
3979
3980        let expected = r"
3981            +-------------------------------------------------------------------------------------------------------+
3982            | struct_b                                                                                              |
3983            +-------------------------------------------------------------------------------------------------------+
3984            | {list: [{leaf_a: 1, leaf_b: 1}]}                                                                      |
3985            | {list: }                                                                                              |
3986            | {list: [{leaf_a: 2, leaf_b: }, {leaf_a: 3, leaf_b: 2}]}                                               |
3987            | {list: }                                                                                              |
3988            | {list: [{leaf_a: 4, leaf_b: }, {leaf_a: 5, leaf_b: }]}                                                |
3989            | {list: [{leaf_a: 6, leaf_b: }, {leaf_a: 7, leaf_b: }, {leaf_a: 8, leaf_b: }, {leaf_a: 9, leaf_b: 1}]} |
3990            | {list: [{leaf_a: 10, leaf_b: }]}                                                                      |
3991            +-------------------------------------------------------------------------------------------------------+
3992        ".trim().split('\n').map(|x| x.trim()).collect::<Vec<_>>().join("\n");
3993
3994        let actual = pretty_format_batches(batches).unwrap().to_string();
3995        assert_eq!(actual, expected);
3996
3997        // Write data
3998        let file = tempfile::tempfile().unwrap();
3999        let props = WriterProperties::builder()
4000            .set_max_row_group_row_count(Some(6))
4001            .build();
4002
4003        let mut writer =
4004            ArrowWriter::try_new(file.try_clone().unwrap(), schema, Some(props)).unwrap();
4005
4006        for batch in batches {
4007            writer.write(batch).unwrap();
4008        }
4009        writer.close().unwrap();
4010
4011        // Read Data
4012        // Should have written entire first batch and first row of second to the first row group
4013        // leaving a single row in the second row group
4014
4015        let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
4016        assert_eq!(&row_group_sizes(builder.metadata()), &[6, 1]);
4017
4018        let batches = builder
4019            .with_batch_size(2)
4020            .build()
4021            .unwrap()
4022            .collect::<ArrowResult<Vec<_>>>()
4023            .unwrap();
4024
4025        assert_eq!(batches.len(), 4);
4026        let batch_counts: Vec<_> = batches.iter().map(|x| x.num_rows()).collect();
4027        assert_eq!(&batch_counts, &[2, 2, 2, 1]);
4028
4029        let actual = pretty_format_batches(&batches).unwrap().to_string();
4030        assert_eq!(actual, expected);
4031    }
4032
4033    #[test]
4034    fn test_arrow_writer_metadata() {
4035        let batch_schema = Schema::new(vec![Field::new("int32", DataType::Int32, false)]);
4036        let file_schema = batch_schema.clone().with_metadata([("foo", "bar")]);
4037
4038        let batch = RecordBatch::try_new(
4039            Arc::new(batch_schema),
4040            vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4])) as _],
4041        )
4042        .unwrap();
4043
4044        let mut buf = Vec::with_capacity(1024);
4045        let mut writer = ArrowWriter::try_new(&mut buf, Arc::new(file_schema), None).unwrap();
4046        writer.write(&batch).unwrap();
4047        writer.close().unwrap();
4048    }
4049
4050    #[test]
4051    fn in_progress_accounting() {
4052        // define schema
4053        let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
4054
4055        // create some data
4056        let a = Int32Array::from(vec![1, 2, 3, 4, 5]);
4057
4058        // build a record batch
4059        let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(a)]).unwrap();
4060
4061        let mut writer = ArrowWriter::try_new(vec![], batch.schema(), None).unwrap();
4062
4063        // starts empty
4064        assert_eq!(writer.in_progress_size(), 0);
4065        assert_eq!(writer.in_progress_rows(), 0);
4066        assert_eq!(writer.memory_size(), 0);
4067        assert_eq!(writer.bytes_written(), 4); // Initial header
4068        writer.write(&batch).unwrap();
4069
4070        // updated on write
4071        let initial_size = writer.in_progress_size();
4072        assert!(initial_size > 0);
4073        assert_eq!(writer.in_progress_rows(), 5);
4074        let initial_memory = writer.memory_size();
4075        assert!(initial_memory > 0);
4076        // memory estimate is larger than estimated encoded size
4077        assert!(
4078            initial_size <= initial_memory,
4079            "{initial_size} <= {initial_memory}"
4080        );
4081
4082        // updated on second write
4083        writer.write(&batch).unwrap();
4084        assert!(writer.in_progress_size() > initial_size);
4085        assert_eq!(writer.in_progress_rows(), 10);
4086        assert!(writer.memory_size() > initial_memory);
4087        assert!(
4088            writer.in_progress_size() <= writer.memory_size(),
4089            "in_progress_size {} <= memory_size {}",
4090            writer.in_progress_size(),
4091            writer.memory_size()
4092        );
4093
4094        // in progress tracking is cleared, but the overall data written is updated
4095        let pre_flush_bytes_written = writer.bytes_written();
4096        writer.flush().unwrap();
4097        assert_eq!(writer.in_progress_size(), 0);
4098        assert_eq!(writer.memory_size(), 0);
4099        assert!(writer.bytes_written() > pre_flush_bytes_written);
4100
4101        writer.close().unwrap();
4102    }
4103
4104    #[test]
4105    fn test_writer_all_null() {
4106        let a = Int32Array::from(vec![1, 2, 3, 4, 5]);
4107        let b = Int32Array::new(vec![0; 5].into(), Some(NullBuffer::new_null(5)));
4108        let batch = RecordBatch::try_from_iter(vec![
4109            ("a", Arc::new(a) as ArrayRef),
4110            ("b", Arc::new(b) as ArrayRef),
4111        ])
4112        .unwrap();
4113
4114        let mut buf = Vec::with_capacity(1024);
4115        let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(), None).unwrap();
4116        writer.write(&batch).unwrap();
4117        writer.close().unwrap();
4118
4119        let bytes = Bytes::from(buf);
4120        let options = ReadOptionsBuilder::new().with_page_index().build();
4121        let reader = SerializedFileReader::new_with_options(bytes, options).unwrap();
4122        let index = reader.metadata().page_index().unwrap();
4123
4124        assert_eq!(index.num_data_pages(0, 0), Some(1)); // 1 page
4125        assert_eq!(index.num_data_pages(0, 1), Some(1)); // 1 page
4126    }
4127
4128    #[test]
4129    fn test_disabled_statistics_with_page() {
4130        let file_schema = Schema::new(vec![
4131            Field::new("a", DataType::Utf8, true),
4132            Field::new("b", DataType::Utf8, true),
4133        ]);
4134        let file_schema = Arc::new(file_schema);
4135
4136        let batch = RecordBatch::try_new(
4137            file_schema.clone(),
4138            vec![
4139                Arc::new(StringArray::from(vec!["a", "b", "c", "d"])) as _,
4140                Arc::new(StringArray::from(vec!["w", "x", "y", "z"])) as _,
4141            ],
4142        )
4143        .unwrap();
4144
4145        let props = WriterProperties::builder()
4146            .set_statistics_enabled(EnabledStatistics::None)
4147            .set_column_statistics_enabled("a".into(), EnabledStatistics::Page)
4148            .build();
4149
4150        let mut buf = Vec::with_capacity(1024);
4151        let mut writer = ArrowWriter::try_new(&mut buf, file_schema.clone(), Some(props)).unwrap();
4152        writer.write(&batch).unwrap();
4153
4154        let metadata = writer.close().unwrap();
4155        assert_eq!(metadata.num_row_groups(), 1);
4156        let row_group = metadata.row_group(0);
4157        assert_eq!(row_group.num_columns(), 2);
4158        // Column "a" has both offset and column index, as requested
4159        assert!(row_group.column(0).offset_index_offset().is_some());
4160        assert!(row_group.column(0).column_index_offset().is_some());
4161        // Column "b" should only have offset index
4162        assert!(row_group.column(1).offset_index_offset().is_some());
4163        assert!(row_group.column(1).column_index_offset().is_none());
4164
4165        let options = ReadOptionsBuilder::new().with_page_index().build();
4166        let reader = SerializedFileReader::new_with_options(Bytes::from(buf), options).unwrap();
4167
4168        let row_group = reader.get_row_group(0).unwrap();
4169        let a_col = row_group.metadata().column(0);
4170        let b_col = row_group.metadata().column(1);
4171
4172        // Column chunk of column "a" should have chunk level statistics
4173        if let Statistics::ByteArray(byte_array_stats) = a_col.statistics().unwrap() {
4174            let min = byte_array_stats.min_opt().unwrap();
4175            let max = byte_array_stats.max_opt().unwrap();
4176
4177            assert_eq!(min.as_bytes(), b"a");
4178            assert_eq!(max.as_bytes(), b"d");
4179        } else {
4180            panic!("expecting Statistics::ByteArray");
4181        }
4182
4183        // The column chunk for column "b" shouldn't have statistics
4184        assert!(b_col.statistics().is_none());
4185
4186        let page_index = reader.metadata().page_index().unwrap();
4187
4188        let a_idx = page_index.column_index(0, 0);
4189        assert!(
4190            matches!(a_idx, Some(ColumnIndexMetaData::BYTE_ARRAY(_))),
4191            "{a_idx:?}"
4192        );
4193        let b_idx = page_index.column_index(0, 1);
4194        assert!(b_idx.is_none(), "{b_idx:?}");
4195    }
4196
4197    #[test]
4198    fn test_disabled_statistics_with_chunk() {
4199        let file_schema = Schema::new(vec![
4200            Field::new("a", DataType::Utf8, true),
4201            Field::new("b", DataType::Utf8, true),
4202        ]);
4203        let file_schema = Arc::new(file_schema);
4204
4205        let batch = RecordBatch::try_new(
4206            file_schema.clone(),
4207            vec![
4208                Arc::new(StringArray::from(vec!["a", "b", "c", "d"])) as _,
4209                Arc::new(StringArray::from(vec!["w", "x", "y", "z"])) as _,
4210            ],
4211        )
4212        .unwrap();
4213
4214        let props = WriterProperties::builder()
4215            .set_statistics_enabled(EnabledStatistics::None)
4216            .set_column_statistics_enabled("a".into(), EnabledStatistics::Chunk)
4217            .build();
4218
4219        let mut buf = Vec::with_capacity(1024);
4220        let mut writer = ArrowWriter::try_new(&mut buf, file_schema.clone(), Some(props)).unwrap();
4221        writer.write(&batch).unwrap();
4222
4223        let metadata = writer.close().unwrap();
4224        assert_eq!(metadata.num_row_groups(), 1);
4225        let row_group = metadata.row_group(0);
4226        assert_eq!(row_group.num_columns(), 2);
4227        // Column "a" should only have offset index
4228        assert!(row_group.column(0).offset_index_offset().is_some());
4229        assert!(row_group.column(0).column_index_offset().is_none());
4230        // Column "b" should only have offset index
4231        assert!(row_group.column(1).offset_index_offset().is_some());
4232        assert!(row_group.column(1).column_index_offset().is_none());
4233
4234        let options = ReadOptionsBuilder::new().with_page_index().build();
4235        let reader = SerializedFileReader::new_with_options(Bytes::from(buf), options).unwrap();
4236
4237        let row_group = reader.get_row_group(0).unwrap();
4238        let a_col = row_group.metadata().column(0);
4239        let b_col = row_group.metadata().column(1);
4240
4241        // Column chunk of column "a" should have chunk level statistics
4242        if let Statistics::ByteArray(byte_array_stats) = a_col.statistics().unwrap() {
4243            let min = byte_array_stats.min_opt().unwrap();
4244            let max = byte_array_stats.max_opt().unwrap();
4245
4246            assert_eq!(min.as_bytes(), b"a");
4247            assert_eq!(max.as_bytes(), b"d");
4248        } else {
4249            panic!("expecting Statistics::ByteArray");
4250        }
4251
4252        // The column chunk for column "b"  shouldn't have statistics
4253        assert!(b_col.statistics().is_none());
4254
4255        let page_index = reader.metadata().page_index().unwrap();
4256
4257        let a_idx = page_index.column_index(0, 0);
4258        assert!(a_idx.is_none(), "{a_idx:?}");
4259        let b_idx = page_index.column_index(0, 1);
4260        assert!(b_idx.is_none(), "{b_idx:?}");
4261    }
4262
4263    #[test]
4264    fn test_arrow_writer_skip_metadata() {
4265        let batch_schema = Schema::new(vec![Field::new("int32", DataType::Int32, false)]);
4266        let file_schema = Arc::new(batch_schema.clone());
4267
4268        let batch = RecordBatch::try_new(
4269            Arc::new(batch_schema),
4270            vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4])) as _],
4271        )
4272        .unwrap();
4273        let skip_options = ArrowWriterOptions::new().with_skip_arrow_metadata(true);
4274
4275        let mut buf = Vec::with_capacity(1024);
4276        let mut writer =
4277            ArrowWriter::try_new_with_options(&mut buf, file_schema.clone(), skip_options).unwrap();
4278        writer.write(&batch).unwrap();
4279        writer.close().unwrap();
4280
4281        let bytes = Bytes::from(buf);
4282        let reader_builder = ParquetRecordBatchReaderBuilder::try_new(bytes).unwrap();
4283        assert_eq!(file_schema, *reader_builder.schema());
4284        if let Some(key_value_metadata) = reader_builder
4285            .metadata()
4286            .file_metadata()
4287            .key_value_metadata()
4288        {
4289            assert!(
4290                !key_value_metadata
4291                    .iter()
4292                    .any(|kv| kv.key.as_str() == ARROW_SCHEMA_META_KEY)
4293            );
4294        }
4295    }
4296
4297    #[test]
4298    fn test_arrow_writer_skip_path_in_schema() {
4299        let batch_schema = Schema::new(vec![Field::new("int32", DataType::Int32, false)]);
4300        let file_schema = Arc::new(batch_schema.clone());
4301
4302        let batch = RecordBatch::try_new(
4303            Arc::new(batch_schema),
4304            vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4])) as _],
4305        )
4306        .unwrap();
4307
4308        // default options should still write path_in_schema
4309        let skip_options = ArrowWriterOptions::new();
4310
4311        let mut buf = Vec::with_capacity(1024);
4312        let mut writer =
4313            ArrowWriter::try_new_with_options(&mut buf, file_schema.clone(), skip_options).unwrap();
4314        writer.write(&batch).unwrap();
4315        writer.close().unwrap();
4316
4317        // override to not write path_in_schema
4318        let skip_options = ArrowWriterOptions::new().with_properties(
4319            WriterProperties::builder()
4320                .set_write_path_in_schema(false)
4321                .build(),
4322        );
4323
4324        let mut buf2 = Vec::with_capacity(1024);
4325        let mut writer =
4326            ArrowWriter::try_new_with_options(&mut buf2, file_schema.clone(), skip_options)
4327                .unwrap();
4328        writer.write(&batch).unwrap();
4329        writer.close().unwrap();
4330
4331        // buf2 should be a bit smaller due to lack of path_in_schema
4332        assert!(buf.len() > buf2.len());
4333    }
4334
4335    #[test]
4336    fn mismatched_schemas() {
4337        let batch_schema = Schema::new(vec![Field::new("count", DataType::Int32, false)]);
4338        let file_schema = Arc::new(Schema::new(vec![Field::new(
4339            "temperature",
4340            DataType::Float64,
4341            false,
4342        )]));
4343
4344        let batch = RecordBatch::try_new(
4345            Arc::new(batch_schema),
4346            vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4])) as _],
4347        )
4348        .unwrap();
4349
4350        let mut buf = Vec::with_capacity(1024);
4351        let mut writer = ArrowWriter::try_new(&mut buf, file_schema.clone(), None).unwrap();
4352
4353        let err = writer.write(&batch).unwrap_err().to_string();
4354        assert_eq!(
4355            err,
4356            "Arrow: Incompatible type. Field 'temperature' has type Float64, array has type Int32"
4357        );
4358    }
4359
4360    #[test]
4361    fn test_page_stats_not_written_by_default() {
4362        let string_field = Field::new("a", DataType::Utf8, false);
4363        let schema = Schema::new(vec![string_field]);
4364        let raw_string_values = vec!["Blart Versenwald III"];
4365        let string_values = StringArray::from(raw_string_values.clone());
4366        let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(string_values)]).unwrap();
4367
4368        let props = WriterProperties::builder()
4369            .set_statistics_enabled(EnabledStatistics::Page)
4370            .set_dictionary_enabled(false)
4371            .set_encoding(Encoding::PLAIN)
4372            .set_compression(crate::basic::Compression::UNCOMPRESSED)
4373            .build();
4374
4375        let file = roundtrip_opts(&batch, props);
4376
4377        // read file and decode page headers
4378        // Note: use the thrift API as there is no Rust API to access the statistics in the page headers
4379
4380        // decode first page header
4381        let first_page = &file[4..];
4382        let mut prot = ThriftSliceInputProtocol::new(first_page);
4383        let hdr = PageHeader::read_thrift(&mut prot).unwrap();
4384        let stats = hdr.data_page_header.unwrap().statistics;
4385
4386        assert!(stats.is_none());
4387    }
4388
4389    #[test]
4390    fn test_page_stats_when_enabled() {
4391        let string_field = Field::new("a", DataType::Utf8, false);
4392        let schema = Schema::new(vec![string_field]);
4393        let raw_string_values = vec!["Blart Versenwald III", "Andrew Lamb"];
4394        let string_values = StringArray::from(raw_string_values.clone());
4395        let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(string_values)]).unwrap();
4396
4397        let props = WriterProperties::builder()
4398            .set_statistics_enabled(EnabledStatistics::Page)
4399            .set_dictionary_enabled(false)
4400            .set_encoding(Encoding::PLAIN)
4401            .set_write_page_header_statistics(true)
4402            .set_compression(crate::basic::Compression::UNCOMPRESSED)
4403            .build();
4404
4405        let file = roundtrip_opts(&batch, props);
4406
4407        // read file and decode page headers
4408        // Note: use the thrift API as there is no Rust API to access the statistics in the page headers
4409
4410        // decode first page header
4411        let first_page = &file[4..];
4412        let mut prot = ThriftSliceInputProtocol::new(first_page);
4413        let hdr = PageHeader::read_thrift(&mut prot).unwrap();
4414        let stats = hdr.data_page_header.unwrap().statistics;
4415
4416        let stats = stats.unwrap();
4417        // check that min/max were actually written to the page
4418        assert!(stats.is_max_value_exact.unwrap());
4419        assert!(stats.is_min_value_exact.unwrap());
4420        assert_eq!(stats.max_value.unwrap(), b"Blart Versenwald III");
4421        assert_eq!(stats.min_value.unwrap(), b"Andrew Lamb");
4422    }
4423
4424    #[test]
4425    fn test_page_stats_truncation() {
4426        let string_field = Field::new("a", DataType::Utf8, false);
4427        let binary_field = Field::new("b", DataType::Binary, false);
4428        let schema = Schema::new(vec![string_field, binary_field]);
4429
4430        let raw_string_values = vec!["Blart Versenwald III"];
4431        let raw_binary_values = [b"Blart Versenwald III".to_vec()];
4432        let raw_binary_value_refs = raw_binary_values
4433            .iter()
4434            .map(|x| x.as_slice())
4435            .collect::<Vec<_>>();
4436
4437        let string_values = StringArray::from(raw_string_values.clone());
4438        let binary_values = BinaryArray::from(raw_binary_value_refs);
4439        let batch = RecordBatch::try_new(
4440            Arc::new(schema),
4441            vec![Arc::new(string_values), Arc::new(binary_values)],
4442        )
4443        .unwrap();
4444
4445        let props = WriterProperties::builder()
4446            .set_statistics_truncate_length(Some(2))
4447            .set_dictionary_enabled(false)
4448            .set_encoding(Encoding::PLAIN)
4449            .set_write_page_header_statistics(true)
4450            .set_compression(crate::basic::Compression::UNCOMPRESSED)
4451            .build();
4452
4453        let file = roundtrip_opts(&batch, props);
4454
4455        // read file and decode page headers
4456        // Note: use the thrift API as there is no Rust API to access the statistics in the page headers
4457
4458        // decode first page header
4459        let first_page = &file[4..];
4460        let mut prot = ThriftSliceInputProtocol::new(first_page);
4461        let hdr = PageHeader::read_thrift(&mut prot).unwrap();
4462        let stats = hdr.data_page_header.unwrap().statistics;
4463        assert!(stats.is_some());
4464        let stats = stats.unwrap();
4465        // check that min/max were properly truncated
4466        assert!(!stats.is_max_value_exact.unwrap());
4467        assert!(!stats.is_min_value_exact.unwrap());
4468        assert_eq!(stats.max_value.unwrap(), b"Bm");
4469        assert_eq!(stats.min_value.unwrap(), b"Bl");
4470
4471        // check second page now
4472        let second_page = &prot.as_slice()[hdr.compressed_page_size as usize..];
4473        let mut prot = ThriftSliceInputProtocol::new(second_page);
4474        let hdr = PageHeader::read_thrift(&mut prot).unwrap();
4475        let stats = hdr.data_page_header.unwrap().statistics;
4476        assert!(stats.is_some());
4477        let stats = stats.unwrap();
4478        // check that min/max were properly truncated
4479        assert!(!stats.is_max_value_exact.unwrap());
4480        assert!(!stats.is_min_value_exact.unwrap());
4481        assert_eq!(stats.max_value.unwrap(), b"Bm");
4482        assert_eq!(stats.min_value.unwrap(), b"Bl");
4483    }
4484
4485    #[test]
4486    fn test_page_encoding_statistics_roundtrip() {
4487        let batch_schema = Schema::new(vec![Field::new(
4488            "int32",
4489            arrow_schema::DataType::Int32,
4490            false,
4491        )]);
4492
4493        let batch = RecordBatch::try_new(
4494            Arc::new(batch_schema.clone()),
4495            vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4])) as _],
4496        )
4497        .unwrap();
4498
4499        let mut file: File = tempfile::tempfile().unwrap();
4500        let mut writer = ArrowWriter::try_new(&mut file, Arc::new(batch_schema), None).unwrap();
4501        writer.write(&batch).unwrap();
4502        let file_metadata = writer.close().unwrap();
4503
4504        assert_eq!(file_metadata.num_row_groups(), 1);
4505        assert_eq!(file_metadata.row_group(0).num_columns(), 1);
4506        assert!(
4507            file_metadata
4508                .row_group(0)
4509                .column(0)
4510                .page_encoding_stats()
4511                .is_some()
4512        );
4513        let chunk_page_stats = file_metadata
4514            .row_group(0)
4515            .column(0)
4516            .page_encoding_stats()
4517            .unwrap();
4518
4519        // check that the read metadata is also correct
4520        let options = ReadOptionsBuilder::new()
4521            .with_page_index()
4522            .with_encoding_stats_as_mask(false)
4523            .build();
4524        let reader = SerializedFileReader::new_with_options(file, options).unwrap();
4525
4526        let rowgroup = reader.get_row_group(0).expect("row group missing");
4527        assert_eq!(rowgroup.num_columns(), 1);
4528        let column = rowgroup.metadata().column(0);
4529        assert!(column.page_encoding_stats().is_some());
4530        let file_page_stats = column.page_encoding_stats().unwrap();
4531        assert_eq!(chunk_page_stats, file_page_stats);
4532    }
4533
4534    #[test]
4535    #[cfg_attr(miri, ignore)] // Takes too long
4536    fn test_different_dict_page_size_limit() {
4537        let array = Arc::new(Int64Array::from_iter(0..1024 * 1024));
4538        let schema = Arc::new(Schema::new(vec![
4539            Field::new("col0", arrow_schema::DataType::Int64, false),
4540            Field::new("col1", arrow_schema::DataType::Int64, false),
4541        ]));
4542        let batch =
4543            arrow_array::RecordBatch::try_new(schema.clone(), vec![array.clone(), array]).unwrap();
4544
4545        let props = WriterProperties::builder()
4546            .set_dictionary_page_size_limit(1024 * 1024)
4547            .set_column_dictionary_page_size_limit(ColumnPath::from("col1"), 1024 * 1024 * 4)
4548            .build();
4549        let mut writer = ArrowWriter::try_new(Vec::new(), schema, Some(props)).unwrap();
4550        writer.write(&batch).unwrap();
4551        let data = Bytes::from(writer.into_inner().unwrap());
4552
4553        let mut metadata = ParquetMetaDataReader::new();
4554        metadata.try_parse(&data).unwrap();
4555        let metadata = metadata.finish().unwrap();
4556        let col0_meta = metadata.row_group(0).column(0);
4557        let col1_meta = metadata.row_group(0).column(1);
4558
4559        let get_dict_page_size = move |meta: &ColumnChunkMetaData| {
4560            let mut reader =
4561                SerializedPageReader::new(Arc::new(data.clone()), meta, 0, None).unwrap();
4562            let page = reader.get_next_page().unwrap().unwrap();
4563            match page {
4564                Page::DictionaryPage { buf, .. } => buf.len(),
4565                _ => panic!("expected DictionaryPage"),
4566            }
4567        };
4568
4569        assert_eq!(get_dict_page_size(col0_meta), 1024 * 1024);
4570        assert_eq!(get_dict_page_size(col1_meta), 1024 * 1024 * 4);
4571    }
4572
4573    #[test]
4574    #[cfg_attr(miri, ignore)] // Takes too long
4575    fn test_arrow_writer_granular_mode_roundtrip() {
4576        // Granular mode subdivides chunks and writes more pages than the
4577        // default batched path. Make sure the data we write back is
4578        // bit-identical to what went in — page-count assertions elsewhere
4579        // only prove pages were cut, not that the encoded data is correct.
4580        //
4581        // Mix value sizes so that the cumulative-byte-budget cutoff
4582        // lands mid-chunk, exercising both batched and granular paths
4583        // within the same `write_batch_internal` call.
4584        let small = "tiny".to_string();
4585        let big = "x".repeat(64 * 1024);
4586        let strings: Vec<String> = (0..256)
4587            .map(|i| {
4588                if i % 16 == 0 {
4589                    big.clone()
4590                } else {
4591                    small.clone()
4592                }
4593            })
4594            .collect();
4595
4596        let schema = Arc::new(Schema::new(vec![Field::new(
4597            "col",
4598            ArrowDataType::Utf8,
4599            false,
4600        )]));
4601        let batch = RecordBatch::try_new(
4602            schema.clone(),
4603            vec![Arc::new(StringArray::from(strings.clone())) as _],
4604        )
4605        .unwrap();
4606
4607        let props = WriterProperties::builder()
4608            .set_dictionary_enabled(false)
4609            .set_data_page_size_limit(16 * 1024)
4610            .build();
4611        let mut writer = ArrowWriter::try_new(Vec::new(), schema, Some(props)).unwrap();
4612        writer.write(&batch).unwrap();
4613        let data = Bytes::from(writer.into_inner().unwrap());
4614
4615        let mut reader = ParquetRecordBatchReader::try_new(data, 1024).unwrap();
4616        let read = reader.next().unwrap().unwrap();
4617        assert!(reader.next().is_none(), "expected one batch");
4618        let col = read
4619            .column(0)
4620            .as_any()
4621            .downcast_ref::<StringArray>()
4622            .unwrap();
4623        assert_eq!(col.len(), strings.len());
4624        for (i, expected) in strings.iter().enumerate() {
4625            assert_eq!(
4626                col.value(i),
4627                expected.as_str(),
4628                "value mismatch at index {i}"
4629            );
4630        }
4631    }
4632
4633    #[test]
4634    fn test_arrow_writer_all_null_string_column() {
4635        // The `LevelDataRef::value_count` Uniform branch with
4636        // `value != max_def` (entirely-null chunk) must return 0 so the
4637        // sub-batch sizer short-circuits to batch mode without trying
4638        // to estimate byte budgets for non-existent values.
4639        let num_rows = 1024;
4640        let schema = Arc::new(Schema::new(vec![Field::new(
4641            "col",
4642            ArrowDataType::Utf8,
4643            true,
4644        )]));
4645        let nulls: Vec<Option<&str>> = vec![None; num_rows];
4646        let batch = RecordBatch::try_new(
4647            schema.clone(),
4648            vec![Arc::new(StringArray::from(nulls)) as _],
4649        )
4650        .unwrap();
4651
4652        let props = WriterProperties::builder()
4653            .set_dictionary_enabled(false)
4654            .set_data_page_size_limit(16 * 1024)
4655            .build();
4656        let mut writer = ArrowWriter::try_new(Vec::new(), schema, Some(props)).unwrap();
4657        writer.write(&batch).unwrap();
4658        let data = Bytes::from(writer.into_inner().unwrap());
4659
4660        // Re-parse the file: row group has one column, every row is
4661        // null, all data pages report `num_rows / page_count` rows.
4662        let mut metadata = ParquetMetaDataReader::new();
4663        metadata.try_parse(&data).unwrap();
4664        let metadata = metadata.finish().unwrap();
4665        let row_group = metadata.row_group(0);
4666        let col_meta = row_group.column(0);
4667        assert_eq!(row_group.num_rows() as usize, num_rows);
4668        // Statistics record `null_count = num_rows` — proves every value
4669        // was written as null.
4670        if let Some(stats) = col_meta.statistics() {
4671            assert_eq!(
4672                stats.null_count_opt().unwrap_or(0) as usize,
4673                num_rows,
4674                "expected all-null column to report null_count = num_rows"
4675            );
4676        }
4677
4678        let mut reader =
4679            SerializedPageReader::new(Arc::new(data.clone()), col_meta, num_rows, None).unwrap();
4680        let mut total_values = 0u32;
4681        while let Some(page) = reader.get_next_page().unwrap() {
4682            if matches!(page, Page::DataPage { .. } | Page::DataPageV2 { .. }) {
4683                total_values += page.num_values();
4684            }
4685        }
4686        assert_eq!(
4687            total_values as usize, num_rows,
4688            "expected every level position to be represented in some page"
4689        );
4690    }
4691
4692    struct WriteBatchesShape {
4693        num_batches: usize,
4694        rows_per_batch: usize,
4695        row_size: usize,
4696    }
4697
4698    /// Helper function to write batches with the provided `WriteBatchesShape` into an `ArrowWriter`
4699    fn write_batches(
4700        WriteBatchesShape {
4701            num_batches,
4702            rows_per_batch,
4703            row_size,
4704        }: WriteBatchesShape,
4705        props: WriterProperties,
4706    ) -> ParquetRecordBatchReaderBuilder<File> {
4707        let schema = Arc::new(Schema::new(vec![Field::new(
4708            "str",
4709            ArrowDataType::Utf8,
4710            false,
4711        )]));
4712        let file = tempfile::tempfile().unwrap();
4713        let mut writer =
4714            ArrowWriter::try_new(file.try_clone().unwrap(), schema.clone(), Some(props)).unwrap();
4715
4716        for batch_idx in 0..num_batches {
4717            let strings: Vec<String> = (0..rows_per_batch)
4718                .map(|i| format!("{:0>width$}", batch_idx * 10 + i, width = row_size))
4719                .collect();
4720            let array = StringArray::from(strings);
4721            let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(array)]).unwrap();
4722            writer.write(&batch).unwrap();
4723        }
4724        writer.close().unwrap();
4725        ParquetRecordBatchReaderBuilder::try_new(file).unwrap()
4726    }
4727
4728    #[test]
4729    // When both limits are None, all data should go into a single row group
4730    fn test_row_group_limit_none_writes_single_row_group() {
4731        let props = WriterProperties::builder()
4732            .set_max_row_group_row_count(None)
4733            .set_max_row_group_bytes(None)
4734            .build();
4735
4736        let builder = write_batches(
4737            WriteBatchesShape {
4738                num_batches: 1,
4739                rows_per_batch: 1000,
4740                row_size: 4,
4741            },
4742            props,
4743        );
4744
4745        assert_eq!(
4746            &row_group_sizes(builder.metadata()),
4747            &[1000],
4748            "With no limits, all rows should be in a single row group"
4749        );
4750    }
4751
4752    #[test]
4753    // When only max_row_group_size is set, respect the row limit
4754    fn test_row_group_limit_rows_only() {
4755        let props = WriterProperties::builder()
4756            .set_max_row_group_row_count(Some(300))
4757            .set_max_row_group_bytes(None)
4758            .build();
4759
4760        let builder = write_batches(
4761            WriteBatchesShape {
4762                num_batches: 1,
4763                rows_per_batch: 1000,
4764                row_size: 4,
4765            },
4766            props,
4767        );
4768
4769        assert_eq!(
4770            &row_group_sizes(builder.metadata()),
4771            &[300, 300, 300, 100],
4772            "Row groups should be split by row count"
4773        );
4774    }
4775
4776    #[test]
4777    #[cfg_attr(miri, ignore)] // Takes too long
4778    // A row limit far smaller than the batch splits it many times over; the split must not
4779    // consume stack proportional to the number of row groups.
4780    fn test_row_group_limit_rows_only_many_splits() {
4781        let props = WriterProperties::builder()
4782            .set_max_row_group_row_count(Some(1))
4783            .set_max_row_group_bytes(None)
4784            .build();
4785
4786        let rows = 50_000;
4787        let builder = write_batches(
4788            WriteBatchesShape {
4789                num_batches: 1,
4790                rows_per_batch: rows,
4791                row_size: 4,
4792            },
4793            props,
4794        );
4795
4796        let sizes = row_group_sizes(builder.metadata());
4797        assert_eq!(sizes.len(), rows, "Every row should get its own row group");
4798        assert_eq!(
4799            sizes.iter().sum::<i64>(),
4800            rows as i64,
4801            "Total rows should be preserved"
4802        );
4803    }
4804
4805    #[test]
4806    // When only max_row_group_bytes is set, respect the byte limit
4807    fn test_row_group_limit_bytes_only() {
4808        let props = WriterProperties::builder()
4809            .set_max_row_group_row_count(None)
4810            // Set byte limit to approximately fit ~30 rows worth of data (~100 bytes each)
4811            .set_max_row_group_bytes(Some(3500))
4812            .build();
4813
4814        let builder = write_batches(
4815            WriteBatchesShape {
4816                num_batches: 10,
4817                rows_per_batch: 10,
4818                row_size: 100,
4819            },
4820            props,
4821        );
4822
4823        let sizes = row_group_sizes(builder.metadata());
4824
4825        assert!(
4826            sizes.len() > 1,
4827            "Should have multiple row groups due to byte limit, got {sizes:?}",
4828        );
4829
4830        let total_rows: i64 = sizes.iter().sum();
4831        assert_eq!(total_rows, 100, "Total rows should be preserved");
4832    }
4833
4834    #[test]
4835    // If an in-progress row group is already oversized, it should be flushed before writing more.
4836    fn test_row_group_limit_bytes_flushes_when_current_group_already_too_large() {
4837        let schema = Arc::new(Schema::new(vec![Field::new(
4838            "str",
4839            ArrowDataType::Utf8,
4840            false,
4841        )]));
4842        let file = tempfile::tempfile().unwrap();
4843
4844        // Start with no byte limit so we can intentionally build an oversized in-progress row group.
4845        let props = WriterProperties::builder()
4846            .set_max_row_group_row_count(None)
4847            .set_max_row_group_bytes(None)
4848            .build();
4849        let mut writer =
4850            ArrowWriter::try_new(file.try_clone().unwrap(), schema.clone(), Some(props)).unwrap();
4851
4852        let first_array = StringArray::from(
4853            (0..10)
4854                .map(|i| format!("{i:0>100}"))
4855                .collect::<Vec<String>>(),
4856        );
4857        let first_batch =
4858            RecordBatch::try_new(schema.clone(), vec![Arc::new(first_array)]).unwrap();
4859        writer.write(&first_batch).unwrap();
4860        assert_eq!(writer.in_progress_rows(), 10);
4861
4862        // Tighten the limit below the current in-progress bytes to exercise:
4863        // `if current_bytes >= max_bytes { self.flush()?; ... }`
4864        writer.max_row_group_bytes = Some(1);
4865
4866        let second_array = StringArray::from(vec!["x".to_string()]);
4867        let second_batch =
4868            RecordBatch::try_new(schema.clone(), vec![Arc::new(second_array)]).unwrap();
4869        writer.write(&second_batch).unwrap();
4870        writer.close().unwrap();
4871        let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
4872
4873        assert_eq!(
4874            &row_group_sizes(builder.metadata()),
4875            &[10, 1],
4876            "The second write should flush an oversized in-progress row group first",
4877        );
4878    }
4879
4880    #[test]
4881    // When both limits are set, the row limit triggers first
4882    fn test_row_group_limit_both_row_wins_single_batch() {
4883        let props = WriterProperties::builder()
4884            .set_max_row_group_row_count(Some(200)) // Will trigger at 200 rows
4885            .set_max_row_group_bytes(Some(1024 * 1024)) // 1MB - won't trigger for small int data
4886            .build();
4887
4888        let builder = write_batches(
4889            WriteBatchesShape {
4890                num_batches: 1,
4891                row_size: 4,
4892                rows_per_batch: 1000,
4893            },
4894            props,
4895        );
4896
4897        assert_eq!(
4898            &row_group_sizes(builder.metadata()),
4899            &[200, 200, 200, 200, 200],
4900            "Row limit should trigger before byte limit"
4901        );
4902    }
4903
4904    #[test]
4905    // When both limits are set, the row limit triggers first
4906    fn test_row_group_limit_both_row_wins_multiple_batches() {
4907        let props = WriterProperties::builder()
4908            .set_max_row_group_row_count(Some(5)) // Will trigger every 5 rows
4909            .set_max_row_group_bytes(Some(9999)) // Won't trigger
4910            .build();
4911
4912        let builder = write_batches(
4913            WriteBatchesShape {
4914                num_batches: 10,
4915                rows_per_batch: 10,
4916                row_size: 100,
4917            },
4918            props,
4919        );
4920
4921        assert_eq!(
4922            &row_group_sizes(builder.metadata()),
4923            &[5; 20],
4924            "Row limit should trigger before byte limit"
4925        );
4926    }
4927
4928    #[test]
4929    // When both limits are set, the byte limit triggers first
4930    fn test_row_group_limit_both_bytes_wins() {
4931        let props = WriterProperties::builder()
4932            .set_max_row_group_row_count(Some(1000)) // Won't trigger for 100 rows
4933            .set_max_row_group_bytes(Some(3500)) // Will trigger at ~30-35 rows
4934            .build();
4935
4936        let builder = write_batches(
4937            WriteBatchesShape {
4938                num_batches: 10,
4939                rows_per_batch: 10,
4940                row_size: 100,
4941            },
4942            props,
4943        );
4944
4945        let sizes = row_group_sizes(builder.metadata());
4946
4947        assert!(
4948            sizes.len() > 1,
4949            "Byte limit should trigger before row limit, got {sizes:?}",
4950        );
4951
4952        assert!(
4953            sizes.iter().all(|&s| s < 1000),
4954            "No row group should hit the row limit"
4955        );
4956
4957        let total_rows: i64 = sizes.iter().sum();
4958        assert_eq!(total_rows, 100, "Total rows should be preserved");
4959    }
4960
4961    #[test]
4962    // Both limits can apply to the same batch: the row limit trims it to 5 rows, and the
4963    // byte limit then trims those 5 down to 4.
4964    fn test_row_group_limit_both_apply_to_same_batch() {
4965        let props = WriterProperties::builder()
4966            .set_max_row_group_row_count(Some(15))
4967            .set_max_row_group_bytes(Some(1500))
4968            .build();
4969
4970        let builder = write_batches(
4971            WriteBatchesShape {
4972                num_batches: 2,
4973                rows_per_batch: 10,
4974                row_size: 100,
4975            },
4976            props,
4977        );
4978
4979        assert_eq!(
4980            &row_group_sizes(builder.metadata()),
4981            &[14, 6],
4982            "Byte limit should still apply to a batch the row limit already split"
4983        );
4984    }
4985
4986    #[test]
4987    fn arrow_column_chunk_close_mut_drops_column_index() {
4988        use crate::arrow::ArrowSchemaConverter;
4989        use crate::file::writer::SerializedFileWriter;
4990
4991        let schema = Arc::new(Schema::new(vec![Field::new("i", DataType::Int32, false)]));
4992        let props = Arc::new(
4993            WriterProperties::builder()
4994                .set_statistics_enabled(EnabledStatistics::Page)
4995                .build(),
4996        );
4997        let parquet_schema = ArrowSchemaConverter::new()
4998            .with_coerce_types(props.coerce_types())
4999            .convert(&schema)
5000            .unwrap();
5001
5002        let mut buf = Vec::with_capacity(1024);
5003        let mut writer =
5004            SerializedFileWriter::new(&mut buf, parquet_schema.root_schema_ptr(), props.clone())
5005                .unwrap();
5006
5007        let factory = ArrowRowGroupWriterFactory::new(&writer, Arc::clone(&schema));
5008        let mut col_writers = factory.create_column_writers(0).unwrap();
5009        let arr: ArrayRef = Arc::new(Int32Array::from_iter_values(0..64));
5010        for leaves in compute_leaves(schema.field(0), &arr).unwrap() {
5011            col_writers[0].write(&leaves).unwrap();
5012        }
5013        let mut chunk = col_writers.pop().unwrap().close().unwrap();
5014
5015        // Immutable accessor exposes the close result produced at close time.
5016        assert!(
5017            chunk.close().column_index.is_some(),
5018            "EnabledStatistics::Page should produce a column_index"
5019        );
5020
5021        // Mutable accessor lets callers drop the page-level index before append.
5022        chunk.close_mut().column_index = None;
5023        assert!(chunk.close().column_index.is_none());
5024
5025        let mut rg = writer.next_row_group().unwrap();
5026        chunk.append_to_row_group(&mut rg).unwrap();
5027        rg.close().unwrap();
5028        let file_meta = writer.close().unwrap();
5029
5030        // After dropping column_index, the resulting file records no column
5031        // index offset/length for this chunk.
5032        let cc = file_meta.row_group(0).column(0);
5033        assert!(cc.column_index_range().is_none());
5034    }
5035
5036    #[test]
5037    #[cfg_attr(miri, ignore)] // Takes too long
5038    fn test_number_distinct_values_exact_count() {
5039        // 50 distinct Int32 values repeated across 100k rows, with every 7th row null.
5040        // Nulls must not be counted as a distinct value.
5041        let cardinality = 50u32;
5042        let array: ArrayRef = Arc::new(Int32Array::from_iter((0..100_000u32).map(|i| {
5043            if i % 7 == 0 {
5044                None
5045            } else {
5046                Some((i % cardinality) as i32)
5047            }
5048        })));
5049        let schema = Arc::new(Schema::new(vec![Field::new("x", DataType::Int32, true)]));
5050        let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
5051
5052        let props = WriterProperties::builder()
5053            .set_write_row_group_number_distinct_values(true)
5054            .build();
5055        let mut buf = Vec::new();
5056        let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(), Some(props)).unwrap();
5057        writer.write(&batch).unwrap();
5058        let metadata = writer.close().unwrap();
5059
5060        let count = metadata
5061            .row_group(0)
5062            .column(0)
5063            .statistics()
5064            .and_then(|s| s.distinct_count_opt())
5065            .expect("distinct_count should be set");
5066        // Must equal cardinality exactly; nulls must not inflate the count.
5067        assert_eq!(count, cardinality as u64);
5068    }
5069
5070    #[test]
5071    fn test_number_distinct_values_view_types() {
5072        // 5 distinct values repeated across 30 rows, with every 4th row null.
5073        // Verifies Utf8View is counted correctly (BinaryView shares the same code path).
5074        let cardinality = 5u32;
5075        let distinct_strings = ["alpha", "beta", "gamma", "delta", "epsilon"];
5076
5077        let string_view_col: ArrayRef = Arc::new(StringViewArray::from_iter((0..30u32).map(|i| {
5078            if i % 4 == 0 {
5079                None
5080            } else {
5081                Some(distinct_strings[(i % cardinality) as usize])
5082            }
5083        })));
5084
5085        let schema = Arc::new(Schema::new(vec![Field::new(
5086            "string_view_col",
5087            DataType::Utf8View,
5088            true,
5089        )]));
5090        let batch = RecordBatch::try_new(schema, vec![string_view_col]).unwrap();
5091
5092        let props = WriterProperties::builder()
5093            .set_write_row_group_number_distinct_values(true)
5094            .build();
5095        let mut parquet_bytes = Vec::new();
5096        let mut writer =
5097            ArrowWriter::try_new(&mut parquet_bytes, batch.schema(), Some(props)).unwrap();
5098        writer.write(&batch).unwrap();
5099        let metadata = writer.close().unwrap();
5100
5101        let distinct_count = metadata
5102            .row_group(0)
5103            .column(0)
5104            .statistics()
5105            .and_then(|s| s.distinct_count_opt())
5106            .expect("distinct_count should be set for Utf8View column");
5107        assert_eq!(distinct_count, cardinality as u64);
5108    }
5109
5110    #[test]
5111    fn test_number_distinct_values_not_written_by_default() {
5112        let array: ArrayRef = Arc::new(Int32Array::from_iter_values(0..100));
5113        let schema = Arc::new(Schema::new(vec![Field::new("x", DataType::Int32, false)]));
5114        let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
5115
5116        let mut buf = Vec::new();
5117        let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(), None).unwrap();
5118        writer.write(&batch).unwrap();
5119        let metadata = writer.close().unwrap();
5120
5121        let count = metadata
5122            .row_group(0)
5123            .column(0)
5124            .statistics()
5125            .and_then(|s| s.distinct_count_opt());
5126        assert!(count.is_none());
5127    }
5128
5129    #[test]
5130    fn test_dictionary_ndv_single_batch() {
5131        // Dictionary array with 3 distinct string values repeated many times.
5132        // NDV must equal the number of distinct values in the dictionary (3),
5133        // not the number of rows.
5134        let keys = Int32Array::from(vec![0, 1, 2, 0, 1, 2, 0, 1, 2]);
5135        let values: ArrayRef = Arc::new(StringArray::from(vec!["cat", "dog", "bird"]));
5136        let dict: ArrayRef = Arc::new(DictionaryArray::<Int32Type>::try_new(keys, values).unwrap());
5137
5138        let schema = Arc::new(Schema::new(vec![Field::new(
5139            "x",
5140            DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8)),
5141            false,
5142        )]));
5143        let batch = RecordBatch::try_new(schema, vec![dict]).unwrap();
5144
5145        let props = WriterProperties::builder()
5146            .set_write_row_group_number_distinct_values(true)
5147            .build();
5148        let mut buf = Vec::new();
5149        let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(), Some(props)).unwrap();
5150        writer.write(&batch).unwrap();
5151        let metadata = writer.close().unwrap();
5152
5153        let count = metadata
5154            .row_group(0)
5155            .column(0)
5156            .statistics()
5157            .and_then(|s| s.distinct_count_opt())
5158            .expect("distinct_count should be set");
5159        assert_eq!(count, 3);
5160    }
5161
5162    #[test]
5163    fn test_dictionary_ndv_excludes_unreferenced_values() {
5164        // Keys only reference indices 0 and 1; value at index 2 ("unreferenced") should not
5165        // count toward NDV even though it appears in the dictionary's values array.
5166        let keys = Int32Array::from(vec![0, 1, 0, 1]);
5167        let values: ArrayRef = Arc::new(StringArray::from(vec!["cat", "dog", "unreferenced"]));
5168        let dict: ArrayRef = Arc::new(DictionaryArray::<Int32Type>::try_new(keys, values).unwrap());
5169
5170        let schema = Arc::new(Schema::new(vec![Field::new(
5171            "x",
5172            DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8)),
5173            false,
5174        )]));
5175        let batch = RecordBatch::try_new(schema, vec![dict]).unwrap();
5176
5177        let props = WriterProperties::builder()
5178            .set_write_row_group_number_distinct_values(true)
5179            .build();
5180        let mut buf = Vec::new();
5181        let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(), Some(props)).unwrap();
5182        writer.write(&batch).unwrap();
5183        let metadata = writer.close().unwrap();
5184
5185        let count = metadata
5186            .row_group(0)
5187            .column(0)
5188            .statistics()
5189            .and_then(|s| s.distinct_count_opt())
5190            .expect("distinct_count should be set");
5191        assert_eq!(
5192            count, 2,
5193            "unreferenced dictionary values must not count toward NDV"
5194        );
5195    }
5196
5197    #[test]
5198    fn test_dictionary_ndv_across_batches_regression() {
5199        // Regression test for https://github.com/apache/arrow-rs/issues/11172.
5200        let make_dict_batch = |a: &str, b: &str| -> RecordBatch {
5201            let keys = Int32Array::from(vec![0, 1, 0, 1]);
5202            let values: ArrayRef = Arc::new(StringArray::from(vec![a, b]));
5203            let dict: ArrayRef =
5204                Arc::new(DictionaryArray::<Int32Type>::try_new(keys, values).unwrap());
5205            let schema = Arc::new(Schema::new(vec![Field::new(
5206                "x",
5207                DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8)),
5208                false,
5209            )]));
5210            RecordBatch::try_new(schema, vec![dict]).unwrap()
5211        };
5212
5213        // batch1: dict = ["cat", "dog"], batch2: dict = ["fish", "cat"]
5214        // Distinct values across both batches: "cat", "dog", "fish" NDV = 3
5215        let batch1 = make_dict_batch("cat", "dog");
5216        let batch2 = make_dict_batch("fish", "cat");
5217
5218        let props = WriterProperties::builder()
5219            .set_write_row_group_number_distinct_values(true)
5220            .build();
5221        let mut buf = Vec::new();
5222        let mut writer = ArrowWriter::try_new(&mut buf, batch1.schema(), Some(props)).unwrap();
5223        writer.write(&batch1).unwrap();
5224        writer.write(&batch2).unwrap();
5225        let metadata = writer.close().unwrap();
5226
5227        let count = metadata
5228            .row_group(0)
5229            .column(0)
5230            .statistics()
5231            .and_then(|s| s.distinct_count_opt())
5232            .expect("distinct_count should be set");
5233        assert_eq!(
5234            count, 3,
5235            "NDV should count distinct values, not distinct key indices"
5236        );
5237    }
5238
5239    #[test]
5240    fn ree_struct_with_ree_child() {
5241        // Struct with a REE string field and a REE int field — confirms
5242        // recursion visits every child and each collapses to the right leaf type.
5243        let run_ends = Int32Array::from(vec![2i32, 3, 5]);
5244
5245        let col_a: ArrayRef = Arc::new(
5246            RunArray::try_new(
5247                &run_ends,
5248                &StringArray::from(vec![Some("foo"), None, Some("bar")]),
5249            )
5250            .unwrap(),
5251        );
5252        let col_b: ArrayRef = Arc::new(
5253            RunArray::try_new(&run_ends, &Int32Array::from(vec![Some(1), None, Some(2)])).unwrap(),
5254        );
5255
5256        let struct_array: ArrayRef = Arc::new(StructArray::new(
5257            Fields::from(vec![
5258                Field::new("a", col_a.data_type().clone(), true),
5259                Field::new("b", col_b.data_type().clone(), true),
5260            ]),
5261            vec![col_a, col_b],
5262            None,
5263        ));
5264
5265        let schema = Arc::new(Schema::new(vec![Field::new(
5266            "row",
5267            struct_array.data_type().clone(),
5268            true,
5269        )]));
5270        let batch = RecordBatch::try_new(schema.clone(), vec![struct_array]).unwrap();
5271
5272        let mut buf = Vec::new();
5273        let mut writer = ArrowWriter::try_new(&mut buf, schema, None).unwrap();
5274        writer.write(&batch).unwrap();
5275        let metadata = writer.close().unwrap();
5276
5277        let parquet_schema = metadata.file_metadata().schema_descr();
5278        assert_eq!(parquet_schema.num_columns(), 2);
5279        assert_eq!(
5280            parquet_schema.column(0).physical_type(),
5281            crate::basic::Type::BYTE_ARRAY
5282        );
5283        assert_eq!(parquet_schema.column(0).path().string(), "row.a");
5284        assert_eq!(
5285            parquet_schema.column(1).physical_type(),
5286            crate::basic::Type::INT32
5287        );
5288        assert_eq!(parquet_schema.column(1).path().string(), "row.b");
5289    }
5290
5291    /// Regression test for https://github.com/apache/arrow-rs/issues/11073
5292    ///
5293    /// This checks that the `ArrowColumnWriter` path produces the same min/max
5294    /// statistics as writing the column directly with the low-level
5295    /// `SerializedFileWriter` API, for both `BinaryArray` (the buggy case)
5296    /// and `FixedSizeBinaryArray` (which already worked correctly).
5297    #[test]
5298    fn test_decimal_byte_array_statistics_signed_comparison() {
5299        use crate::basic::{LogicalType, Repetition, Type as PhysicalType};
5300        use crate::data_type::{ByteArray, ByteArrayType};
5301        use crate::schema::types::Type as SchemaType;
5302
5303        // 1-byte two's-complement decimal values: -1, 0, 1
5304        const VALUES: [&[u8]; 3] = [&[0xffu8], &[0x00], &[0x01]];
5305
5306        fn new_file(physical: PhysicalType) -> SerializedFileWriter<Vec<u8>> {
5307            let mut field = SchemaType::primitive_type_builder("value", physical)
5308                .with_repetition(Repetition::REQUIRED)
5309                .with_logical_type(Some(LogicalType::decimal(0, 2)))
5310                .with_precision(2)
5311                .with_scale(0);
5312            if physical == PhysicalType::FIXED_LEN_BYTE_ARRAY {
5313                field = field.with_length(1);
5314            }
5315            let root = SchemaType::group_type_builder("root")
5316                .with_fields(vec![Arc::new(field.build().unwrap())])
5317                .build()
5318                .unwrap();
5319            SerializedFileWriter::new(Vec::new(), Arc::new(root), Default::default()).unwrap()
5320        }
5321
5322        fn read_minmax(file: Vec<u8>) -> (i8, i8) {
5323            let reader = SerializedFileReader::new(Bytes::from(file)).unwrap();
5324            let metadata = reader.metadata();
5325            assert_eq!(metadata.file_metadata().num_rows(), 3);
5326            let stats = metadata
5327                .row_group(0)
5328                .column(0)
5329                .statistics()
5330                .expect("missing statistics");
5331            (
5332                stats.min_bytes_opt().unwrap()[0] as i8,
5333                stats.max_bytes_opt().unwrap()[0] as i8,
5334            )
5335        }
5336
5337        fn direct_parquet() -> Vec<u8> {
5338            let mut file = new_file(PhysicalType::BYTE_ARRAY);
5339            let mut group = file.next_row_group().unwrap();
5340            let mut column = group.next_column().unwrap().unwrap();
5341            let values = VALUES
5342                .iter()
5343                .map(|v| ByteArray::from(v.to_vec()))
5344                .collect::<Vec<_>>();
5345            column
5346                .typed::<ByteArrayType>()
5347                .write_batch(&values, None, None)
5348                .unwrap();
5349            column.close().unwrap();
5350            group.close().unwrap();
5351            file.into_inner().unwrap()
5352        }
5353
5354        fn arrow_bounds(mut file: SerializedFileWriter<Vec<u8>>, array: ArrayRef) -> Vec<u8> {
5355            let field = Field::new("value", array.data_type().clone(), false);
5356            let schema = Arc::new(Schema::new(vec![field.clone()]));
5357            let factory = ArrowRowGroupWriterFactory::new(&file, schema);
5358            let mut writer: ArrowColumnWriter = factory.create_column_writers(0).unwrap().remove(0);
5359            let leaf = compute_leaves(&field, &array).unwrap().remove(0);
5360            writer.write(&leaf).unwrap();
5361            let mut group = file.next_row_group().unwrap();
5362            writer
5363                .close()
5364                .unwrap()
5365                .append_to_row_group(&mut group)
5366                .unwrap();
5367            group.close().unwrap();
5368            file.into_inner().unwrap()
5369        }
5370
5371        fn arrow_binary() -> Vec<u8> {
5372            let file = new_file(PhysicalType::BYTE_ARRAY);
5373            let array: ArrayRef = Arc::new(BinaryArray::from_vec(VALUES.to_vec()));
5374            arrow_bounds(file, array)
5375        }
5376
5377        fn arrow_fixed_size_binary() -> Vec<u8> {
5378            let file = new_file(PhysicalType::FIXED_LEN_BYTE_ARRAY);
5379            let array: ArrayRef =
5380                Arc::new(FixedSizeBinaryArray::try_from_iter(VALUES.into_iter()).unwrap());
5381            arrow_bounds(file, array)
5382        }
5383
5384        let direct = read_minmax(direct_parquet());
5385        let fixed = read_minmax(arrow_fixed_size_binary());
5386        let binary = read_minmax(arrow_binary());
5387
5388        assert_eq!(direct, (-1, 1), "incorrect direct Parquet stats");
5389        assert_eq!(fixed, (-1, 1), "incorrect Arrow FixedSizeBinaryArray stats");
5390        assert_eq!(binary, (-1, 1), "incorrect Arrow BinaryArray stats");
5391    }
5392}