Skip to main content

delta_kernel/engine/arrow_utils/
mod.rs

1//! Some utilities for working with arrow data types
2
3pub(crate) mod apply_schema;
4
5use std::borrow::Cow;
6use std::collections::{HashMap, HashSet};
7use std::ops::Range;
8use std::sync::{Arc, OnceLock};
9
10use delta_kernel_derive::internal_api;
11use itertools::Itertools;
12use tracing::debug;
13
14use self::apply_schema::apply_schema_to_struct;
15use crate::arrow::array::cast::AsArray;
16use crate::arrow::array::{
17    make_array, new_null_array, Array as ArrowArray, ArrayRef as ArrowArrayRef, GenericListArray,
18    MapArray, OffsetSizeTrait, PrimitiveArray, RecordBatch, RecordBatchOptions, StringArray,
19    StructArray,
20};
21use crate::arrow::buffer::NullBuffer;
22use crate::arrow::compute::{cast_with_options, CastOptions};
23use crate::arrow::datatypes::{
24    DataType as ArrowDataType, Field as ArrowField, FieldRef as ArrowFieldRef,
25    Fields as ArrowFields, Int64Type, Schema as ArrowSchema, SchemaRef as ArrowSchemaRef,
26};
27use crate::arrow::json::writer::{make_encoder, LineDelimited, NullableEncoder};
28use crate::arrow::json::{Encoder, EncoderFactory, EncoderOptions, ReaderBuilder, WriterBuilder};
29use crate::engine::arrow_conversion::{TryFromKernel as _, TryIntoArrow as _};
30use crate::engine::arrow_data::ArrowEngineData;
31use crate::engine::ensure_data_types::DataTypeCompat;
32use crate::engine_data::FilteredEngineData;
33use crate::parquet::arrow::{ProjectionMask, PARQUET_FIELD_ID_META_KEY};
34use crate::parquet::file::metadata::RowGroupMetaData;
35use crate::parquet::schema::types::SchemaDescriptor;
36use crate::schema::{
37    ArrayType, ColumnMetadataKey, DataType, MapType, MetadataColumnSpec, MetadataValue,
38    PrimitiveType, Schema, SchemaRef, StructField, StructType,
39};
40use crate::transforms::{transform_output_type, SchemaTransform};
41use crate::utils::require;
42use crate::{DeltaResult, EngineData, Error};
43
44macro_rules! prim_array_cmp {
45    ( $left_arr: ident, $right_arr: ident, $(($data_ty: pat, $prim_ty: ty)),+ ) => {
46
47        return match $left_arr.data_type() {
48        $(
49            $data_ty => {
50                let prim_array = $left_arr.as_primitive_opt::<$prim_ty>()
51                        .ok_or(Error::invalid_expression(
52                            format!("Cannot cast to primitive array: {}", $left_arr.data_type()))
53                        )?;
54                    let list_array = $right_arr.as_list_opt::<i32>()
55                        .ok_or(Error::invalid_expression(
56                            format!("Cannot cast to list array: {}", $right_arr.data_type()))
57                        )?;
58                crate::arrow::compute::kernels::comparison::in_list(prim_array, list_array)
59            }
60        )+
61            _ => Err(ArrowError::CastError(
62                        format!("Bad Comparison between: {:?} and {:?}",
63                            $left_arr.data_type(),
64                            $right_arr.data_type())
65                        )
66                )
67        }.map_err(Error::generic_err);
68    };
69}
70
71pub(crate) use prim_array_cmp;
72
73type FieldIndex = usize;
74type FlattenedRangeIterator<T> = std::iter::Flatten<std::vec::IntoIter<Range<T>>>;
75
76/// contains information about a StructField matched to a parquet struct field
77///
78/// # Lifetime Parameters
79/// * `'k` - The lifetime of the referenced kernel StructField
80struct KernelFieldInfo<'k> {
81    /// The index of the struct field in its parent struct
82    parquet_index: FieldIndex,
83    /// A reference to the struct field
84    field: &'k StructField,
85}
86
87/// Contains a information about a parquet field and the matching `KernelFieldInfo` if one
88/// exists. Parquet struct fields are matched to Kernel fields in [`match_parquet_fields`].
89///
90/// # Lifetime Parameters
91/// * `'k` - The lifetime of the referenced kernel StructField
92/// * `'p` - The lifetime of the referenced parquet ArrowField
93struct MatchedParquetField<'p, 'k> {
94    /// The index of the parquet field
95    parquet_index: FieldIndex,
96    /// A reference to the parquet field in the arrow schema
97    parquet_field: &'p ArrowField,
98    /// If present, this is a `KernelFieldInfo` belonging to a matching kernel `StructField`
99    kernel_field_info: Option<KernelFieldInfo<'k>>,
100}
101
102/// Create an [`Error::Arrow`] with a backtrace from the given message.
103#[internal_api]
104pub(crate) fn make_arrow_error(s: impl Into<String>) -> Error {
105    Error::Arrow(crate::arrow::error::ArrowError::InvalidArgumentError(
106        s.into(),
107    ))
108    .with_backtrace()
109}
110
111/// Prepares to enumerate row indexes of rows in a parquet file, accounting for row group skipping.
112#[internal_api]
113pub(crate) struct RowIndexBuilder {
114    row_group_row_index_ranges: Vec<Range<i64>>,
115    row_group_ordinals: Option<Vec<usize>>,
116}
117
118impl RowIndexBuilder {
119    #[internal_api]
120    pub(crate) fn new(row_groups: &[RowGroupMetaData]) -> Self {
121        let mut row_group_row_index_ranges = Vec::with_capacity(row_groups.len());
122        let mut offset = 0;
123        for row_group in row_groups {
124            let num_rows = row_group.num_rows();
125            row_group_row_index_ranges.push(offset..offset + num_rows);
126            offset += num_rows;
127        }
128        Self {
129            row_group_row_index_ranges,
130            row_group_ordinals: None,
131        }
132    }
133
134    /// Only produce row indexes for the row groups specified by the ordinals that survived row
135    /// group skipping. The ordinals must be in 0..num_row_groups.
136    #[internal_api]
137    pub(crate) fn select_row_groups(&mut self, ordinals: &[usize]) {
138        // NOTE: Don't apply the filtering until we actually build the iterator, because the
139        // filtering is not idempotent and `with_row_groups` could be called more than once.
140        self.row_group_ordinals = Some(ordinals.to_vec())
141    }
142
143    /// Build an iterator of row indexes, filtering out row groups that were skipped.
144    ///
145    /// # Errors
146    ///
147    /// Returns an error if there are duplicate or out of bounds row group ordinals.
148    #[internal_api]
149    pub(crate) fn build(self) -> DeltaResult<FlattenedRangeIterator<i64>> {
150        let starting_offsets = match self.row_group_ordinals {
151            Some(ordinals) => {
152                let mut seen_ordinals = HashSet::with_capacity(ordinals.len());
153                ordinals
154                    .iter()
155                    .map(|&i| {
156                        // We verify that there are no duplicate or out of bounds ordinals
157                        if !seen_ordinals.insert(i) {
158                            return Err(Error::generic("Found duplicate row group ordinal"));
159                        }
160                        // We have to clone here to avoid modifying the original vector in each
161                        // iteration
162                        self.row_group_row_index_ranges
163                            .get(i)
164                            .cloned()
165                            .ok_or_else(|| {
166                                Error::generic(format!("Row group ordinal {i} is out of bounds"))
167                            })
168                    })
169                    .try_collect()?
170            }
171            None => self.row_group_row_index_ranges,
172        };
173        Ok(starting_offsets.into_iter().flatten())
174    }
175}
176
177/// Applies post-processing to data read from parquet files. This includes `reorder_struct_array` to
178/// ensure schema compatibility, as well as `fix_nested_null_masks` to ensure that leaf columns have
179/// accurate null masks that row visitors rely on for correctness.
180/// `row_indexes` are passed through to `reorder_struct_array`.
181/// `file_location` is used to populate file metadata columns if requested.
182///
183/// If `target_schema` is provided, rewrites the batch's schema wrappers to match the kernel
184/// schema via `apply_schema_to_struct`. Specifically, at every nesting level (struct child, list
185/// element, map key/value):
186///
187/// - field names are taken from the kernel schema (producer names are kept only for list element
188///   and map key/value positions, where the kernel `ArrayType`/`MapType` is unnamed);
189/// - field nullability is taken from the kernel schema;
190/// - field metadata is replaced wholesale with kernel-derived metadata (translating
191///   `parquet.field.id` to `PARQUET:field_id` and propagating kernel-only annotations such as
192///   `delta.typeChanges`);
193/// - if both the source and kernel fields carry a `PARQUET:field_id` and they disagree, the call
194///   errors (defense against malformed inputs);
195/// - top-level `RecordBatch::schema().metadata()` is not preserved (the rebuilt schema is created
196///   via `ArrowSchema::new`).
197///
198/// **Type validation.** `apply_schema_to_struct` runs `ensure_data_types(.., Full)` at every
199/// primitive leaf. This is safe because `reorder_struct_array` above has already resolved every
200/// `DataTypeCompat::NeedsCast` into an actual `arrow::compute::cast`, so post-reorder leaf types
201/// are `Identical` to the kernel target.
202///
203/// **Cost.** O(F) per batch where F is the total number of fields (including nested). Row data
204/// (Arrow buffers, offsets, null buffers) is shared via `Arc` and never copied.
205#[internal_api]
206pub(crate) fn fixup_parquet_read(
207    batch: RecordBatch,
208    requested_ordering: &[ReorderIndex],
209    row_indexes: Option<&mut FlattenedRangeIterator<i64>>,
210    file_location: Option<&str>,
211    target_schema: Option<&SchemaRef>,
212) -> DeltaResult<ArrowEngineData> {
213    let data = reorder_struct_array(batch.into(), requested_ordering, row_indexes, file_location)?;
214    let data = fix_nested_null_masks(data);
215    let data = if let Some(schema) = target_schema {
216        apply_schema_to_struct(&data, schema)?
217    } else {
218        data
219    };
220    Ok(data.into())
221}
222
223/*
224* The code below implements proper pruning of columns when reading parquet, reordering of columns to
225* match the specified schema, and insertion of null columns if the requested schema includes a
226* nullable column that isn't included in the parquet file.
227*
228* At a high level there are three schemas/concepts to worry about:
229*  - The parquet file's physical schema (= the columns that are actually available), called
230*    "parquet_schema" below
231*  - The requested logical schema from the engine (= the columns we actually want), called
232*    "requested_schema" below
233*  - The Read schema (and intersection of 1. and 2., in logical schema order). This is never
234*    materialized, but is useful to be able to refer to here
235*  - A `ProjectionMask` that goes to the parquet reader which specifies which subset of columns from
236*    the file schema to actually read. (See "Example" below)
237*
238* In other words, the ProjectionMask is the intersection of the parquet schema and logical schema,
239* and then mapped to indices in the parquet file. Columns unique to the file schema need to be
240* masked out (= ignored), while columns unique to the logical schema need to be backfilled with
241* nulls.
242*
243* We also have to worry about field ordering differences between the read schema and logical
244* schema. We represent any reordering needed as a tree. Each level of the tree is a vec of
245* `ReorderIndex`s. Each element's index represents a column that will be in the read parquet data
246* (as an arrow StructArray) at that level and index. The `ReorderIndex::index` field of the element
247* is the position that the column should appear in the final output.
248
249* The algorithm has three parts, handled by `get_requested_indices`, `generate_mask` and
250* `reorder_struct_array` respectively.
251
252* `get_requested_indices` generates indices to select, along with reordering information:
253* 1. Loop over each field in parquet_schema, keeping track of how many physical fields (i.e. leaf
254*    columns) we have seen so far
255* 2. If a requested field matches the physical field, push the index of the field onto the mask.
256
257* 3. Also push a ReorderIndex element that indicates where this item should be in the final output,
258*    and if it needs any transformation (i.e. casting, create null column)
259* 4. If a nested element (struct/map/list) is encountered, recurse into it, pushing indices onto
260*    the same vector, but producing a new reorder level, which is added to the parent with a `Nested`
261*    transform
262*
263* `generate_mask` is simple, and just calls `ProjectionMask::leaves` in the parquet crate with the
264* indices computed by `get_requested_indices`
265*
266* `reorder_struct_array` handles reordering and data transforms:
267* 1. First check if we need to do any transformations (see doc comment for
268*    `ordering_needs_transform`)
269* 2. If nothing is required we're done (return); otherwise:
270* 3. Create a Vec[None, ..., None] of placeholders that will hold the correctly ordered columns
271* 4. Deconstruct the existing struct array and then loop over the `ReorderIndex` list
272* 5. Use the `ReorderIndex::index` value to put the column at the correct location
273* 6. Additionally, if `ReorderIndex::transform` is not `Identity`, then if it is:
274*      - `Cast`: cast the column to the specified type
275*      - `Missing`: put a column of `null` at the correct location
276*      - `Nested([child_order])` and the data is a `StructArray`: recursively call
277*         `reorder_struct_array` on the column with `child_order` to correctly ordered the child
278*         array
279*      - `Nested` and the data is a `List<StructArray>`: get the inner struct array out of the list,
280*         reorder it recursively as above, rebuild the list, and the put the column at the correct
281*         location
282*      - `Nested` and the data is a `Map`. We expect the child order to contain two elements. The
283*         first specifies any needed reordering in the keys (i.e. if the key contains a struct),
284*         and the second any reordering needed in the values.
285*
286* Example:
287* The parquet crate `ProjectionMask::leaves` method only considers leaf columns -- a "flat" schema --
288* so a struct column is purely a schema level thing and doesn't "count" wrt. column indices.
289*
290* So if we have the following file physical schema:
291*
292*  a
293*    d
294*    x
295*  b
296*    y
297*      z
298*    e
299*    f
300*  c
301*
302* and a logical requested schema of:
303*
304*  b
305*    f
306*    e
307*  a
308*    x
309*  c
310*
311* The mask is [1, 3, 4, 5] because a, b, and y don't contribute to the column indices.
312*
313* The reorder tree is:
314* [
315*   // col a is at position 0 in the struct array, and should be moved to position 1
316*   { index: 1, Nested([{ index: 0 }]) },
317*   // col b is at position 1 in the struct array, and should be moved to position 0
318*   //   also, the inner struct array needs to be reordered to swap 'f' and 'e'
319*   { index: 0, Nested([{ index: 1 }, {index: 0}]) },
320*   // col c is at position 2 in the struct array, and should stay there
321*   { index: 2 }
322* ]
323*/
324
325/// Reordering is specified as a tree. Each level is a vec of `ReorderIndex`s. Each element's
326/// position represents a column that will be in the read parquet data at that level and
327/// position. The `index` of the element is the position that the column should appear in the final
328/// output. The `transform` indicates what, if any, transforms are needed. See the docs for
329/// [`ReorderIndexTransform`] for the meaning.
330#[derive(Debug, PartialEq)]
331#[internal_api]
332pub(crate) struct ReorderIndex {
333    pub index: usize,
334    transform: ReorderIndexTransform,
335}
336
337#[derive(Debug, PartialEq)]
338#[internal_api]
339pub(crate) enum ReorderIndexTransform {
340    /// For a non-nested type, indicates that we need to cast to the contained type
341    Cast(ArrowDataType),
342    /// Used for struct/list/map. Potentially transform child fields using contained reordering
343    Nested(Vec<ReorderIndex>),
344    /// No work needed to transform this data
345    Identity,
346    /// Data is missing, fill in with a null column
347    Missing(ArrowFieldRef),
348    /// Row index column requested, compute it
349    RowIndex(ArrowFieldRef),
350    /// File path column requested, populate with file path
351    FilePath(ArrowFieldRef),
352}
353
354impl ReorderIndex {
355    fn new(index: usize, transform: ReorderIndexTransform) -> Self {
356        ReorderIndex { index, transform }
357    }
358
359    fn cast(index: usize, target: ArrowDataType) -> Self {
360        ReorderIndex::new(index, ReorderIndexTransform::Cast(target))
361    }
362
363    fn nested(index: usize, children: Vec<ReorderIndex>) -> Self {
364        ReorderIndex::new(index, ReorderIndexTransform::Nested(children))
365    }
366
367    fn identity(index: usize) -> Self {
368        ReorderIndex::new(index, ReorderIndexTransform::Identity)
369    }
370
371    fn missing(index: usize, field: ArrowFieldRef) -> Self {
372        ReorderIndex::new(index, ReorderIndexTransform::Missing(field))
373    }
374
375    fn row_index(index: usize, field: ArrowFieldRef) -> Self {
376        ReorderIndex::new(index, ReorderIndexTransform::RowIndex(field))
377    }
378
379    fn file_path(index: usize, field: ArrowFieldRef) -> Self {
380        ReorderIndex::new(index, ReorderIndexTransform::FilePath(field))
381    }
382
383    /// Check if this reordering requires a transformation anywhere. See comment below on
384    /// [`ordering_needs_transform`] to understand why this is needed.
385    fn needs_transform(&self) -> bool {
386        match self.transform {
387            // if we're casting, inserting null, or generating row index/file path, we need to
388            // transform
389            ReorderIndexTransform::Cast(_)
390            | ReorderIndexTransform::Missing(_)
391            | ReorderIndexTransform::RowIndex(_)
392            | ReorderIndexTransform::FilePath(_) => true,
393            // if our nested ordering needs a transform, we need a transform
394            ReorderIndexTransform::Nested(ref children) => ordering_needs_transform(children),
395            // no transform needed
396            ReorderIndexTransform::Identity => false,
397        }
398    }
399}
400
401// count the number of physical columns, including nested ones in an `ArrowField`
402fn count_cols(field: &ArrowField) -> usize {
403    _count_cols(field.data_type())
404}
405
406fn _count_cols(dt: &ArrowDataType) -> usize {
407    match dt {
408        ArrowDataType::Struct(fields) => fields.iter().map(|f| count_cols(f)).sum(),
409        ArrowDataType::Union(fields, _) => fields.iter().map(|(_, f)| count_cols(f)).sum(),
410        ArrowDataType::List(field)
411        | ArrowDataType::LargeList(field)
412        | ArrowDataType::FixedSizeList(field, _)
413        | ArrowDataType::Map(field, _) => count_cols(field),
414        ArrowDataType::Dictionary(_, value_field) => _count_cols(value_field.as_ref()),
415        _ => 1, // other types are "real" fields, so count
416    }
417}
418
419/// Validate that a given field in a parquet file which is presumed to represent data of the
420/// `VARIANT` type is represented as `STRUCT<metadata: BINARY, value: BINARY>`. This is to make
421/// sure that the default engine does not try to read shredded Variants, which it currently does
422/// not support.
423fn validate_parquet_variant(field: &ArrowField) -> DeltaResult<()> {
424    fn variant_parquet_error(field_name: &String) -> Error {
425        Error::Generic(format!(
426            "The field {field_name} presumed to be of Variant type might be \
427            shredded in the parquet file. The default engine does not support \
428            shredded reads yet."
429        ))
430    }
431    match field.data_type() {
432        ArrowDataType::Struct(fields) => {
433            if fields.len() != 2 {
434                return Err(variant_parquet_error(field.name()));
435            }
436            if !matches!(
437                (fields[0].name().as_str(), fields[1].name().as_str()),
438                ("value", "metadata") | ("metadata", "value")
439            ) {
440                return Err(variant_parquet_error(field.name()));
441            }
442            Ok(())
443        }
444        _ => Err(variant_parquet_error(field.name())),
445    }
446}
447
448/// helper function, does the same as `get_requested_indices` but at an offset. used to recurse into
449/// structs, lists, and maps. `parquet_offset` is how many parquet fields exist before processing
450/// this potentially nested schema. returns the number of parquet fields in `fields` (regardless of
451/// if they are selected or not) and reordering information for the requested fields.
452fn get_indices(
453    start_parquet_offset: usize,
454    requested_schema: &Schema,
455    fields: &ArrowFields,
456    mask_indices: &mut Vec<usize>,
457) -> DeltaResult<(usize, Vec<ReorderIndex>)> {
458    let mut found_fields = HashSet::with_capacity(requested_schema.num_fields());
459    let mut reorder_indices = Vec::with_capacity(requested_schema.num_fields());
460    // Missing entries for structs found in parquet but with no selected leaves. These must
461    // be appended after all input-consuming entries (Identity/Nested/Cast) because
462    // `reorder_struct_array` uses vec position as the index into the parquet reader output.
463    let mut deferred_missing = Vec::new();
464    let mut parquet_offset = start_parquet_offset;
465    // for each field, get its position in the parquet (via enumerate), a reference to the arrow
466    // field, and info about where it appears in the requested_schema, or None if the field is not
467    // requested
468    let matched_parquet_fields = match_parquet_fields(requested_schema, fields);
469    for MatchedParquetField {
470        parquet_index,
471        parquet_field: field,
472        kernel_field_info,
473    } in matched_parquet_fields
474    {
475        debug!(
476            "Getting indices for field {} with offset {parquet_offset}, with index {parquet_index}",
477            field.name()
478        );
479        if let Some(KernelFieldInfo {
480            parquet_index: index,
481            field: requested_field,
482            ..
483        }) = kernel_field_info
484        {
485            // If the field is a variant, make sure the parquet schema matches the unshredded
486            // variant representation. This is to ensure that shredded reads are not
487            // performed.
488            if requested_field.data_type == DataType::unshredded_variant() {
489                validate_parquet_variant(field)?;
490            }
491            match field.data_type() {
492                ArrowDataType::Struct(fields) => {
493                    if let DataType::Struct(ref requested_schema)
494                    | DataType::Variant(ref requested_schema) = requested_field.data_type
495                    {
496                        let mask_before = mask_indices.len();
497                        let (parquet_advance, children) = get_indices(
498                            parquet_index + parquet_offset,
499                            requested_schema.as_ref(),
500                            fields,
501                            mask_indices,
502                        )?;
503                        // advance the number of parquet fields, but subtract 1 because the
504                        // struct will be counted by the `enumerate` call but doesn't count as
505                        // an actual index. Use saturating_sub to handle empty structs (0 fields).
506                        parquet_offset += parquet_advance.saturating_sub(1);
507                        // If no leaf columns were selected (mask unchanged), the parquet
508                        // reader will omit this struct entirely. We cannot create a Nested
509                        // entry because it would index into a column that doesn't exist.
510                        // The recursive call is still needed for the correct
511                        // `parquet_advance` value.
512                        found_fields.insert(requested_field.name());
513                        if mask_indices.len() > mask_before {
514                            reorder_indices.push(ReorderIndex::nested(index, children));
515                        } else {
516                            // The recursive call resolved all children (as nullable/missing
517                            // or the struct is empty), but no parquet leaves were selected.
518                            // Defer the Missing entry so it appears after all entries that
519                            // consume parquet input columns.
520                            debug_assert_eq!(children.len(), requested_schema.num_fields());
521                            deferred_missing.push(ReorderIndex::missing(
522                                index,
523                                Arc::new(requested_field.try_into_arrow()?),
524                            ));
525                        }
526                    } else {
527                        return Err(Error::unexpected_column_type(field.name()));
528                    }
529                }
530                ArrowDataType::List(list_field)
531                | ArrowDataType::LargeList(list_field)
532                | ArrowDataType::ListView(list_field) => {
533                    // we just want to transparently recurse into lists, need to transform the
534                    // kernel list data type into a schema
535                    if let DataType::Array(array_type) = requested_field.data_type() {
536                        let requested_schema = StructType::new_unchecked([StructField::new(
537                            list_field.name().clone(), // so we find it in the inner call
538                            array_type.element_type.clone(),
539                            array_type.contains_null,
540                        )]);
541                        let mask_before = mask_indices.len();
542                        let (parquet_advance, mut children) = get_indices(
543                            parquet_index + parquet_offset,
544                            &requested_schema,
545                            &[list_field.clone()].into(),
546                            mask_indices,
547                        )?;
548                        // see comment above in struct match arm
549                        parquet_offset += parquet_advance - 1;
550                        found_fields.insert(requested_field.name());
551                        if mask_indices.len() <= mask_before {
552                            // No leaves selected inside this list. Defer a Missing entry.
553                            deferred_missing.push(ReorderIndex::missing(
554                                index,
555                                Arc::new(requested_field.try_into_arrow()?),
556                            ));
557                        } else if children.len() != 1 {
558                            return Err(Error::generic(
559                                "List call should not have generated more than one reorder index",
560                            ));
561                        } else {
562                            // safety, checked that we have 1 element
563                            let mut children = children.swap_remove(0);
564                            // the index is wrong, as it's the index from the inner schema.
565                            // Adjust it to be our index
566                            children.index = index;
567                            reorder_indices.push(children);
568                        }
569                    } else {
570                        return Err(Error::unexpected_column_type(list_field.name()));
571                    }
572                }
573                ArrowDataType::Map(key_val_field, _) => {
574                    match (key_val_field.data_type(), requested_field.data_type()) {
575                        (ArrowDataType::Struct(inner_fields), DataType::Map(map_type)) => {
576                            let mut key_val_names =
577                                inner_fields.iter().map(|f| f.name().to_string());
578                            let key_name = key_val_names.next().ok_or_else(|| {
579                                Error::generic("map fields didn't include a key col")
580                            })?;
581                            let val_name = key_val_names.next().ok_or_else(|| {
582                                Error::generic("map fields didn't include a val col")
583                            })?;
584                            if key_val_names.next().is_some() {
585                                return Err(Error::generic("map fields had more than 2 members"));
586                            }
587                            let inner_schema = map_type.as_struct_schema(key_name, val_name);
588                            let mask_before = mask_indices.len();
589                            let (parquet_advance, mut children) = get_indices(
590                                parquet_index + parquet_offset,
591                                &inner_schema,
592                                inner_fields,
593                                mask_indices,
594                            )?;
595
596                            // advance the number of parquet fields, but subtract 1 because the
597                            // map will be counted by the `enumerate` call but doesn't count as
598                            // an actual index.
599                            parquet_offset += parquet_advance - 1;
600                            found_fields.insert(requested_field.name());
601                            if mask_indices.len() <= mask_before {
602                                // No leaves selected inside this map. Defer a Missing entry.
603                                deferred_missing.push(ReorderIndex::missing(
604                                    index,
605                                    Arc::new(requested_field.try_into_arrow()?),
606                                ));
607                            } else if children.len() != 2 {
608                                return Err(Error::generic(
609                                    "Map call should have generated exactly two reorder indices",
610                                ));
611                            } else {
612                                // vec indexing is safe, we checked len above
613                                let mut num_identity_transforms = 0;
614                                if !children[0].needs_transform() {
615                                    children[0] = ReorderIndex::identity(0);
616                                    num_identity_transforms += 1;
617                                }
618                                if !children[1].needs_transform() {
619                                    children[1] = ReorderIndex::identity(1);
620                                    num_identity_transforms += 1;
621                                }
622                                let transform = match num_identity_transforms {
623                                    2 => ReorderIndex::identity(index),
624                                    _ => ReorderIndex::nested(index, children),
625                                };
626                                reorder_indices.push(transform);
627                            }
628                        }
629                        _ => {
630                            return Err(Error::unexpected_column_type(field.name()));
631                        }
632                    }
633                }
634                _ => {
635                    // We don't care about matching on nullability or metadata here. These can
636                    // differ between the delta schema and the parquet schema without causing
637                    // issues in reading the data. We fix them up in expression evaluation later.
638                    match super::ensure_data_types::ensure_data_types(
639                        &requested_field.data_type,
640                        field.data_type(),
641                        super::ensure_data_types::ValidationMode::TypesAndNames,
642                    )? {
643                        DataTypeCompat::Identical => {
644                            reorder_indices.push(ReorderIndex::identity(index))
645                        }
646                        DataTypeCompat::NeedsCast(target) => {
647                            reorder_indices.push(ReorderIndex::cast(index, target))
648                        }
649                        DataTypeCompat::Nested => {
650                            return Err(Error::internal_error(
651                                "Comparing nested types in get_indices",
652                            ))
653                        }
654                    }
655                    found_fields.insert(requested_field.name());
656                    mask_indices.push(parquet_offset + parquet_index);
657                }
658            }
659        } else {
660            // We're NOT selecting this field, but we still need to track how many leaf columns we
661            // skipped over
662            debug!("Skipping over un-selected field: {}", field.name());
663            // offset by number of inner fields. subtract one, because the enumerate still
664            // counts this logical "parent" field
665            parquet_offset += count_cols(field).saturating_sub(1);
666        }
667    }
668
669    // Append deferred Missing entries after all input-consuming entries from the main loop.
670    reorder_indices.extend(deferred_missing);
671
672    if found_fields.len() != requested_schema.num_fields() {
673        // some fields are missing, but they might be nullable or metadata columns, need to insert
674        // them into the reorder_indices
675        for (requested_position, field) in requested_schema.fields().enumerate() {
676            if !found_fields.contains(field.name()) {
677                match field.get_metadata_column_spec() {
678                    Some(MetadataColumnSpec::RowIndex) => {
679                        debug!("Inserting a row index column: {}", field.name());
680                        reorder_indices.push(ReorderIndex::row_index(
681                            requested_position,
682                            Arc::new(field.try_into_arrow()?),
683                        ));
684                    }
685                    Some(MetadataColumnSpec::FilePath) => {
686                        debug!("Inserting a file path column: {}", field.name());
687                        reorder_indices.push(ReorderIndex::file_path(
688                            requested_position,
689                            Arc::new(field.try_into_arrow()?),
690                        ));
691                    }
692                    Some(metadata_spec) => {
693                        return Err(Error::Generic(format!(
694                            "Metadata column {metadata_spec:?} is not supported by the default parquet reader"
695                        )));
696                    }
697                    None if field.nullable => {
698                        debug!("Inserting missing and nullable field: {}", field.name());
699                        reorder_indices.push(ReorderIndex::missing(
700                            requested_position,
701                            Arc::new(field.try_into_arrow()?),
702                        ));
703                    }
704                    None => {
705                        return Err(Error::Generic(format!(
706                            "Requested field not found in parquet schema, and field is not nullable: {}",
707                            field.name()
708                        )));
709                    }
710                }
711            }
712        }
713    }
714    Ok((
715        parquet_offset + fields.len() - start_parquet_offset,
716        reorder_indices,
717    ))
718}
719
720/// Constructs an iterator where each parquet Field in `fields` is matched
721/// with a a kernel `KernelFieldInfo` representing a StructField.
722///
723/// The iterator returned has a [`MatchedParquetField`] for each element in `parquet_fields`.
724fn match_parquet_fields<'k, 'p>(
725    kernel_schema: &'k StructType,
726    parquet_fields: &'p ArrowFields,
727) -> impl Iterator<Item = MatchedParquetField<'p, 'k>> {
728    type FieldId = i64;
729
730    // Lazily construct a map from the field id to its StructField name.
731    let field_id_to_name: OnceLock<HashMap<FieldId, &String>> = OnceLock::new();
732    let init_field_map = || {
733        kernel_schema
734            .fields()
735            .filter_map(
736                |field| match field.get_config_value(&ColumnMetadataKey::ParquetFieldId) {
737                    Some(MetadataValue::Number(fid)) => Some((*fid, field.name())),
738                    _ => None,
739                },
740            )
741            .collect()
742    };
743
744    parquet_fields
745        .iter()
746        .enumerate()
747        // move is used to take ownership of the `get_matching_kernel_field` closure so that the
748        // iterator can be returned
749        .map(move |(parquet_index, parquet_field)| {
750            // Get the parquet field id
751            let parquet_field_id = parquet_field
752                .metadata()
753                .get(PARQUET_FIELD_ID_META_KEY)
754                .and_then(|x| x.parse::<FieldId>().ok());
755
756            // Get kernel field name by parquet field id if present. Otherwise fallback to using
757            // parquet name.
758            let field_name = parquet_field_id
759                .and_then(|field_id| {
760                    // If the fid to name map hasn't been initialized, construct it and get the
761                    // field name
762                    field_id_to_name
763                        .get_or_init(init_field_map)
764                        .get(&field_id)
765                        .copied()
766                })
767                .unwrap_or_else(|| parquet_field.name());
768
769            // Map the parquet ArrowField to the matching kernel KernelFieldInfo if present.
770            let kernel_field_info =
771                kernel_schema
772                    .field_with_index(field_name)
773                    .and_then(|(idx, field)| {
774                        (!field.is_metadata_column()).then_some(KernelFieldInfo {
775                            parquet_index: idx,
776                            field,
777                        })
778                    });
779
780            MatchedParquetField {
781                parquet_index,
782                parquet_field,
783                kernel_field_info,
784            }
785        })
786}
787
788/// Get the indices in `parquet_schema` of the specified columns in `requested_schema`. This returns
789/// a tuple of (mask_indices: Vec<parquet_schema_index>, reorder_indices:
790/// Vec<requested_index>). `mask_indices` is used for generating the mask for reading from the
791/// parquet file, and simply contains an entry for each index we wish to select from the parquet
792/// file set to the index of the requested column in the parquet. `reorder_indices` is used for
793/// re-ordering. See the documentation for [`ReorderIndex`] to understand what each element in the
794/// returned array means.
795#[internal_api]
796pub(crate) fn get_requested_indices(
797    requested_schema: &SchemaRef,
798    parquet_schema: &ArrowSchemaRef,
799) -> DeltaResult<(Vec<usize>, Vec<ReorderIndex>)> {
800    let mut mask_indices = vec![];
801    let (_, reorder_indexes) = get_indices(
802        0,
803        requested_schema,
804        parquet_schema.fields(),
805        &mut mask_indices,
806    )?;
807    Ok((mask_indices, reorder_indexes))
808}
809
810/// Create a mask that will only select the specified indices from the parquet. `indices` can be
811/// computed from a [`Schema`] using [`get_requested_indices`]
812#[internal_api]
813pub(crate) fn generate_mask(
814    _requested_schema: &SchemaRef,
815    _parquet_schema: &ArrowSchemaRef,
816    parquet_physical_schema: &SchemaDescriptor,
817    indices: &[usize],
818) -> Option<ProjectionMask> {
819    // TODO: Determine if it's worth checking if we're selecting everything and returning None in
820    // that case
821    Some(ProjectionMask::leaves(
822        parquet_physical_schema,
823        indices.to_owned(),
824    ))
825}
826
827/// Check if an ordering requires transforming the data in any way. This is true if the indices are
828/// NOT in ascending order (so we have to reorder things), or if we need to do any transformation on
829/// the data read from parquet. We check the ordering here, and also call
830/// `ReorderIndex::needs_transform` on each element to check for other transforms, and to check
831/// `Nested` variants recursively.
832fn ordering_needs_transform(requested_ordering: &[ReorderIndex]) -> bool {
833    if requested_ordering.is_empty() {
834        return false;
835    }
836    // we have >=1 element. check that the first element doesn't need a transform
837    if requested_ordering[0].needs_transform() {
838        return true;
839    }
840    // Check for all elements if we need a transform. This is true if any elements are not in order
841    // (i.e. element[i].index < element[i+1].index), or any element needs a transform
842    requested_ordering
843        .windows(2)
844        .any(|ri| (ri[0].index >= ri[1].index) || ri[1].needs_transform())
845}
846
847/// Check if an ordering requires row index computation.
848///
849/// The function only checks if a RowIndex transform is present at the top-level, since metadata
850/// columns are not allowed to be nested.
851#[internal_api]
852pub(crate) fn ordering_needs_row_indexes(requested_ordering: &[ReorderIndex]) -> bool {
853    requested_ordering
854        .iter()
855        .any(|reorder_index| matches!(&reorder_index.transform, ReorderIndexTransform::RowIndex(_)))
856}
857
858// we use this as a placeholder for an array and its associated field. We can fill in a Vec of None
859// of this type and then set elements of the Vec to Some(FieldArrayOpt) for each column
860type FieldArrayOpt = Option<(Arc<ArrowField>, Arc<dyn ArrowArray>)>;
861
862/// Creates an array for a missing field. For non-nullable structs, produces a non-null struct
863/// (no null buffer) with recursively missing children, preserving the non-null constraint at
864/// every level. For all other types (or nullable structs), produces an all-null array.
865fn new_missing_array(field: &ArrowField, num_rows: usize) -> Arc<dyn ArrowArray> {
866    match (field.is_nullable(), field.data_type()) {
867        (false, ArrowDataType::Struct(child_fields)) => {
868            let child_arrays: Vec<Arc<dyn ArrowArray>> = child_fields
869                .iter()
870                .map(|f| new_missing_array(f, num_rows))
871                .collect();
872            Arc::new(StructArray::new(child_fields.clone(), child_arrays, None))
873        }
874        _ => new_null_array(field.data_type(), num_rows),
875    }
876}
877
878/// Reorder a RecordBatch to match `requested_ordering`. For each non-zero value in
879/// `requested_ordering`, the column at that index will be added in order to the returned batch.
880///
881/// If the requested ordering contains a [`ReorderIndexTransform::RowIndex`], `row_indexes`
882/// must not be `None` to append a row index column to the output.
883/// If the requested ordering contains a [`ReorderIndexTransform::FilePath`], `file_location`
884/// must not be `None` to append a file path column to the output.
885pub(crate) fn reorder_struct_array(
886    input_data: StructArray,
887    requested_ordering: &[ReorderIndex],
888    mut row_indexes: Option<&mut FlattenedRangeIterator<i64>>,
889    file_location: Option<&str>,
890) -> DeltaResult<StructArray> {
891    debug!("Reordering {input_data:?} with ordering: {requested_ordering:?}");
892    if !ordering_needs_transform(requested_ordering) {
893        // indices is already sorted, meaning we requested in the order that the columns were
894        // stored in the parquet
895        Ok(input_data)
896    } else {
897        // requested an order different from the parquet, reorder
898        debug!("Have requested reorder {requested_ordering:#?} on {input_data:?}");
899        let num_rows = input_data.len();
900        let num_cols = requested_ordering.len();
901        let (input_fields, input_cols, null_buffer) = input_data.into_parts();
902        let mut final_fields_cols: Vec<FieldArrayOpt> = vec![None; num_cols];
903        for (parquet_position, reorder_index) in requested_ordering.iter().enumerate() {
904            // for each item, reorder_index.index() tells us where to put it, and its position in
905            // requested_ordering tells us where it is in the parquet data
906            match &reorder_index.transform {
907                ReorderIndexTransform::Cast(target) => {
908                    let col = input_cols[parquet_position].as_ref();
909                    let col = Arc::new(crate::arrow::compute::cast(col, target)?);
910                    let new_field = Arc::new(
911                        input_fields[parquet_position]
912                            .as_ref()
913                            .clone()
914                            .with_data_type(col.data_type().clone()),
915                    );
916                    final_fields_cols[reorder_index.index] = Some((new_field, col));
917                }
918                ReorderIndexTransform::Nested(children) => {
919                    let input_field_name = input_fields[parquet_position].name();
920                    match input_cols[parquet_position].data_type() {
921                        ArrowDataType::Struct(_) => {
922                            let struct_array = input_cols[parquet_position].as_struct().clone();
923                            let result_array = Arc::new(reorder_struct_array(
924                                struct_array,
925                                children,
926                                None, /* Nested structures don't need row indexes since metadata
927                                       * columns can't be nested */
928                                None, /* No file_location passed since metadata columns can't be
929                                       * nested */
930                            )?);
931                            // create the new field specifying the correct order for the struct
932                            let new_field = Arc::new(ArrowField::new_struct(
933                                input_field_name,
934                                result_array.fields().clone(),
935                                input_fields[parquet_position].is_nullable(),
936                            ));
937                            final_fields_cols[reorder_index.index] =
938                                Some((new_field, result_array));
939                        }
940                        ArrowDataType::List(_) => {
941                            let list_array = input_cols[parquet_position].as_list::<i32>().clone();
942                            final_fields_cols[reorder_index.index] =
943                                reorder_list(list_array, input_field_name, children)?;
944                        }
945                        ArrowDataType::LargeList(_) => {
946                            let list_array = input_cols[parquet_position].as_list::<i64>().clone();
947                            final_fields_cols[reorder_index.index] =
948                                reorder_list(list_array, input_field_name, children)?;
949                        }
950                        ArrowDataType::Map(_, _) => {
951                            let map_array = input_cols[parquet_position].as_map().clone();
952                            final_fields_cols[reorder_index.index] =
953                                reorder_map(map_array, input_field_name, children)?;
954                        }
955                        _ => {
956                            return Err(Error::internal_error(
957                                "Nested reorder can only apply to struct/list/map.",
958                            ));
959                        }
960                    }
961                }
962                ReorderIndexTransform::Identity => {
963                    final_fields_cols[reorder_index.index] = Some((
964                        input_fields[parquet_position].clone(), // cheap Arc clone
965                        input_cols[parquet_position].clone(),   // cheap Arc clone
966                    ));
967                }
968                ReorderIndexTransform::Missing(field) => {
969                    let array = new_missing_array(field, num_rows);
970                    final_fields_cols[reorder_index.index] = Some((field.clone(), array));
971                }
972                ReorderIndexTransform::RowIndex(field) => {
973                    let Some(ref mut row_index_iter) = row_indexes else {
974                        return Err(Error::generic(
975                            "Row index column requested but row index iterator not provided",
976                        ));
977                    };
978                    let row_index_array: PrimitiveArray<Int64Type> =
979                        row_index_iter.take(num_rows).collect();
980                    require!(
981                        row_index_array.len() == num_rows,
982                        Error::internal_error(
983                            "Row index iterator exhausted before reaching the end of the file"
984                        )
985                    );
986                    final_fields_cols[reorder_index.index] =
987                        Some((Arc::clone(field), Arc::new(row_index_array)));
988                }
989                ReorderIndexTransform::FilePath(field) => {
990                    let Some(file_path) = file_location else {
991                        return Err(Error::generic(
992                            "File path column requested but file location not provided",
993                        ));
994                    };
995                    let file_path_array = StringArray::from(vec![file_path; num_rows]);
996                    final_fields_cols[reorder_index.index] =
997                        Some((Arc::clone(field), Arc::new(file_path_array)));
998                }
999            }
1000        }
1001        let num_cols = final_fields_cols.len();
1002        let (field_vec, reordered_columns): (Vec<Arc<ArrowField>>, _) =
1003            final_fields_cols.into_iter().flatten().unzip();
1004        if field_vec.len() != num_cols {
1005            Err(Error::internal_error("Found a None in final_fields_cols."))
1006        } else {
1007            Ok(StructArray::try_new(
1008                field_vec.into(),
1009                reordered_columns,
1010                null_buffer,
1011            )?)
1012        }
1013    }
1014}
1015
1016fn reorder_list<O: OffsetSizeTrait>(
1017    list_array: GenericListArray<O>,
1018    input_field_name: &str,
1019    children: &[ReorderIndex],
1020) -> DeltaResult<FieldArrayOpt> {
1021    let (list_field, offset_buffer, maybe_sa, null_buf) = list_array.into_parts();
1022    if let Some(struct_array) = maybe_sa.as_struct_opt() {
1023        let struct_array = struct_array.clone();
1024        let result_array = Arc::new(reorder_struct_array(
1025            struct_array,
1026            children,
1027            None, /* Nested structures don't need row indexes since metadata columns can't be
1028                   * nested */
1029            None, // No file_location passed since metadata columns can't be nested
1030        )?);
1031        let new_list_field = Arc::new(ArrowField::new_struct(
1032            list_field.name(),
1033            result_array.fields().clone(),
1034            result_array.is_nullable(),
1035        ));
1036        let new_field = Arc::new(ArrowField::new_list(
1037            input_field_name,
1038            new_list_field.clone(),
1039            list_field.is_nullable(),
1040        ));
1041        let list = Arc::new(GenericListArray::try_new(
1042            new_list_field,
1043            offset_buffer,
1044            result_array,
1045            null_buf,
1046        )?);
1047        Ok(Some((new_field, list)))
1048    } else {
1049        Err(Error::internal_error(
1050            "Nested reorder of list should have had struct child.",
1051        ))
1052    }
1053}
1054
1055fn reorder_map(
1056    map_array: MapArray,
1057    input_field_name: &str,
1058    children: &[ReorderIndex],
1059) -> DeltaResult<FieldArrayOpt> {
1060    let (map_field, offset_buffer, struct_array, null_buf, ordered) = map_array.into_parts();
1061    let result_array = reorder_struct_array(
1062        struct_array,
1063        children,
1064        None, // Nested structures don't need row indexes since metadata columns can't be nested
1065        None, // No file_location passed since metadata columns can't be nested
1066    )?;
1067    let result_fields = result_array.fields();
1068    let new_map_field = Arc::new(ArrowField::new_struct(
1069        map_field.name(),
1070        result_fields.clone(),
1071        result_array.is_nullable(),
1072    ));
1073    let key_field = result_fields[0].clone();
1074    let val_field = result_fields[1].clone();
1075    let new_field = Arc::new(ArrowField::new_map(
1076        input_field_name,
1077        map_field.name(),
1078        key_field,
1079        val_field,
1080        ordered,
1081        map_field.is_nullable(),
1082    ));
1083    let map = Arc::new(MapArray::try_new(
1084        new_map_field,
1085        offset_buffer,
1086        result_array,
1087        null_buf,
1088        ordered,
1089    )?);
1090    Ok(Some((new_field, map)))
1091}
1092
1093/// Use this function to recursively compute properly unioned null masks for all nested
1094/// columns of a record batch, making it safe to project out and consume nested columns.
1095///
1096/// Arrow does not guarantee that the null masks associated with nested columns are accurate --
1097/// instead, the reader must consult the union of logical null masks the column and all
1098/// ancestors. The parquet reader stopped doing this automatically as of arrow-53.3, for example.
1099pub fn fix_nested_null_masks(batch: StructArray) -> StructArray {
1100    compute_nested_null_masks(batch, None)
1101}
1102
1103/// Splits a StructArray into its parts, unions in the parent null mask, and uses the result to
1104/// recursively update the children as well before putting everything back together.
1105fn compute_nested_null_masks(sa: StructArray, parent_nulls: Option<&NullBuffer>) -> StructArray {
1106    let (fields, columns, nulls) = sa.into_parts();
1107    let nulls = NullBuffer::union(parent_nulls, nulls.as_ref());
1108    let columns = columns
1109        .into_iter()
1110        .map(|column| match column.data_type() {
1111            // NullArray (void columns) does not accept a null buffer — all values are
1112            // already null by definition, so propagating the parent null mask is a no-op.
1113            ArrowDataType::Null => column,
1114            ArrowDataType::Struct(_) => {
1115                let sa = column.as_struct();
1116                Arc::new(compute_nested_null_masks(sa.clone(), nulls.as_ref())) as _
1117            }
1118            _ => {
1119                let data = column.to_data();
1120                let nulls = NullBuffer::union(nulls.as_ref(), data.nulls());
1121                let builder = data.into_builder().nulls(nulls);
1122                // Use an unchecked build to avoid paying a redundant O(k) validation cost for a
1123                // `RecordBatch` with k leaf columns.
1124                //
1125                // SAFETY: The builder was constructed from an `ArrayData` we extracted from the
1126                // column. The change we make is the null buffer, via `NullBuffer::union` with input
1127                // null buffers that were _also_ extracted from the column and its parent. A union
1128                // can only _grow_ the set of NULL rows, so data validity is preserved. Even if the
1129                // `parent_nulls` somehow had a length mismatch --- which it never should, having
1130                // also been extracted from our grandparent --- the mismatch would have already
1131                // caused `NullBuffer::union` to panic.
1132                let data = unsafe { builder.build_unchecked() };
1133                make_array(data)
1134            }
1135        })
1136        .collect();
1137
1138    // Use an unchecked constructor to avoid paying O(n*k) a redundant null buffer validation cost
1139    // for a `RecordBatch` with n rows and k leaf columns.
1140    //
1141    // SAFETY: We are simply reassembling the input `StructArray` we previously broke apart, with
1142    // updated null buffers. See above for details about null buffer safety.
1143    unsafe { StructArray::new_unchecked(fields, columns, nulls) }
1144}
1145
1146/// Parse a column of JSON strings into a typed `RecordBatch` matching `schema`. N input
1147/// rows produce N output rows.
1148///
1149/// Arrow lacks the functionality to json-parse a string column into a struct column, so we
1150/// implement it here.
1151///
1152/// Failure-prone primitive leaves (`Timestamp`, `TimestampNtz`, `Date`, `Decimal`) produce
1153/// per-cell NULL when the typed decoder rejects a value (extended-year timestamps,
1154/// decimals that overflow the declared precision, etc.). Other leaf type mismatches still
1155/// surface as batch-level errors.
1156#[internal_api]
1157pub(crate) fn parse_json(
1158    json_strings: Box<dyn EngineData>,
1159    schema: SchemaRef,
1160) -> DeltaResult<Box<dyn EngineData>> {
1161    let json_strings: RecordBatch = ArrowEngineData::try_from_engine_data(json_strings)?.into();
1162    let result = parse_json_impl(json_strings.column(0).as_ref(), schema)?;
1163    Ok(Box::new(ArrowEngineData::new(result)))
1164}
1165
1166/// Raw implementation of [`parse_json`]; see there for the per-cell NULL contract.
1167///
1168/// Accepts any string array type (`StringArray`, `LargeStringArray`, `StringViewArray`) to
1169/// avoid narrowing casts that could overflow.
1170pub(crate) fn parse_json_impl(
1171    json_strings: &dyn ArrowArray,
1172    schema: SchemaRef,
1173) -> DeltaResult<RecordBatch> {
1174    let num_rows = json_strings.len();
1175    match json_strings.data_type() {
1176        ArrowDataType::Utf8 => {
1177            parse_json_inner(json_strings.as_string::<i32>().iter(), num_rows, schema)
1178        }
1179        ArrowDataType::LargeUtf8 => {
1180            parse_json_inner(json_strings.as_string::<i64>().iter(), num_rows, schema)
1181        }
1182        ArrowDataType::Utf8View => {
1183            parse_json_inner(json_strings.as_string_view().iter(), num_rows, schema)
1184        }
1185        dt => Err(Error::generic(format!(
1186            "Expected string array for JSON parsing, got {dt}"
1187        ))),
1188    }
1189}
1190
1191fn parse_json_inner<'a>(
1192    json_strings: impl Iterator<Item = Option<&'a str>>,
1193    num_rows: usize,
1194    schema: SchemaRef,
1195) -> DeltaResult<RecordBatch> {
1196    // arrow-json's typed Timestamp/TimestampNtz/Date/Decimal decoders fail the entire batch
1197    // on a single bad cell, so rewrite those leaves to `String` first and safe-cast back to
1198    // the target type. `Cow::Borrowed` means nothing was rewritten; skip the cast pass.
1199    match StringifyFailureProneLeaves.transform_struct(schema.as_ref()) {
1200        Cow::Borrowed(_) => {
1201            let arrow_target = Arc::new(ArrowSchema::try_from_kernel(schema.as_ref())?);
1202            decode_with_arrow_json(json_strings, num_rows, arrow_target)
1203        }
1204        Cow::Owned(relaxed) => {
1205            let arrow_target = Arc::new(ArrowSchema::try_from_kernel(schema.as_ref())?);
1206            let arrow_relaxed = Arc::new(ArrowSchema::try_from_kernel(&relaxed)?);
1207            let decoded = decode_with_arrow_json(json_strings, num_rows, arrow_relaxed)?;
1208            safe_cast_back(decoded, &arrow_target)
1209        }
1210    }
1211}
1212
1213/// Runs arrow-json's typed `Decoder` against the given schema and returns the resulting
1214/// `RecordBatch`. Each input string must contain exactly one JSON object; missing inputs
1215/// (`None`) decode as `{}` so the row stays present with all-NULL fields.
1216fn decode_with_arrow_json<'a>(
1217    json_strings: impl Iterator<Item = Option<&'a str>>,
1218    num_rows: usize,
1219    schema: ArrowSchemaRef,
1220) -> DeltaResult<RecordBatch> {
1221    if num_rows == 0 {
1222        return Ok(RecordBatch::new_empty(schema));
1223    }
1224
1225    let mut decoder = ReaderBuilder::new(schema)
1226        .with_batch_size(num_rows)
1227        .with_coerce_primitive(true)
1228        .build_decoder()?;
1229
1230    for (json, row_number) in json_strings.zip(1..) {
1231        let line = json.unwrap_or("{}");
1232        let consumed = decoder.decode(line.as_bytes())?;
1233        // did we fail to decode the whole line, or was the line partial
1234        if consumed != line.len() || decoder.has_partial_record() {
1235            return Err(Error::Generic(format!(
1236                "Malformed JSON: Multiple, partial, or 0 JSON objects on row {row_number}"
1237            )));
1238        }
1239        // did we decode exactly one record
1240        if decoder.len() != row_number {
1241            return Err(Error::Generic(format!(
1242                "Malformed JSON: Multiple, partial, or 0 JSON objects on row {row_number}"
1243            )));
1244        }
1245    }
1246    // Get the final batch out
1247    if let Some(batch) = decoder.flush()? {
1248        if batch.num_rows() != num_rows {
1249            return Err(Error::Generic(format!(
1250                "Unexpected number of rows decoded. Got {}, expected{}",
1251                batch.num_rows(),
1252                num_rows
1253            )));
1254        }
1255        return Ok(batch);
1256    }
1257    Err(Error::generic(
1258        "Malformed JSON: exited parse_json_impl without deserializing anything useful",
1259    ))
1260}
1261
1262/// Rewrites failure-prone primitives (`Timestamp`, `TimestampNtz`, `Date`, `Decimal`) to
1263/// `String` so the typed decoder accepts any well-formed JSON string for those cells.
1264///
1265/// `Array`/`Map`/`Variant` are not visited: Delta doesn't track min/max stats for them,
1266/// so a failure-prone leaf only ever shows up inside a `Struct` for our callers.
1267struct StringifyFailureProneLeaves;
1268
1269impl<'a> SchemaTransform<'a> for StringifyFailureProneLeaves {
1270    transform_output_type!(|'a, T| Cow<'a, T>);
1271
1272    fn transform_primitive(&mut self, ptype: &'a PrimitiveType) -> Cow<'a, PrimitiveType> {
1273        use PrimitiveType::*;
1274        match ptype {
1275            Timestamp | TimestampNtz | Date | Decimal(_) => Cow::Owned(String),
1276            _ => Cow::Borrowed(ptype),
1277        }
1278    }
1279
1280    fn transform_array(&mut self, atype: &'a ArrayType) -> Cow<'a, ArrayType> {
1281        Cow::Borrowed(atype)
1282    }
1283
1284    fn transform_map(&mut self, mtype: &'a MapType) -> Cow<'a, MapType> {
1285        Cow::Borrowed(mtype)
1286    }
1287
1288    fn transform_variant(&mut self, stype: &'a StructType) -> Cow<'a, StructType> {
1289        Cow::Borrowed(stype)
1290    }
1291}
1292
1293/// Safe-casts each column of `decoded` back to its target type. `safe: true` produces
1294/// per-cell NULL on parse failure rather than failing the whole batch.
1295fn safe_cast_back(decoded: RecordBatch, target: &ArrowSchemaRef) -> DeltaResult<RecordBatch> {
1296    let opts = CastOptions {
1297        safe: true,
1298        ..Default::default()
1299    };
1300    let (_, columns, row_count) = decoded.into_parts();
1301    let columns = columns
1302        .into_iter()
1303        .zip(target.fields().iter())
1304        .map(|(arr, field)| cast_array_to_type(arr, field.data_type(), &opts))
1305        .collect::<DeltaResult<Vec<_>>>()?;
1306    Ok(RecordBatch::try_new_with_options(
1307        target.clone(),
1308        columns,
1309        &RecordBatchOptions::new().with_row_count(Some(row_count)),
1310    )?)
1311}
1312
1313/// Casts each column to the type of the `target` field at the same position, so the columns can be
1314/// assembled into a [`RecordBatch`] with `target` as its schema.
1315///
1316/// A map's inner entry field and an array's inner element field are named by whoever wrote the
1317/// file, so they can disagree with what kernel expects:
1318///
1319/// | Container | Kernel expects | Some writers emit |
1320/// |-----------|----------------|-------------------|
1321/// | map       | `key_value`    | `entries`         |
1322/// | array     | `element`      | `item`            |
1323///
1324/// Kernel's two names are defined as [`MAP_ROOT_DEFAULT`] (`key_value`) and [`LIST_ARRAY_ROOT`]
1325/// (`element`), and [`arrow_conversion`] applies them whenever it converts a kernel schema to an
1326/// Arrow schema.
1327///
1328/// Arrow counts those names as part of the type, so a column whose names differ is unequal to
1329/// `target` and [`RecordBatch::try_new`] rejects it. Casting to `target` rebuilds the container
1330/// under `target`'s names.
1331///
1332/// This is a general cast, not a rename: it also converts primitive types (`Int32` to `Int64`) and
1333/// renames struct fields. Columns whose type already equals `target` pass through untouched.
1334///
1335/// # Errors
1336///
1337/// Casts strictly, so a leaf whose type cannot convert (`Utf8` to `Date32` on a non-date string)
1338/// errors rather than nulling the offending cells.
1339///
1340/// [`MAP_ROOT_DEFAULT`]: crate::engine::arrow_conversion::MAP_ROOT_DEFAULT
1341/// [`LIST_ARRAY_ROOT`]: crate::engine::arrow_conversion::LIST_ARRAY_ROOT
1342/// [`arrow_conversion`]: crate::engine::arrow_conversion
1343#[cfg(test)]
1344pub(crate) fn coerce_columns_to_schema(
1345    columns: Vec<ArrowArrayRef>,
1346    target: &ArrowSchemaRef,
1347) -> DeltaResult<Vec<ArrowArrayRef>> {
1348    let opts = CastOptions {
1349        safe: false,
1350        ..Default::default()
1351    };
1352    columns
1353        .into_iter()
1354        .zip(target.fields().iter())
1355        .map(|(arr, field)| cast_array_to_type(arr, field.data_type(), &opts))
1356        .collect()
1357}
1358
1359/// Casts one Arrow [`ArrowArray`] of any type to `target`.
1360///
1361/// A struct tracks which of its rows are null separately from its children, so casting a child
1362/// means rebuilding the struct around it. This recurses into `Struct` by hand, carrying that
1363/// row-level null information onto the rebuilt struct: given a struct column whose row 0 is null,
1364/// row 0 is still null after the cast. Everything else, `Map` and `List` included, goes to
1365/// [`cast_with_options`], which rebuilds the container using the field names in `target`.
1366///
1367/// `opts` decides what a failed leaf cast does: `safe: true` nulls the cell, strict errors.
1368fn cast_array_to_type(
1369    array: ArrowArrayRef,
1370    target: &ArrowDataType,
1371    opts: &CastOptions<'_>,
1372) -> DeltaResult<ArrowArrayRef> {
1373    if array.data_type() == target {
1374        return Ok(array);
1375    }
1376    match target {
1377        ArrowDataType::Struct(target_fields) => {
1378            let s = array.as_struct_opt().ok_or_else(|| {
1379                Error::generic(format!(
1380                    "cannot cast {} to a struct target",
1381                    array.data_type()
1382                ))
1383            })?;
1384            let nulls = s.nulls().cloned();
1385            require!(
1386                s.columns().len() == target_fields.len(),
1387                Error::generic(format!(
1388                    "cannot cast struct with {} children to target with {} fields",
1389                    s.columns().len(),
1390                    target_fields.len()
1391                ))
1392            );
1393            let new_children = s
1394                .columns()
1395                .iter()
1396                .zip(target_fields.iter())
1397                .map(|(c, f)| cast_array_to_type(c.clone(), f.data_type(), opts))
1398                .collect::<DeltaResult<Vec<_>>>()?;
1399            Ok(Arc::new(StructArray::try_new(
1400                target_fields.clone(),
1401                new_children,
1402                nulls,
1403            )?))
1404        }
1405        _ => Ok(cast_with_options(&array, target, opts)?),
1406    }
1407}
1408
1409pub(crate) fn filter_to_record_batch(
1410    filtered_data: FilteredEngineData,
1411) -> DeltaResult<RecordBatch> {
1412    let filtered = filtered_data.apply_selection_vector()?;
1413    let arrow_data = ArrowEngineData::try_from_engine_data(filtered)?;
1414    Ok((*arrow_data).into())
1415}
1416
1417// we want to keep nulls in our partition map, so we end up with data in the log like:
1418// {partitionValues:{"foo": null}}, which is what is generally expected. Without this we would
1419// get: {partitionValues:{}}
1420struct NullValueMapEncoder<'a> {
1421    field: &'a ArrowFieldRef,
1422    array: &'a MapArray,
1423}
1424
1425impl<'a> Encoder for NullValueMapEncoder<'a> {
1426    fn encode(&mut self, idx: usize, out: &mut Vec<u8>) {
1427        let options = EncoderOptions::default().with_explicit_nulls(true);
1428        // this unwrap is technically unsafe, but we _know_ that the array is a MapArray, and that
1429        // `make_encoder` won't return an error for that. It would still be nice if we could return
1430        // a `Result`, but we cannot
1431        #[allow(clippy::unwrap_used)]
1432        let mut encoder = make_encoder(self.field, self.array, &options).unwrap();
1433        encoder.encode(idx, out);
1434    }
1435}
1436
1437/// This is a special encoder factory that will use the default encoder for all array types except
1438/// MapArrays. For MapArrays, it will make a `NullValueMapEncoder` which encodes the map preserving
1439/// keys that have null values.
1440#[derive(Debug)]
1441struct NullValueMapEncoderFactory;
1442
1443impl EncoderFactory for NullValueMapEncoderFactory {
1444    fn make_default_encoder<'a>(
1445        &self,
1446        field: &'a ArrowFieldRef,
1447        array: &'a dyn ArrowArray,
1448        _options: &'a EncoderOptions,
1449    ) -> Result<Option<NullableEncoder<'a>>, crate::arrow::error::ArrowError> {
1450        // It would be tempting to use `make_encoder` below, but we can't because we have to create
1451        // a new `EncoderOptions` in order to set `with_explicit_nulls`. Then the lifetime of the
1452        // created encoder becomes tied to the lifetime of the `EncoderOptions`, and we cannot
1453        // return it from this method as the options would be freed here.  We _also_ can't put the
1454        // options inside the NullValueMapEncoderFactory, because this method takes `&self` not
1455        // `&'a self`, and we can't change that as it's part of the trait definition.
1456        match array.data_type() {
1457            ArrowDataType::Map(_, _) => {
1458                let array = array.as_map();
1459                let encoder = NullValueMapEncoder { field, array };
1460                let array_encoder = Box::new(encoder) as Box<dyn Encoder + 'a>;
1461                let nulls = array.nulls().cloned();
1462                Ok(Some(NullableEncoder::new(array_encoder, nulls)))
1463            }
1464            _ => Ok(None),
1465        }
1466    }
1467}
1468
1469/// serialize an arrow RecordBatch to a JSON string by appending to a buffer.
1470// TODO (zach): this should stream data to the JSON writer and output an iterator.
1471#[internal_api]
1472pub(crate) fn to_json_bytes(
1473    data: impl Iterator<Item = DeltaResult<FilteredEngineData>> + Send,
1474) -> DeltaResult<Vec<u8>> {
1475    let builder = WriterBuilder::new().with_encoder_factory(Arc::new(NullValueMapEncoderFactory));
1476    let mut writer = builder.build::<_, LineDelimited>(Vec::new());
1477    for chunk in data {
1478        let batch = filter_to_record_batch(chunk?)?;
1479        writer.write(&batch)?;
1480    }
1481    writer.finish()?;
1482    Ok(writer.into_inner())
1483}
1484
1485/// Applies post-processing to data read from a JSON file. Inserts synthesized metadata columns
1486/// (e.g. [`MetadataColumnSpec::FilePath`]) at the positions specified by `reorder_indices`.
1487///
1488/// `reorder_indices` should be built once per schema via [`build_json_reorder_indices`] and
1489/// reused for every batch from the same file.
1490#[internal_api]
1491pub(crate) fn fixup_json_read(
1492    batch: RecordBatch,
1493    reorder_indices: &[ReorderIndex],
1494    file_location: &str,
1495) -> DeltaResult<ArrowEngineData> {
1496    let data = reorder_struct_array(batch.into(), reorder_indices, None, Some(file_location))?;
1497    Ok(data.into())
1498}
1499
1500/// Builds the [`ReorderIndex`] vec for post-processing JSON read batches.
1501///
1502/// The JSON reader is given a schema with metadata columns stripped (see [`json_arrow_schema`]).
1503/// Its output therefore has non-metadata columns at contiguous indices 0..N in schema order.
1504/// This function maps those source indices -- and any metadata column specs -- into a
1505/// `Vec<ReorderIndex>` that `reorder_struct_array` can use to produce the final batch with
1506/// every column at its correct position.
1507///
1508/// Build the index vec once per schema (e.g. once per file); apply it to every batch produced
1509/// by the reader via `reorder_struct_array`.
1510///
1511/// # Companion function
1512/// - Use [`json_arrow_schema`] to strip metadata columns before passing the schema to the JSON
1513///   reader.
1514#[internal_api]
1515pub(crate) fn build_json_reorder_indices(schema: &StructType) -> DeltaResult<Vec<ReorderIndex>> {
1516    // Real columns: position in reorder_indices IS the source column index (0..N in schema
1517    // order), and reorder_index.index carries the output position.
1518    let mut reorder_indices = Vec::with_capacity(schema.num_fields());
1519    // Metadata columns are appended after all real columns. reorder_struct_array never reads
1520    // source data for metadata transforms, so their vec position doesn't correspond to a source
1521    // column. Unsupported specs use Missing (null fill); non-nullable violations surface
1522    // naturally via StructArray::try_new.
1523    let mut metadata_entries = Vec::new();
1524
1525    for (output_pos, field) in schema.fields().enumerate() {
1526        match field.get_metadata_column_spec() {
1527            None => reorder_indices.push(ReorderIndex::identity(output_pos)),
1528            Some(spec) => metadata_entries.push((output_pos, field, spec)),
1529        }
1530    }
1531
1532    for (output_pos, field, spec) in metadata_entries {
1533        let field = Arc::new(field.try_into_arrow()?);
1534        let rindex = match spec {
1535            MetadataColumnSpec::FilePath => ReorderIndex::file_path(output_pos, field),
1536            _ => ReorderIndex::missing(output_pos, field),
1537        };
1538        reorder_indices.push(rindex);
1539    }
1540
1541    Ok(reorder_indices)
1542}
1543
1544/// Builds an Arrow [`ArrowSchema`] from `schema` containing only the "real" JSON columns,
1545/// omitting any fields annotated with [`MetadataColumnSpec`].
1546///
1547/// Pass the returned schema to Arrow's JSON reader; then call [`build_json_reorder_indices`]
1548/// once on the same schema and apply `reorder_struct_array` to each resulting batch to
1549/// insert the synthesized metadata columns at their correct positions.
1550#[internal_api]
1551pub(crate) fn json_arrow_schema(schema: &StructType) -> DeltaResult<ArrowSchema> {
1552    let json_fields = schema.with_fields_filtered(|f| f.get_metadata_column_spec().is_none())?;
1553    Ok(ArrowSchema::try_from_kernel(&json_fields)?)
1554}
1555
1556#[cfg(test)]
1557mod tests {
1558    use std::sync::Arc;
1559
1560    use rstest::rstest;
1561
1562    use super::*;
1563    use crate::arrow::array::{
1564        Array, ArrayRef as ArrowArrayRef, AsArray, BooleanArray, GenericListArray, Int32Array,
1565        Int32Builder, Int64Array, ListArray, MapArray, MapBuilder, MapFieldNames, StringArray,
1566        StringBuilder, StructArray, StructBuilder,
1567    };
1568    use crate::arrow::buffer::{OffsetBuffer, ScalarBuffer};
1569    use crate::arrow::datatypes::{
1570        DataType as ArrowDataType, Field as ArrowField, Fields as ArrowFields, Int32Type,
1571        Schema as ArrowSchema, SchemaRef as ArrowSchemaRef,
1572    };
1573    use crate::engine::arrow_conversion::TryIntoArrow;
1574    use crate::schema::{
1575        schema, schema_ref, ArrayType, ColumnMetadataKey, DataType, MapType, MetadataColumnSpec,
1576        MetadataValue, StructField, StructType,
1577    };
1578    use crate::table_features::ColumnMappingMode;
1579    use crate::unit_test_utils::assert_result_error_with_message;
1580
1581    fn column_mapping_cases() -> [ColumnMappingMode; 3] {
1582        [
1583            ColumnMappingMode::Id,
1584            ColumnMappingMode::Name,
1585            ColumnMappingMode::None,
1586        ]
1587    }
1588
1589    /// Generates the logical name for a field given its id.
1590    /// This is "logical-{fieldId}".
1591    fn logical_name(field_id: i64) -> String {
1592        format!("logical-{field_id}")
1593    }
1594
1595    /// Generates the physical name for a field given its id.
1596    /// This is "physical-{fieldId}".
1597    fn physical_name(field_id: i64) -> String {
1598        format!("physical-{field_id}")
1599    }
1600
1601    /// Generates the name that should be written to parquet from the field id.
1602    /// This is the physical name for Id/Name modes, and logical name for None mode.
1603    fn parquet_name(field_id: i64, mode: ColumnMappingMode) -> String {
1604        match mode {
1605            ColumnMappingMode::Id | ColumnMappingMode::Name => physical_name(field_id),
1606            ColumnMappingMode::None => logical_name(field_id),
1607        }
1608    }
1609
1610    /// Generates the column mapping metadata for a logical struct field given the field id.
1611    /// Returns empty metadata for `None` mode, since no annotations should be present.
1612    fn column_mapping_metadata(
1613        field_id: i64,
1614        mode: ColumnMappingMode,
1615    ) -> HashMap<String, MetadataValue> {
1616        match mode {
1617            ColumnMappingMode::None => HashMap::new(),
1618            _ => kernel_fid_and_name(field_id, physical_name(field_id)),
1619        }
1620    }
1621
1622    /// Generates metadata for a parquet field with id `field_id`.
1623    fn arrow_fid(field_id: i64) -> HashMap<String, String> {
1624        HashMap::from([(PARQUET_FIELD_ID_META_KEY.to_string(), field_id.to_string())])
1625    }
1626
1627    /// Generates appropriate column mapping metadata for a kernel struct field with column mapping
1628    /// id `field_id`.
1629    fn kernel_fid_and_name(field_id: i64, name: impl AsRef<str>) -> HashMap<String, MetadataValue> {
1630        HashMap::from([
1631            (
1632                ColumnMetadataKey::ColumnMappingId.as_ref().to_string(),
1633                field_id.into(),
1634            ),
1635            (
1636                ColumnMetadataKey::ColumnMappingPhysicalName
1637                    .as_ref()
1638                    .to_string(),
1639                name.as_ref().to_string().into(),
1640            ),
1641        ])
1642    }
1643
1644    /// Helper function to create mock row group metadata for testing
1645    fn create_mock_row_group(num_rows: i64) -> RowGroupMetaData {
1646        use crate::parquet::basic::{Encoding, Type as PhysicalType};
1647        use crate::parquet::file::metadata::ColumnChunkMetaData;
1648        use crate::parquet::schema::types::Type;
1649
1650        // Create a minimal schema descriptor
1651        let schema = Arc::new(SchemaDescriptor::new(Arc::new(
1652            Type::group_type_builder("schema")
1653                .with_fields(vec![Arc::new(
1654                    Type::primitive_type_builder("test_col", PhysicalType::INT32)
1655                        .build()
1656                        .unwrap(),
1657                )])
1658                .build()
1659                .unwrap(),
1660        )));
1661
1662        // Create a minimal column chunk metadata
1663        let column_chunk = ColumnChunkMetaData::builder(schema.column(0))
1664            .set_encodings(vec![Encoding::PLAIN])
1665            .set_total_compressed_size(100)
1666            .set_total_uncompressed_size(100)
1667            .set_num_values(num_rows)
1668            .build()
1669            .unwrap();
1670
1671        RowGroupMetaData::builder(schema)
1672            .set_num_rows(num_rows)
1673            .set_total_byte_size(100)
1674            .set_column_metadata(vec![column_chunk])
1675            .build()
1676            .unwrap()
1677    }
1678
1679    #[test]
1680    fn test_json_parsing() {
1681        static EXPECTED_JSON_ERR_STR: &str = "Generic delta kernel error: Malformed JSON: Multiple, partial, or 0 JSON objects on row";
1682        fn check_parse_fails(input: Vec<Option<&str>>, schema: SchemaRef, expected_start: &str) {
1683            let result = parse_json_impl(&StringArray::from(input), schema);
1684            let err = result.expect_err("Expected an error");
1685            let msg = err.to_string();
1686            assert!(
1687                msg.starts_with(expected_start),
1688                "Error message was not what was expected"
1689            );
1690        }
1691
1692        let requested_schema = Arc::new(
1693            StructType::try_new(vec![
1694                StructField::nullable("a", DataType::INTEGER),
1695                StructField::nullable("b", DataType::STRING),
1696                StructField::nullable("c", DataType::INTEGER),
1697            ])
1698            .unwrap(),
1699        );
1700        let input: Vec<&str> = vec![];
1701        let result = parse_json_impl(&StringArray::from(input), requested_schema.clone()).unwrap();
1702        assert_eq!(result.num_rows(), 0);
1703
1704        for input in [
1705            vec![Some("")],
1706            vec![Some(" \n\t")],
1707            vec![Some(r#"{ "a": 1"#)],
1708            vec![Some("{}{}")],
1709            vec![Some(r#"{} { "a": 1"#)],
1710            vec![Some(r#"{} { "a": 1"#), Some("}")],
1711            vec![Some(r#"{ "a": 1"#), Some(r#", "b": "b"}"#)],
1712        ] {
1713            check_parse_fails(input, requested_schema.clone(), EXPECTED_JSON_ERR_STR);
1714        }
1715
1716        // this one is an error from within the tape decoder, so has a different format
1717        check_parse_fails(
1718            vec![Some(r#""a""#)],
1719            requested_schema.clone(),
1720            "Json error: expected { got \"a\"",
1721        );
1722
1723        let input: Vec<Option<&str>> = vec![None, Some(r#"{"a": 1, "b": "2", "c": 3}"#), None];
1724        let result = parse_json_impl(&StringArray::from(input), requested_schema).unwrap();
1725        assert_eq!(result.num_rows(), 3);
1726        assert_eq!(result.column(0).null_count(), 2);
1727        assert_eq!(result.column(1).null_count(), 2);
1728        assert_eq!(result.column(2).null_count(), 2);
1729    }
1730
1731    #[test]
1732    fn test_parse_json_with_long_strings() {
1733        // See issue#1139: https://github.com/delta-io/delta-kernel-rs/issues/1139
1734        let schema = Arc::new(
1735            StructType::try_new(vec![StructField::nullable("long_val", DataType::STRING)]).unwrap(),
1736        );
1737        let long_string = "a".repeat(1_000_000); // 1MB string
1738        let json_string = format!(r#"{{"long_val": "{long_string}"}}"#);
1739        let input: Vec<Option<&str>> = vec![Some(&json_string)];
1740
1741        let batch = parse_json_impl(&StringArray::from(input), schema).unwrap();
1742        assert_eq!(batch.num_rows(), 1);
1743        let long_col = batch.column(0).as_string::<i32>();
1744        assert_eq!(long_col.value(0), long_string);
1745    }
1746
1747    #[test]
1748    fn test_parse_json_large_string_array() {
1749        // See issue#1923: parse_json should handle LargeStringArray (64-bit offsets)
1750        use crate::arrow::array::LargeStringArray;
1751        use crate::engine::arrow_data::ArrowEngineData;
1752
1753        let large_strings = LargeStringArray::from(vec![
1754            Some(r#"{"a": 1, "b": "hello"}"#),
1755            None,
1756            Some(r#"{"a": 3, "b": "world"}"#),
1757        ]);
1758        let field = Arc::new(ArrowField::new("s", ArrowDataType::LargeUtf8, true));
1759        let schema = Arc::new(ArrowSchema::new(vec![field]));
1760        let batch =
1761            RecordBatch::try_new(schema, vec![Arc::new(large_strings) as ArrowArrayRef]).unwrap();
1762        let engine_data: Box<dyn crate::EngineData> = Box::new(ArrowEngineData::new(batch));
1763
1764        let output_schema: crate::schema::SchemaRef = Arc::new(StructType::new_unchecked(vec![
1765            StructField::nullable("a", DataType::INTEGER),
1766            StructField::nullable("b", DataType::STRING),
1767        ]));
1768        let result = parse_json(engine_data, output_schema).unwrap();
1769        let result = ArrowEngineData::try_from_engine_data(result).unwrap();
1770        let batch: RecordBatch = result.into();
1771        assert_eq!(batch.num_rows(), 3);
1772        assert_eq!(batch.column(0).null_count(), 1);
1773        assert_eq!(batch.column(1).null_count(), 1);
1774    }
1775
1776    #[test]
1777    fn test_parse_json_string_view_array() {
1778        use crate::arrow::array::StringViewArray;
1779        use crate::engine::arrow_data::ArrowEngineData;
1780
1781        let view_strings = StringViewArray::from(vec![
1782            Some(r#"{"a": 1, "b": "hello"}"#),
1783            None,
1784            Some(r#"{"a": 3, "b": "world"}"#),
1785        ]);
1786        let field = Arc::new(ArrowField::new("s", ArrowDataType::Utf8View, true));
1787        let schema = Arc::new(ArrowSchema::new(vec![field]));
1788        let batch =
1789            RecordBatch::try_new(schema, vec![Arc::new(view_strings) as ArrowArrayRef]).unwrap();
1790        let engine_data: Box<dyn crate::EngineData> = Box::new(ArrowEngineData::new(batch));
1791
1792        let output_schema: crate::schema::SchemaRef = Arc::new(StructType::new_unchecked(vec![
1793            StructField::nullable("a", DataType::INTEGER),
1794            StructField::nullable("b", DataType::STRING),
1795        ]));
1796        let result = parse_json(engine_data, output_schema).unwrap();
1797        let result = ArrowEngineData::try_from_engine_data(result).unwrap();
1798        let batch: RecordBatch = result.into();
1799        assert_eq!(batch.num_rows(), 3);
1800        assert_eq!(batch.column(0).null_count(), 1);
1801        assert_eq!(batch.column(1).null_count(), 1);
1802    }
1803
1804    #[test]
1805    fn test_parse_json_rejects_non_string_array() {
1806        use crate::engine::arrow_data::ArrowEngineData;
1807
1808        let int_array = Int32Array::from(vec![1, 2, 3]);
1809        let field = Arc::new(ArrowField::new("s", ArrowDataType::Int32, true));
1810        let schema = Arc::new(ArrowSchema::new(vec![field]));
1811        let batch =
1812            RecordBatch::try_new(schema, vec![Arc::new(int_array) as ArrowArrayRef]).unwrap();
1813        let engine_data: Box<dyn crate::EngineData> = Box::new(ArrowEngineData::new(batch));
1814
1815        let output_schema: crate::schema::SchemaRef = schema_ref! { nullable "a": INTEGER };
1816        let err = match parse_json(engine_data, output_schema) {
1817            Err(e) => e.to_string(),
1818            Ok(_) => panic!("Expected error for non-string array input"),
1819        };
1820        assert!(
1821            err.contains("Expected string array for JSON parsing"),
1822            "Unexpected error: {err}"
1823        );
1824    }
1825
1826    #[test]
1827    fn test_parse_json_impl_strict_leaf_errors_propagate() {
1828        // Type mismatches on strict (non-failure-prone) leaves still surface as batch-level
1829        // errors, so the expression-level caller can fall back to its all-null backstop.
1830        let schema = Arc::new(
1831            StructType::try_new(vec![StructField::nullable("a", DataType::LONG)]).unwrap(),
1832        );
1833        let input: Vec<Option<&str>> = vec![Some(r#"{"a": "not_a_number"}"#)];
1834        assert!(parse_json_impl(&StringArray::from(input), schema).is_err());
1835    }
1836
1837    // === Per-cell NULL on failure-prone leaf parse failures ===
1838
1839    /// Parses `inputs` against a single-column schema `{column_name: leaf_type}` and asserts
1840    /// that rows in `expected_null_rows` are NULL while every other row is non-null.
1841    fn assert_per_cell_null_isolation(
1842        column_name: &str,
1843        leaf_type: DataType,
1844        inputs: &[&str],
1845        expected_null_rows: &[usize],
1846    ) {
1847        let schema = Arc::new(
1848            StructType::try_new(vec![StructField::nullable(column_name, leaf_type)]).unwrap(),
1849        );
1850        let inputs: Vec<Option<&str>> = inputs.iter().copied().map(Some).collect();
1851        let batch = parse_json_impl(&StringArray::from(inputs.clone()), schema)
1852            .expect("parse_json_impl should not error on failure-prone leaf parse failures");
1853        assert_eq!(batch.num_rows(), inputs.len());
1854        let col = batch.column(0);
1855        assert_eq!(
1856            col.null_count(),
1857            expected_null_rows.len(),
1858            "unexpected null count for column {column_name}",
1859        );
1860        for row in 0..inputs.len() {
1861            let want_null = expected_null_rows.contains(&row);
1862            assert_eq!(
1863                col.is_null(row),
1864                want_null,
1865                "row {row} of column {column_name}: expected is_null={want_null}",
1866            );
1867        }
1868    }
1869
1870    /// Per-cell NULL isolation across the four failure-prone leaf types. Each case feeds
1871    /// a 3-row single-column batch where row 1 is malformed in a way that defeats
1872    /// arrow-json's typed decoder; rows 0 and 2 must round-trip to valid typed cells.
1873    #[rstest]
1874    #[case::timestamp_extended_year(
1875        DataType::TIMESTAMP,
1876        &[
1877            r#"{"v": "2024-01-01T00:00:00Z"}"#,
1878            r#"{"v": "+48690-07-02T22:50:38.211Z"}"#,
1879            r#"{"v": "2025-06-01T00:00:00Z"}"#,
1880        ],
1881    )]
1882    #[case::timestamp_ntz_garbage(
1883        DataType::TIMESTAMP_NTZ,
1884        &[
1885            r#"{"v": "2024-01-01T00:00:00"}"#,
1886            r#"{"v": "not-a-timestamp"}"#,
1887            r#"{"v": "2025-06-01T00:00:00"}"#,
1888        ],
1889    )]
1890    #[case::date_out_of_range_month(
1891        DataType::DATE,
1892        &[
1893            r#"{"v": "2024-01-01"}"#,
1894            r#"{"v": "2024-13-01"}"#,
1895            r#"{"v": "2025-06-30"}"#,
1896        ],
1897    )]
1898    #[case::decimal_overflow(
1899        DataType::decimal(10, 2).unwrap(),
1900        &[
1901            r#"{"v": "10.50"}"#,
1902            r#"{"v": "99999999999.99"}"#,
1903            r#"{"v": "5.25"}"#,
1904        ],
1905    )]
1906    fn test_parse_json_safe_cast_per_cell_null(
1907        #[case] leaf_type: DataType,
1908        #[case] inputs: &[&str],
1909    ) {
1910        assert_per_cell_null_isolation("v", leaf_type, inputs, &[1]);
1911    }
1912
1913    #[test]
1914    fn test_parse_json_safe_cast_intermixed_struct_per_cell_isolation() {
1915        // Single struct with mixed failure-prone and strict leaves. The bad EventTime on row 1
1916        // must not contaminate IngestTime / Price / UserId on the same row, nor any field on
1917        // rows 0 / 2. This is the per-cell isolation property end-to-end.
1918        let schema = Arc::new(
1919            StructType::try_new(vec![
1920                StructField::nullable("EventTime", DataType::TIMESTAMP),
1921                StructField::nullable("IngestTime", DataType::TIMESTAMP),
1922                StructField::nullable("Price", DataType::decimal(10, 2).unwrap()),
1923                StructField::nullable("UserId", DataType::LONG),
1924            ])
1925            .unwrap(),
1926        );
1927        let inputs: Vec<Option<&str>> = vec![
1928            Some(
1929                r#"{"EventTime": "2024-01-01T00:00:00Z", "IngestTime": "2024-01-01T00:00:01Z", "Price": "10.50", "UserId": 1}"#,
1930            ),
1931            Some(
1932                r#"{"EventTime": "+48690-07-02T22:50:38.211Z", "IngestTime": "2024-06-01T00:00:01Z", "Price": "20.00", "UserId": 2}"#,
1933            ),
1934            Some(
1935                r#"{"EventTime": "2025-06-01T00:00:00Z", "IngestTime": "2025-06-01T00:00:01Z", "Price": "30.75", "UserId": 3}"#,
1936            ),
1937        ];
1938        let batch = parse_json_impl(&StringArray::from(inputs.clone()), schema).unwrap();
1939        assert_eq!(batch.num_rows(), 3);
1940
1941        let event_time = batch.column_by_name("EventTime").unwrap();
1942        let ingest_time = batch.column_by_name("IngestTime").unwrap();
1943        let price = batch.column_by_name("Price").unwrap();
1944        let user_id = batch.column_by_name("UserId").unwrap();
1945
1946        assert!(!event_time.is_null(0) && event_time.is_null(1) && !event_time.is_null(2));
1947        assert_eq!(event_time.null_count(), 1);
1948
1949        for col in [&ingest_time, &price, &user_id] {
1950            assert_eq!(
1951                col.null_count(),
1952                0,
1953                "row 1's bad EventTime should not contaminate other fields in the same row"
1954            );
1955        }
1956    }
1957
1958    #[test]
1959    fn test_parse_json_safe_cast_nested_stats_shape() {
1960        // Mirrors the Delta stats StructType:
1961        //   { numRecords: Long, nullCount: { EventTime: Long, UserId: Long },
1962        //     minValues: { EventTime: Timestamp, UserId: Long },
1963        //     maxValues: { EventTime: Timestamp, UserId: Long },
1964        //     tightBounds: Bool }
1965        let null_count_struct = StructType::try_new(vec![
1966            StructField::nullable("EventTime", DataType::LONG),
1967            StructField::nullable("UserId", DataType::LONG),
1968        ])
1969        .unwrap();
1970        let min_max_struct = StructType::try_new(vec![
1971            StructField::nullable("EventTime", DataType::TIMESTAMP),
1972            StructField::nullable("UserId", DataType::LONG),
1973        ])
1974        .unwrap();
1975        let schema = Arc::new(
1976            StructType::try_new(vec![
1977                StructField::nullable("numRecords", DataType::LONG),
1978                StructField::nullable("nullCount", null_count_struct),
1979                StructField::nullable("minValues", min_max_struct.clone()),
1980                StructField::nullable("maxValues", min_max_struct),
1981                StructField::nullable("tightBounds", DataType::BOOLEAN),
1982            ])
1983            .unwrap(),
1984        );
1985
1986        let inputs: Vec<Option<&str>> = vec![
1987            Some(
1988                r#"{"numRecords": 10, "nullCount": {"EventTime": 0, "UserId": 0},
1989                    "minValues": {"EventTime": "2024-01-01T00:00:00Z", "UserId": 1},
1990                    "maxValues": {"EventTime": "2024-01-31T00:00:00Z", "UserId": 100},
1991                    "tightBounds": true}"#,
1992            ),
1993            Some(
1994                r#"{"numRecords": 20, "nullCount": {"EventTime": 0, "UserId": 0},
1995                    "minValues": {"EventTime": "+48690-07-02T22:50:38.211Z", "UserId": 5},
1996                    "maxValues": {"EventTime": "+48690-07-02T22:50:38.211Z", "UserId": 200},
1997                    "tightBounds": true}"#,
1998            ),
1999            Some(
2000                r#"{"numRecords": 30, "nullCount": {"EventTime": 0, "UserId": 0},
2001                    "minValues": {"EventTime": "2025-01-01T00:00:00Z", "UserId": 10},
2002                    "maxValues": {"EventTime": "2025-12-31T00:00:00Z", "UserId": 300},
2003                    "tightBounds": true}"#,
2004            ),
2005        ];
2006        let batch = parse_json_impl(&StringArray::from(inputs), schema).unwrap();
2007        assert_eq!(batch.num_rows(), 3);
2008
2009        let num_records = batch.column_by_name("numRecords").unwrap();
2010        assert_eq!(num_records.null_count(), 0);
2011
2012        let min_values = batch.column_by_name("minValues").unwrap().as_struct();
2013        let max_values = batch.column_by_name("maxValues").unwrap().as_struct();
2014        let min_event = min_values.column_by_name("EventTime").unwrap();
2015        let max_event = max_values.column_by_name("EventTime").unwrap();
2016        let min_user = min_values.column_by_name("UserId").unwrap();
2017        let max_user = max_values.column_by_name("UserId").unwrap();
2018
2019        // EventTime min/max for row 1 must be NULL; rows 0 and 2 must be populated.
2020        assert!(!min_event.is_null(0) && min_event.is_null(1) && !min_event.is_null(2));
2021        assert!(!max_event.is_null(0) && max_event.is_null(1) && !max_event.is_null(2));
2022        // UserId min/max stay populated on every row even when the sibling EventTime fails.
2023        assert_eq!(min_user.null_count(), 0);
2024        assert_eq!(max_user.null_count(), 0);
2025    }
2026
2027    #[test]
2028    fn test_parse_json_safe_cast_all_null_input() {
2029        // Every row decodes to `{}` (the unwrap_or default for `None`), so every output cell
2030        // is NULL and no error surfaces from the safe-cast pass.
2031        let schema = Arc::new(
2032            StructType::try_new(vec![
2033                StructField::nullable("ts", DataType::TIMESTAMP),
2034                StructField::nullable("n", DataType::LONG),
2035            ])
2036            .unwrap(),
2037        );
2038        let inputs: Vec<Option<&str>> = vec![None, None, None];
2039        let batch = parse_json_impl(&StringArray::from(inputs), schema).unwrap();
2040        assert_eq!(batch.num_rows(), 3);
2041        for col in batch.columns() {
2042            assert_eq!(col.null_count(), 3);
2043        }
2044    }
2045
2046    #[test]
2047    fn simple_mask_indices() {
2048        column_mapping_cases().into_iter().for_each(|mode| {
2049            let requested_schema = StructType::new_unchecked([
2050                StructField::not_null(logical_name(0), DataType::INTEGER)
2051                    .with_metadata(column_mapping_metadata(0, mode)),
2052                StructField::nullable(logical_name(1), DataType::STRING)
2053                    .with_metadata(column_mapping_metadata(1, mode)),
2054                StructField::nullable(logical_name(2), DataType::INTEGER)
2055                    .with_metadata(column_mapping_metadata(2, mode)),
2056            ])
2057            .make_physical(mode)
2058            .unwrap()
2059            .into();
2060            let parquet_schema = Arc::new(ArrowSchema::new(vec![
2061                ArrowField::new(parquet_name(0, mode), ArrowDataType::Int32, false)
2062                    .with_metadata(arrow_fid(0)),
2063                ArrowField::new(parquet_name(1, mode), ArrowDataType::Utf8, true)
2064                    .with_metadata(arrow_fid(1)),
2065                ArrowField::new(parquet_name(2, mode), ArrowDataType::Int32, true)
2066                    .with_metadata(arrow_fid(2)),
2067            ]));
2068            let (mask_indices, reorder_indices) =
2069                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
2070            let expect_mask = vec![0, 1, 2];
2071            let expect_reorder = vec![
2072                ReorderIndex::identity(0),
2073                ReorderIndex::identity(1),
2074                ReorderIndex::identity(2),
2075            ];
2076            assert_eq!(mask_indices, expect_mask);
2077            assert_eq!(reorder_indices, expect_reorder);
2078        });
2079    }
2080
2081    #[test]
2082    fn test_variant_masks() {
2083        fn unshredded_variant_parquet_schema() -> ArrowField {
2084            ArrowField::new(
2085                "v",
2086                ArrowDataType::Struct(
2087                    vec![
2088                        ArrowField::new("metadata", ArrowDataType::Binary, false),
2089                        ArrowField::new("value", ArrowDataType::Binary, false),
2090                    ]
2091                    .into(),
2092                ),
2093                true,
2094            )
2095        }
2096        fn shredded_variant_parquet_schema() -> ArrowField {
2097            ArrowField::new(
2098                "v",
2099                ArrowDataType::Struct(
2100                    vec![
2101                        ArrowField::new("metadata", ArrowDataType::Binary, false),
2102                        ArrowField::new("value", ArrowDataType::Binary, true),
2103                        ArrowField::new("typed_value", ArrowDataType::Int32, true),
2104                    ]
2105                    .into(),
2106                ),
2107                true,
2108            )
2109        }
2110        fn incorrect_variant_parquet_schema() -> ArrowField {
2111            ArrowField::new(
2112                "v",
2113                ArrowDataType::Struct(
2114                    vec![
2115                        ArrowField::new("field1", ArrowDataType::Binary, false),
2116                        ArrowField::new("field2", ArrowDataType::Binary, false),
2117                    ]
2118                    .into(),
2119                ),
2120                true,
2121            )
2122        }
2123        fn scalar_variant_parquet_schema() -> ArrowField {
2124            ArrowField::new("v", ArrowDataType::Int16, true)
2125        }
2126        // Top level variant
2127        let requested_schema = schema_ref! { nullable "v": (DataType::unshredded_variant()) };
2128        let unshredded_parquet_schema =
2129            Arc::new(ArrowSchema::new(vec![unshredded_variant_parquet_schema()]));
2130        let shredded_parquet_schema =
2131            Arc::new(ArrowSchema::new(vec![shredded_variant_parquet_schema()]));
2132        let incorrect_parquet_schema =
2133            Arc::new(ArrowSchema::new(vec![incorrect_variant_parquet_schema()]));
2134        let scalar_parquet_schema =
2135            Arc::new(ArrowSchema::new(vec![scalar_variant_parquet_schema()]));
2136        let result_unshredded =
2137            get_requested_indices(&requested_schema, &unshredded_parquet_schema);
2138        assert!(result_unshredded.is_ok());
2139        let result_shredded = get_requested_indices(&requested_schema, &shredded_parquet_schema);
2140        assert!(matches!(result_shredded,
2141            Err(e) if e.to_string().contains("The default engine does not support shredded reads")));
2142        let result_incorrect = get_requested_indices(&requested_schema, &incorrect_parquet_schema);
2143        assert!(matches!(result_incorrect,
2144            Err(e) if e.to_string().contains("The default engine does not support shredded reads")));
2145        let result_scalar = get_requested_indices(&requested_schema, &scalar_parquet_schema);
2146        assert!(matches!(result_scalar,
2147            Err(e) if e.to_string().contains("The default engine does not support shredded reads")));
2148
2149        // Struct of Variant
2150        let requested_schema = Arc::new(StructType::new_unchecked([StructField::nullable(
2151            "struct_v",
2152            StructType::new_unchecked([StructField::nullable("v", DataType::unshredded_variant())]),
2153        )]));
2154        let unshredded_parquet_schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
2155            "struct_v",
2156            ArrowDataType::Struct(vec![unshredded_variant_parquet_schema()].into()),
2157            true,
2158        )]));
2159        let shredded_parquet_schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
2160            "struct_v",
2161            ArrowDataType::Struct(vec![shredded_variant_parquet_schema()].into()),
2162            true,
2163        )]));
2164        let result_unshredded =
2165            get_requested_indices(&requested_schema, &unshredded_parquet_schema);
2166        let result_shredded = get_requested_indices(&requested_schema, &shredded_parquet_schema);
2167        assert!(result_unshredded.is_ok());
2168        assert!(matches!(result_shredded,
2169            Err(e) if e.to_string().contains("The default engine does not support shredded reads")));
2170        // Array of Variant
2171        let requested_schema = Arc::new(StructType::new_unchecked([StructField::nullable(
2172            "array_v",
2173            ArrayType::new(DataType::unshredded_variant(), true),
2174        )]));
2175        let unshredded_parquet_schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
2176            "array_v",
2177            ArrowDataType::List(Arc::new(unshredded_variant_parquet_schema())),
2178            true,
2179        )]));
2180        let shredded_parquet_schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
2181            "array_v",
2182            ArrowDataType::List(Arc::new(shredded_variant_parquet_schema())),
2183            true,
2184        )]));
2185        let result_unshredded =
2186            get_requested_indices(&requested_schema, &unshredded_parquet_schema);
2187        let result_shredded = get_requested_indices(&requested_schema, &shredded_parquet_schema);
2188        assert!(result_unshredded.is_ok());
2189        assert!(matches!(result_shredded,
2190            Err(e) if e.to_string().contains("The default engine does not support shredded reads")));
2191
2192        // Map of Variant
2193        let requested_schema = Arc::new(StructType::new_unchecked([StructField::nullable(
2194            "map_v",
2195            MapType::new(DataType::STRING, DataType::unshredded_variant(), true),
2196        )]));
2197        let unshredded_parquet_schema = Arc::new(ArrowSchema::new(vec![ArrowField::new_map(
2198            "map_v",
2199            "struc_v",
2200            ArrowField::new("s", ArrowDataType::Utf8, false),
2201            unshredded_variant_parquet_schema(),
2202            false,
2203            false,
2204        )]));
2205        let shredded_parquet_schema = Arc::new(ArrowSchema::new(vec![ArrowField::new_map(
2206            "map_v",
2207            "struc_v",
2208            ArrowField::new("s", ArrowDataType::Utf8, false),
2209            shredded_variant_parquet_schema(),
2210            false,
2211            false,
2212        )]));
2213        let result_unshredded =
2214            get_requested_indices(&requested_schema, &unshredded_parquet_schema);
2215        let result_shredded = get_requested_indices(&requested_schema, &shredded_parquet_schema);
2216        assert!(result_unshredded.is_ok());
2217        assert!(matches!(result_shredded,
2218            Err(e) if e.to_string().contains("The default engine does not support shredded reads")));
2219    }
2220
2221    #[test]
2222    fn ensure_data_types_fails_correctly() {
2223        column_mapping_cases().into_iter().for_each(|mode| {
2224            let requested_schema = StructType::new_unchecked([
2225                StructField::not_null(logical_name(0), DataType::INTEGER)
2226                    .with_metadata(column_mapping_metadata(0, mode)),
2227                StructField::nullable(logical_name(1), DataType::INTEGER)
2228                    .with_metadata(column_mapping_metadata(1, mode)),
2229            ])
2230            .make_physical(mode)
2231            .unwrap()
2232            .into();
2233            let parquet_schema = Arc::new(ArrowSchema::new(vec![
2234                ArrowField::new(parquet_name(0, mode), ArrowDataType::Int32, false)
2235                    .with_metadata(arrow_fid(0)),
2236                ArrowField::new(parquet_name(1, mode), ArrowDataType::Utf8, true)
2237                    .with_metadata(arrow_fid(1)),
2238            ]));
2239            let res = get_requested_indices(&requested_schema, &parquet_schema);
2240            assert_result_error_with_message(
2241                res,
2242                "Invalid argument error: Incorrect datatype. Expected integer, got Utf8",
2243            );
2244
2245            let requested_schema = StructType::new_unchecked([
2246                StructField::not_null(logical_name(0), DataType::INTEGER)
2247                    .with_metadata(column_mapping_metadata(0, mode)),
2248                StructField::nullable(logical_name(1), DataType::STRING)
2249                    .with_metadata(column_mapping_metadata(1, mode)),
2250            ])
2251            .make_physical(mode)
2252            .unwrap()
2253            .into();
2254            let parquet_schema = Arc::new(ArrowSchema::new(vec![
2255                ArrowField::new(parquet_name(0, mode), ArrowDataType::Int32, false),
2256                ArrowField::new(parquet_name(1, mode), ArrowDataType::Int32, true),
2257            ]));
2258            let res = get_requested_indices(&requested_schema, &parquet_schema);
2259            assert_result_error_with_message(
2260                res,
2261                "Invalid argument error: Incorrect datatype. Expected Utf8, got Int32",
2262            );
2263        })
2264    }
2265
2266    #[test]
2267    fn mask_with_map() {
2268        column_mapping_cases().into_iter().for_each(|mode| {
2269            let requested_schema = StructType::new_unchecked([StructField::not_null(
2270                logical_name(0),
2271                MapType::new(DataType::INTEGER, DataType::STRING, false),
2272            )
2273            .with_metadata(column_mapping_metadata(0, mode))])
2274            .make_physical(mode)
2275            .unwrap()
2276            .into();
2277
2278            // The key and value may have field ids not present in the delta schema
2279            let parquet_schema = Arc::new(ArrowSchema::new(vec![ArrowField::new_map(
2280                parquet_name(0, mode),
2281                "entries",
2282                ArrowField::new("i", ArrowDataType::Int32, false),
2283                ArrowField::new("s", ArrowDataType::Utf8, false),
2284                false,
2285                false,
2286            )
2287            .with_metadata(arrow_fid(1))]));
2288            let (mask_indices, reorder_indices) =
2289                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
2290            let expect_mask = vec![0, 1];
2291            let expect_reorder = vec![ReorderIndex::identity(0)];
2292            assert_eq!(mask_indices, expect_mask);
2293            assert_eq!(reorder_indices, expect_reorder);
2294        });
2295    }
2296
2297    #[test]
2298    fn simple_reorder_indices() {
2299        column_mapping_cases().into_iter().for_each(|mode| {
2300            let requested_schema = StructType::new_unchecked([
2301                StructField::not_null(logical_name(0), DataType::INTEGER)
2302                    .with_metadata(column_mapping_metadata(0, mode)),
2303                StructField::nullable(logical_name(1), DataType::STRING)
2304                    .with_metadata(column_mapping_metadata(1, mode)),
2305                StructField::nullable(logical_name(2), DataType::INTEGER)
2306                    .with_metadata(column_mapping_metadata(2, mode)),
2307            ])
2308            .make_physical(mode)
2309            .unwrap()
2310            .into();
2311            let parquet_schema = Arc::new(ArrowSchema::new(vec![
2312                ArrowField::new(parquet_name(2, mode), ArrowDataType::Int32, true)
2313                    .with_metadata(arrow_fid(2)),
2314                ArrowField::new(parquet_name(0, mode), ArrowDataType::Int32, false)
2315                    .with_metadata(arrow_fid(0)),
2316                ArrowField::new(parquet_name(1, mode), ArrowDataType::Utf8, true)
2317                    .with_metadata(arrow_fid(1)),
2318            ]));
2319            let (mask_indices, reorder_indices) =
2320                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
2321            let expect_mask = vec![0, 1, 2];
2322            let expect_reorder = vec![
2323                ReorderIndex::identity(2),
2324                ReorderIndex::identity(0),
2325                ReorderIndex::identity(1),
2326            ];
2327            assert_eq!(mask_indices, expect_mask);
2328            assert_eq!(reorder_indices, expect_reorder);
2329        })
2330    }
2331
2332    #[test]
2333    fn simple_nullable_field_missing() {
2334        column_mapping_cases().into_iter().for_each(|mode| {
2335            let requested_schema = StructType::new_unchecked([
2336                StructField::not_null(logical_name(0), DataType::INTEGER)
2337                    .with_metadata(column_mapping_metadata(0, mode)),
2338                StructField::nullable(logical_name(1), DataType::STRING)
2339                    .with_metadata(column_mapping_metadata(1, mode)),
2340                StructField::nullable(logical_name(2), DataType::INTEGER)
2341                    .with_metadata(column_mapping_metadata(2, mode)),
2342            ])
2343            .make_physical(mode)
2344            .unwrap()
2345            .into();
2346            let parquet_schema = Arc::new(ArrowSchema::new(vec![
2347                ArrowField::new(parquet_name(0, mode), ArrowDataType::Int32, false)
2348                    .with_metadata(arrow_fid(0)),
2349                ArrowField::new(parquet_name(2, mode), ArrowDataType::Int32, true)
2350                    .with_metadata(arrow_fid(2)),
2351            ]));
2352            let (mask_indices, reorder_indices) =
2353                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
2354            let expect_mask = vec![0, 1];
2355            let expected_arrow_field = requested_schema
2356                .field(parquet_name(1, mode))
2357                .unwrap()
2358                .try_into_arrow()
2359                .unwrap();
2360            let expect_reorder = vec![
2361                ReorderIndex::identity(0),
2362                ReorderIndex::identity(2),
2363                ReorderIndex::missing(1, Arc::new(expected_arrow_field)),
2364            ];
2365            assert_eq!(mask_indices, expect_mask);
2366            assert_eq!(reorder_indices, expect_reorder);
2367        });
2368    }
2369
2370    #[test]
2371    fn get_requested_indices_by_id_only() {
2372        let requested_schema = StructType::new_unchecked([
2373            StructField::not_null("i_logical", DataType::INTEGER)
2374                .with_metadata(kernel_fid_and_name(1, "i_physical")),
2375            StructField::nullable("s_logical", DataType::STRING)
2376                .with_metadata(kernel_fid_and_name(2, "s_physical")),
2377            StructField::nullable("i2_logical", DataType::INTEGER)
2378                .with_metadata(kernel_fid_and_name(3, "i2_physical")),
2379        ])
2380        .make_physical(ColumnMappingMode::Id)
2381        .unwrap()
2382        .into();
2383        let parquet_schema = Arc::new(ArrowSchema::new(vec![
2384            ArrowField::new("not-i", ArrowDataType::Int32, false).with_metadata(arrow_fid(1)),
2385            ArrowField::new("not-i2", ArrowDataType::Int32, true).with_metadata(arrow_fid(3)),
2386        ]));
2387        let (mask_indices, reorder_indices) =
2388            get_requested_indices(&requested_schema, &parquet_schema).unwrap();
2389        let expect_mask = vec![0, 1];
2390        let expected_arrow_field = requested_schema
2391            .field("s_physical")
2392            .unwrap()
2393            .try_into_arrow()
2394            .unwrap();
2395        let expect_reorder = vec![
2396            ReorderIndex::identity(0),
2397            ReorderIndex::identity(2),
2398            ReorderIndex::missing(1, Arc::new(expected_arrow_field)),
2399        ];
2400        assert_eq!(mask_indices, expect_mask);
2401        assert_eq!(reorder_indices, expect_reorder);
2402    }
2403
2404    #[test]
2405    fn get_requested_indices_by_id_falls_back_to_name() {
2406        let requested_schema = StructType::new_unchecked([
2407            StructField::not_null("i_logical", DataType::INTEGER)
2408                .with_metadata(kernel_fid_and_name(1, "i_physical")),
2409            StructField::nullable("s_logical", DataType::STRING)
2410                .with_metadata(kernel_fid_and_name(2, "s_physical")),
2411            StructField::nullable("i2_logical", DataType::INTEGER)
2412                .with_metadata(kernel_fid_and_name(3, "i2_physical")),
2413        ])
2414        .make_physical(ColumnMappingMode::Id)
2415        .unwrap()
2416        .into();
2417        let parquet_schema = Arc::new(ArrowSchema::new(vec![
2418            ArrowField::new("i_logical", ArrowDataType::Int32, false).with_metadata(arrow_fid(1)),
2419            ArrowField::new("i2_physical", ArrowDataType::Int32, true).with_metadata(arrow_fid(3)),
2420        ]));
2421        let (mask_indices, reorder_indices) =
2422            get_requested_indices(&requested_schema, &parquet_schema).unwrap();
2423        let expect_mask = vec![0, 1];
2424        let expected_arrow_field = requested_schema
2425            .field("s_physical")
2426            .unwrap()
2427            .try_into_arrow()
2428            .unwrap();
2429        let expect_reorder = vec![
2430            ReorderIndex::identity(0),
2431            ReorderIndex::identity(2),
2432            ReorderIndex::missing(1, Arc::new(expected_arrow_field)),
2433        ];
2434        assert_eq!(mask_indices, expect_mask);
2435        assert_eq!(reorder_indices, expect_reorder);
2436    }
2437
2438    fn nested_parquet_schema(mode: ColumnMappingMode) -> ArrowSchemaRef {
2439        Arc::new(ArrowSchema::new(vec![
2440            ArrowField::new(parquet_name(1, mode), ArrowDataType::Int32, false)
2441                .with_metadata(arrow_fid(1)),
2442            ArrowField::new(
2443                parquet_name(3, mode),
2444                ArrowDataType::Struct(
2445                    vec![
2446                        ArrowField::new(parquet_name(4, mode), ArrowDataType::Int32, false)
2447                            .with_metadata(arrow_fid(4)),
2448                        ArrowField::new(parquet_name(5, mode), ArrowDataType::Utf8, false)
2449                            .with_metadata(arrow_fid(5)),
2450                    ]
2451                    .into(),
2452                ),
2453                false,
2454            )
2455            .with_metadata(arrow_fid(3)),
2456            ArrowField::new(parquet_name(2, mode), ArrowDataType::Int32, false)
2457                .with_metadata(arrow_fid(2)),
2458        ]))
2459    }
2460
2461    #[test]
2462    fn test_match_parquet_fields_filters_metadata_columns() {
2463        let kernel_schema = StructType::new_unchecked([
2464            StructField::not_null("regular_field", DataType::INTEGER),
2465            StructField::create_metadata_column("row_index", MetadataColumnSpec::RowIndex),
2466            StructField::nullable("another_field", DataType::STRING),
2467        ]);
2468
2469        let parquet_fields: ArrowFields = vec![
2470            ArrowField::new("regular_field", ArrowDataType::Int32, false),
2471            ArrowField::new("row_index", ArrowDataType::Int64, false),
2472            ArrowField::new("another_field", ArrowDataType::Utf8, true),
2473        ]
2474        .into();
2475
2476        let matched_fields: Vec<_> =
2477            match_parquet_fields(&kernel_schema, &parquet_fields).collect();
2478
2479        assert_eq!(matched_fields.len(), 3);
2480
2481        // First field (regular_field) should have kernel_field_info
2482        assert!(matched_fields[0].kernel_field_info.is_some());
2483        assert_eq!(matched_fields[0].parquet_field.name(), "regular_field");
2484
2485        // Second field (row_index metadata column) should have None for kernel_field_info
2486        assert!(matched_fields[1].kernel_field_info.is_none());
2487        assert_eq!(matched_fields[1].parquet_field.name(), "row_index");
2488
2489        // Third field (another_field) should have kernel_field_info
2490        assert!(matched_fields[2].kernel_field_info.is_some());
2491        assert_eq!(matched_fields[2].parquet_field.name(), "another_field");
2492    }
2493
2494    #[test]
2495    fn test_ordering_needs_row_indexes() {
2496        // Test case 1: No row index needed
2497        let ordering_no_row_index = vec![
2498            ReorderIndex::identity(0),
2499            ReorderIndex::cast(1, ArrowDataType::Int64),
2500            ReorderIndex::missing(
2501                2,
2502                Arc::new(ArrowField::new("missing", ArrowDataType::Utf8, true)),
2503            ),
2504        ];
2505        assert!(!ordering_needs_row_indexes(&ordering_no_row_index));
2506
2507        // Test case 2: Row index needed at top level
2508        let ordering_with_row_index = vec![
2509            ReorderIndex::identity(0),
2510            ReorderIndex::row_index(
2511                1,
2512                Arc::new(ArrowField::new("row_idx", ArrowDataType::Int64, false)),
2513            ),
2514        ];
2515        assert!(ordering_needs_row_indexes(&ordering_with_row_index));
2516
2517        // Test case 3: Empty ordering
2518        assert!(!ordering_needs_row_indexes(&[]));
2519    }
2520
2521    #[test]
2522    fn test_reorder_struct_array_missing_row_indexes() {
2523        // Test that we get a proper error when row indexes are needed but not provided
2524        let arry = make_struct_array();
2525        let reorder = vec![
2526            ReorderIndex::identity(0),
2527            ReorderIndex::row_index(
2528                1,
2529                Arc::new(ArrowField::new("row_idx", ArrowDataType::Int64, false)),
2530            ),
2531        ];
2532
2533        let result = reorder_struct_array(arry, &reorder, None, None);
2534        assert_result_error_with_message(
2535            result,
2536            "Row index column requested but row index iterator not provided",
2537        );
2538    }
2539
2540    #[test]
2541    fn test_reorder_struct_array_with_row_indexes() {
2542        // Test that row indexes work when properly provided
2543        let arry = make_struct_array();
2544        let reorder = vec![
2545            ReorderIndex::identity(0),
2546            ReorderIndex::row_index(
2547                1,
2548                Arc::new(ArrowField::new("row_idx", ArrowDataType::Int64, false)),
2549            ),
2550        ];
2551
2552        // Create a mock row index iterator
2553        #[allow(clippy::single_range_in_vec_init)]
2554        let mut row_indexes = vec![(0..4)].into_iter().flatten();
2555
2556        let ordered = reorder_struct_array(arry, &reorder, Some(&mut row_indexes), None).unwrap();
2557        assert_eq!(ordered.column_names(), vec!["b", "row_idx"]);
2558
2559        // Verify the row index column contains the expected values
2560        let row_idx_col = ordered.column(1).as_primitive::<Int64Type>();
2561        assert_eq!(row_idx_col.values(), &[0, 1, 2, 3]);
2562    }
2563
2564    #[test]
2565    fn simple_row_index_field() {
2566        let requested_schema = Arc::new(StructType::new_unchecked([
2567            StructField::not_null("i", DataType::INTEGER),
2568            StructField::create_metadata_column("my_row_index", MetadataColumnSpec::RowIndex),
2569            StructField::nullable("i2", DataType::INTEGER),
2570        ]));
2571        let parquet_schema = Arc::new(ArrowSchema::new(vec![
2572            ArrowField::new("i", ArrowDataType::Int32, false),
2573            ArrowField::new("i2", ArrowDataType::Int32, true),
2574        ]));
2575        let (mask_indices, reorder_indices) =
2576            get_requested_indices(&requested_schema, &parquet_schema).unwrap();
2577        let expect_mask = vec![0, 1];
2578        let mut arrow_row_index_field =
2579            ArrowField::new("my_row_index", ArrowDataType::Int64, false);
2580        arrow_row_index_field.set_metadata(HashMap::from([(
2581            "delta.metadataSpec".to_string(),
2582            "row_index".to_string(),
2583        )]));
2584        let expect_reorder = vec![
2585            ReorderIndex::identity(0),
2586            ReorderIndex::identity(2),
2587            ReorderIndex::row_index(1, Arc::new(arrow_row_index_field)),
2588        ];
2589        assert_eq!(mask_indices, expect_mask);
2590        assert_eq!(reorder_indices, expect_reorder);
2591    }
2592
2593    #[test]
2594    fn simple_file_path_field() {
2595        let requested_schema = Arc::new(StructType::new_unchecked([
2596            StructField::not_null("i", DataType::INTEGER),
2597            StructField::create_metadata_column("_file", MetadataColumnSpec::FilePath),
2598            StructField::nullable("i2", DataType::INTEGER),
2599        ]));
2600        let parquet_schema = Arc::new(ArrowSchema::new(vec![
2601            ArrowField::new("i", ArrowDataType::Int32, false),
2602            ArrowField::new("i2", ArrowDataType::Int32, true),
2603        ]));
2604        let (mask_indices, reorder_indices) =
2605            get_requested_indices(&requested_schema, &parquet_schema).unwrap();
2606        let expect_mask = vec![0, 1];
2607        let mut arrow_file_path_field = ArrowField::new("_file", ArrowDataType::Utf8, false);
2608        arrow_file_path_field.set_metadata(HashMap::from([(
2609            "delta.metadataSpec".to_string(),
2610            "_file".to_string(),
2611        )]));
2612        let expect_reorder = vec![
2613            ReorderIndex::identity(0),
2614            ReorderIndex::identity(2),
2615            ReorderIndex::file_path(1, Arc::new(arrow_file_path_field)),
2616        ];
2617        assert_eq!(mask_indices, expect_mask);
2618        assert_eq!(reorder_indices, expect_reorder);
2619    }
2620
2621    #[test]
2622    fn test_reorder_struct_array_with_file_path() {
2623        // Test that file paths work when properly provided
2624        let arry = make_struct_array();
2625        let reorder = vec![
2626            ReorderIndex::identity(0),
2627            ReorderIndex::file_path(
2628                1,
2629                Arc::new(ArrowField::new("_file", ArrowDataType::Utf8, false)),
2630            ),
2631        ];
2632
2633        let file_location = "s3://bucket/path/to/file.parquet";
2634        let ordered = reorder_struct_array(arry, &reorder, None, Some(file_location)).unwrap();
2635        assert_eq!(ordered.column_names(), vec!["b", "_file"]);
2636
2637        // Verify the file path column is a plain StringArray with the path repeated for each row.
2638        let file_path_col = ordered.column(1);
2639        let string_array = file_path_col
2640            .as_any()
2641            .downcast_ref::<StringArray>()
2642            .expect("Expected StringArray");
2643        assert_eq!(string_array.len(), 4);
2644        assert!(string_array.iter().all(|v| v == Some(file_location)));
2645    }
2646
2647    #[test]
2648    fn test_reorder_struct_array_missing_file_path() {
2649        // Test that error occurs when file path is requested but not provided
2650        let arry = make_struct_array();
2651        let reorder = vec![
2652            ReorderIndex::identity(0),
2653            ReorderIndex::file_path(
2654                1,
2655                Arc::new(ArrowField::new("_file", ArrowDataType::Utf8, false)),
2656            ),
2657        ];
2658
2659        let result = reorder_struct_array(arry, &reorder, None, None);
2660        assert_result_error_with_message(
2661            result,
2662            "File path column requested but file location not provided",
2663        );
2664    }
2665
2666    #[test]
2667    fn test_row_index_builder_no_skipping() {
2668        let row_groups = vec![
2669            create_mock_row_group(5), // 5 rows: indexes 0-4
2670            create_mock_row_group(3), // 3 rows: indexes 5-7
2671            create_mock_row_group(4), // 4 rows: indexes 8-11
2672        ];
2673
2674        let builder = RowIndexBuilder::new(&row_groups);
2675        let row_indexes: Vec<i64> = builder.build().unwrap().collect();
2676
2677        // Should produce consecutive indexes from 0 to 11
2678        assert_eq!(row_indexes, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
2679    }
2680
2681    #[test]
2682    fn test_row_index_builder_with_skipping() {
2683        let row_groups = vec![
2684            create_mock_row_group(5), // 5 rows: indexes 0-4
2685            create_mock_row_group(3), // 3 rows: indexes 5-7 (will be skipped)
2686            create_mock_row_group(4), // 4 rows: indexes 8-11
2687            create_mock_row_group(2), // 2 rows: indexes 12-13 (will be skipped)
2688        ];
2689
2690        let mut builder = RowIndexBuilder::new(&row_groups);
2691        builder.select_row_groups(&[0, 2]);
2692
2693        let row_indexes: Vec<i64> = builder.build().unwrap().collect();
2694
2695        // Should produce indexes from row groups 0 and 2: [0-4] and [8-11]
2696        assert_eq!(row_indexes, vec![0, 1, 2, 3, 4, 8, 9, 10, 11]);
2697    }
2698
2699    #[test]
2700    fn test_row_index_builder_single_row_group() {
2701        let row_groups = vec![create_mock_row_group(7)];
2702
2703        let mut builder = RowIndexBuilder::new(&row_groups);
2704        builder.select_row_groups(&[0]);
2705
2706        let row_indexes: Vec<i64> = builder.build().unwrap().collect();
2707
2708        assert_eq!(row_indexes, vec![0, 1, 2, 3, 4, 5, 6]);
2709    }
2710
2711    #[test]
2712    fn test_row_index_builder_empty_selection() {
2713        let row_groups = vec![create_mock_row_group(3), create_mock_row_group(2)];
2714
2715        let mut builder = RowIndexBuilder::new(&row_groups);
2716        builder.select_row_groups(&[]);
2717
2718        let row_indexes: Vec<i64> = builder.build().unwrap().collect();
2719
2720        // Should produce no indexes
2721        assert_eq!(row_indexes, Vec::<i64>::new());
2722    }
2723
2724    #[test]
2725    fn test_row_index_builder_out_of_order_selection() {
2726        let row_groups = vec![
2727            create_mock_row_group(2), // 2 rows: indexes 0-1
2728            create_mock_row_group(3), // 3 rows: indexes 2-4
2729            create_mock_row_group(1), // 1 row: index 5
2730        ];
2731
2732        let mut builder = RowIndexBuilder::new(&row_groups);
2733        builder.select_row_groups(&[2, 0]);
2734
2735        let row_indexes: Vec<i64> = builder.build().unwrap().collect();
2736
2737        // Should produce indexes in the order specified: group 2 first, then group 0
2738        assert_eq!(row_indexes, vec![5, 0, 1]);
2739    }
2740
2741    #[test]
2742    fn test_row_index_builder_out_of_bounds_row_group_ordinals() {
2743        let row_groups = vec![create_mock_row_group(2)];
2744
2745        let mut builder = RowIndexBuilder::new(&row_groups);
2746        builder.select_row_groups(&[1]);
2747
2748        let result = builder.build();
2749        assert_result_error_with_message(result, "Row group ordinal 1 is out of bounds");
2750    }
2751
2752    #[test]
2753    fn test_row_index_builder_duplicate_row_group_ordinals() {
2754        let row_groups = vec![create_mock_row_group(2), create_mock_row_group(3)];
2755
2756        let mut builder = RowIndexBuilder::new(&row_groups);
2757        builder.select_row_groups(&[1, 1]);
2758
2759        let result = builder.build();
2760        assert_result_error_with_message(result, "Found duplicate row group ordinal");
2761    }
2762
2763    #[test]
2764    fn nested_indices() {
2765        column_mapping_cases().into_iter().for_each(|mode| {
2766            let requested_schema = StructType::new_unchecked([
2767                StructField::not_null(logical_name(1), DataType::INTEGER)
2768                    .with_metadata(column_mapping_metadata(1, mode)),
2769                StructField::not_null(
2770                    logical_name(3),
2771                    StructType::new_unchecked([
2772                        StructField::not_null(logical_name(4), DataType::INTEGER)
2773                            .with_metadata(column_mapping_metadata(4, mode)),
2774                        StructField::not_null(logical_name(5), DataType::STRING)
2775                            .with_metadata(column_mapping_metadata(5, mode)),
2776                    ]),
2777                )
2778                .with_metadata(column_mapping_metadata(3, mode)),
2779                StructField::not_null(logical_name(2), DataType::INTEGER)
2780                    .with_metadata(column_mapping_metadata(2, mode)),
2781            ])
2782            .make_physical(mode)
2783            .unwrap()
2784            .into();
2785            let parquet_schema = nested_parquet_schema(mode);
2786            let (mask_indices, reorder_indices) =
2787                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
2788            let expect_mask = vec![0, 1, 2, 3];
2789            let expect_reorder = vec![
2790                ReorderIndex::identity(0),
2791                ReorderIndex::nested(
2792                    1,
2793                    vec![ReorderIndex::identity(0), ReorderIndex::identity(1)],
2794                ),
2795                ReorderIndex::identity(2),
2796            ];
2797            assert_eq!(mask_indices, expect_mask);
2798            assert_eq!(reorder_indices, expect_reorder);
2799        });
2800    }
2801    #[test]
2802    fn nested_indices_reorder() {
2803        column_mapping_cases().into_iter().for_each(|mode| {
2804            let requested_schema = StructType::new_unchecked([
2805                StructField::not_null(
2806                    logical_name(3),
2807                    StructType::new_unchecked([
2808                        StructField::not_null(logical_name(5), DataType::STRING)
2809                            .with_metadata(column_mapping_metadata(5, mode)),
2810                        StructField::not_null(logical_name(4), DataType::INTEGER)
2811                            .with_metadata(column_mapping_metadata(4, mode)),
2812                    ]),
2813                )
2814                .with_metadata(column_mapping_metadata(3, mode)),
2815                StructField::not_null(logical_name(2), DataType::INTEGER)
2816                    .with_metadata(column_mapping_metadata(2, mode)),
2817                StructField::not_null(logical_name(1), DataType::INTEGER)
2818                    .with_metadata(column_mapping_metadata(1, mode)),
2819            ])
2820            .make_physical(mode)
2821            .unwrap()
2822            .into();
2823            let parquet_schema = nested_parquet_schema(mode);
2824            let (mask_indices, reorder_indices) =
2825                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
2826            let expect_mask = vec![0, 1, 2, 3];
2827            let expect_reorder = vec![
2828                ReorderIndex::identity(2),
2829                ReorderIndex::nested(
2830                    0,
2831                    vec![ReorderIndex::identity(1), ReorderIndex::identity(0)],
2832                ),
2833                ReorderIndex::identity(1),
2834            ];
2835            assert_eq!(mask_indices, expect_mask);
2836            assert_eq!(reorder_indices, expect_reorder);
2837        });
2838    }
2839
2840    #[test]
2841    fn nested_indices_mask_inner() {
2842        column_mapping_cases().into_iter().for_each(|mode| {
2843            let requested_schema = StructType::new_unchecked([
2844                StructField::not_null(logical_name(1), DataType::INTEGER)
2845                    .with_metadata(column_mapping_metadata(1, mode)),
2846                StructField::not_null(
2847                    logical_name(3),
2848                    StructType::new_unchecked([StructField::not_null(
2849                        logical_name(4),
2850                        DataType::INTEGER,
2851                    )
2852                    .with_metadata(column_mapping_metadata(4, mode))]),
2853                )
2854                .with_metadata(column_mapping_metadata(3, mode)),
2855                StructField::not_null(logical_name(2), DataType::INTEGER)
2856                    .with_metadata(column_mapping_metadata(2, mode)),
2857            ])
2858            .make_physical(mode)
2859            .unwrap()
2860            .into();
2861            let parquet_schema = nested_parquet_schema(mode);
2862            let (mask_indices, reorder_indices) =
2863                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
2864            let expect_mask = vec![0, 1, 3];
2865            let expect_reorder = vec![
2866                ReorderIndex::identity(0),
2867                ReorderIndex::nested(1, vec![ReorderIndex::identity(0)]),
2868                ReorderIndex::identity(2),
2869            ];
2870            assert_eq!(mask_indices, expect_mask);
2871            assert_eq!(reorder_indices, expect_reorder);
2872        })
2873    }
2874
2875    #[test]
2876    fn unmatched_struct_before_selected_leaf_ordering() {
2877        // Regression: when a struct with no matching children appears BEFORE a selected
2878        // leaf in parquet order, the Missing entry must be deferred so the leaf's
2879        // Identity entry gets the correct parquet_position in reorder_struct_array.
2880        let requested_schema: SchemaRef = Arc::new(StructType::new_unchecked([
2881            StructField::nullable("a", DataType::LONG),
2882            StructField::nullable(
2883                "stats",
2884                StructType::new_unchecked([StructField::nullable("age", DataType::LONG)]),
2885            ),
2886        ]));
2887        // Parquet has stats BEFORE a
2888        let parquet_schema = Arc::new(ArrowSchema::new(vec![
2889            ArrowField::new(
2890                "stats",
2891                ArrowDataType::Struct(
2892                    vec![ArrowField::new("id", ArrowDataType::Int64, true)].into(),
2893                ),
2894                true,
2895            ),
2896            ArrowField::new("a", ArrowDataType::Int64, true),
2897        ]));
2898        let (mask_indices, reorder_indices) =
2899            get_requested_indices(&requested_schema, &parquet_schema).unwrap();
2900        // Only "a" should be in the mask (leaf index 1, after stats.id at index 0)
2901        assert_eq!(mask_indices, vec![1]);
2902        let expected_stats_field = Arc::new(
2903            requested_schema
2904                .field("stats")
2905                .unwrap()
2906                .try_into_arrow()
2907                .unwrap(),
2908        );
2909        // Identity for "a" must come FIRST (parquet_position 0), then Missing for stats
2910        assert_eq!(
2911            reorder_indices,
2912            vec![
2913                ReorderIndex::identity(0),
2914                ReorderIndex::missing(1, expected_stats_field),
2915            ]
2916        );
2917    }
2918
2919    #[test]
2920    fn simple_list_mask() {
2921        column_mapping_cases().into_iter().for_each(|mode| {
2922            let requested_schema = StructType::new_unchecked([
2923                StructField::not_null(logical_name(1), DataType::INTEGER)
2924                    .with_metadata(column_mapping_metadata(1, mode)),
2925                StructField::not_null(logical_name(2), ArrayType::new(DataType::INTEGER, false))
2926                    .with_metadata(column_mapping_metadata(2, mode)),
2927                StructField::not_null(logical_name(3), DataType::INTEGER)
2928                    .with_metadata(column_mapping_metadata(3, mode)),
2929            ])
2930            .make_physical(mode)
2931            .unwrap()
2932            .into();
2933            let parquet_schema = Arc::new(ArrowSchema::new(vec![
2934                ArrowField::new(parquet_name(1, mode), ArrowDataType::Int32, false)
2935                    .with_metadata(arrow_fid(1)),
2936                ArrowField::new(
2937                    parquet_name(2, mode),
2938                    ArrowDataType::List(Arc::new(ArrowField::new(
2939                        "nested",
2940                        ArrowDataType::Int32,
2941                        false,
2942                    ))),
2943                    false,
2944                )
2945                .with_metadata(arrow_fid(2)),
2946                ArrowField::new(parquet_name(3, mode), ArrowDataType::Int32, false)
2947                    .with_metadata(arrow_fid(3)),
2948            ]));
2949            let (mask_indices, reorder_indices) =
2950                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
2951            let expect_mask = vec![0, 1, 2];
2952            let expect_reorder = vec![
2953                ReorderIndex::identity(0),
2954                ReorderIndex::identity(1),
2955                ReorderIndex::identity(2),
2956            ];
2957            assert_eq!(mask_indices, expect_mask);
2958            assert_eq!(reorder_indices, expect_reorder);
2959        });
2960    }
2961
2962    #[test]
2963    fn list_skip_earlier_element() {
2964        column_mapping_cases().into_iter().for_each(|mode| {
2965            let requested_schema = StructType::new_unchecked([StructField::not_null(
2966                logical_name(1),
2967                ArrayType::new(DataType::INTEGER, false),
2968            )
2969            .with_metadata(column_mapping_metadata(1, mode))])
2970            .make_physical(mode)
2971            .unwrap()
2972            .into();
2973            let parquet_schema = Arc::new(ArrowSchema::new(vec![
2974                ArrowField::new(parquet_name(0, mode), ArrowDataType::Int32, false)
2975                    .with_metadata(arrow_fid(0)),
2976                ArrowField::new(
2977                    parquet_name(1, mode),
2978                    ArrowDataType::List(Arc::new(
2979                        ArrowField::new(parquet_name(2, mode), ArrowDataType::Int32, false)
2980                            .with_metadata(arrow_fid(2)),
2981                    )),
2982                    false,
2983                )
2984                .with_metadata(arrow_fid(1)),
2985            ]));
2986            let (mask_indices, reorder_indices) =
2987                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
2988            let expect_mask = vec![1];
2989            let expect_reorder = vec![ReorderIndex::identity(0)];
2990            assert_eq!(mask_indices, expect_mask);
2991            assert_eq!(reorder_indices, expect_reorder);
2992        });
2993    }
2994
2995    #[test]
2996    fn nested_indices_list() {
2997        column_mapping_cases().into_iter().for_each(|mode| {
2998            let requested_schema = StructType::new_unchecked([
2999                StructField::not_null(logical_name(0), DataType::INTEGER)
3000                    .with_metadata(column_mapping_metadata(0, mode)),
3001                StructField::not_null(
3002                    logical_name(1),
3003                    ArrayType::new(
3004                        StructType::new_unchecked([
3005                            StructField::not_null(logical_name(3), DataType::INTEGER)
3006                                .with_metadata(column_mapping_metadata(3, mode)),
3007                            StructField::not_null(logical_name(4), DataType::STRING)
3008                                .with_metadata(column_mapping_metadata(4, mode)),
3009                        ]),
3010                        false,
3011                    ),
3012                )
3013                .with_metadata(column_mapping_metadata(1, mode)),
3014                StructField::not_null(logical_name(2), DataType::INTEGER)
3015                    .with_metadata(column_mapping_metadata(2, mode)),
3016            ])
3017            .make_physical(mode)
3018            .unwrap()
3019            .into();
3020            let parquet_schema = Arc::new(ArrowSchema::new(vec![
3021                ArrowField::new(parquet_name(0, mode), ArrowDataType::Int32, false)
3022                    .with_metadata(arrow_fid(0)),
3023                ArrowField::new(
3024                    parquet_name(1, mode),
3025                    ArrowDataType::List(Arc::new(ArrowField::new(
3026                        "nested",
3027                        ArrowDataType::Struct(
3028                            vec![
3029                                ArrowField::new(parquet_name(3, mode), ArrowDataType::Int32, false)
3030                                    .with_metadata(arrow_fid(3)),
3031                                ArrowField::new(parquet_name(4, mode), ArrowDataType::Utf8, false)
3032                                    .with_metadata(arrow_fid(4)),
3033                            ]
3034                            .into(),
3035                        ),
3036                        false,
3037                    ))),
3038                    false,
3039                )
3040                .with_metadata(arrow_fid(1)),
3041                ArrowField::new(parquet_name(2, mode), ArrowDataType::Int32, false)
3042                    .with_metadata(arrow_fid(2)),
3043            ]));
3044            let (mask_indices, reorder_indices) =
3045                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
3046            let expect_mask = vec![0, 1, 2, 3];
3047            let expect_reorder = vec![
3048                ReorderIndex::identity(0),
3049                ReorderIndex::nested(
3050                    1,
3051                    vec![ReorderIndex::identity(0), ReorderIndex::identity(1)],
3052                ),
3053                ReorderIndex::identity(2),
3054            ];
3055            assert_eq!(mask_indices, expect_mask);
3056            assert_eq!(reorder_indices, expect_reorder);
3057        });
3058    }
3059
3060    #[test]
3061    fn nested_indices_unselected_list() {
3062        column_mapping_cases().into_iter().for_each(|mode| {
3063            let requested_schema = StructType::new_unchecked([
3064                StructField::not_null(logical_name(1), DataType::INTEGER)
3065                    .with_metadata(column_mapping_metadata(1, mode)),
3066                StructField::not_null(logical_name(3), DataType::INTEGER)
3067                    .with_metadata(column_mapping_metadata(3, mode)),
3068            ])
3069            .make_physical(mode)
3070            .unwrap()
3071            .into();
3072            let parquet_schema = Arc::new(ArrowSchema::new(vec![
3073                ArrowField::new(parquet_name(1, mode), ArrowDataType::Int32, false)
3074                    .with_metadata(arrow_fid(1)),
3075                ArrowField::new(
3076                    parquet_name(2, mode),
3077                    ArrowDataType::List(Arc::new(ArrowField::new(
3078                        "nested",
3079                        ArrowDataType::Struct(
3080                            vec![
3081                                ArrowField::new(parquet_name(4, mode), ArrowDataType::Int32, false)
3082                                    .with_metadata(arrow_fid(4)),
3083                                ArrowField::new(parquet_name(5, mode), ArrowDataType::Utf8, false)
3084                                    .with_metadata(arrow_fid(5)),
3085                            ]
3086                            .into(),
3087                        ),
3088                        false,
3089                    ))),
3090                    false,
3091                )
3092                .with_metadata(arrow_fid(2)),
3093                ArrowField::new(parquet_name(3, mode), ArrowDataType::Int32, false)
3094                    .with_metadata(arrow_fid(3)),
3095            ]));
3096            let (mask_indices, reorder_indices) =
3097                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
3098            let expect_mask = vec![0, 3];
3099            let expect_reorder = vec![ReorderIndex::identity(0), ReorderIndex::identity(1)];
3100            assert_eq!(mask_indices, expect_mask);
3101            assert_eq!(reorder_indices, expect_reorder);
3102        });
3103    }
3104
3105    #[test]
3106    fn nested_indices_list_mask_inner() {
3107        column_mapping_cases().into_iter().for_each(|mode| {
3108            let requested_schema = StructType::new_unchecked([
3109                StructField::not_null(logical_name(1), DataType::INTEGER)
3110                    .with_metadata(column_mapping_metadata(1, mode)),
3111                StructField::not_null(
3112                    logical_name(2),
3113                    ArrayType::new(
3114                        StructType::new_unchecked([StructField::not_null(
3115                            logical_name(4),
3116                            DataType::INTEGER,
3117                        )
3118                        .with_metadata(column_mapping_metadata(4, mode))]),
3119                        false,
3120                    ),
3121                )
3122                .with_metadata(column_mapping_metadata(2, mode)),
3123                StructField::not_null(logical_name(3), DataType::INTEGER)
3124                    .with_metadata(column_mapping_metadata(3, mode)),
3125            ])
3126            .make_physical(mode)
3127            .unwrap()
3128            .into();
3129            let parquet_schema = Arc::new(ArrowSchema::new(vec![
3130                ArrowField::new(parquet_name(1, mode), ArrowDataType::Int32, false)
3131                    .with_metadata(arrow_fid(1)),
3132                ArrowField::new(
3133                    parquet_name(2, mode),
3134                    ArrowDataType::List(Arc::new(ArrowField::new(
3135                        "nested",
3136                        ArrowDataType::Struct(
3137                            vec![
3138                                ArrowField::new(parquet_name(4, mode), ArrowDataType::Int32, false)
3139                                    .with_metadata(arrow_fid(4)),
3140                                ArrowField::new(parquet_name(5, mode), ArrowDataType::Utf8, false)
3141                                    .with_metadata(arrow_fid(5)),
3142                            ]
3143                            .into(),
3144                        ),
3145                        false,
3146                    ))),
3147                    false,
3148                )
3149                .with_metadata(arrow_fid(2)),
3150                ArrowField::new(parquet_name(3, mode), ArrowDataType::Int32, false)
3151                    .with_metadata(arrow_fid(3)),
3152            ]));
3153            let (mask_indices, reorder_indices) =
3154                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
3155            let expect_mask = vec![0, 1, 3];
3156            let expect_reorder = vec![
3157                ReorderIndex::identity(0),
3158                ReorderIndex::nested(1, vec![ReorderIndex::identity(0)]),
3159                ReorderIndex::identity(2),
3160            ];
3161            assert_eq!(mask_indices, expect_mask);
3162            assert_eq!(reorder_indices, expect_reorder);
3163        });
3164    }
3165
3166    #[test]
3167    fn nested_indices_list_mask_inner_reorder() {
3168        column_mapping_cases().into_iter().for_each(|mode| {
3169            let requested_schema = StructType::new_unchecked([
3170                StructField::not_null(logical_name(1), DataType::INTEGER)
3171                    .with_metadata(column_mapping_metadata(1, mode)),
3172                StructField::not_null(
3173                    logical_name(2),
3174                    ArrayType::new(
3175                        StructType::new_unchecked([
3176                            StructField::not_null(logical_name(6), DataType::STRING)
3177                                .with_metadata(column_mapping_metadata(6, mode)),
3178                            StructField::not_null(logical_name(5), DataType::INTEGER)
3179                                .with_metadata(column_mapping_metadata(5, mode)),
3180                        ]),
3181                        false,
3182                    ),
3183                )
3184                .with_metadata(column_mapping_metadata(2, mode)),
3185                StructField::not_null(logical_name(3), DataType::INTEGER)
3186                    .with_metadata(column_mapping_metadata(3, mode)),
3187            ])
3188            .make_physical(mode)
3189            .unwrap()
3190            .into();
3191            let parquet_schema = Arc::new(ArrowSchema::new(vec![
3192                ArrowField::new(parquet_name(1, mode), ArrowDataType::Int32, false)
3193                    .with_metadata(arrow_fid(1)),
3194                ArrowField::new(
3195                    parquet_name(2, mode),
3196                    ArrowDataType::List(Arc::new(ArrowField::new(
3197                        "nested",
3198                        ArrowDataType::Struct(
3199                            vec![
3200                                ArrowField::new(parquet_name(4, mode), ArrowDataType::Int32, false)
3201                                    .with_metadata(arrow_fid(4)),
3202                                ArrowField::new(parquet_name(5, mode), ArrowDataType::Int32, false)
3203                                    .with_metadata(arrow_fid(5)),
3204                                ArrowField::new(parquet_name(6, mode), ArrowDataType::Utf8, false)
3205                                    .with_metadata(arrow_fid(6)),
3206                            ]
3207                            .into(),
3208                        ),
3209                        false,
3210                    ))),
3211                    false,
3212                )
3213                .with_metadata(arrow_fid(2)),
3214                ArrowField::new(parquet_name(3, mode), ArrowDataType::Int32, false)
3215                    .with_metadata(arrow_fid(3)),
3216            ]));
3217            let (mask_indices, reorder_indices) =
3218                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
3219            let expect_mask = vec![0, 2, 3, 4];
3220            let expect_reorder = vec![
3221                ReorderIndex::identity(0),
3222                ReorderIndex::nested(
3223                    1,
3224                    vec![ReorderIndex::identity(1), ReorderIndex::identity(0)],
3225                ),
3226                ReorderIndex::identity(2),
3227            ];
3228            assert_eq!(mask_indices, expect_mask);
3229            assert_eq!(reorder_indices, expect_reorder);
3230        });
3231    }
3232
3233    #[test]
3234    fn skipped_struct() {
3235        column_mapping_cases().into_iter().for_each(|mode| {
3236            let requested_schema = StructType::new_unchecked([
3237                StructField::not_null(logical_name(1), DataType::INTEGER)
3238                    .with_metadata(column_mapping_metadata(1, mode)),
3239                StructField::not_null(
3240                    logical_name(2),
3241                    StructType::new_unchecked([
3242                        StructField::not_null(logical_name(4), DataType::INTEGER)
3243                            .with_metadata(column_mapping_metadata(4, mode)),
3244                        StructField::not_null(logical_name(5), DataType::STRING)
3245                            .with_metadata(column_mapping_metadata(5, mode)),
3246                    ]),
3247                )
3248                .with_metadata(column_mapping_metadata(2, mode)),
3249                StructField::not_null(logical_name(3), DataType::INTEGER)
3250                    .with_metadata(column_mapping_metadata(3, mode)),
3251            ])
3252            .make_physical(mode)
3253            .unwrap()
3254            .into();
3255            let parquet_schema = Arc::new(ArrowSchema::new(vec![
3256                ArrowField::new(
3257                    "skipped",
3258                    ArrowDataType::Struct(
3259                        vec![
3260                            ArrowField::new(parquet_name(7, mode), ArrowDataType::Int32, false)
3261                                .with_metadata(arrow_fid(7)),
3262                            ArrowField::new(parquet_name(8, mode), ArrowDataType::Utf8, false)
3263                                .with_metadata(arrow_fid(8)),
3264                        ]
3265                        .into(),
3266                    ),
3267                    false,
3268                )
3269                .with_metadata(arrow_fid(6)),
3270                ArrowField::new(parquet_name(3, mode), ArrowDataType::Int32, false)
3271                    .with_metadata(arrow_fid(3)),
3272                ArrowField::new(
3273                    parquet_name(2, mode),
3274                    ArrowDataType::Struct(
3275                        vec![
3276                            ArrowField::new(parquet_name(4, mode), ArrowDataType::Int32, false)
3277                                .with_metadata(arrow_fid(4)),
3278                            ArrowField::new(parquet_name(5, mode), ArrowDataType::Utf8, false)
3279                                .with_metadata(arrow_fid(5)),
3280                        ]
3281                        .into(),
3282                    ),
3283                    false,
3284                )
3285                .with_metadata(arrow_fid(2)),
3286                ArrowField::new(parquet_name(1, mode), ArrowDataType::Int32, false)
3287                    .with_metadata(arrow_fid(1)),
3288            ]));
3289            let (mask_indices, reorder_indices) =
3290                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
3291            let expect_mask = vec![2, 3, 4, 5];
3292            let expect_reorder = vec![
3293                ReorderIndex::identity(2),
3294                ReorderIndex::nested(
3295                    1,
3296                    vec![ReorderIndex::identity(0), ReorderIndex::identity(1)],
3297                ),
3298                ReorderIndex::identity(0),
3299            ];
3300            assert_eq!(mask_indices, expect_mask);
3301            assert_eq!(reorder_indices, expect_reorder);
3302        });
3303    }
3304
3305    #[test]
3306    fn reorder_map_with_structs() {
3307        let requested_schema = schema_ref! {
3308            not_null "i": INTEGER,
3309            not_null "map": {
3310                { not_null "k1": STRING, not_null "k2": STRING }
3311                    => not_null { not_null "v2": STRING, not_null "v1": STRING }
3312            },
3313        };
3314        let parquet_schema = Arc::new(ArrowSchema::new(vec![
3315            ArrowField::new("i", ArrowDataType::Int32, false),
3316            ArrowField::new_map(
3317                "map",
3318                "entries",
3319                ArrowField::new(
3320                    "i",
3321                    ArrowDataType::Struct(
3322                        vec![
3323                            ArrowField::new("k1", ArrowDataType::Utf8, false),
3324                            ArrowField::new("k2", ArrowDataType::Utf8, false),
3325                        ]
3326                        .into(),
3327                    ),
3328                    false,
3329                ),
3330                ArrowField::new(
3331                    "v",
3332                    ArrowDataType::Struct(
3333                        vec![
3334                            ArrowField::new("v1", ArrowDataType::Utf8, false),
3335                            ArrowField::new("v2", ArrowDataType::Utf8, false),
3336                        ]
3337                        .into(),
3338                    ),
3339                    false,
3340                ),
3341                false,
3342                false,
3343            ),
3344        ]));
3345        let (mask_indices, reorder_indices) =
3346            get_requested_indices(&requested_schema, &parquet_schema).unwrap();
3347        let expect_mask = vec![0, 1, 2, 3, 4];
3348        let expect_reorder = vec![
3349            ReorderIndex::identity(0),
3350            ReorderIndex::nested(
3351                1,
3352                vec![
3353                    ReorderIndex::identity(0), // key does not need re-ordering
3354                    ReorderIndex::nested(
3355                        1,
3356                        vec![ReorderIndex::identity(1), ReorderIndex::identity(0)],
3357                    ),
3358                ],
3359            ),
3360        ];
3361        assert_eq!(mask_indices, expect_mask);
3362        assert_eq!(reorder_indices, expect_reorder);
3363    }
3364
3365    fn make_struct_array() -> StructArray {
3366        let boolean = Arc::new(BooleanArray::from(vec![false, false, true, true]));
3367        let int = Arc::new(Int32Array::from(vec![42, 28, 19, 31]));
3368        StructArray::from(vec![
3369            (
3370                Arc::new(ArrowField::new("b", ArrowDataType::Boolean, false)),
3371                boolean.clone() as ArrowArrayRef,
3372            ),
3373            (
3374                Arc::new(ArrowField::new("c", ArrowDataType::Int32, false)),
3375                int.clone() as ArrowArrayRef,
3376            ),
3377        ])
3378    }
3379
3380    #[test]
3381    fn simple_reorder_struct() {
3382        let arry = make_struct_array();
3383        let reorder = vec![ReorderIndex::identity(1), ReorderIndex::identity(0)];
3384        let ordered = reorder_struct_array(arry, &reorder, None, None).unwrap();
3385        assert_eq!(ordered.column_names(), vec!["c", "b"]);
3386    }
3387
3388    #[test]
3389    fn nested_reorder_struct() {
3390        let arry1 = Arc::new(make_struct_array());
3391        let arry2 = Arc::new(make_struct_array());
3392        let fields: ArrowFields = vec![
3393            Arc::new(ArrowField::new("b", ArrowDataType::Boolean, false)),
3394            Arc::new(ArrowField::new("c", ArrowDataType::Int32, false)),
3395        ]
3396        .into();
3397        let nested = StructArray::from(vec![
3398            (
3399                Arc::new(ArrowField::new(
3400                    "struct1",
3401                    ArrowDataType::Struct(fields.clone()),
3402                    false,
3403                )),
3404                arry1 as ArrowArrayRef,
3405            ),
3406            (
3407                Arc::new(ArrowField::new(
3408                    "struct2",
3409                    ArrowDataType::Struct(fields),
3410                    false,
3411                )),
3412                arry2 as ArrowArrayRef,
3413            ),
3414        ]);
3415        let reorder = vec![
3416            ReorderIndex::nested(
3417                1,
3418                vec![ReorderIndex::identity(1), ReorderIndex::identity(0)],
3419            ),
3420            ReorderIndex::nested(
3421                0,
3422                vec![
3423                    ReorderIndex::identity(0),
3424                    ReorderIndex::identity(1),
3425                    ReorderIndex::missing(
3426                        2,
3427                        Arc::new(ArrowField::new("s", ArrowDataType::Utf8, true)),
3428                    ),
3429                ],
3430            ),
3431        ];
3432        let ordered = reorder_struct_array(nested, &reorder, None, None).unwrap();
3433        assert_eq!(ordered.column_names(), vec!["struct2", "struct1"]);
3434        let ordered_s2 = ordered.column(0).as_struct();
3435        assert_eq!(ordered_s2.column_names(), vec!["b", "c", "s"]);
3436        let ordered_s1 = ordered.column(1).as_struct();
3437        assert_eq!(ordered_s1.column_names(), vec!["c", "b"]);
3438    }
3439
3440    #[test]
3441    fn reorder_list_of_struct() {
3442        let boolean = Arc::new(BooleanArray::from(vec![
3443            false, false, true, true, false, true,
3444        ]));
3445        let int = Arc::new(Int32Array::from(vec![42, 28, 19, 31, 0, 3]));
3446        let list_sa = StructArray::from(vec![
3447            (
3448                Arc::new(ArrowField::new("b", ArrowDataType::Boolean, false)),
3449                boolean.clone() as ArrowArrayRef,
3450            ),
3451            (
3452                Arc::new(ArrowField::new("c", ArrowDataType::Int32, false)),
3453                int.clone() as ArrowArrayRef,
3454            ),
3455        ]);
3456        let offsets = OffsetBuffer::new(ScalarBuffer::from(vec![0, 3, 6]));
3457        let list_field = ArrowField::new("item", list_sa.data_type().clone(), false);
3458        let list = Arc::new(GenericListArray::new(
3459            Arc::new(list_field),
3460            offsets,
3461            Arc::new(list_sa),
3462            None,
3463        ));
3464        let fields: ArrowFields = vec![
3465            Arc::new(ArrowField::new("b", ArrowDataType::Boolean, false)),
3466            Arc::new(ArrowField::new("c", ArrowDataType::Int32, false)),
3467        ]
3468        .into();
3469        let list_dt = Arc::new(ArrowField::new(
3470            "list",
3471            ArrowDataType::new_list(ArrowDataType::Struct(fields), false),
3472            false,
3473        ));
3474        let struct_array = StructArray::from(vec![(list_dt, list as ArrowArrayRef)]);
3475        let reorder = vec![ReorderIndex::nested(
3476            0,
3477            vec![ReorderIndex::identity(1), ReorderIndex::identity(0)],
3478        )];
3479        let ordered = reorder_struct_array(struct_array, &reorder, None, None).unwrap();
3480        let ordered_list_col = ordered.column(0).as_list::<i32>();
3481        for i in 0..ordered_list_col.len() {
3482            let array_item = ordered_list_col.value(i);
3483            let struct_item = array_item.as_struct();
3484            assert_eq!(struct_item.column_names(), vec!["c", "b"]);
3485        }
3486    }
3487
3488    // boy howdy this is more complicated than expected
3489    fn build_arrow_map() -> MapArray {
3490        let key_struct_builder = StructBuilder::from_fields(
3491            ArrowFields::from(vec![
3492                ArrowField::new("k1", ArrowDataType::Int32, false),
3493                ArrowField::new("k2", ArrowDataType::Int32, false),
3494            ]),
3495            1,
3496        );
3497        let value_struct_builder = StructBuilder::from_fields(
3498            ArrowFields::from(vec![
3499                ArrowField::new("v1", ArrowDataType::Int32, false),
3500                ArrowField::new("v2", ArrowDataType::Int32, false),
3501            ]),
3502            1,
3503        );
3504        let mut map_builder = MapBuilder::new(None, key_struct_builder, value_struct_builder);
3505
3506        let (key_builder, value_builder) = map_builder.entries();
3507        let key_k1_builder = key_builder.field_builder::<Int32Builder>(0).unwrap();
3508        key_k1_builder.append_value(1);
3509        let key_k2_builder = key_builder.field_builder::<Int32Builder>(1).unwrap();
3510        key_k2_builder.append_value(2);
3511        key_builder.append(true);
3512
3513        let value_v1_builder = value_builder.field_builder::<Int32Builder>(0).unwrap();
3514        value_v1_builder.append_value(1);
3515        let value_v2_builder = value_builder.field_builder::<Int32Builder>(1).unwrap();
3516        value_v2_builder.append_value(2);
3517        value_builder.append(true);
3518        map_builder.append(true).unwrap();
3519        map_builder.finish()
3520    }
3521
3522    #[test]
3523    fn reorder_map_of_struct() {
3524        let int_array = Arc::new(Int32Array::from(vec![42]));
3525        let int_dt = Arc::new(ArrowField::new("i", int_array.data_type().clone(), false));
3526        let map_array = Arc::new(build_arrow_map());
3527        let map_dt = Arc::new(ArrowField::new("map", map_array.data_type().clone(), false));
3528        let struct_array = StructArray::from(vec![
3529            (int_dt, int_array as ArrowArrayRef),
3530            (map_dt, map_array as ArrowArrayRef),
3531        ]);
3532        let reorder = vec![
3533            ReorderIndex::identity(1),
3534            ReorderIndex::nested(
3535                0,
3536                vec![
3537                    ReorderIndex::identity(0),
3538                    ReorderIndex::nested(
3539                        1,
3540                        vec![ReorderIndex::identity(1), ReorderIndex::identity(0)],
3541                    ),
3542                ],
3543            ),
3544        ];
3545        let ordered = reorder_struct_array(struct_array, &reorder, None, None).unwrap();
3546        assert_eq!(ordered.column_names(), vec!["map", "i"]);
3547        if let ArrowDataType::Map(field, _) = ordered.column(0).data_type() {
3548            if let ArrowDataType::Struct(fields) = field.data_type() {
3549                fn assert_col_order(field: &ArrowField, expected: Vec<&str>) {
3550                    if let ArrowDataType::Struct(fields) = field.data_type() {
3551                        let names: Vec<&str> =
3552                            fields.iter().map(|field| field.name().as_str()).collect();
3553                        assert_eq!(names, expected);
3554                    } else {
3555                        panic!("Expected struct field");
3556                    }
3557                }
3558                assert_col_order(&fields[0], vec!["k1", "k2"]);
3559                assert_col_order(&fields[1], vec!["v2", "v1"]);
3560            } else {
3561                panic!("Inner field should have been a struct");
3562            }
3563        } else {
3564            panic!("Column 0 should have been a map");
3565        }
3566    }
3567
3568    #[test]
3569    fn no_matches() {
3570        column_mapping_cases().into_iter().for_each(|mode| {
3571            let requested_schema = StructType::new_unchecked([
3572                StructField::nullable(logical_name(1), DataType::STRING)
3573                    .with_metadata(column_mapping_metadata(1, mode)),
3574                StructField::nullable(logical_name(2), DataType::INTEGER)
3575                    .with_metadata(column_mapping_metadata(2, mode)),
3576            ])
3577            .make_physical(mode)
3578            .unwrap()
3579            .into();
3580            let nots_field =
3581                ArrowField::new("NOTs", ArrowDataType::Utf8, true).with_metadata(arrow_fid(3));
3582            let noti2_field =
3583                ArrowField::new("NOTi2", ArrowDataType::Int32, true).with_metadata(arrow_fid(4));
3584            let parquet_schema = Arc::new(ArrowSchema::new(vec![
3585                nots_field.clone(),
3586                noti2_field.clone(),
3587            ]));
3588            let (mask_indices, reorder_indices) =
3589                get_requested_indices(&requested_schema, &parquet_schema).unwrap();
3590            let expect_mask: Vec<usize> = vec![];
3591
3592            // Build expected arrow fields using proper conversion
3593            let mut fields = requested_schema.fields();
3594            let expected_field1: Arc<ArrowField> =
3595                Arc::new(fields.next().unwrap().try_into_arrow().unwrap());
3596            let expected_field2: Arc<ArrowField> =
3597                Arc::new(fields.next().unwrap().try_into_arrow().unwrap());
3598
3599            let expect_reorder = vec![
3600                ReorderIndex::missing(0, expected_field1),
3601                ReorderIndex::missing(1, expected_field2),
3602            ];
3603            assert_eq!(mask_indices, expect_mask);
3604            assert_eq!(reorder_indices, expect_reorder);
3605        });
3606    }
3607
3608    #[test]
3609    fn empty_requested_schema() {
3610        let requested_schema = Arc::new(StructType::new_unchecked([]));
3611        let parquet_schema = Arc::new(ArrowSchema::new(vec![
3612            ArrowField::new("i", ArrowDataType::Int32, false),
3613            ArrowField::new("s", ArrowDataType::Utf8, true),
3614            ArrowField::new("i2", ArrowDataType::Int32, true),
3615        ]));
3616        let (mask_indices, reorder_indices) =
3617            get_requested_indices(&requested_schema, &parquet_schema).unwrap();
3618        let expect_mask: Vec<usize> = vec![];
3619        let expect_reorder = vec![];
3620        assert_eq!(mask_indices, expect_mask);
3621        assert_eq!(reorder_indices, expect_reorder);
3622    }
3623
3624    #[test]
3625    fn test_write_json() -> DeltaResult<()> {
3626        let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
3627            "string",
3628            ArrowDataType::Utf8,
3629            true,
3630        )]));
3631        let data = RecordBatch::try_new(
3632            schema.clone(),
3633            vec![Arc::new(StringArray::from(vec!["string1", "string2"]))],
3634        )?;
3635        let data: Box<dyn EngineData> = Box::new(ArrowEngineData::new(data));
3636        let filtered_data = FilteredEngineData::with_all_rows_selected(data);
3637        let json = to_json_bytes(Box::new(std::iter::once(Ok(filtered_data))))?;
3638        assert_eq!(
3639            json,
3640            "{\"string\":\"string1\"}\n{\"string\":\"string2\"}\n".as_bytes()
3641        );
3642        Ok(())
3643    }
3644
3645    #[test]
3646    fn test_to_json_bytes_filters_data() -> DeltaResult<()> {
3647        // Create test data with 4 rows
3648        let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
3649            "value",
3650            ArrowDataType::Utf8,
3651            true,
3652        )]));
3653        let record_batch = RecordBatch::try_new(
3654            schema.clone(),
3655            vec![Arc::new(StringArray::from(vec![
3656                "row0", "row1", "row2", "row3",
3657            ]))],
3658        )?;
3659
3660        // Helper function to create EngineData from the same record batch
3661        let create_engine_data =
3662            || -> Box<dyn EngineData> { Box::new(ArrowEngineData::new(record_batch.clone())) };
3663
3664        // Test case 1: All rows selected (should include all 4 rows)
3665        let all_selected =
3666            FilteredEngineData::try_new(create_engine_data(), vec![true, true, true, true])?;
3667        let json_all = to_json_bytes(Box::new(std::iter::once(Ok(all_selected))))?;
3668        assert_eq!(
3669            json_all,
3670            "{\"value\":\"row0\"}\n{\"value\":\"row1\"}\n{\"value\":\"row2\"}\n{\"value\":\"row3\"}\n".as_bytes()
3671        );
3672
3673        // Test case 2: Only first and last rows selected (should include only 2 rows)
3674        let partial_selected =
3675            FilteredEngineData::try_new(create_engine_data(), vec![true, false, false, true])?;
3676        let json_partial = to_json_bytes(Box::new(std::iter::once(Ok(partial_selected))))?;
3677        assert_eq!(
3678            json_partial,
3679            "{\"value\":\"row0\"}\n{\"value\":\"row3\"}\n".as_bytes()
3680        );
3681
3682        // Test case 3: Only middle rows selected (should include only 2 rows)
3683        let middle_selected =
3684            FilteredEngineData::try_new(create_engine_data(), vec![false, true, true, false])?;
3685        let json_middle = to_json_bytes(Box::new(std::iter::once(Ok(middle_selected))))?;
3686        assert_eq!(
3687            json_middle,
3688            "{\"value\":\"row1\"}\n{\"value\":\"row2\"}\n".as_bytes()
3689        );
3690
3691        // Test case 4: No rows selected (should produce empty output)
3692        let none_selected =
3693            FilteredEngineData::try_new(create_engine_data(), vec![false, false, false, false])?;
3694        let json_none = to_json_bytes(Box::new(std::iter::once(Ok(none_selected))))?;
3695        assert_eq!(json_none, "".as_bytes());
3696
3697        // Test case 5: Only one row selected (should include only 1 row)
3698        let one_selected =
3699            FilteredEngineData::try_new(create_engine_data(), vec![false, true, false, false])?;
3700        let json_one = to_json_bytes(Box::new(std::iter::once(Ok(one_selected))))?;
3701        assert_eq!(json_one, "{\"value\":\"row1\"}\n".as_bytes());
3702
3703        // Test case 6: Only one row selected implicitly by short vector
3704        let one_selected =
3705            FilteredEngineData::try_new(create_engine_data(), vec![false, false, false])?;
3706        let json_one = to_json_bytes(Box::new(std::iter::once(Ok(one_selected))))?;
3707        assert_eq!(json_one, "{\"value\":\"row3\"}\n".as_bytes());
3708
3709        Ok(())
3710    }
3711
3712    #[test]
3713    fn test_arrow_broken_nested_null_masks() {
3714        use crate::arrow::datatypes::{DataType, Field, Schema};
3715        use crate::engine::arrow_utils::fix_nested_null_masks;
3716        use crate::parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
3717
3718        // Parse some JSON into a nested schema
3719        let schema = Arc::new(Schema::new(vec![Field::new(
3720            "outer",
3721            DataType::Struct(ArrowFields::from(vec![
3722                Field::new(
3723                    "inner_nullable",
3724                    DataType::Struct(ArrowFields::from(vec![
3725                        Field::new("leaf_non_null", DataType::Int32, false),
3726                        Field::new("leaf_nullable", DataType::Int32, true),
3727                    ])),
3728                    true,
3729                ),
3730                Field::new(
3731                    "inner_non_null",
3732                    DataType::Struct(ArrowFields::from(vec![
3733                        Field::new("leaf_non_null", DataType::Int32, false),
3734                        Field::new("leaf_nullable", DataType::Int32, true),
3735                    ])),
3736                    false,
3737                ),
3738            ])),
3739            true,
3740        )]));
3741        let json_string = r#"
3742{ }
3743{ "outer" : { "inner_non_null" : { "leaf_non_null" : 1 } } }
3744{ "outer" : { "inner_non_null" : { "leaf_non_null" : 2, "leaf_nullable" : 3 } } }
3745{ "outer" : { "inner_non_null" : { "leaf_non_null" : 4 }, "inner_nullable" : { "leaf_non_null" : 5 } } }
3746{ "outer" : { "inner_non_null" : { "leaf_non_null" : 6 }, "inner_nullable" : { "leaf_non_null" : 7, "leaf_nullable": 8 } } }
3747"#;
3748        let batch1 = crate::arrow::json::ReaderBuilder::new(schema.clone())
3749            .build(json_string.as_bytes())
3750            .unwrap()
3751            .next()
3752            .unwrap()
3753            .unwrap();
3754
3755        macro_rules! assert_nulls {
3756            ( $column: expr, $nulls: expr ) => {
3757                assert_eq!($column.nulls().unwrap(), &NullBuffer::from(&$nulls[..]));
3758            };
3759        }
3760
3761        // If any of these tests ever fail, it means the arrow JSON reader started producing
3762        // incomplete nested NULL masks. If that happens, we need to update all JSON reads to call
3763        // `fix_nested_null_masks`.
3764        let outer_1 = batch1.column(0).as_struct();
3765        assert_nulls!(outer_1, [false, true, true, true, true]);
3766        let inner_nullable_1 = outer_1.column(0).as_struct();
3767        assert_nulls!(inner_nullable_1, [false, false, false, true, true]);
3768        let nullable_leaf_non_null_1 = inner_nullable_1.column(0);
3769        assert_nulls!(nullable_leaf_non_null_1, [false, false, false, true, true]);
3770        let nullable_leaf_nullable_1 = inner_nullable_1.column(1);
3771        assert_nulls!(nullable_leaf_nullable_1, [false, false, false, false, true]);
3772        let inner_non_null_1 = outer_1.column(1).as_struct();
3773        assert_nulls!(inner_non_null_1, [false, true, true, true, true]);
3774        let non_null_leaf_non_null_1 = inner_non_null_1.column(0);
3775        assert_nulls!(non_null_leaf_non_null_1, [false, true, true, true, true]);
3776        let non_null_leaf_nullable_1 = inner_non_null_1.column(1);
3777        assert_nulls!(non_null_leaf_nullable_1, [false, false, true, false, false]);
3778
3779        // Write the batch to a parquet file and read it back
3780        let mut buffer = vec![];
3781        let mut writer =
3782            crate::parquet::arrow::ArrowWriter::try_new(&mut buffer, schema.clone(), None).unwrap();
3783        writer.write(&batch1).unwrap();
3784        writer.close().unwrap(); // writer must be closed to write footer
3785        let batch2 = ParquetRecordBatchReaderBuilder::try_new(bytes::Bytes::from(buffer))
3786            .unwrap()
3787            .build()
3788            .unwrap()
3789            .next()
3790            .unwrap()
3791            .unwrap();
3792
3793        // Starting from arrow-53.3, the parquet reader started returning broken nested NULL masks.
3794        let batch2 = RecordBatch::from(fix_nested_null_masks(batch2.into()));
3795
3796        // Verify the data survived the round trip
3797        let outer_2 = batch2.column(0).as_struct();
3798        assert_eq!(outer_2, outer_1);
3799        let inner_nullable_2 = outer_2.column(0).as_struct();
3800        assert_eq!(inner_nullable_2, inner_nullable_1);
3801        let nullable_leaf_non_null_2 = inner_nullable_2.column(0);
3802        assert_eq!(nullable_leaf_non_null_2, nullable_leaf_non_null_1);
3803        let nullable_leaf_nullable_2 = inner_nullable_2.column(1);
3804        assert_eq!(nullable_leaf_nullable_2, nullable_leaf_nullable_1);
3805        let inner_non_null_2 = outer_2.column(1).as_struct();
3806        assert_eq!(inner_non_null_2, inner_non_null_1);
3807        let non_null_leaf_non_null_2 = inner_non_null_2.column(0);
3808        assert_eq!(non_null_leaf_non_null_2, non_null_leaf_non_null_1);
3809        let non_null_leaf_nullable_2 = inner_non_null_2.column(1);
3810        assert_eq!(non_null_leaf_nullable_2, non_null_leaf_nullable_1);
3811    }
3812
3813    // --- Tests for build_json_reorder_indices and json_arrow_schema ---
3814
3815    const FILE_PATH: &str = "s3://bucket/test.json";
3816
3817    struct JsonInsertCase {
3818        /// Full schema; may include a `FilePath` metadata column at any position.
3819        schema: StructType,
3820        /// Field names that [`json_arrow_schema`] should expose (metadata columns stripped).
3821        expected_json_names: &'static [&'static str],
3822        /// Column names in the final output after [`reorder_struct_array`].
3823        expected_output_names: &'static [&'static str],
3824        /// Index of the `_file` column in the output, or `None` when the schema has no FilePath.
3825        file_path_col: Option<usize>,
3826    }
3827
3828    /// Verifies that `json_arrow_schema` + `build_json_reorder_indices` + `reorder_struct_array`
3829    /// correctly insert (or omit) the `_file` column at the position declared in the schema.
3830    #[rstest]
3831    #[case::no_file_path(JsonInsertCase {
3832        schema: StructType::new_unchecked([
3833            StructField::not_null("a", DataType::INTEGER),
3834            StructField::nullable("b", DataType::INTEGER),
3835        ]),
3836        expected_json_names: &["a", "b"],
3837        expected_output_names: &["a", "b"],
3838        file_path_col: None,
3839    })]
3840    #[case::file_path_at_start(JsonInsertCase {
3841        schema: StructType::new_unchecked([
3842            StructField::create_metadata_column("_file", MetadataColumnSpec::FilePath),
3843            StructField::not_null("a", DataType::INTEGER),
3844            StructField::nullable("b", DataType::INTEGER),
3845        ]),
3846        expected_json_names: &["a", "b"],
3847        expected_output_names: &["_file", "a", "b"],
3848        file_path_col: Some(0),
3849    })]
3850    #[case::file_path_in_middle(JsonInsertCase {
3851        schema: StructType::new_unchecked([
3852            StructField::not_null("a", DataType::INTEGER),
3853            StructField::create_metadata_column("_file", MetadataColumnSpec::FilePath),
3854            StructField::nullable("b", DataType::INTEGER),
3855        ]),
3856        expected_json_names: &["a", "b"],
3857        expected_output_names: &["a", "_file", "b"],
3858        file_path_col: Some(1),
3859    })]
3860    #[case::file_path_at_end(JsonInsertCase {
3861        schema: StructType::new_unchecked([
3862            StructField::not_null("a", DataType::INTEGER),
3863            StructField::nullable("b", DataType::INTEGER),
3864            StructField::create_metadata_column("_file", MetadataColumnSpec::FilePath),
3865        ]),
3866        expected_json_names: &["a", "b"],
3867        expected_output_names: &["a", "b", "_file"],
3868        file_path_col: Some(2),
3869    })]
3870    fn test_json_file_path_insertion(#[case] case: JsonInsertCase) {
3871        // json_arrow_schema exposes only the non-metadata fields.
3872        let json_schema = json_arrow_schema(&case.schema).unwrap();
3873        let json_names: Vec<_> = json_schema
3874            .fields()
3875            .iter()
3876            .map(|f| f.name().as_str())
3877            .collect();
3878        assert_eq!(json_names, case.expected_json_names);
3879
3880        // Build an input batch with the JSON schema (real columns only, each an Int32Array).
3881        let arrow_schema = Arc::new(json_schema);
3882        let cols: Vec<ArrowArrayRef> = (0..arrow_schema.fields().len())
3883            .map(|_| Arc::new(Int32Array::from(vec![1i32, 2, 3])) as _)
3884            .collect();
3885        let batch = RecordBatch::try_new(arrow_schema, cols).unwrap();
3886
3887        // build_json_reorder_indices + reorder_struct_array inserts the _file column.
3888        let indices = build_json_reorder_indices(&case.schema).unwrap();
3889        let result = RecordBatch::from(
3890            reorder_struct_array(batch.into(), &indices, None, Some(FILE_PATH)).unwrap(),
3891        );
3892
3893        // Verify output column order and row count.
3894        let schema = result.schema();
3895        let output_names: Vec<_> = schema.fields().iter().map(|f| f.name().as_str()).collect();
3896        assert_eq!(output_names, case.expected_output_names);
3897        assert_eq!(result.num_rows(), 3);
3898
3899        // When FilePath is in the schema, verify a plain StringArray with the path for every row.
3900        // When absent, verify no _file column leaked into the output.
3901        if let Some(idx) = case.file_path_col {
3902            let arr = result
3903                .column(idx)
3904                .as_any()
3905                .downcast_ref::<StringArray>()
3906                .expect("_file column should be a StringArray");
3907            assert!(arr.iter().all(|v| v == Some(FILE_PATH)));
3908        } else {
3909            assert!(
3910                result.schema().fields().iter().all(|f| f.name() != "_file"),
3911                "_file should not appear when not declared in the schema"
3912            );
3913        }
3914    }
3915
3916    #[test]
3917    fn test_build_json_reorder_indices_unsupported_metadata_column_errors() {
3918        // RowIndex is not supported for JSON reads. All metadata column specs are non-nullable,
3919        // so the Missing transform inserts a null array — reorder_struct_array errors because
3920        // the field is declared non-nullable.
3921        let schema = StructType::new_unchecked([
3922            StructField::not_null("a", DataType::INTEGER),
3923            StructField::create_metadata_column("row_index", MetadataColumnSpec::RowIndex),
3924        ]);
3925        let arrow_schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
3926            "a",
3927            ArrowDataType::Int32,
3928            false,
3929        )]));
3930        let batch = RecordBatch::try_new(
3931            arrow_schema,
3932            vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
3933        )
3934        .unwrap();
3935
3936        let indices = build_json_reorder_indices(&schema).unwrap();
3937        assert!(reorder_struct_array(batch.into(), &indices, None, None).is_err());
3938    }
3939
3940    #[test]
3941    fn ensure_we_encode_maps_with_null_values() {
3942        let schema = ArrowSchema::new(vec![
3943            ArrowField::new("str_col", ArrowDataType::Utf8, false),
3944            ArrowField::new(
3945                "map_col",
3946                ArrowDataType::Map(
3947                    Arc::new(ArrowField::new(
3948                        "entries",
3949                        ArrowDataType::Struct(
3950                            vec![
3951                                ArrowField::new("keys", ArrowDataType::Utf8, false),
3952                                ArrowField::new("values", ArrowDataType::Utf8, true),
3953                            ]
3954                            .into(),
3955                        ),
3956                        false,
3957                    )),
3958                    false, // sorted
3959                ),
3960                false,
3961            ),
3962        ]);
3963        let s_array = StringArray::from(vec!["foo"]);
3964
3965        let string_builder = StringBuilder::new();
3966        let string_builder2 = StringBuilder::new();
3967        let mut map_builder = MapBuilder::new(None, string_builder, string_builder2);
3968
3969        // Append one entry: "bar" -> null
3970        map_builder.keys().append_value("bar");
3971        map_builder.values().append_null();
3972        map_builder.append(true).unwrap(); // finish the map row
3973
3974        let map_array: MapArray = map_builder.finish();
3975        let batch = RecordBatch::try_new(
3976            Arc::new(schema),
3977            vec![Arc::new(s_array), Arc::new(map_array)],
3978        )
3979        .unwrap();
3980
3981        let data: Box<dyn EngineData> = Box::new(ArrowEngineData::new(batch));
3982        let filtered_data = FilteredEngineData::with_all_rows_selected(data);
3983        let json = to_json_bytes(Box::new(std::iter::once(Ok(filtered_data)))).unwrap();
3984        assert_eq!(
3985            json,
3986            "{\"str_col\":\"foo\",\"map_col\":{\"bar\":null}}\n".as_bytes()
3987        );
3988    }
3989
3990    #[rstest]
3991    fn struct_with_all_nullable_children_unmatched_is_missing(
3992        #[values(true, false)] struct_nullable: bool,
3993    ) {
3994        // When a struct exists in parquet but none of its children match the requested
3995        // schema, the struct should be treated as missing regardless of its nullability.
3996        let info_field = if struct_nullable {
3997            StructField::nullable(
3998                "info",
3999                StructType::new_unchecked([StructField::nullable("z", DataType::LONG)]),
4000            )
4001        } else {
4002            StructField::not_null(
4003                "info",
4004                StructType::new_unchecked([StructField::nullable("z", DataType::LONG)]),
4005            )
4006        };
4007        let requested_schema = Arc::new(StructType::new_unchecked([
4008            StructField::not_null("a", DataType::LONG),
4009            info_field,
4010        ]));
4011        let parquet_schema = Arc::new(ArrowSchema::new(vec![
4012            ArrowField::new("a", ArrowDataType::Int64, true),
4013            ArrowField::new(
4014                "info",
4015                ArrowDataType::Struct(
4016                    vec![
4017                        ArrowField::new("x", ArrowDataType::Int64, true),
4018                        ArrowField::new("y", ArrowDataType::Utf8, true),
4019                    ]
4020                    .into(),
4021                ),
4022                !struct_nullable,
4023            ),
4024        ]));
4025        let (mask_indices, reorder_indices) =
4026            get_requested_indices(&requested_schema, &parquet_schema).unwrap();
4027        assert_eq!(mask_indices, vec![0]);
4028        let expected_info_field = Arc::new(
4029            requested_schema
4030                .field("info")
4031                .unwrap()
4032                .try_into_arrow()
4033                .unwrap(),
4034        );
4035        assert_eq!(
4036            reorder_indices,
4037            vec![
4038                ReorderIndex::identity(0),
4039                ReorderIndex::missing(1, expected_info_field),
4040            ]
4041        );
4042    }
4043
4044    #[test]
4045    fn reorder_non_nullable_missing_struct_produces_non_null_struct() {
4046        // The Missing transform for a non-nullable struct should produce a struct with
4047        // null_count == 0 and all-null children.
4048        let a_array: Arc<dyn ArrowArray> = Arc::new(Int64Array::from(vec![1, 2, 3]));
4049        let input = StructArray::from(vec![(
4050            Arc::new(ArrowField::new("a", ArrowDataType::Int64, false)),
4051            a_array,
4052        )]);
4053        let missing_field = Arc::new(ArrowField::new(
4054            "info",
4055            ArrowDataType::Struct(vec![ArrowField::new("z", ArrowDataType::Int64, true)].into()),
4056            false,
4057        ));
4058        let reorder = vec![
4059            ReorderIndex::identity(0),
4060            ReorderIndex::missing(1, missing_field),
4061        ];
4062        let ordered = reorder_struct_array(input, &reorder, None, None).unwrap();
4063        assert_eq!(ordered.column_names(), vec!["a", "info"]);
4064        let info = ordered.column(1).as_struct();
4065        assert_eq!(info.null_count(), 0);
4066        assert_eq!(info.column(0).null_count(), 3);
4067    }
4068
4069    #[test]
4070    fn reorder_nested_non_nullable_missing_struct_recurses() {
4071        // Non-nullable struct containing a non-nullable struct child: both levels should
4072        // have null_count == 0, with the leaf nullable child being all-null.
4073        let a_array: Arc<dyn ArrowArray> = Arc::new(Int64Array::from(vec![1, 2]));
4074        let input = StructArray::from(vec![(
4075            Arc::new(ArrowField::new("a", ArrowDataType::Int64, false)),
4076            a_array,
4077        )]);
4078        let inner_struct =
4079            ArrowDataType::Struct(vec![ArrowField::new("leaf", ArrowDataType::Int64, true)].into());
4080        let missing_field = Arc::new(ArrowField::new(
4081            "outer",
4082            ArrowDataType::Struct(vec![ArrowField::new("inner", inner_struct, false)].into()),
4083            false,
4084        ));
4085        let reorder = vec![
4086            ReorderIndex::identity(0),
4087            ReorderIndex::missing(1, missing_field),
4088        ];
4089        let ordered = reorder_struct_array(input, &reorder, None, None).unwrap();
4090        let outer = ordered.column(1).as_struct();
4091        assert_eq!(outer.null_count(), 0);
4092        let inner = outer.column(0).as_struct();
4093        assert_eq!(inner.null_count(), 0);
4094        assert_eq!(inner.column(0).null_count(), 2);
4095    }
4096
4097    #[test]
4098    fn empty_struct_is_matched() {
4099        // Delta protocol allows empty structs. An empty struct has no children, so no
4100        // leaf columns are selected, but the struct itself should still be matched.
4101        let requested_schema = Arc::new(StructType::new_unchecked([
4102            StructField::not_null("a", DataType::LONG),
4103            StructField::not_null("empty", StructType::new_unchecked([])),
4104        ]));
4105        let parquet_schema = Arc::new(ArrowSchema::new(vec![
4106            ArrowField::new("a", ArrowDataType::Int64, true),
4107            ArrowField::new("empty", ArrowDataType::Struct(ArrowFields::empty()), false),
4108        ]));
4109        let (mask_indices, reorder_indices) =
4110            get_requested_indices(&requested_schema, &parquet_schema).unwrap();
4111        assert_eq!(mask_indices, vec![0]);
4112        let expected_empty_field = Arc::new(
4113            requested_schema
4114                .field("empty")
4115                .unwrap()
4116                .try_into_arrow()
4117                .unwrap(),
4118        );
4119        assert_eq!(
4120            reorder_indices,
4121            vec![
4122                ReorderIndex::identity(0),
4123                ReorderIndex::missing(1, expected_empty_field),
4124            ]
4125        );
4126    }
4127
4128    // === fixup_parquet_read coercion behavior ===
4129
4130    /// Verifies that `fixup_parquet_read` rewrites the batch's nullability flags at every
4131    /// nesting level (struct field, list element, map value) to match the kernel schema, while
4132    /// preserving the Arrow-mandated non-nullability of map entries and map keys.
4133    #[test]
4134    fn test_fixup_parquet_read_coerces_nullability_at_all_levels() {
4135        // Source: outer (non-null) {
4136        //   lst: List<Int32 (non-null)> (non-null),
4137        //   mp:  Map<Utf8, Int32 (non-null)> (non-null),
4138        // }
4139        let src_list_elem = Arc::new(ArrowField::new("element", ArrowDataType::Int32, false));
4140        let src_list = ArrowField::new("lst", ArrowDataType::List(src_list_elem.clone()), false);
4141        let src_map_entries = Arc::new(ArrowField::new(
4142            "entries",
4143            ArrowDataType::Struct(ArrowFields::from(vec![
4144                ArrowField::new("key", ArrowDataType::Utf8, false),
4145                ArrowField::new("value", ArrowDataType::Int32, false),
4146            ])),
4147            false,
4148        ));
4149        let src_map = ArrowField::new(
4150            "mp",
4151            ArrowDataType::Map(src_map_entries.clone(), false),
4152            false,
4153        );
4154        let src_outer = ArrowField::new(
4155            "outer",
4156            ArrowDataType::Struct(ArrowFields::from(vec![src_list.clone(), src_map.clone()])),
4157            false,
4158        );
4159
4160        let offsets_1 = || OffsetBuffer::new(ScalarBuffer::from(vec![0i32, 1]));
4161        let list_col: ArrowArrayRef = Arc::new(
4162            GenericListArray::<i32>::try_new(
4163                src_list_elem,
4164                offsets_1(),
4165                Arc::new(Int32Array::from(vec![1])),
4166                None,
4167            )
4168            .unwrap(),
4169        );
4170        let entries_fields = match src_map_entries.data_type() {
4171            ArrowDataType::Struct(f) => f.clone(),
4172            _ => unreachable!(),
4173        };
4174        let entries = StructArray::try_new(
4175            entries_fields,
4176            vec![
4177                Arc::new(StringArray::from(vec!["a"])) as _,
4178                Arc::new(Int32Array::from(vec![10])) as _,
4179            ],
4180            None,
4181        )
4182        .unwrap();
4183        let map_col: ArrowArrayRef = Arc::new(
4184            MapArray::try_new(src_map_entries, offsets_1(), entries, None, false).unwrap(),
4185        );
4186        let outer_col: ArrowArrayRef = Arc::new(
4187            StructArray::try_new(
4188                ArrowFields::from(vec![src_list, src_map]),
4189                vec![list_col, map_col],
4190                None,
4191            )
4192            .unwrap(),
4193        );
4194
4195        let src_schema = Arc::new(ArrowSchema::new(vec![src_outer]));
4196        let batch = RecordBatch::try_new(src_schema, vec![outer_col]).unwrap();
4197
4198        // Kernel target: all nullable where Arrow allows it.
4199        let target_schema: SchemaRef =
4200            Arc::new(StructType::new_unchecked([StructField::nullable(
4201                "outer",
4202                StructType::new_unchecked([
4203                    StructField::nullable("lst", ArrayType::new(DataType::INTEGER, true)),
4204                    StructField::nullable(
4205                        "mp",
4206                        MapType::new(DataType::STRING, DataType::INTEGER, true),
4207                    ),
4208                ]),
4209            )]));
4210
4211        let ordering = [ReorderIndex::identity(0)];
4212        let result =
4213            fixup_parquet_read(batch, &ordering, None, None, Some(&target_schema)).unwrap();
4214        let result_batch: RecordBatch = result.into();
4215
4216        let schema = result_batch.schema();
4217        let outer_field = schema.field(0);
4218        assert!(outer_field.is_nullable(), "outer should be nullable");
4219
4220        let outer_children = match outer_field.data_type() {
4221            ArrowDataType::Struct(f) => f,
4222            other => panic!("expected Struct, got {other:?}"),
4223        };
4224
4225        let lst_field = &outer_children[0];
4226        assert!(lst_field.is_nullable(), "lst should be nullable");
4227        let lst_element = match lst_field.data_type() {
4228            ArrowDataType::List(e) => e,
4229            other => panic!("expected List, got {other:?}"),
4230        };
4231        assert!(lst_element.is_nullable(), "list element should be nullable");
4232
4233        let mp_field = &outer_children[1];
4234        assert!(mp_field.is_nullable(), "mp should be nullable");
4235        let (mp_entries, mp_key, mp_value) = match mp_field.data_type() {
4236            ArrowDataType::Map(entries, _) => {
4237                let inner = match entries.data_type() {
4238                    ArrowDataType::Struct(f) => f,
4239                    other => panic!("expected Struct entries, got {other:?}"),
4240                };
4241                (entries.as_ref(), inner[0].clone(), inner[1].clone())
4242            }
4243            other => panic!("expected Map, got {other:?}"),
4244        };
4245        assert!(
4246            !mp_entries.is_nullable(),
4247            "map entries field must remain non-null per Arrow spec"
4248        );
4249        assert!(
4250            !mp_key.is_nullable(),
4251            "map key field must remain non-null per Arrow spec"
4252        );
4253        assert!(mp_value.is_nullable(), "map value should be nullable");
4254    }
4255
4256    /// Verifies that `fixup_parquet_read` rewrites nested struct child names to match the kernel
4257    /// schema. Complements the engine-level `test_read_parquet_with_field_id_matching` (which
4258    /// only renames the top-level fields) by exercising the nested rename path directly at the
4259    /// `fixup_parquet_read` boundary.
4260    #[test]
4261    fn test_fixup_parquet_read_renames_nested_struct_fields() {
4262        // Source: outer { src_x: Int32, src_y: Struct { src_z: Int32 } }
4263        let src_inner_fields =
4264            ArrowFields::from(vec![ArrowField::new("src_z", ArrowDataType::Int32, false)]);
4265        let src_inner = ArrowField::new(
4266            "src_y",
4267            ArrowDataType::Struct(src_inner_fields.clone()),
4268            false,
4269        );
4270        let src_outer_fields = ArrowFields::from(vec![
4271            ArrowField::new("src_x", ArrowDataType::Int32, false),
4272            src_inner,
4273        ]);
4274        let src_outer = ArrowField::new(
4275            "outer",
4276            ArrowDataType::Struct(src_outer_fields.clone()),
4277            false,
4278        );
4279
4280        let inner_z: ArrowArrayRef = Arc::new(Int32Array::from(vec![42, 43, 44]));
4281        let inner_struct: ArrowArrayRef =
4282            Arc::new(StructArray::try_new(src_inner_fields, vec![inner_z], None).unwrap());
4283        let outer_x: ArrowArrayRef = Arc::new(Int32Array::from(vec![1, 2, 3]));
4284        let outer_col: ArrowArrayRef = Arc::new(
4285            StructArray::try_new(src_outer_fields, vec![outer_x, inner_struct], None).unwrap(),
4286        );
4287
4288        let src_schema = Arc::new(ArrowSchema::new(vec![src_outer]));
4289        let batch = RecordBatch::try_new(src_schema, vec![outer_col]).unwrap();
4290
4291        // Kernel target with different names at every level: tgt_x, tgt_y, tgt_z.
4292        let target_schema: SchemaRef = schema_ref! {
4293            not_null "outer": {
4294                not_null "tgt_x": INTEGER,
4295                not_null "tgt_y": { not_null "tgt_z": INTEGER },
4296            },
4297        };
4298
4299        let ordering = [ReorderIndex::identity(0)];
4300        let result =
4301            fixup_parquet_read(batch, &ordering, None, None, Some(&target_schema)).unwrap();
4302        let result_batch: RecordBatch = result.into();
4303
4304        let schema = result_batch.schema();
4305        let outer_field = schema.field(0);
4306        assert_eq!(outer_field.name(), "outer");
4307
4308        let outer_children = match outer_field.data_type() {
4309            ArrowDataType::Struct(f) => f,
4310            other => panic!("expected Struct, got {other:?}"),
4311        };
4312        assert_eq!(outer_children[0].name(), "tgt_x");
4313        assert_eq!(outer_children[1].name(), "tgt_y");
4314
4315        let nested_children = match outer_children[1].data_type() {
4316            ArrowDataType::Struct(f) => f,
4317            other => panic!("expected nested Struct, got {other:?}"),
4318        };
4319        assert_eq!(nested_children[0].name(), "tgt_z");
4320    }
4321
4322    /// Verify that `fix_nested_null_masks` handles a struct with a NullArray child column
4323    /// (void type) when the parent struct has non-trivial nulls. NullArray does not accept
4324    /// a null buffer, so the propagation must skip it without panicking.
4325    #[test]
4326    fn test_nested_null_masks_with_null_array_child() {
4327        use crate::arrow::array::NullArray;
4328        use crate::arrow::datatypes::{DataType, Field};
4329        use crate::engine::arrow_utils::fix_nested_null_masks;
4330
4331        // Build: struct<val: int32, void_col: null> with 4 rows, parent null at row 0
4332        let int_field = Field::new("val", DataType::Int32, true);
4333        let null_field = Field::new("void_col", DataType::Null, true);
4334        let fields = ArrowFields::from(vec![int_field, null_field]);
4335
4336        let int_col: ArrowArrayRef = Arc::new(Int32Array::from(vec![
4337            Some(10),
4338            Some(20),
4339            Some(30),
4340            Some(40),
4341        ]));
4342        let null_col: ArrowArrayRef = Arc::new(NullArray::new(4));
4343
4344        // Parent struct has row 0 null
4345        let parent_nulls = NullBuffer::from(&[false, true, true, true][..]);
4346        let sa = StructArray::new(fields.clone(), vec![int_col, null_col], Some(parent_nulls));
4347
4348        // Wrap in an outer struct (as fix_nested_null_masks expects a top-level StructArray)
4349        let outer_field = Field::new("outer", DataType::Struct(fields), true);
4350        let outer = StructArray::new(
4351            ArrowFields::from(vec![outer_field]),
4352            vec![Arc::new(sa)],
4353            None,
4354        );
4355
4356        // This should NOT panic — previously it would crash because NullArray rejects null buffers
4357        let result = fix_nested_null_masks(outer);
4358
4359        // Verify the NullArray child survived unchanged
4360        let inner = result.column(0).as_struct();
4361        assert_eq!(inner.len(), 4);
4362
4363        let void_col = inner.column(1);
4364        assert_eq!(*void_col.data_type(), DataType::Null);
4365        assert_eq!(void_col.len(), 4);
4366        // NullArray has no null bitmap, so null_count() returns 0 in Arrow even though
4367        // all values are conceptually null. Verify the quirk explicitly.
4368        assert_eq!(void_col.null_count(), 0);
4369
4370        // Verify the int column got the parent null propagated (row 0 is now null)
4371        let val_col = inner.column(0);
4372        assert!(val_col.is_null(0), "row 0 should be null (parent null)");
4373        assert!(!val_col.is_null(1));
4374        assert!(!val_col.is_null(2));
4375        assert!(!val_col.is_null(3));
4376    }
4377
4378    // === coerce_columns_to_schema ===
4379
4380    // Wraps [`TryIntoArrow`] in an `Arc` for brevity at the call sites below. Going through the
4381    // kernel conversion is the point: that is what names the map entry `key_value` and the array
4382    // element `element`, which hand-written Arrow fields would not.
4383    fn kernel_target_schema(schema: StructType) -> ArrowSchemaRef {
4384        Arc::new((&schema).try_into_arrow().unwrap())
4385    }
4386
4387    // A one-entry `{"k": "v"}` map whose entry struct is named `entry`. That struct holds the
4388    // key/value pair of every entry, and is the field whose name writers disagree on.
4389    fn map_string_string_with_entry_name(entry: &str) -> MapArray {
4390        let names = MapFieldNames {
4391            entry: entry.to_string(),
4392            key: "key".to_string(),
4393            value: "value".to_string(),
4394        };
4395        let mut builder = MapBuilder::new(Some(names), StringBuilder::new(), StringBuilder::new());
4396        builder.keys().append_value("k");
4397        builder.values().append_value("v");
4398        builder.append(true).unwrap();
4399        builder.finish()
4400    }
4401
4402    #[test]
4403    fn test_coerce_columns_renames_entries_map_to_key_value() {
4404        let map = map_string_string_with_entry_name("entries"); // <-- writer's name
4405        let ArrowDataType::Map(entry_field, _) = map.data_type() else {
4406            panic!("expected map");
4407        };
4408        assert_eq!(entry_field.name(), "entries");
4409
4410        let target = kernel_target_schema(schema! { nullable "m": { STRING => nullable STRING } });
4411        let coerced = coerce_columns_to_schema(vec![Arc::new(map)], &target).unwrap();
4412
4413        let ArrowDataType::Map(out_entry, _) = coerced[0].data_type() else {
4414            panic!("expected map");
4415        };
4416        assert_eq!(out_entry.name(), "key_value"); // <-- kernel's name
4417        assert_eq!(coerced[0].data_type(), target.field(0).data_type());
4418    }
4419
4420    #[test]
4421    fn test_coerce_columns_renames_list_item_to_element() {
4422        // Arrow's `ListArray::from_iter_primitive` names the element field `item`.
4423        let list: ListArray =
4424            ListArray::from_iter_primitive::<Int32Type, _, _>([Some(vec![Some(1), Some(2)])]);
4425        let ArrowDataType::List(elem_field) = list.data_type() else {
4426            panic!("expected list");
4427        };
4428        assert_eq!(elem_field.name(), "item"); // <-- writer's name
4429
4430        let target = kernel_target_schema(schema! { nullable "l": [ nullable INTEGER ] });
4431        let coerced = coerce_columns_to_schema(vec![Arc::new(list)], &target).unwrap();
4432
4433        let ArrowDataType::List(out_elem) = coerced[0].data_type() else {
4434            panic!("expected list");
4435        };
4436        assert_eq!(out_elem.name(), "element"); // <-- kernel's name
4437        assert_eq!(coerced[0].data_type(), target.field(0).data_type());
4438    }
4439
4440    #[test]
4441    fn test_coerce_columns_passes_through_matching_columns() {
4442        let col: ArrowArrayRef = Arc::new(Int32Array::from(vec![1, 2, 3]));
4443        let target = kernel_target_schema(schema! { nullable "i": INTEGER });
4444
4445        let coerced = coerce_columns_to_schema(vec![col.clone()], &target).unwrap();
4446
4447        // An already-matching column is returned as the same allocation (no rebuild).
4448        assert!(Arc::ptr_eq(&col, &coerced[0]));
4449    }
4450
4451    // `metaData.configuration` arrives this shape: the map that needs renaming sits one level down,
4452    // reachable only through the struct-recursion arm.
4453    #[test]
4454    fn test_coerce_columns_renames_map_nested_in_struct() {
4455        let map = map_string_string_with_entry_name("entries"); // <-- writer's name
4456        let config_field = ArrowField::new("config", map.data_type().clone(), true);
4457        let outer = StructArray::from(vec![(
4458            Arc::new(config_field),
4459            Arc::new(map) as ArrowArrayRef,
4460        )]);
4461
4462        let target = kernel_target_schema(schema! {
4463            nullable "meta": { nullable "config": { STRING => nullable STRING } },
4464        });
4465        let coerced = coerce_columns_to_schema(vec![Arc::new(outer)], &target).unwrap();
4466
4467        assert_eq!(coerced[0].data_type(), target.field(0).data_type());
4468    }
4469
4470    // Beyond renaming, this is a real cast: a primitive column converts to the target's type.
4471    #[test]
4472    fn test_coerce_columns_casts_primitive_to_target_type() {
4473        let col: ArrowArrayRef = Arc::new(Int32Array::from(vec![1, 2, 3])); // <-- source: Int32
4474        let target = kernel_target_schema(schema! { nullable "n": LONG }); // <-- target: Int64
4475
4476        let coerced = coerce_columns_to_schema(vec![col], &target).unwrap();
4477
4478        assert_eq!(coerced[0].data_type(), &ArrowDataType::Int64);
4479    }
4480
4481    // Casting a child rebuilds the struct around it, which must keep the struct's own null rows.
4482    #[test]
4483    fn test_coerce_columns_preserves_struct_null_rows() {
4484        let child: ArrowArrayRef = Arc::new(Int32Array::from(vec![Some(1), Some(2)]));
4485        let fields: ArrowFields = vec![ArrowField::new("n", ArrowDataType::Int32, true)].into();
4486        let nulls = NullBuffer::from(vec![false, true]); // <-- row 0 is a null struct
4487        let outer = StructArray::try_new(fields, vec![child], Some(nulls)).unwrap();
4488
4489        // Widening the child to Int64 forces the struct to be rebuilt.
4490        let target = kernel_target_schema(schema! { nullable "s": { nullable "n": LONG } });
4491        let coerced = coerce_columns_to_schema(vec![Arc::new(outer)], &target).unwrap();
4492
4493        let out = coerced[0].as_struct();
4494        assert!(out.is_null(0), "null struct row must survive the rebuild");
4495        assert!(!out.is_null(1));
4496        assert_eq!(out.column(0).data_type(), &ArrowDataType::Int64);
4497    }
4498
4499    #[test]
4500    fn test_coerce_columns_incompatible_leaf_type_errors() {
4501        let col: ArrowArrayRef = Arc::new(StringArray::from(vec!["not_a_date"]));
4502        let target = kernel_target_schema(schema! { nullable "d": DATE });
4503
4504        assert!(coerce_columns_to_schema(vec![col], &target).is_err());
4505    }
4506
4507    // A primitive source against a struct target: the struct arm must reject it, not panic on the
4508    // `as_struct` downcast.
4509    #[test]
4510    fn test_coerce_columns_non_struct_source_to_struct_target_errors() {
4511        let col: ArrowArrayRef = Arc::new(Int32Array::from(vec![1, 2, 3]));
4512        let target = kernel_target_schema(schema! { nullable "s": { nullable "n": INTEGER } });
4513
4514        let result = coerce_columns_to_schema(vec![col], &target);
4515        assert_result_error_with_message(result, "to a struct target");
4516    }
4517
4518    // A struct source with fewer children than the struct target has fields.
4519    #[test]
4520    fn test_coerce_columns_struct_child_count_mismatch_errors() {
4521        let child: ArrowArrayRef = Arc::new(Int32Array::from(vec![1, 2]));
4522        let fields: ArrowFields = vec![ArrowField::new("a", ArrowDataType::Int32, true)].into();
4523        let source = StructArray::try_new(fields, vec![child], None).unwrap();
4524
4525        let target = kernel_target_schema(schema! {
4526            nullable "s": { nullable "a": INTEGER, nullable "b": INTEGER },
4527        });
4528
4529        let result = coerce_columns_to_schema(vec![Arc::new(source)], &target);
4530        assert_result_error_with_message(result, "cannot cast struct with 1 children");
4531    }
4532
4533    // Recursion must reach through every container: struct -> list -> map, with `entries`/`item`
4534    // names at each level, all renamed in one pass.
4535    #[test]
4536    fn test_coerce_columns_recurses_through_struct_list_map() {
4537        let map = map_string_string_with_entry_name("entries"); // <-- writer's name
4538        let list_field = Arc::new(ArrowField::new("item", map.data_type().clone(), true));
4539        let list = ListArray::new(
4540            list_field,
4541            OffsetBuffer::from_lengths([1]),
4542            Arc::new(map),
4543            None,
4544        );
4545        let outer = StructArray::from(vec![(
4546            Arc::new(ArrowField::new("maps", list.data_type().clone(), true)),
4547            Arc::new(list) as ArrowArrayRef,
4548        )]);
4549
4550        let target = kernel_target_schema(schema! {
4551            nullable "s": { nullable "maps": [ nullable { STRING => nullable STRING } ] },
4552        });
4553        let coerced = coerce_columns_to_schema(vec![Arc::new(outer)], &target).unwrap();
4554
4555        assert_eq!(coerced[0].data_type(), target.field(0).data_type());
4556    }
4557}